diff --git a/CHANGELOG.md b/CHANGELOG.md index c68f262..b935895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changes -* 2.3.next in progress +* 2.4.next in progress + * Fix [#439](https://github.com/seancorfield/honeysql/issues/439) by rewriting how DDL options are processed; also fixes [#386](https://github.com/seancorfield/honeysql/issues/386) and [#437](https://github.com/seancorfield/honeysql/issues/437); **Whilst this is intended to be purely a bug fix, it has the potential to be a breaking change -- hence the version jump to 2.4!** * Fix [#438](https://github.com/seancorfield/honeysql/issues/438) by supporting options on `TRUNCATE`. * Address [#435](https://github.com/seancorfield/honeysql/issues/435) by showing `CREATE TEMP TABLE` etc. diff --git a/build.clj b/build.clj index 6c65ce6..e9233d8 100644 --- a/build.clj +++ b/build.clj @@ -17,7 +17,7 @@ [org.corfield.build :as bb])) (def lib 'com.github.seancorfield/honeysql) -(defn- the-version [patch] (format "2.3.%s" patch)) +(defn- the-version [patch] (format "2.4.%s" patch)) (def version (the-version (b/git-count-revs nil))) (def snapshot (the-version "999-SNAPSHOT")) diff --git a/doc/postgresql.md b/doc/postgresql.md index d1dbd39..4721f30 100644 --- a/doc/postgresql.md +++ b/doc/postgresql.md @@ -316,14 +316,14 @@ user=> (-> (create-table :cities) ;; default values for columns: user=> (-> (create-table :distributors) (with-columns [[:did :integer [:primary-key] - ;; "serial" is inlined as 'SERIAL': + ;; "serial" is inlined as 'serial': [:default [:nextval "serial"]]] [:name [:varchar 40] [:not nil]]]) (sql/format {:pretty true})) ;; newlines inserted for readability: [" CREATE TABLE distributors -(did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL) +(did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL) "] ;; PostgreSQL CHECK constraint is supported: user=> (-> (create-table :products) @@ -335,7 +335,7 @@ user=> (-> (create-table :products) (sql/format {:pretty true})) [" CREATE TABLE products -(product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price))) +(product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price))) "] ;; conditional creation: user=> (-> (create-table :products :if-not-exists) diff --git a/src/honey/sql.cljc b/src/honey/sql.cljc index 27f798d..f91589f 100644 --- a/src/honey/sql.cljc +++ b/src/honey/sql.cljc @@ -875,6 +875,17 @@ (str " " (str/join ", " (map #(format-simple-clause % "column/index operations") clauses)))))] [(str (sql-kw k) " " (format-entity x))])) +(def ^:private special-ddl-keywords + "If these are found in DDL, they should map to the given + SQL string instead of what sql-kw would do." + {:auto-increment "AUTO_INCREMENT"}) + +(defn- sql-kw-ddl + "Handle SQL keywords in DDL (allowing for special/exceptions)." + [id] + (or (get special-ddl-keywords (sym->kw id)) + (sql-kw id))) + (defn- format-ddl-options "Given a sequence of options for a DDL statement (the part that comes between the entity name being created/dropped and the @@ -888,11 +899,14 @@ (str/join " " (map (fn [e] (if (ident? e) - (sql-kw e) + (sql-kw-ddl e) (format-simple-expr e context))) opt)) + (ident? opt) + (sql-kw-ddl opt) :else - (sql-kw opt)))) + (throw (ex-info "expected symbol or keyword" + {:unexpected opt}))))) (defn- destructure-ddl-item [table context] (let [params @@ -908,7 +922,7 @@ [(butlast (butlast coll)) (last (butlast coll)) ine] [(butlast coll) (last coll) nil])] (into [(str/join " " (map sql-kw prequel)) - (format-entity table) + (when table (format-entity table)) (when ine (sql-kw ine))] (when opts (format-ddl-options opts context))))) @@ -925,7 +939,10 @@ (comment (destructure-ddl-item [:foo [:abc [:continue :wibble] :identity]] "test") (destructure-ddl-item [:foo] "test") - (format-truncate :truncate [:foo])) + (destructure-ddl-item [:id [:int :unsigned :auto-increment]] "test") + (destructure-ddl-item [[[:foreign-key :bar]] :quux [[:wibble :wobble]]] "test") + (format-truncate :truncate [:foo]) + ) (defn- format-create [q k item as] (let [[pre entity ine & more] @@ -968,13 +985,27 @@ [(str/join " " (remove nil? (into [(sql-kw k) if-exists tables] more)))])) (defn- format-single-column [xs] - (reset! *formatted-column* true) - (str/join " " (cons (format-simple-expr (first xs) "column operation") - (map #(binding [*formatted-column* (atom false)] - (cond-> (format-simple-expr % "column operation") - (not @*formatted-column*) - (upper-case))) - (rest xs))))) + (let [[col & options] (if (ident? (first xs)) xs (cons nil xs)) + [pre col ine & options] + (destructure-ddl-item [col options] "column operation")] + (when (seq pre) (throw (ex-info "column syntax error" {:unexpected pre}))) + (when (seq ine) (throw (ex-info "column syntax error" {:unexpected ine}))) + (str/join " " (filter seq (cons col options))))) + +(comment + (destructure-ddl-item [:foo [:abc [:continue :wibble] :identity]] "test") + (destructure-ddl-item [:foo] "test") + (destructure-ddl-item [:id [:int :unsigned :auto-increment]] "test") + (format-single-column [:id :int :unsigned :auto-increment]) + (format-single-column [[:constraint :code_title] [:primary-key :code :title]]) + (destructure-ddl-item [[[:foreign-key :bar]] :quux [[:wibble :wobble]]] "test") + + (format-truncate :truncate [:foo]) + + (destructure-ddl-item [:address [:text]] "test") + (format-single-column [:address :text]) + (format-single-column [:did :uuid [:default [:gen_random_uuid]]]) + ) (defn- format-table-columns [_ xs] [(str "(" @@ -990,6 +1021,12 @@ (let [items (if (and (sequential? spec) (sequential? (first spec))) spec [spec])] [(str/join ", " (for [item items] (format-add-single-item k item)))])) +(comment + (format-add-item :add-column [:address :text]) + (format-add-single-item :add-column [:address :text]) + (format-single-column [:address :text]) + ) + (defn- format-rename-item [k [x y]] [(str (sql-kw k) " " (format-entity x) " TO " (format-entity y))]) @@ -1357,7 +1394,7 @@ ;; bigquery column types: :bigquery/array (fn [_ spec] [(str "ARRAY<" - (str/join " " (map #(format-simple-expr % "column operation") spec)) + (str/join " " (map #(sql-kw %) spec)) ">")]) :bigquery/struct (fn [_ spec] [(str "STRUCT<" diff --git a/test/honey/sql/postgres_test.cljc b/test/honey/sql/postgres_test.cljc index 00218b1..803117a 100644 --- a/test/honey/sql/postgres_test.cljc +++ b/test/honey/sql/postgres_test.cljc @@ -185,7 +185,7 @@ [:location :point]]) sql/format)))) (testing "create table with foreign key reference" - (is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES CITIES(CITY), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"] + (is (= ["CREATE TABLE weather (city VARCHAR(80) REFERENCES cities(city), temp_lo INT, temp_hi INT, prcp REAL, date DATE)"] (-> (create-table :weather) (with-columns [[:city [:varchar :80] [:references :cities :city]] [:temp_lo :int] @@ -194,7 +194,7 @@ [:date :date]]) sql/format)))) (testing "creating table with table level constraint" - (is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(CODE, TITLE))"] + (is (= ["CREATE TABLE films (code CHAR(5), title VARCHAR(40), did INTEGER, date_prod DATE, kind VARCHAR(10), CONSTRAINT code_title PRIMARY KEY(code, title))"] (-> (create-table :films) (with-columns [[:code [:char 5]] [:title [:varchar 40]] @@ -204,7 +204,7 @@ [[:constraint :code_title] [:primary-key :code :title]]]) sql/format)))) (testing "creating table with column level constraint" - (is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT FIRSTKEY PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"] + (is (= ["CREATE TABLE films (code CHAR(5) CONSTRAINT firstkey PRIMARY KEY, title VARCHAR(40) NOT NULL, did INTEGER NOT NULL, date_prod DATE, kind VARCHAR(10))"] (-> (create-table :films) (with-columns [[:code [:char 5] [:constraint :firstkey] [:primary-key]] [:title [:varchar 40] [:not nil]] @@ -213,13 +213,13 @@ [:kind [:varchar 10]]]) sql/format)))) (testing "creating table with columns with default values" - (is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('SERIAL'), name VARCHAR(40) NOT NULL)"] + (is (= ["CREATE TABLE distributors (did INTEGER PRIMARY KEY DEFAULT NEXTVAL('serial'), name VARCHAR(40) NOT NULL)"] (-> (create-table :distributors) (with-columns [[:did :integer [:primary-key] [:default [:nextval "serial"]]] [:name [:varchar 40] [:not nil]]]) sql/format)))) (testing "creating table with column checks" - (is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(PRICE > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"] + (is (= ["CREATE TABLE products (product_no INTEGER, name TEXT, price NUMERIC CHECK(price > 0), discounted_price NUMERIC, CHECK((discounted_price > 0) AND (price > discounted_price)))"] (-> (create-table :products) (with-columns [[:product_no :integer] [:name :text] @@ -228,6 +228,32 @@ [[:check [:and [:> :discounted_price 0] [:> :price :discounted_price]]]]]) sql/format))))) +(deftest references-issue-386 + (is (= ["CREATE TABLE IF NOT EXISTS user (id VARCHAR(255) NOT NULL PRIMARY KEY, company_id INT NOT NULL, name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY(company_id) REFERENCES company(id))"] + (-> {:create-table [:user :if-not-exists] + :with-columns + [[:id [:varchar 255] [:not nil] [:primary-key]] + [:company-id :int [:not nil]] + [:name [:varchar 255] [:not nil]] + [:password [:varchar 255] [:not nil]] + [:created-time :datetime [:default :CURRENT_TIMESTAMP]] + [:updated-time :datetime [:default :CURRENT_TIMESTAMP] + :on :update :CURRENT_TIMESTAMP] + [[:foreign-key :company-id] [:references :company :id]]]} + (sql/format))))) + +(deftest create-table-issue-437 + (is (= ["CREATE TABLE bar (did UUID DEFAULT GEN_RANDOM_UUID(), foo_id VARCHAR NOT NULL, PRIMARY KEY(did, foo_id), FOREIGN KEY(foo_id) REFERENCES foo(id) ON DELETE CASCADE)"] + (-> (create-table :bar) + (with-columns + [[:did :uuid [:default [:gen_random_uuid]]] + [:foo-id :varchar [:not nil]] + [[:primary-key :did :foo-id]] + [[:foreign-key :foo-id] + [:references :foo :id] + :on-delete :cascade]]) + (sql/format))))) + (deftest over-test (testing "window function over on select statemt" (is (= ["SELECT id, AVG(salary) OVER (PARTITION BY department ORDER BY designation ASC) AS Average, MAX(salary) OVER w AS MaxSalary FROM employee WINDOW w AS (PARTITION BY department)"]