;;; ;;; Tools to handle MS SQL data type casting rules ;;; (in-package :pgloader.source.mssql) (defparameter *mssql-default-cast-rules* `((:source (:type "char") :target (:type "text" :drop-typemod t)) (:source (:type "nchar") :target (:type "text" :drop-typemod t)) (:source (:type "varchar") :target (:type "text" :drop-typemod t)) (:source (:type "nvarchar") :target (:type "text" :drop-typemod t)) (:source (:type "ntext") :target (:type "text" :drop-typemod t)) (:source (:type "xml") :target (:type "xml" :drop-typemod t)) (:source (:type "int" :auto-increment t) :target (:type "bigserial" :drop-default t)) (:source (:type "tinyint") :target (:type "smallint")) (:source (:type "tinyint" :auto-increment t) :target (:type "serial")) (:source (:type "bit") :target (:type "boolean") :using pgloader.transforms::sql-server-bit-to-boolean) (:source (:type "uniqueidentifier") :target (:type "uuid") :using pgloader.transforms::sql-server-uniqueidentifier-to-uuid) (:source (:type "hierarchyid") :target (:type "bytea") :using pgloader.transforms::byte-vector-to-bytea) (:source (:type "geography") :target (:type "bytea") :using pgloader.transforms::byte-vector-to-bytea) (:source (:type "float") :target (:type "float") :using pgloader.transforms::float-to-string) (:source (:type "real") :target (:type "real") :using pgloader.transforms::float-to-string) (:source (:type "double") :target (:type "double precision") :using pgloader.transforms::float-to-string) (:source (:type "numeric") :target (:type "numeric") :using pgloader.transforms::float-to-string) (:source (:type "decimal") :target (:type "numeric") :using pgloader.transforms::float-to-string) (:source (:type "money") :target (:type "numeric") :using pgloader.transforms::float-to-string) (:source (:type "smallmoney") :target (:type "numeric") :using pgloader.transforms::float-to-string) (:source (:type "binary") :target (:type "bytea") :using pgloader.transforms::byte-vector-to-bytea) (:source (:type "image") :target (:type "bytea") :using pgloader.transforms::byte-vector-to-bytea) (:source (:type "varbinary") :target (:type "bytea") :using pgloader.transforms::byte-vector-to-bytea) (:source (:type "smalldatetime") :target (:type "timestamptz")) (:source (:type "datetime") :target (:type "timestamptz")) (:source (:type "datetime2") :target (:type "timestamptz"))) "Data Type Casting to migrate from MSSQL to PostgreSQL") ;;; ;;; Specific implementation of schema migration, see the API in ;;; src/pgsql/schema.lisp ;;; (defstruct (mssql-column (:constructor make-mssql-column (schema table-name name type default nullable identity character-maximum-length numeric-precision numeric-precision-radix numeric-scale datetime-precision character-set-name collation-name))) schema table-name name type default nullable identity character-maximum-length numeric-precision numeric-precision-radix numeric-scale datetime-precision character-set-name collation-name) (defmethod mssql-column-ctype ((col mssql-column)) "Build the ctype definition from the full mssql-column information." (let ((type (mssql-column-type col))) (cond ((member type '("float" "real") :test #'string=) ;; see https://msdn.microsoft.com/en-us/library/ms173773.aspx ;; scale is supposed to be nil, and useless in PostgreSQL, so we ;; just ignore it (format nil "~a(~a)" type (mssql-column-numeric-precision col))) ((member type '("decimal" "numeric" ) :test #'string=) ;; https://msdn.microsoft.com/en-us/library/ms187746.aspx (cond ((null (mssql-column-numeric-precision col)) type) (t (format nil "~a(~a,~a)" type (mssql-column-numeric-precision col) (or (mssql-column-numeric-scale col) 0))))) ((member type '("char" "nchar" "varchar" "nvarchar" "binary") :test #'string=) ;; the user might have a CAST rule with keep typemod, so we need ;; to deal with character-maximum-length for now (if (= -1 (mssql-column-character-maximum-length col)) type (format nil "~a(~a)" type (mssql-column-character-maximum-length col)))) (t type)))) (defmethod cast ((field mssql-column) &key &allow-other-keys) "Return the PostgreSQL type definition from given MS SQL column definition." (with-slots (schema table-name name type default nullable) field (declare (ignore schema)) ; FIXME (let* ((ctype (mssql-column-ctype field)) (extra (when (mssql-column-identity field) :auto-increment)) (pgcol (apply-casting-rules table-name name type ctype default nullable extra))) ;; the MS SQL driver smartly maps data to the proper CL type, but the ;; pgloader API only wants to see text representations to send down the ;; COPY protocol. (unless (column-transform pgcol) (setf (column-transform pgcol) (lambda (val) (if val (format nil "~a" val) :null)))) ;; normalize default values ;; see pgloader.psql:*pgsql-default-values* (let ((default (column-default pgcol))) (setf (column-default pgcol) (cond ((and (null default) (column-nullable pgcol)) :null) ((and (stringp default) (string= "NULL" default)) :null) ;; fix stupid N'' behavior from MS SQL column default ((and (stringp default) (uiop:string-enclosed-p "N'" default "'")) (subseq default 2 (+ (length default) -1))) ((and (stringp default) ;; address CURRENT_TIMESTAMP(6) and other spellings (or (uiop:string-prefix-p "CURRENT_TIMESTAMP" default) (string= "CURRENT TIMESTAMP" default))) :current-timestamp) ((and (stringp default) (or (string= "newid()" default) (string= "newsequentialid()" default) (string= "GENERATE_UUID" default))) :generate-uuid) (t (column-default pgcol))))) pgcol)))