/ src / modeling / db-metadata.lisp
db-metadata.lisp
  1  (in-package :ami.modeling)
  2  
  3  (defclass table-metadata ()
  4    ((name :initarg :name :accessor table-metadata-name)
  5     (fields :initform (make-hash-table) :accessor table-metadata-fields)))
  6  
  7  (defmethod print-object ((tm table-metadata) out)
  8    (print-unreadable-object (tm out :type t)
  9      (format out "~s" (table-metadata-name tm))))
 10  
 11  (defclass column-metadata ()
 12    ((name :initarg :name :accessor col-metadata-name)
 13     (data-type :initarg :data-type :accessor col-metadata-data-type)
 14     (is-nullable :initarg :is-nullable :accessor col-metadata-is-nullable)
 15     (is-unique :initarg :is-unique :accessor col-metadata-is-unique)
 16     (is-primary-key :initarg :is-primary-key :accessor col-metadata-is-primary-key)
 17     (is-foreign-key :initarg :is-foreign-key :accessor col-metadata-is-foreign-key)))
 18  
 19  (defmethod print-object ((cm column-metadata) out)
 20    (print-unreadable-object (cm out :type t)
 21      (format out "~s ~a ~:[~; PRIMARY KEY~]~:[~; NOT NULL~]~:[~; UNIQUE~]"
 22              (col-metadata-name cm)
 23              (col-metadata-data-type cm)
 24              (col-metadata-is-primary-key cm)
 25              (not (col-metadata-is-nullable cm))
 26              (col-metadata-is-unique cm))))
 27  
 28  (defun table-exists (table-name)
 29    (let ((tbl (car (run-query 
 30                     "SELECT table_name FROM information_schema.tables WHERE table_name = $1" 
 31                     `(,table-name)))))
 32      tbl))
 33  
 34  (define-condition migration-error (error) 
 35    ((error-desc :initarg :error-desc :accessor migration-error-desc)))
 36  
 37  (define-condition change-in-uniqueness-not-supported (migration-error) ())
 38  (define-condition change-in-pkey-not-supported (migration-error) ())
 39  (define-condition change-in-fkey-not-supported (migration-error) ())
 40  (define-condition change-in-data-type-not-supported (migration-error) ())
 41  
 42  (defmethod equal-field-p ((cm column-metadata) (fd field-definition))
 43    "Predicate checking if a model field definition matches the column metadata as gotten from the DB"
 44    (and (eql (col-metadata-is-nullable cm) (fdef-nullable fd))
 45         (eql (col-metadata-is-unique cm) (fdef-unique fd))
 46         (eql (col-metadata-is-primary-key cm) (fdef-primary-key fd))
 47         (eql (col-metadata-is-foreign-key cm) (fdef-foreign-key fd))
 48         (eql (col-metadata-data-type cm) (db-type fd))))
 49  
 50  (defmethod generate-field-migration ((cm column-metadata) (fd field-definition))
 51    "Generate the needed SQL statements to make a postgres column match the given field-defition.
 52  
 53  The column metadata has to be given already and can be created by using the `get-table-information' function."
 54    (let ((alters nil))
 55      (when (not (eql (col-metadata-is-nullable cm) (fdef-nullable fd)))
 56        (push (format nil "ALTER COLUMN \"~a\" ~:[DROP~;SET~] NOT NULL" (col-metadata-name cm) (not (fdef-nullable fd))) alters))
 57      (when (not (eql (col-metadata-is-unique cm) (fdef-unique fd)))
 58        (error 'change-in-uniqueness-not-supported))
 59      (when (not (eql (col-metadata-is-primary-key cm) (fdef-primary-key fd)))
 60        (error 'change-in-pkey-not-supported))
 61      (when (not (eql (col-metadata-is-foreign-key cm) (fdef-foreign-key fd)))
 62        (error 'change-in-fkey-not-supported))
 63      (when (not (eql (col-metadata-data-type cm) (db-type fd)))
 64        (error 'change-in-data-type-not-supported))
 65      alters))
 66  
 67  (defun get-table-information (table-name package)
 68    "Returns a `table-metadata' object with all the needed metadata of the given `table-name'.
 69  
 70  The model package has to be given so the model columns can be compared properly.
 71  
 72  TODO(Marce): Maybe instead take a mdef so this implementation detail is not exposed?"
 73    (let ((tbl-info (run-query "
 74  WITH table_constraints AS (
 75          SELECT * FROM information_schema.table_constraints
 76          JOIN information_schema.constraint_column_usage AS cu USING (constraint_name)
 77          WHERE cu.table_name = $1
 78  )
 79  SELECT column_name, data_type, is_nullable = 'YES' AS is_nullable,
 80  EXISTS (SELECT * FROM table_constraints AS tc WHERE tc.column_name = c.column_name AND constraint_type = 'UNIQUE') AS is_unique,
 81  EXISTS (SELECT * FROM table_constraints AS tc WHERE tc.column_name = c.column_name AND constraint_type = 'PRIMARY KEY') AS is_primary_key,
 82  EXISTS (SELECT * FROM table_constraints AS tc WHERE tc.column_name = c.column_name AND constraint_type = 'FOREIGN KEY') AS is_foreign_key
 83  FROM information_schema.columns AS c WHERE table_name = $1"
 84                               `(,table-name)))
 85          (tbl-info-inst (make-instance 'table-metadata :name table-name)))
 86      (dolist (col-info tbl-info)
 87        (let ((col-metadata (make-instance 'column-metadata
 88                                           :name (getf col-info :|column_name|)
 89                                           :data-type (alexandria:format-symbol 
 90                                                       (find-package "KEYWORD")
 91                                                       "~@:(~a~)"
 92                                                       (getf col-info :|data_type|))
 93                                           :is-nullable (getf col-info :|is_nullable|)
 94                                           :is-unique (getf col-info :|is_unique|)
 95                                           :is-primary-key (getf col-info :|is_primary_key|)
 96                                           :is-foreign-key (getf col-info :|is_foregin_key|))))
 97          (setf (gethash (alexandria:format-symbol package "~@:(~a~)" (col-metadata-name col-metadata)) 
 98                         (table-metadata-fields tbl-info-inst))
 99                col-metadata)))
100      tbl-info-inst))
101  
102