diff --git a/cl-postgres.asd b/cl-postgres.asd index de0e29c..c19fdb9 100644 --- a/cl-postgres.asd +++ b/cl-postgres.asd @@ -27,6 +27,7 @@ :components ((:file "package") (:file "features") (:file "config") + (:file "oid" :depends-on ("package" "config")) (:file "errors" :depends-on ("package")) (:file "data-types" :depends-on ("package" "config")) (:file "sql-string" :depends-on ("package" "config" "data-types")) @@ -36,7 +37,6 @@ (:file "communicate" :depends-on (#.*string-file* "sql-string" "config")) (:file "messages" :depends-on ("communicate" "config")) - (:file "oid" :depends-on ("package" "config")) (:file "ieee-floats" :depends-on ("config")) (:file "interpret" :depends-on ("oid" "communicate" "ieee-floats" "config")) diff --git a/cl-postgres/tests/tests-saslprep.lisp b/cl-postgres/tests/tests-saslprep.lisp index f7ae3a2..6de885a 100644 --- a/cl-postgres/tests/tests-saslprep.lisp +++ b/cl-postgres/tests/tests-saslprep.lisp @@ -24,12 +24,12 @@ (is (not (cl-postgres::code-point-printable-ascii-p 163)))) (test char-mapped-to-nothing-p - (is (not (cl-postgres::char-mapped-to-nothing-p #\LATIN_CAPITAL_LETTER_O_WITH_DIAERESIS))) - (is (cl-postgres::char-mapped-to-nothing-p #\ZERO_WIDTH_SPACE))) + (is (not (cl-postgres::char-mapped-to-nothing-p (code-char 214)))) + (is (cl-postgres::char-mapped-to-nothing-p (code-char 8203)))) (test char-mapped-to-space-p - (is (not (cl-postgres::char-mapped-to-space-p #\LATIN_CAPITAL_LETTER_O_WITH_DIAERESIS))) - (is (cl-postgres::char-mapped-to-space-p #\ZERO_WIDTH_SPACE)) + (is (not (cl-postgres::char-mapped-to-space-p (code-char 214)))) + (is (cl-postgres::char-mapped-to-space-p (code-char 8203))) (is (cl-postgres::char-mapped-to-space-p (code-char 5760)))) (test string-mapped-to-nothing-p @@ -56,8 +56,8 @@ (test saslprep-normalize (is (equal (cl-postgres::saslprep-normalize - (coerce (vector #\a #\LATIN_CAPITAL_LETTER_O_WITH_DIAERESIS - (code-char 8193) #\c #\ZERO_WIDTH_SPACE + (coerce (vector #\a (code-char 214) + (code-char 8193) #\c (code-char 8203) (code-char 65025) (code-char 1214) #\d) 'string)) diff --git a/doc/postmodern.html b/doc/postmodern.html index 98ea6a6..1eeae5b 100644 --- a/doc/postmodern.html +++ b/doc/postmodern.html @@ -1,7 +1,7 @@ - + Postmodern Reference Manual @@ -246,7 +246,7 @@

Postmodern Reference Manual

Table of Contents

-
-

Overview

-
+
+

Overview

+

This is the reference manual for the component named postmodern, which is part of a library of the same name. @@ -1000,9 +1000,9 @@

macro query (query &rest args/format)

Some Examples:

-
-

Default

-
+
+

Default

+

The default is :lists

@@ -1013,9 +1013,9 @@

Default

-
-

Single

-
+
+

Single

+

Returns a single field. Will throw an error if the queries returns more than one field or more than one row

@@ -1026,9 +1026,9 @@

Single

-
-

List

-
+
+

List

+

Returns a list containing the selected fields. Will throw an error if the query returns more than one row

@@ -1039,9 +1039,9 @@

List

-
-

Lists

-
+
+

Lists

+

This is the default

@@ -1052,9 +1052,9 @@

Lists

-
-

Alist

-
+
+

Alist

+

Returns an alist containing the field name as a keyword and the selected fields. Will throw an error if the query returns more than one row.

@@ -1065,9 +1065,9 @@

Alist

-
-

Str-alist

-
+
+

Str-alist

+

Returns an alist containing the field name as a lower case string and the selected fields. Will throw an error if the query returns more than one row.

@@ -1078,9 +1078,9 @@

Str-alist

-
-

Alists

-
+
+

Alists

+

Returns a list of alists containing the field name as a keyword and the selected fields.

@@ -1092,9 +1092,9 @@

Alists

-
-

Str-alists

-
+
+

Str-alists

+

Returns a list of alists containing the field name as a lower case string and the selected fields.

@@ -1106,9 +1106,9 @@

Str-alists

-
-

Plist

-
+
+

Plist

+

Returns a plist containing the field name as a keyword and the selected fields. Will throw an error if the query returns more than one row.

@@ -1119,9 +1119,9 @@

Plist

-
-

Plists

-
+
+

Plists

+

Returns a list of plists containing the field name as a keyword and the selected fields.

@@ -1132,9 +1132,9 @@

Plists

-
-

Vectors

-
+
+

Vectors

+

Returns a vector of vectors where each internal vector is a returned row from the query. The field names are not included. NOTE: It will return an empty vector instead of NIL if there is no result.

@@ -1152,9 +1152,9 @@

Vectors

-
-

Array-hash

-
+
+

Array-hash

+

Returns a vector of hashtables where each hash table is a returned row from the query with field name as the key expressed as a lower case string.

@@ -1172,9 +1172,9 @@

Array-hash

-
-

Dao

-
+
+

Dao

+

Returns a list of daos of the type specified

@@ -1188,9 +1188,9 @@

Dao

-
-

Column

-
+
+

Column

+

Returns a list of field values of a single field. Will throw an error if more than one field is selected

@@ -1204,9 +1204,9 @@

Column

-
-

Json-strs

-
+
+

Json-strs

+

Return a list of strings where the row returned is a json object expressed as a string

@@ -1244,9 +1244,9 @@

Json-strs

-
-

Json-str

-
+
+

Json-str

+

Return a single string where the row returned is a json object expressed as a string

@@ -1261,9 +1261,9 @@

Json-str

-
-

Json-array-str

-
+
+

Json-array-str

+

Return a string containing a json array, each element in the array is a selected row expressed as a json object. NOTE: If there is no result, this will return a string with an empty json array.

@@ -1280,9 +1280,9 @@

Json-array-str

-
-

Second value returned

-
+
+

Second value returned

+

If the database returns information about the amount rows that were affected, such as with updating or deleting queries, this is returned as a second value. @@ -2332,9 +2332,9 @@

function add-comment (type name comment &optio

-
-

find-comments (type identifier)

-
+
+

find-comments (type identifier)

+

Returns the comments attached to a particular database object. The allowed types are :database :schema :table :columns (all the columns in a table) @@ -3507,9 +3507,9 @@

function rename-table (old-name new-name)

-
-

function rename-column (table-name old-name new-name)

-
+
+

function rename-column (table-name old-name new-name)

+

→ boolean

@@ -3684,6 +3684,16 @@

function execute-file (filename &optional (pr statement will have been commited.

+

+Execute-file allows the sql file to include other sql files, with the +meta-commands \i or \include which look for a file location relative to your +default pathname (current working directory) or \ir or \include_relative which +look for a file location relative to the initial sql file. If the file is not +found in the expected location, execute-file will look to see if the requested +file is in the other possible location. If that does not work, it will trigger +an error with a restart which allows you to provide a new name for the file. +

+

If you want the standard transction treatment such that all statements succeed or no statement succeeds, then ensure that the file starts with a "begin @@ -3696,6 +3706,12 @@

function execute-file (filename &optional (pr print the count of the query and the query to the REPL.

+

+The default setting is to remove sql comments from the file before executing +the sql code. If that causes problems, the remove-comments parameter can be +set to nil. +

+

IMPORTANT NOTE: This utility function assumes that the file containing the sql queries can be trusted and bypasses the normal postmodern parameterization of @@ -3906,7 +3922,10 @@

function parse-queries (file-content)

function read-queries (filename)

-Read SQL queries in a given file and split them, returns a list. +Read SQL queries in a given file and split them, returns a list. Track included +files so there is no accidental infinite loop. The default setting is to remove +sql comments from the file before executing the sql code. If that causes problems, +the remove-comments parameter can be set to nil.

diff --git a/doc/postmodern.org b/doc/postmodern.org index 5b12b43..7f8207f 100644 --- a/doc/postmodern.org +++ b/doc/postmodern.org @@ -2180,6 +2180,14 @@ the file will be run independently, but if one statement fails, subsequent query statements will not be run, but any statement prior to the failing statement will have been commited. +Execute-file allows the sql file to include other sql files, with the +meta-commands \i or \include which look for a file location relative to your +default pathname (current working directory) or \ir or \include_relative which +look for a file location relative to the initial sql file. If the file is not +found in the expected location, execute-file will look to see if the requested +file is in the other possible location. If that does not work, it will trigger +an error with a restart which allows you to provide a new name for the file. + If you want the standard transction treatment such that all statements succeed or no statement succeeds, then ensure that the file starts with a "begin transaction" statement and finishes with an "end transaction" statement. See @@ -2188,6 +2196,10 @@ the test file test-execute-file-broken-transaction.sql as an example. For debugging purposes, if the optional print parameter is set to t, format will print the count of the query and the query to the REPL. +The default setting is to remove sql comments from the file before executing +the sql code. If that causes problems, the remove-comments parameter can be +set to nil. + IMPORTANT NOTE: This utility function assumes that the file containing the sql queries can be trusted and bypasses the normal postmodern parameterization of queries. @@ -2337,7 +2349,10 @@ Read SQL queries in given string and split them, returns a list. :CUSTOM_ID: function-read-queries :END: -Read SQL queries in a given file and split them, returns a list. +Read SQL queries in a given file and split them, returns a list. Track included +files so there is no accidental infinite loop. The default setting is to remove +sql comments from the file before executing the sql code. If that causes problems, +the remove-comments parameter can be set to nil. ** function sql-escape-string (string) :PROPERTIES: :CUSTOM_ID: function-sql-escape-string diff --git a/postmodern.asd b/postmodern.asd index a19dfb1..ebac42f 100644 --- a/postmodern.asd +++ b/postmodern.asd @@ -26,7 +26,6 @@ "s-sql" "global-vars" "split-sequence" - "cl-unicode" "uiop" (:feature :postmodern-use-mop "closer-mop") (:feature :postmodern-thread-safe "bordeaux-threads")) diff --git a/postmodern/execute-file.lisp b/postmodern/execute-file.lisp index efc25a9..91544a4 100644 --- a/postmodern/execute-file.lisp +++ b/postmodern/execute-file.lisp @@ -145,13 +145,13 @@ should return (:tag (push-new-tag state)) - (:eot ; check the tag stack + (:eot ; check the tag stack (cond ((= 1 (length (parser-tags state))) ;; it's an opening tag, collect the text now (format-current-tag state) (reset-state state :tagp t)) - (t ; are we closing the current tag? + (t ; are we closing the current tag? (if (maybe-close-tags state) (reset-state state :tagp t) @@ -198,43 +198,290 @@ should return (unless (eq :eat (parser-state state)) (error e))))) -(defun read-lines (filename &optional (q (make-string-output-stream))) - "Read lines from given filename and return them in a stream. Recursively - apply \i include instructions." - (with-open-file (s filename :direction :input) - (loop - for line = (read-line s nil) - while line - do (if (or (and (> (length line) 3) - (string= "\\i " (subseq line 0 3))) - (and (> (length line) 4) - (string= "\\ir " (subseq line 0 4)))) - (let ((include-filename - (merge-pathnames (subseq line 3) - (directory-namestring filename)))) - (read-lines include-filename q)) - (format q "~a~%" line)) - finally (return q)))) +(defstruct comment-parser + buffer + (stream (make-string-output-stream)) + (state '(:base))) + +(defparameter single-line-comment-scanner + (cl-ppcre:create-scanner "--.*")) + + +;; +;; If a single line comment in within a multiline comment, Postgresql will +;; ignore the single line comment. +;; comments begin with /* and end with */ +;; possible states: +;; :base +;; :mlc (inside a multiline comment) +;; :mb? (maybe beginning a new multiline comment) +;; :me? (maybe ending a multiline comment) +;; :sb? (maybe beginning a single line comment) +;; :slc (inside a single line comment) +;; :sq (inside an sql quote) + +(defun parse-comments (str &optional (state (make-comment-parser))) + (loop for char across str + do +; (format t "~a ~a~%" char (char-code char)) + (case char + (#\' (case (first (comment-parser-state state)) + (:base (push :sq (comment-parser-state state)) + (write-char #\' (comment-parser-stream state))) + (:mlc ) + (:slc ) + (:sb? (pop (comment-parser-state state)) + (write-char #\' (comment-parser-stream state))) + (:mb? ; faked multi-line beginning, return to earlier state + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) + :base) + (write-char #\/ (comment-parser-stream state)) + (write-char #\/ (comment-parser-stream state)))) + (:me? (pop (comment-parser-state state))) + (:sq (pop (comment-parser-state state)) + (write-char #\' (comment-parser-stream state))))) + (#\- (case (first (comment-parser-state state)) + (:base (push :sb? (comment-parser-state state))) + (:mlc ) + (:slc ) + (:sq (write-char char (comment-parser-stream state))) + (:sb? (setf (first (comment-parser-state state)) :slc)) + (:mb? ; faked multi-line beginning, return to earlier state + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) + :base) + (write-char #\/ (comment-parser-stream state)) + (write-char #\/ (comment-parser-stream state)))) + (:me? (pop (comment-parser-state state))))) + (#\newline (case (first (comment-parser-state state)) + (:base + (write-char char (comment-parser-stream state))) + (:mlc ) + (:sq (write-char char (comment-parser-stream state))) + (:slc (pop (comment-parser-state state)) + (write-char char (comment-parser-stream state))) + (:sb? (pop (comment-parser-state state)) + (write-char char (comment-parser-stream state))) + (:mb? ; faked multi-line beginning, return to earlier state + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) + :base) + (write-char #\/ (comment-parser-stream state)) + (write-char #\/ (comment-parser-stream state)))) + (:me? (pop (comment-parser-state state)) + (write-char char (comment-parser-stream state))))) + (#\/ (case (first (comment-parser-state state)) + (:base (push :mb? (comment-parser-state state))) + (:sb? (pop (comment-parser-state state))) + (:slc ) + (:sq (write-char char (comment-parser-stream state))) + (:mb? ; faked beginning, return to earlier state (:base or :mlc) + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) + :base) + (write-char #\/ (comment-parser-stream state)) + (write-char #\/ (comment-parser-stream state)))) + (:mlc (push :mb? (comment-parser-state state)) + ) + (:me? ; actual ending of a multi-line comment + ; need to pop both the :me? amd tej :mlc + (pop (comment-parser-state state)) + (pop (comment-parser-state state))))) + (#\* (case (first (comment-parser-state state)) + (:base (write-char char (comment-parser-stream state))) + (:mb? (setf (first (comment-parser-state state)) :mlc)) + (:mlc ; maybe starting the end of a nested multi-line comment + (push :me? (comment-parser-state state))) + (:sq (write-char char (comment-parser-stream state))) + (:me? ; fake ending of a multi-line comment + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) :mlc) + (push :me? (comment-parser-state state)))))) + (otherwise (case (first (comment-parser-state state)) + (:base + (write-char char (comment-parser-stream state))) + (:mb? + (pop (comment-parser-state state)) + (when (eq (first (comment-parser-state state)) + :base) + (write-char #\/ (comment-parser-stream state)) + (write-char char (comment-parser-stream state)))) + (:me? ; fake ending of a multi-line comment + (pop (comment-parser-state state))) + (:sb? ; fake single line comment + (pop (comment-parser-state state)) + (write-char #\- (comment-parser-stream state)) + (write-char char (comment-parser-stream state))) + (:sq (write-char char (comment-parser-stream state)))))) + :finally (return + (get-output-stream-string (comment-parser-stream state))))) + + +(defun remove-sql-comments (str) + "Take a string input, replace all the multi-line comments and single line comments, +returning the resulting string." + (parse-comments str)) + +(define-condition missing-i-file (error) + ((%filename :reader filename :initarg :filename) + (%base-filename :reader base-filename :initarg :base-filename) + (%meta-cmd :reader meta-cmd :initarg :meta-cmd)) + (:report (lambda (condition stream) + (format stream "We tried but failed to find file ~a at the location +specified by the ~a meta command. + +Note that meta-commands \\i or \\include in the sql file look for a file location +relative to your default pathname (current working directory), in this case: +~a. + +Meta-commands \\ir or \\include_relative look for a file location relative to the +initial sql file, in this case: +~a. + + As a fallback, we also looked for it where the ~a meta command would have specified. +Can you double check that the file actually exists where it is supposed to be?" + (filename condition) + (if (eq (meta-cmd condition) 'i) + "\\i or \\include" + "\\ir or \\include_relative") + (uiop::get-pathname-defaults) + (directory-namestring (base-filename condition)) + (if (eq (meta-cmd condition) 'i) + "\\ir or \\include_relative" + "\\i or \\include"))))) + +(defun line-has-includes (line) + "Returns 'i if the first characters in a line are the postgresql include file +commands: \i or \include. Returns 'ir if the first characters in a line are postgresql +include commands \ir or \include_relative. Returns nil otherwise." + (let ((new-line (string-trim '(#\space #\tab) line))) + (cond ((and (> (length new-line) 3) + (string= "\\i " (subseq new-line 0 3))) + (values 'i (string-trim '(#\space #\tab) (subseq new-line 3)))) + ((and (> (length new-line) 9) + (string= "\\include " (subseq new-line 0 9))) + (values 'i (string-trim '(#\space #\tab) (subseq new-line 9)))) + ((and (> (length new-line) 4) + (string= "\\ir " (subseq new-line 0 4))) + (values 'ir (string-trim '(#\space #\tab) (subseq new-line 4)))) + ((and (> (length new-line) 18) + (string= "\\include_relative " (subseq new-line 0 18))) + (values 'ir (string-trim '(#\space #\tab) (subseq new-line 18)))) + (t nil)))) + +(defun find-included-filename (meta-cmd new-filename base-filename) + "Create full pathname if included using a \ir metacommand or \include_relative." + (when new-filename + (restart-case + (let ((relative-pathname (merge-pathnames new-filename + (directory-namestring base-filename))) + (working-pathname (merge-pathnames new-filename + (uiop::get-pathname-defaults)))) + (cond ((and (eq meta-cmd 'ir) + (uiop:file-exists-p relative-pathname)) + relative-pathname) + ((and (eq meta-cmd 'i) + (uiop:file-exists-p working-pathname)) + working-pathname) + ((and (eq meta-cmd 'ir) + (uiop:file-exists-p working-pathname)) + (warn + (format nil "Using fallback to find file based on working directory position")) + working-pathname) + ((and (eq meta-cmd 'i) + (uiop:file-exists-p relative-pathname)) + (warn + (format nil "Using fallback to find file based on relative directory position")) + relative-pathname) + (t (error 'missing-i-file :meta-cmd meta-cmd + :filename new-filename :base-filename base-filename)))) + (use-other-values (new-full-filename) + :report "Use a different filename location to be included." + :interactive (lambda () + (flet ((get-value () + (format t "~&Enter new value for sql file to be included: ") + (read-line))) + (list (string (get-value))))) + (find-included-filename meta-cmd new-full-filename base-filename))))) + + +(defun read-sql-file (filename &key (included-files nil) + (output-stream (make-string-output-stream)) + (remove-comments t)) + "Read a given file and (default) remove the comments. Read lines from the redacted result +and return them in a stream. Recursively apply \i include instructions." + (if (uiop:file-exists-p filename) + (with-input-from-string + (s (if remove-comments + (remove-sql-comments (alexandria:read-file-into-string filename)) + (alexandria:read-file-into-string filename))) + (loop + :for line := (read-line s nil) + :while line + :do + (multiple-value-bind (meta-cmd new-filename) + (line-has-includes line) + (if (or (eq meta-cmd 'i) + (eq meta-cmd 'ir)) + (let ((include-filename + (find-included-filename meta-cmd new-filename filename))) + (when new-filename + (if (not (member include-filename included-files)) + (progn + (push include-filename included-files) + (read-sql-file include-filename :included-files included-files + :output-stream output-stream + :remove-comments remove-comments)) + (progn + (warn + (format nil + "~a: Duplicate attempts to include sql files ~a skipped~%" + *package* filename)) + "")))) + (format output-stream "~a~%" line))) + :finally (return output-stream))) + (warn (format nil "~a: file ~a doesn't seem to exist. If this was supposed to be an included file, please note that \\i looks for a file location relative to your default pathname, in this case ~a. \\ir looks for a file location relative to the initial included file location, in the case ~a~%" + *package* filename + (uiop::get-pathname-defaults) + (if filename + (directory-namestring filename) + nil)) + ""))) + +(defun read-queries (filename &key (remove-comments t)) + "Read SQL queries in given file and split them, returns a list. Track included +files so there is no accidental infinite loop. The default setting is to remove +sql comments from the file before executing the sql code. If that causes problems, +the remove-comments parameter can be set to nil." + (parse-queries + (get-output-stream-string + (read-sql-file filename :remove-comments remove-comments)))) (defun parse-queries (file-content) "Read SQL queries in given string and split them, returns a list" (with-input-from-string (s (concatenate 'string file-content ";")) (let ((whitespace '(#\Space #\Tab #\Newline #\Linefeed #\Page #\Return))) - (flet ((emptyp (query) (every (alexandria:rcurry #'member whitespace) query))) + (flet ((emptyp (query) + (every (alexandria:rcurry #'member whitespace) query))) (loop :for query := (parse-query s) :while (and query (not (emptyp query))) :collect query))))) -(defun read-queries (filename) - "Read SQL queries in given file and split them, returns a list" - (parse-queries (get-output-stream-string (read-lines filename)))) - -(defun execute-file (pathname &optional (print nil)) +(defun execute-file (pathname &optional (print nil) (remove-comments t)) "This function will execute sql queries stored in a file. Each sql statement in the file will be run independently, but if one statement fails, subsequent query statements will not be run, but any statement prior to the failing statement will have been commited. +Execute-file allows the sql file to include other sql files, with the +meta-commands \i or \include which look for a file location relative to your +default pathname (current working directory) or \ir or \include_relative which +look for a file location relative to the initial sql file. If the file is not +found in the expected location, execute-file will look to see if the requested +file is in the other possible location. If that does not work, it will trigger +an error with a restart which allows you to provide a new name for the file. + If you want the standard transction treatment such that all statements succeed or no statement succeeds, then ensure that the file starts with a begin transaction statement and finishes with an end transaction statement. See the @@ -243,10 +490,14 @@ test file test-execute-file-broken-transaction.sql as an example. For debugging purposes, if the optional print parameter is set to t, format will print the count of the query and the query to the REPL. +The default setting is to remove sql comments from the file before executing +the sql code. If that causes problems, the remove-comments parameter can be +set to nil. + IMPORTANT NOTE: This utility function assumes that the file containing the sql queries can be trusted and bypasses the normal postmodern parameterization of queries." - (let ((queries (read-queries pathname)) + (let ((queries (read-queries pathname :remove-comments remove-comments)) (cnt 0)) (dolist (query queries) (when print diff --git a/postmodern/tests/sub1/tef-4.sql b/postmodern/tests/sub1/tef-4.sql new file mode 100644 index 0000000..b268fad --- /dev/null +++ b/postmodern/tests/sub1/tef-4.sql @@ -0,0 +1,5 @@ +/* test' comment tef-4-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (10, 'Catharina', 32, 'Vienna', 'sub1/tef-4.sql','2011-04-13'); diff --git a/postmodern/tests/sub1/tef-5.sql b/postmodern/tests/sub1/tef-5.sql new file mode 100644 index 0000000..28959f0 --- /dev/null +++ b/postmodern/tests/sub1/tef-5.sql @@ -0,0 +1,5 @@ +/* test' comment tef-5-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (11, 'Lisa', 32, 'Frankfurt', 'sub1/tef-5.sql','2011-04-13'); diff --git a/postmodern/tests/sub1/tef-7.sql b/postmodern/tests/sub1/tef-7.sql new file mode 100644 index 0000000..6a9e3dc --- /dev/null +++ b/postmodern/tests/sub1/tef-7.sql @@ -0,0 +1,5 @@ +/* test' comment tef-7-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (155, 'Victor', 32, 'Warsaw', 'tef-7.sql','2011-04-13'); diff --git a/postmodern/tests/tef-1.sql b/postmodern/tests/tef-1.sql new file mode 100644 index 0000000..05167fe --- /dev/null +++ b/postmodern/tests/tef-1.sql @@ -0,0 +1,11 @@ +/* test' comment tef-1-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (7, 'Robert', 32, 'Paris', 'tef-1.sql','2011-04-13'); + +/* +\i tef-2.sql +*/ + +-- \i tef-1.sql diff --git a/postmodern/tests/tef-2.sql b/postmodern/tests/tef-2.sql new file mode 100644 index 0000000..4cc63ca --- /dev/null +++ b/postmodern/tests/tef-2.sql @@ -0,0 +1,5 @@ +/* test' comment tef-2-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (8, 'Juan', 32, 'Madrid', 'tef-2.sql','2011-04-13'); diff --git a/postmodern/tests/tef-3.sql b/postmodern/tests/tef-3.sql new file mode 100644 index 0000000..65ef834 --- /dev/null +++ b/postmodern/tests/tef-3.sql @@ -0,0 +1,5 @@ +/* test' comment tef-3-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (9, 'Julian', 32, 'Athens', 'tef-3.sql','2011-04-13'); diff --git a/postmodern/tests/tef-6.sql b/postmodern/tests/tef-6.sql new file mode 100644 index 0000000..858a298 --- /dev/null +++ b/postmodern/tests/tef-6.sql @@ -0,0 +1,5 @@ +/* test' comment tef-6-1 +with multiple lines + --*/ + + insert into company_employees (id,name,age,address,include_file,join_date) values (13, 'Stan', 32, 'Warsaw', 'tef-6.sql','2011-04-13'); diff --git a/postmodern/tests/test-execute-file.lisp b/postmodern/tests/test-execute-file.lisp index 4647b62..2841b71 100644 --- a/postmodern/tests/test-execute-file.lisp +++ b/postmodern/tests/test-execute-file.lisp @@ -7,25 +7,29 @@ (in-suite :postmodern-execute-file) -(defparameter good-file (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file.sql")) -(defparameter bad-file (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file-broken.sql")) -(defparameter bad-file-with-transaction (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file-broken-transaction.sql")) +(defparameter *good-file* (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file.sql")) + +(defparameter *first-include-good-file* (asdf:system-relative-pathname :postmodern "postmodern/tests/test-first-include-execute-file.sql")) + +(defparameter *fail-include-file* (asdf:system-relative-pathname :postmodern "postmodern/tests/test-fail-include-execute-file.sql")) + +(defparameter *bad-file* (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file-broken.sql")) +(defparameter *bad-file-with-transaction* (asdf:system-relative-pathname :postmodern "postmodern/tests/test-execute-file-broken-transaction.sql")) (test simple-execute-file (with-test-connection (when (table-exists-p 'company-employees) (query (:drop-table :if-exists 'company-employees :cascade))) - (pomo:execute-file good-file) + (pomo:execute-file *good-file*) (is (table-exists-p 'company-employees)) - (is (equal "paul" (query (:select 'name :from 'company-employees :where (:= 'id 1)) :single))) - (is (equal 6 (query (:select (:count 'id) :from 'company-employees) :single))) - (query (:drop-table :if-exists 'company-employees :cascade)))) + (is (equal "Paul" (query (:select 'name :from 'company-employees :where (:= 'id 1)) :single))) + (is (equal 11 (query (:select (:count 'id) :from 'company-employees) :single))))) (test broken-execute-file (with-test-connection (when (table-exists-p 'company-employees) (query (:drop-table :if-exists 'company-employees :cascade))) - (signals error (pomo:execute-file bad-file)) + (signals error (pomo:execute-file *bad-file*)) (is (table-exists-p 'company-employees)) (is (equal "paul" (query (:select 'name :from 'company-employees :where (:= 'id 1)) :single))) ;; the bad-file should stop executing on the attempt to insert a record with the same id as the first insertion @@ -36,7 +40,98 @@ (with-test-connection (when (table-exists-p 'company-employees) (query (:drop-table :if-exists 'company-employees :cascade))) - (signals error (pomo:execute-file bad-file-with-transaction))) + (signals error (pomo:execute-file *bad-file-with-transaction*))) (with-test-connection (is (not (table-exists-p 'company-employees))) (query (:drop-table :if-exists 'company-employees :cascade)))) + +(test fail-include-execute-file + (with-test-connection + (when (table-exists-p 'company-employees) + (query (:drop-table :if-exists 'company-employees :cascade))) + (signals error (pomo:execute-file *fail-include-file*)) + (query (:drop-table :if-exists 'company-employees :cascade)))) + +;; Test Parse Comments + +(test basic-multi-line1 + (is (equal (postmodern::parse-comments " something1 /* comment */ something2") + " something1 something2"))) + +(test basic-multi-line2 + (is (equal (postmodern::parse-comments " something1 /* + comment */ something2") + " something1 something2"))) + +(test basic-single-line + (is (equal (postmodern::parse-comments " something1 -- comment */ something2") + " something1 "))) + +(test multi-line-within-single-line + (is (equal (postmodern::parse-comments " something1 -- /* comment */ something2") + " something1 "))) + +(test multi-line-within-multi-line + (is (equal (postmodern::parse-comments " something1 /* outside comment + /* inside comment */ bad-something2 */ something2") + " something1 something2"))) + +(test broken-nested-muli-line-comments + (is (equal (pomo::parse-comments "/* comment /* still the same comment */") + ""))) + +(test single-line-within-multi-line + (is (equal (postmodern::parse-comments " something1 /* comm -- ent */ something2") + " something1 something2"))) + +(test basic-fake-single-line + (is (equal (postmodern::parse-comments " something1 - something2") + " something1 - something2"))) + +(test basic-fake-muli-line + (is (equal (postmodern::parse-comments " something1 / something2") + " something1 / something2"))) + +(test multi-line-within-sql-string1 + (is (equal (postmodern::parse-comments " something ('my wonder /* something */ company ')") + " something ('my wonder /* something */ company ')"))) + +(test multi-line-within-sql-string2 + (is (equal (postmodern::parse-comments "insert into a (d) values ('/*');") + "insert into a (d) values ('/*');"))) + +(test single-line-within-sql-string1 + (is (equal (postmodern::parse-comments " something ('my wonder -- company ')") + " something ('my wonder -- company ')"))) + +(test single-line-within-sql-string2 + (is (equal (postmodern::parse-comments "insert into a (d) values ('-- /*');") + "insert into a (d) values ('-- /*');"))) + +(test asterisk-no-comment + (is (equal (pomo::parse-comments "select * from x") + "select * from x"))) + +(test unicode-escapes + (is (equal (pomo::parse-comments "U&'d\\0061t\\+000061'") + "U&'d\\0061t\\+000061'"))) + +(test dollar-quoted-string-constants1 + (is (equal (pomo::parse-comments "$$Dianne's horse$$") + "$$Dianne's horse$$"))) + +(test dollar-quoted-string-constants2 + (is (equal (pomo::parse-comments "$function$ +BEGIN + RETURN ($1 ~ $q$[\\t\\r\\n\\v\\]$q$); +END; +$function$") + "$function$ +BEGIN + RETURN ($1 ~ $q$[\\t\\r\\n\\v\\]$q$); +END; +$function$"))) + +(test single-quote-sql + (is (equal (pomo::parse-comments "REAL '1.23' -- string style") + "REAL '1.23' "))) diff --git a/postmodern/tests/test-execute-file.sql b/postmodern/tests/test-execute-file.sql index d3c3709..0e969f6 100644 --- a/postmodern/tests/test-execute-file.sql +++ b/postmodern/tests/test-execute-file.sql @@ -1,21 +1,44 @@ +drop table if exists company_employees; + create table company_employees( id bigserial primary key not null, name text not null, age int not null, address char(50), - salary real, + include_file text, join_date date ); --- Test comment 1 -insert into company_employees (id,name,age,address,salary,join_date) values (1, 'paul', 32, 'London', 20100.00,'2001-07-13'); -insert into company_employees (id,name,age,address,salary,join_date) values (2, 'ziad', 32, 'Beirut', 20000.00,'2003-03-13'); -/* test comment 2 +-- ;Test comment 1;; +insert into company_employees (id,name,age,address,include_file,join_date) values (1, 'Paul', 32, 'London', 'test-execute-file','2001-07-13'); +insert into company_employees (id,name,age,address,include_file,join_date) values (2, 'Ziad', 32, 'Beirut', 'test-execute-file','2003-03-13'); +/* test' comment 2 with multiple lines + --*/ +/* +/* + */ -insert into company_employees (id,name,age,address,salary,join_date) values (3, 'john', 32, 'Toronto', 20100.00,'2005-07-13'); -insert into company_employees (id,name,age,address,salary,join_date) values (4, 'yasmin', 32, 'Mumbai', 20000.00,'2007-03-13'); -/* test comment 3 (asterisk in second line of multiline comment) - * with multiple lines +***/ + --\i ./postmodern/tests/tef-11.sql +\i tef-1.sql -- an included file, will need to use fallback to find it +\ir tef-6.sql -- an included file using file location relative to this file + \ir tef-3.sql +\i sub1/tef-4.sql +\ir ./sub1/tef-5.sql + + insert into company_employees (id,name,age,address,include_file,join_date) values (3, 'John', 32, 'Toronto', 'test-execute-file','2005-07-13'); + -- Yet another comments +insert into company_employees (id,name,age,address,include_file,join_date) values (4, 'Yasmin', 32, 'Mumbai', 'test-execute-file','2007-03-13'); +/* ;test comment 3 (asterisk in /second/ line of multiline comment) + * with multiple lines;; */ -insert into company_employees (id,name,age,address,salary,join_date) values (5, 'susan', 32, 'Vancouver', 20100.00,'2009-07-13'); -insert into company_employees (id,name,age,address,salary,join_date) values (6, 'johanna', 32, 'Berlin', 20000.00,'2011-03-13'); + insert into company_employees (id,name,age,address,include_file,join_date) values (5, 'Susan', 32, 'Vancouver', 'test-execute-file','2009-07-13'); +/* ;test comment 4 (asterisk in second line of multiline comment) + *** with multiple lines;; + /* test' comment 4-1 + with multiple lines + /***/ + */ + * did I say something wrong? +--*/ +insert into company_employees (id,name,age,address,include_file,join_date) values (6, 'Johanna', 32, 'Berlin', 'test-execute-file','2011-03-13'); diff --git a/postmodern/tests/test-fail-include-execute-file.sql b/postmodern/tests/test-fail-include-execute-file.sql new file mode 100644 index 0000000..08ee4ce --- /dev/null +++ b/postmodern/tests/test-fail-include-execute-file.sql @@ -0,0 +1,33 @@ +drop table if exists company_employees; + +create table company_employees( + id bigserial primary key not null, + name text not null, + age int not null, + address char(50), + include_file text , + join_date date +); + +insert into company_employees (id,name,age,address,include_file,join_date) values (1, 'Paul', 32, 'London', 'first-include-execute-file','2001-07-13'); + +\i ./postmodern/tests/tef-11.sql + + insert into company_employees (id,name,age,address,include_file,join_date) values (3, 'John', 32, 'Toronto', 'first-include-execute-file','2005-07-13'); + + \ir tef-6.sql + + insert into company_employees (id,name,age,address,include_file,join_date) values (6, 'Johanna', 32, 'Berlin', 'first-include-execute-file','2011-03-13'); + + \ir tef-3.sql + + insert into company_employees (id,name,age,address,include_file,join_date) values (4, 'Yasmin', 32, 'Mumbai', 'first-include-execute-file','2007-03-13'); + + \i ./postmodern/tests/sub1/tef-4.sql + + insert into company_employees (id,name,age,address,include_file,join_date) values (5, 'Susan', 32, 'Vancouver', 'first-include-execute-file','2009-07-13'); + +\ir ./sub1/tef-5.sql + + +insert into company_employees (id,name,age,address,include_file,join_date) values (2, 'Ziad', 32, 'Beirut', 'first-include-execute-file','2003-03-13'); diff --git a/postmodern/util.lisp b/postmodern/util.lisp index 77730f6..91e8ef0 100644 --- a/postmodern/util.lisp +++ b/postmodern/util.lisp @@ -6,7 +6,7 @@ (defun valid-sql-character-p (chr) "Returns t if chr is letter, underscore, digits or dollar sign" - (or (cl-unicode:has-property chr "Letter") + (or (uax-15:unicode-letter-p chr) (digit-char-p chr) (eq chr #\_) (eq chr #\$))) @@ -25,7 +25,7 @@ First test is for a quoted string, which has less restrictions. " (notany #'code-char-0-p str)) str) ((and (stringp str) - (or (cl-unicode:has-property (char str 0) "Letter") + (or (uax-15:unicode-letter-p (char str 0)) (eq (char str 0) #\_)) (every #'valid-sql-character-p str))) (t nil)))