From c9dd959fbdd09a391f766f7458d7d6a3d1db453c Mon Sep 17 00:00:00 2001 From: Dimitri Fontaine Date: Fri, 20 Sep 2013 22:52:45 +0200 Subject: [PATCH] Implement MySQL migration option "downcase|quote identifier". --- mysql-cast-rules.lisp | 16 ++++++++++---- mysql.lisp | 50 +++++++++++++++++++++++++++++++------------ parser.lisp | 23 ++++++++++++++------ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/mysql-cast-rules.lisp b/mysql-cast-rules.lisp index 897a906..2f2fe54 100644 --- a/mysql-cast-rules.lisp +++ b/mysql-cast-rules.lisp @@ -7,6 +7,12 @@ ;;; ;;; Some functions to deal with ENUM types ;;; +(defun apply-identifier-case (identifier case) + "Return a SQL string to use in the output part of a MySQL query." + (ecase case + (:downcase (string-downcase identifier)) + (:quote (format nil "\"~a\"" identifier)))) + (defun explode-mysql-enum (ctype) "Convert MySQL ENUM expression into a list of labels." ;; from: "ENUM('small', 'medium', 'large')" @@ -14,15 +20,17 @@ (mapcar (lambda (x) (string-trim "' )" x)) (sq:split-sequence #\, ctype :start (position #\' ctype)))) -(defun get-enum-type-name (table-name column-name) +(defun get-enum-type-name (table-name column-name identifier-case) "Return the Type Name we're going to use in PostgreSQL." - (format nil "~a_~a" table-name column-name)) + (apply-identifier-case (format nil "~a_~a" table-name column-name) + identifier-case)) -(defun get-create-enum (table-name column-name ctype) +(defun get-create-enum (table-name column-name ctype + &key (identifier-case :downcase)) "Return a PostgreSQL CREATE ENUM TYPE statement from MySQL enum column." (with-output-to-string (s) (format s "CREATE TYPE ~a AS ENUM (~{'~a'~^, ~});" - (get-enum-type-name table-name column-name) + (get-enum-type-name table-name column-name identifier-case) (explode-mysql-enum ctype)))) (defun cast-enum (table-name column-name type ctype typemod) diff --git a/mysql.lisp b/mysql.lisp index c3b54e9..2b0f170 100644 --- a/mysql.lisp +++ b/mysql.lisp @@ -72,45 +72,65 @@ order by table_name, ordinal_position" dbname))) ;; free resources (cl-mysql:disconnect))) -(defun get-create-type (table-name cols &key include-drop) +(defun get-create-type (table-name cols &key identifier-case include-drop) "MySQL declares ENUM types inline, PostgreSQL wants explicit CREATE TYPE." (loop for (name dtype ctype default nullable extra) in cols when (string-equal "enum" dtype) collect (when include-drop - (let ((type-name (get-enum-type-name table-name name))) + (let* ((type-name + (get-enum-type-name table-name name identifier-case))) (format nil "DROP TYPE IF EXISTS ~a;" type-name))) - and collect (get-create-enum table-name name ctype))) + and collect (get-create-enum table-name name ctype + :identifier-case identifier-case))) -(defun get-create-table (table-name cols) +(defun get-create-table (table-name cols &key identifier-case) "Return a PostgreSQL CREATE TABLE statement from MySQL columns" (with-output-to-string (s) - (format s "CREATE TABLE ~a ~%(~%" table-name) + (let ((table-name (apply-identifier-case table-name identifier-case))) + (format s "CREATE TABLE ~a ~%(~%" table-name)) (loop for ((name dtype ctype default nullable extra) . last?) on cols for pg-coldef = (cast table-name name dtype ctype default nullable extra) - do (format s " \"~a\" ~22t ~a~:[~;,~]~%" name pg-coldef last?)) + for colname = (apply-identifier-case name identifier-case) + do (format s " ~a ~22t ~a~:[~;,~]~%" colname pg-coldef last?)) (format s ");~%"))) -(defun get-drop-table-if-exists (table-name) +(defun get-drop-table-if-exists (table-name &key identifier-case) "Return the PostgreSQL DROP TABLE IF EXISTS statement for TABLE-NAME." - (format nil "DROP TABLE IF EXISTS ~a;~%" table-name)) + (let ((table-name (apply-identifier-case table-name identifier-case))) + (format nil "DROP TABLE IF EXISTS ~a;~%" table-name))) -(defun get-pgsql-create-tables (all-columns &key include-drop) +(defun get-pgsql-create-tables (all-columns + &key + include-drop + (identifier-case :downcase)) "Return the list of CREATE TABLE statements to run against PostgreSQL" (loop for (table-name . cols) in all-columns - for extra-types = (get-create-type table-name cols :include-drop include-drop) - when include-drop collect (get-drop-table-if-exists table-name) + for extra-types = (get-create-type table-name cols + :identifier-case identifier-case + :include-drop include-drop) + when include-drop + collect (get-drop-table-if-exists table-name + :identifier-case identifier-case) + when extra-types append extra-types - collect (get-create-table table-name cols))) + + collect (get-create-table table-name cols + :identifier-case identifier-case))) (defun pgsql-create-tables (dbname all-columns - &key (pg-dbname dbname) include-drop) + &key + (identifier-case :downcase) + (pg-dbname dbname) + include-drop) "Create all MySQL tables in database dbname in PostgreSQL" (loop for nb-tables from 0 - for sql in (get-pgsql-create-tables all-columns :include-drop include-drop) + for sql in (get-pgsql-create-tables all-columns + :identifier-case identifier-case + :include-drop include-drop) do (pgloader.pgsql:execute pg-dbname sql) finally (return nb-tables))) @@ -430,6 +450,7 @@ order by ordinal_position" dbname table-name))) (include-drop nil) (create-indexes t) (reset-sequences t) + (identifier-case :downcase) ; or :quote (truncate t) only-tables) "Export MySQL data and Import it into PostgreSQL" @@ -443,6 +464,7 @@ order by ordinal_position" dbname table-name))) (with-silent-timing *state* dbname (format nil "~:[~;DROP then ~]CREATE TABLES" include-drop) (pgsql-create-tables dbname all-columns + :identifier-case identifier-case :pg-dbname pg-dbname :include-drop include-drop))) diff --git a/parser.lisp b/parser.lisp index b4ee4b1..91871b8 100644 --- a/parser.lisp +++ b/parser.lisp @@ -154,7 +154,10 @@ Here's a quick description of the format we're parsing here: (def-keyword-rule "name") (def-keyword-rule "tables") (def-keyword-rule "indexes") - (def-keyword-rule "sequences")) + (def-keyword-rule "sequences") + (def-keyword-rule "downcase") + (def-keyword-rule "quote") + (def-keyword-rule "identifiers")) (defrule kw-auto-increment (and "auto_increment" (* (or #\Tab #\Space))) (:constant :auto-increment)) @@ -370,12 +373,19 @@ Here's a quick description of the format we're parsing here: (defrule option-reset-sequences (and kw-reset kw-sequences) (:constant (cons :reset-sequences t))) +(defrule option-identifiers-case (and (or kw-downcase kw-quote) kw-identifiers) + (:lambda (id-case) + (destructuring-bind (action id) id-case + (declare (ignore id)) + (cons :identifier-case action)))) + (defrule mysql-option (or option-workers option-truncate option-drop-tables option-create-tables option-create-indexes - option-reset-sequences)) + option-reset-sequences + option-identifiers-case)) (defrule another-mysql-option (and #\, ignore-whitespace mysql-option) (:lambda (source) @@ -891,10 +901,11 @@ LOAD FROM http:///tapoueh.org/db.t LOAD DATABASE FROM mysql://localhost:3306/dbname INTO postgresql://localhost/db WITH drop tables, - truncate, - create tables, - create indexes, - reset sequences + truncate, + create tables, + create indexes, + reset sequences, + downcase identifiers SET guc_1 = 'value', guc_2 = 'other value' CAST column col1 to timestamptz drop default using zero-dates-to-null, type varchar to text,