From 2bd181171b53dfa967503e53c90fb8201b46980e Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Thu, 9 Dec 2021 15:19:37 +0100 Subject: [PATCH] Support for `better-sqlite3` Driver (#4871) Signed-off-by: blam Co-authored-by: Igor Savin --- .github/workflows/integration-tests.yml | 8 +- lib/client.js | 1 + lib/constants.js | 2 + lib/dialects/better-sqlite3/index.js | 72 + lib/dialects/sqlite3/index.js | 2 + .../schema/internal/sqlite-ddl-operations.js | 6 +- package.json | 5 + .../query/misc/additional.spec.js | 2 + .../integration2/query/select/selects.spec.js | 1 + test/integration2/schema/foreign-keys.spec.js | 25 +- test/integration2/schema/misc.spec.js | 13 + test/integration2/schema/primary-keys.spec.js | 25 +- test/integration2/schema/set-nullable.spec.js | 8 +- .../util/knex-instance-provider.js | 20 + test/knexfile.js | 25 +- test/unit/schema-builder/better-sqlite3.js | 4450 +++++++++++++++++ test/util/db-helpers.js | 8 +- 17 files changed, 4648 insertions(+), 25 deletions(-) create mode 100644 lib/dialects/better-sqlite3/index.js create mode 100644 test/unit/schema-builder/better-sqlite3.js diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index d4e2282068..9538e2aaa5 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: node-version: [17.x, 16.x, 14.x, 12.x] - database-type: [postgres, pgnative, mysql, mssql, sqlite3, cockroachdb] + database-type: [postgres, pgnative, mysql, mssql, sqlite3, cockroachdb, better-sqlite3] steps: - name: Checkout Repository @@ -41,7 +41,7 @@ jobs: --detach \ --build \ "${{ matrix.database-type }}" - if: matrix.database-type != 'sqlite3' + if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3' - name: Initialize Database(s) run: | @@ -50,7 +50,7 @@ jobs: up \ --detach \ "wait${{ matrix.database-type }}" - if: matrix.database-type != 'sqlite3' + if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3' - name: Run npm install run: npm install @@ -70,4 +70,4 @@ jobs: docker-compose \ --file "scripts/docker-compose.yml" \ down - if: matrix.database-type != 'sqlite3' + if: matrix.database-type != 'sqlite3' && matrix.database-type != 'better-sqlite3' diff --git a/lib/client.js b/lib/client.js index d7a46bbe5a..ec554215f4 100644 --- a/lib/client.js +++ b/lib/client.js @@ -51,6 +51,7 @@ class Client extends EventEmitter { `Using 'this.dialect' to identify the client is deprecated and support for it will be removed in the future. Please use configuration option 'client' instead.` ); } + const dbClient = this.config.client || this.dialect; if (!dbClient) { throw new Error( diff --git a/lib/constants.js b/lib/constants.js index 9336541c18..326b13729b 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -16,6 +16,7 @@ const SUPPORTED_CLIENTS = Object.freeze( 'redshift', 'sqlite3', 'cockroachdb', + 'better-sqlite3', ].concat(Object.keys(CLIENT_ALIASES)) ); @@ -29,6 +30,7 @@ const DRIVER_NAMES = Object.freeze({ Redshift: 'pg-redshift', SQLite: 'sqlite3', CockroachDB: 'cockroachdb', + BetterSQLite3: 'better-sqlite3', }); const POOL_CONFIG_OPTIONS = Object.freeze([ diff --git a/lib/dialects/better-sqlite3/index.js b/lib/dialects/better-sqlite3/index.js new file mode 100644 index 0000000000..d68ee07f92 --- /dev/null +++ b/lib/dialects/better-sqlite3/index.js @@ -0,0 +1,72 @@ +// better-sqlite3 Client +// ------- +const Client_SQLite3 = require('../sqlite3'); + +class Client_BetterSQLite3 extends Client_SQLite3 { + _driver() { + return require('better-sqlite3'); + } + + // Get a raw connection from the database, returning a promise with the connection object. + async acquireRawConnection() { + return new this.driver(this.connectionSettings.filename); + } + + // Used to explicitly close a connection, called internally by the pool when + // a connection times out or the pool is shutdown. + async destroyRawConnection(connection) { + return connection.close(); + } + + // Runs the query on the specified connection, providing the bindings and any + // other necessary prep work. + async _query(connection, obj) { + if (!obj.sql) throw new Error('The query is empty'); + + if (!connection) { + throw new Error('No connection provided'); + } + + const statement = connection.prepare(obj.sql); + const bindings = this._formatBindings(obj.bindings); + + if (statement.reader) { + const response = await statement.all(bindings); + obj.response = response; + return obj; + } + + const response = await statement.run(bindings); + obj.response = response; + obj.context = { + lastID: response.lastInsertRowid, + changes: response.changes, + }; + + return obj; + } + + _formatBindings(bindings) { + if (!bindings) { + return []; + } + return bindings.map((binding) => { + if (binding instanceof Date) { + return binding.valueOf(); + } + + if (typeof binding === 'boolean') { + return String(binding); + } + + return binding; + }); + } +} + +Object.assign(Client_BetterSQLite3.prototype, { + // The "dialect", for reference . + driverName: 'better-sqlite3', +}); + +module.exports = Client_BetterSQLite3; diff --git a/lib/dialects/sqlite3/index.js b/lib/dialects/sqlite3/index.js index 86d75e97f5..82653b8c3b 100644 --- a/lib/dialects/sqlite3/index.js +++ b/lib/dialects/sqlite3/index.js @@ -132,6 +132,7 @@ class Client_SQLite3 extends Client { // We need the context here, as it contains // the "this.lastID" or "this.changes" obj.context = this; + return resolver(obj); }); }); @@ -144,6 +145,7 @@ class Client_SQLite3 extends Client { return new Promise(function (resolver, rejecter) { stream.on('error', rejecter); stream.on('end', resolver); + return client ._query(connection, obj) .then((obj) => obj.response) diff --git a/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js b/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js index 16a7057dc8..2340ae9c00 100644 --- a/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js +++ b/lib/dialects/sqlite3/schema/internal/sqlite-ddl-operations.js @@ -11,15 +11,15 @@ function copyData(sourceTable, targetTable, columns) { } function dropOriginal(tableName) { - return `DROP TABLE "${tableName}"`; + return `DROP TABLE '${tableName}'`; } function renameTable(tableName, alteredName) { - return `ALTER TABLE "${tableName}" RENAME TO "${alteredName}"`; + return `ALTER TABLE '${tableName}' RENAME TO '${alteredName}'`; } function getTableSql(tableName) { - return `SELECT type, sql FROM sqlite_master WHERE (type="table" OR (type="index" AND sql IS NOT NULL)) AND tbl_name="${tableName}"`; + return `SELECT type, sql FROM sqlite_master WHERE (type='table' OR (type='index' AND sql IS NOT NULL)) AND tbl_name='${tableName}'`; } module.exports = { diff --git a/package.json b/package.json index 56e4974787..fccb467bf7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test:mysql2": "cross-env DB=mysql2 npm run test:db", "test:oracledb": "cross-env DB=oracledb npm run test:db", "test:sqlite": "cross-env DB=sqlite3 npm run test:db", + "test:better-sqlite3": "cross-env DB=better-sqlite3 npm run test:db", "test:postgres": "cross-env DB=postgres npm run test:db", "test:cockroachdb": "cross-env DB=cockroachdb npm run test:db", "test:pgnative": "cross-env DB=pgnative npm run test:db", @@ -85,6 +86,9 @@ }, "sqlite3": { "optional": true + }, + "better-sqlite3": { + "optional": true } }, "lint-staged": { @@ -96,6 +100,7 @@ "devDependencies": { "@types/node": "^16.11.6", "@vscode/sqlite3": "^5.0.7", + "better-sqlite3": "^7.4.5", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "chai-subset-in-order": "^3.1.0", diff --git a/test/integration2/query/misc/additional.spec.js b/test/integration2/query/misc/additional.spec.js index b7c0390356..2742992206 100644 --- a/test/integration2/query/misc/additional.spec.js +++ b/test/integration2/query/misc/additional.spec.js @@ -350,6 +350,8 @@ describe('Additional', function () { "SELECT table_name FROM information_schema.tables WHERE table_schema='public'", [drivers.Redshift]: "SELECT table_name FROM information_schema.tables WHERE table_schema='public'", + [drivers.BetterSQLite3]: + "SELECT name FROM sqlite_master WHERE type='table';", [drivers.SQLite]: "SELECT name FROM sqlite_master WHERE type='table';", [drivers.Oracle]: 'select TABLE_NAME from USER_TABLES', diff --git a/test/integration2/query/select/selects.spec.js b/test/integration2/query/select/selects.spec.js index 29416b51e8..6cbdc1b505 100644 --- a/test/integration2/query/select/selects.spec.js +++ b/test/integration2/query/select/selects.spec.js @@ -1207,6 +1207,7 @@ describe('Selects', function () { 'sqlite3', 'oracledb', 'cockroachdb', + 'better-sqlite3', ]); if (knex.client.driverName !== 'cockroachdb') { diff --git a/test/integration2/schema/foreign-keys.spec.js b/test/integration2/schema/foreign-keys.spec.js index 582860e1d1..4544cf287a 100644 --- a/test/integration2/schema/foreign-keys.spec.js +++ b/test/integration2/schema/foreign-keys.spec.js @@ -7,6 +7,7 @@ const { isPostgreSQL, isSQLite, isCockroachDB, + isBetterSQLite3, } = require('../../util/db-helpers'); describe('Schema', () => { @@ -76,8 +77,8 @@ describe('Schema', () => { expect(queries.sql).to.eql([ 'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`))', 'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;', - 'DROP TABLE "foreign_keys_table_one"', - 'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"', + "DROP TABLE 'foreign_keys_table_one'", + "ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'", ]); } @@ -112,8 +113,8 @@ describe('Schema', () => { expect(queries.sql).to.eql([ 'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`) ON DELETE CASCADE)', 'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;', - 'DROP TABLE "foreign_keys_table_one"', - 'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"', + "DROP TABLE 'foreign_keys_table_one'", + "ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'", ]); }); @@ -138,8 +139,8 @@ describe('Schema', () => { expect(queries.sql).to.eql([ 'CREATE TABLE `_knex_temp_alter111` (`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `fkey_two` integer NOT NULL, `fkey_three` integer NOT NULL, CONSTRAINT `fk_fkey_threeee` FOREIGN KEY (`fkey_three`) REFERENCES `foreign_keys_table_three` (`id`) ON UPDATE CASCADE)', 'INSERT INTO _knex_temp_alter111 SELECT * FROM foreign_keys_table_one;', - 'DROP TABLE "foreign_keys_table_one"', - 'ALTER TABLE "_knex_temp_alter111" RENAME TO "foreign_keys_table_one"', + "DROP TABLE 'foreign_keys_table_one'", + "ALTER TABLE '_knex_temp_alter111' RENAME TO 'foreign_keys_table_one'", ]); }); @@ -199,7 +200,11 @@ describe('Schema', () => { }); throw new Error("Shouldn't reach this"); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + `insert into \`foreign_keys_table_one\` (\`fkey_three\`, \`fkey_two\`) values (99, 9999) - FOREIGN KEY constraint failed` + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( `insert into \`foreign_keys_table_one\` (\`fkey_three\`, \`fkey_two\`) values (99, 9999) - SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed` ); @@ -330,7 +335,11 @@ describe('Schema', () => { }); throw new Error("Shouldn't reach this"); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + `insert into \`foreign_keys_table_one\` (\`fkey_four_part1\`, \`fkey_four_part2\`, \`fkey_three\`, \`fkey_two\`) values ('a', 'b', 99, 9999) - FOREIGN KEY constraint failed` + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( `insert into \`foreign_keys_table_one\` (\`fkey_four_part1\`, \`fkey_four_part2\`, \`fkey_three\`, \`fkey_two\`) values ('a', 'b', 99, 9999) - SQLITE_CONSTRAINT_FOREIGNKEY: FOREIGN KEY constraint failed` ); diff --git a/test/integration2/schema/misc.spec.js b/test/integration2/schema/misc.spec.js index 515093c1f3..c747298489 100644 --- a/test/integration2/schema/misc.spec.js +++ b/test/integration2/schema/misc.spec.js @@ -13,6 +13,7 @@ const { isMssql, isCockroachDB, isPostgreSQL, + isBetterSQLite3, } = require('../../util/db-helpers'); const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider'); const logger = require('../../integration/logger'); @@ -1305,6 +1306,9 @@ describe('Schema (misc)', () => { tester('pg-redshift', [ 'create table "bool_test" ("one" boolean, "two" boolean default \'0\', "three" boolean default \'1\', "four" boolean default \'1\', "five" boolean default \'0\')', ]); + tester('better-sqlite3', [ + "create table `bool_test` (`one` boolean, `two` boolean default '0', `three` boolean default '1', `four` boolean default '1', `five` boolean default '0')", + ]); tester('sqlite3', [ "create table `bool_test` (`one` boolean, `two` boolean default '0', `three` boolean default '1', `four` boolean default '1', `five` boolean default '0')", ]); @@ -1785,6 +1789,15 @@ describe('Schema (misc)', () => { const autoinc = !!res.rows[0].ident; expect(autoinc).to.equal(true); + } else if (isBetterSQLite3(knex)) { + const res = await knex.raw( + `SELECT 'is-autoincrement' as ident + FROM sqlite_master + WHERE tbl_name = ? AND sql LIKE '%AUTOINCREMENT%'`, + [tableName] + ); + const autoinc = !!res[0].ident; + expect(autoinc).to.equal(true); } else if (isSQLite(knex)) { const res = await knex.raw( `SELECT "is-autoincrement" as ident diff --git a/test/integration2/schema/primary-keys.spec.js b/test/integration2/schema/primary-keys.spec.js index 54b9415c73..fa8d5d4504 100644 --- a/test/integration2/schema/primary-keys.spec.js +++ b/test/integration2/schema/primary-keys.spec.js @@ -4,6 +4,7 @@ const { isSQLite, isPgBased, isCockroachDB, + isBetterSQLite3, } = require('../../util/db-helpers'); const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider'); @@ -45,7 +46,11 @@ describe('Schema', () => { await knex('primary_table').insert({ id_four: 1 }); throw new Error(`Shouldn't reach this`); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + 'insert into `primary_table` (`id_four`) values (1) - UNIQUE constraint failed: primary_table.id_four' + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( 'insert into `primary_table` (`id_four`) values (1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_four' ); @@ -76,7 +81,11 @@ describe('Schema', () => { await knex('primary_table').insert({ id_four: 1 }); throw new Error(`Shouldn't reach this`); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + 'insert into `primary_table` (`id_four`) values (1) - UNIQUE constraint failed: primary_table.id_four' + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( 'insert into `primary_table` (`id_four`) values (1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_four' ); @@ -112,7 +121,11 @@ describe('Schema', () => { try { await knex('primary_table').insert({ id_two: 1, id_three: 1 }); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' ); @@ -154,7 +167,11 @@ describe('Schema', () => { try { await knex('primary_table').insert({ id_two: 1, id_three: 1 }); } catch (err) { - if (isSQLite(knex)) { + if (isBetterSQLite3(knex)) { + expect(err.message).to.equal( + 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' + ); + } else if (isSQLite(knex)) { expect(err.message).to.equal( 'insert into `primary_table` (`id_three`, `id_two`) values (1, 1) - SQLITE_CONSTRAINT_PRIMARYKEY: UNIQUE constraint failed: primary_table.id_two, primary_table.id_three' ); diff --git a/test/integration2/schema/set-nullable.spec.js b/test/integration2/schema/set-nullable.spec.js index 29ec200ad0..aa79e617e7 100644 --- a/test/integration2/schema/set-nullable.spec.js +++ b/test/integration2/schema/set-nullable.spec.js @@ -6,6 +6,7 @@ const { isSQLite, isPostgreSQL, isMysql, + isBetterSQLite3, isOracle, } = require('../../util/db-helpers'); const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider'); @@ -48,8 +49,8 @@ describe('Schema', () => { expect(queries.sql).to.eql([ 'CREATE TABLE `_knex_temp_alter111` (`id_nullable` integer NULL, `id_not_nullable` integer)', 'INSERT INTO _knex_temp_alter111 SELECT * FROM primary_table;', - 'DROP TABLE "primary_table"', - 'ALTER TABLE "_knex_temp_alter111" RENAME TO "primary_table"', + "DROP TABLE 'primary_table'", + "ALTER TABLE '_knex_temp_alter111' RENAME TO 'primary_table'", ]); } @@ -88,6 +89,9 @@ describe('Schema', () => { errorMessage = 'cannot be null'; } else if (isOracle(knex)) { errorMessage = 'ORA-01400: cannot insert NULL into'; + } else if (isBetterSQLite3(knex)) { + errorMessage = + 'insert into `primary_table` (`id_not_nullable`, `id_nullable`) values (1, NULL) - NOT NULL constraint failed: primary_table.id_nullable'; } else if (isSQLite(knex)) { errorMessage = 'insert into `primary_table` (`id_not_nullable`, `id_nullable`) values (1, NULL) - SQLITE_CONSTRAINT_NOTNULL: NOT NULL constraint failed: primary_table.id_nullable'; diff --git a/test/integration2/util/knex-instance-provider.js b/test/integration2/util/knex-instance-provider.js index 786358948c..f5a68e5c60 100644 --- a/test/integration2/util/knex-instance-provider.js +++ b/test/integration2/util/knex-instance-provider.js @@ -12,6 +12,7 @@ const Db = { SQLite: 'sqlite3', Oracle: 'oracledb', CockroachDB: 'cockroachdb', + BetterSqlite3: 'better-sqlite3', }; const defaultDbs = [ @@ -22,6 +23,7 @@ const defaultDbs = [ Db.SQLite, Db.MSSQL, Db.CockroachDB, + Db.BetterSqlite3, ]; function getAllDbs() { @@ -43,6 +45,16 @@ const poolSqlite = { }, }; +const poolBetterSqlite = { + min: 0, + max: 1, + acquireTimeoutMillis: 1000, + afterCreate: function (connection, callback) { + connection.prepare('PRAGMA foreign_keys = ON').run(); + callback(null, connection); + }, +}; + const mysqlPool = Object.assign({}, pool, { afterCreate: function (connection, callback) { promisify(connection.query) @@ -145,6 +157,14 @@ const testConfigs = { seeds, }, + 'better-sqlite3': { + client: 'better-sqlite3', + connection: testConfig.sqlite3 || ':memory:', + pool: poolBetterSqlite, + migrations, + seeds, + }, + mssql: { client: 'mssql', connection: testConfig.mssql || { diff --git a/test/knexfile.js b/test/knexfile.js index bc01a3c27d..764bd5263c 100644 --- a/test/knexfile.js +++ b/test/knexfile.js @@ -10,8 +10,8 @@ const _ = require('lodash'); // excluding redshift, oracle, and mssql dialects from default integrations test const testIntegrationDialects = ( process.env.DB || - 'sqlite3 postgres pgnative mysql mysql2 mssql oracledb cockroachdb' -).match(/\w+/g); + 'sqlite3 postgres pgnative mysql mysql2 mssql oracledb cockroachdb better-sqlite3' +).match(/[\w-]+/g); console.log(`ENV DB: ${process.env.DB}`); @@ -32,6 +32,17 @@ const poolSqlite = { }, }; +const poolBetterSqlite = { + min: 0, + max: 1, + acquireTimeoutMillis: 1000, + afterCreate: function (connection, callback) { + assert.ok(typeof connection.__knexUid !== 'undefined'); + connection.prepare('PRAGMA foreign_keys = ON').run(); + callback(null, connection); + }, +}; + const mysqlPool = _.extend({}, pool, { afterCreate: function (connection, callback) { promisify(connection.query) @@ -164,6 +175,16 @@ const testConfigs = { seeds, }, + 'better-sqlite3': { + client: 'better-sqlite3', + connection: testConfig.sqlite3 || { + filename: __dirname + '/test.sqlite3', + }, + pool: poolBetterSqlite, + migrations, + seeds, + }, + mssql: { client: 'mssql', connection: testConfig.mssql || { diff --git a/test/unit/schema-builder/better-sqlite3.js b/test/unit/schema-builder/better-sqlite3.js new file mode 100644 index 0000000000..0965bfa1fc --- /dev/null +++ b/test/unit/schema-builder/better-sqlite3.js @@ -0,0 +1,4450 @@ +'use strict'; + +const { expect } = require('chai'); + +let tableSql; + +const sinon = require('sinon'); +const Client_BetterSQLite3 = require('../../../lib/dialects/better-sqlite3'); +const client = new Client_BetterSQLite3({ client: 'better-sqlite3' }); +const { + parseCreateTable, + parseCreateIndex, +} = require('../../../lib/dialects/sqlite3/schema/internal/parser'); +const { + compileCreateTable, + compileCreateIndex, +} = require('../../../lib/dialects/sqlite3/schema/internal/compiler'); + +const _ = require('lodash'); +const { equal, deepEqual } = require('assert'); +const knex = require('../../../knex'); + +describe('BetterSQLite3 SchemaBuilder', function () { + it('basic create table', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.increments('id'); + table.string('email'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`id` integer not null primary key autoincrement, `email` varchar(255))' + ); + }); + + it('basic create table like', function () { + tableSql = client + .schemaBuilder() + .createTableLike('users', 'users_like') + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` as select * from `users_like` where 0=1' + ); + }); + + it('test create table like with additionnal columns', function () { + tableSql = client + .schemaBuilder() + .createTableLike('users_like', 'users', function (table) { + table.text('add_col'); + table.integer('add_num_col'); + }); + + expect(tableSql.toSQL().length).to.equal(3); + expect(tableSql.toSQL()[0].sql).to.equal( + 'create table `users_like` as select * from `users` where 0=1' + ); + expect(tableSql.toSQL()[1].sql).to.equal( + 'alter table `users_like` add column `add_col` text' + ); + expect(tableSql.toSQL()[2].sql).to.equal( + 'alter table `users_like` add column `add_num_col` integer' + ); + }); + + describe('views', function () { + let knexSqlite3; + + before(function () { + knexSqlite3 = knex({ + client: 'sqlite3', + connection: { + filename: '', + }, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexSqlite3('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "create view `adults` (`name`) as select `name` from `users` where `age` > '18'" + ); + }); + + it('basic create view without columns', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.as(knexSqlite3('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "create view `adults` as select `name` from `users` where `age` > '18'" + ); + }); + + it('create view or replace', async function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('create view with check options', async function () { + expect(() => { + client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as( + knexSqlite3('users').select('name').where('age', '>', '18') + ); + view.localCheckOption(); + }) + .toSQL(); + }).to.throw('check option definition is not supported by this dialect.'); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `users`'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `myschema`.`users`'); + }); + + it('rename and change default of column of view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('rename view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + }).to.throw( + 'rename view is not supported by this dialect (instead drop then create another view).' + ); + }); + + it('create materialized view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as( + knexSqlite3('users').select('name').where('age', '>', '18') + ); + }) + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + + it('refresh view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + }); + + it('create json table', function () { + tableSql = client + .schemaBuilder() + .createTable('user', function (table) { + table.json('preferences'); + }) + .table('user', function (t) {}) + .toSQL(); + expect(tableSql[0].sql).to.equal( + 'create table `user` (`preferences` json)' + ); + }); + + it('basic create table without primary key', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.increments('id'); + table.increments('other_id', { primaryKey: false }); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`id` integer not null primary key autoincrement, `other_id` integer not null autoincrement)' + ); + }); + + it('basic alter table', function () { + tableSql = client + .schemaBuilder() + .alterTable('users', function (table) { + table.increments('id'); + table.string('email'); + }) + .toSQL(); + + equal(2, tableSql.length); + const expected = [ + 'alter table `users` add column `id` integer not null primary key autoincrement', + 'alter table `users` add column `email` varchar(255)', + ]; + expect(expected).to.eql(_.map(tableSql, 'sql')); + }); + + it('drop table', function () { + tableSql = client.schemaBuilder().dropTable('users').toSQL(); + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop table `users`'); + }); + + it('drop table if exists', function () { + tableSql = client.schemaBuilder().dropTableIfExists('users').toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop table if exists `users`'); + }); + + it('drop unique', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.dropUnique('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop index `users_foo_unique`'); + }); + + it('drop unique, custom', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.dropUnique(null, 'foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop index `foo`'); + }); + + it('drop index', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.dropIndex('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop index `users_foo_index`'); + }); + + it('drop index, custom', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.dropIndex(null, 'foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'drop index `foo`'); + }); + + it('rename table', function () { + tableSql = client.schemaBuilder().renameTable('users', 'foo').toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` rename to `foo`'); + }); + + it('adding primary key', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo'); + table.primary('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), primary key (`foo`))' + ); + }); + + it('adding primary key with specific identifier', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo'); + table.primary('foo', 'pk-users'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), constraint `pk-users` primary key (`foo`))' + ); + }); + + it('adding composite primary key', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo'); + table.string('order_id'); + table.primary(['foo', 'order_id']); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), `order_id` varchar(255), primary key (`foo`, `order_id`))' + ); + }); + + it('adding composite primary key with specific identifier', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo'); + table.string('order_id'); + table.primary(['foo', 'order_id'], 'pk-users'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), `order_id` varchar(255), constraint `pk-users` primary key (`foo`, `order_id`))' + ); + }); + + it('adding primary key fluently', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo').primary(); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), primary key (`foo`))' + ); + }); + + it('adding primary key fluently with specific identifier', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo').primary('pk-users'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), constraint `pk-users` primary key (`foo`))' + ); + }); + + it('adding foreign key', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo').primary(); + table.string('order_id'); + table.foreign('order_id').references('id').on('orders'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), `order_id` varchar(255), foreign key(`order_id`) references `orders`(`id`), primary key (`foo`))' + ); + }); + + it('adding foreign key with specific identifier', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo').primary(); + table.string('order_id'); + table + .foreign('order_id', 'fk-users-orders') + .references('id') + .on('orders'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), `order_id` varchar(255), constraint `fk-users-orders` foreign key(`order_id`) references `orders`(`id`), primary key (`foo`))' + ); + }); + + it('adding foreign key fluently', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table.string('foo').primary(); + table.string('order_id').references('id').on('orders'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`foo` varchar(255), `order_id` varchar(255), foreign key(`order_id`) references `orders`(`id`), primary key (`foo`))' + ); + }); + + it('adds a unique key with autogenerated name', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.unique('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'create unique index `users_foo_unique` on `users` (`foo`)' + ); + }); + + it('adding unique key with specific name', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.unique('foo', 'bar'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'create unique index `bar` on `users` (`foo`)'); + }); + + it('adding index', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.index(['foo', 'bar'], 'baz'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'create index `baz` on `users` (`foo`, `bar`)'); + }); + + it('adding index with a predicate', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.index(['foo', 'bar'], 'baz', { + predicate: client.queryBuilder().whereRaw('email = "foo@bar"'), + }); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create index `baz` on `users` (`foo`, `bar`) where email = "foo@bar"' + ); + }); + + it('adding index with a where not null predicate', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.index(['foo', 'bar'], 'baz', { + predicate: client.queryBuilder().whereNotNull('email'), + }); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create index `baz` on `users` (`foo`, `bar`) where `email` is not null' + ); + }); + + it('adding incrementing id', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.increments('id'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'alter table `users` add column `id` integer not null primary key autoincrement' + ); + }); + + it('adding big incrementing id', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.bigIncrements('id'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'alter table `users` add column `id` integer not null primary key autoincrement' + ); + }); + + it('adding big incrementing id without primary key', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.bigIncrements('id', { primaryKey: false }); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'alter table `users` add column `id` integer not null autoincrement' + ); + }); + + it('adding string', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.string('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` varchar(255)'); + }); + + it('allows setting a value in the string length, although unused by sqlite3', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.string('foo', 100); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` varchar(100)'); + }); + + it('correctly interprets defaultTo(null)', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.string('foo').defaultTo(null); + }) + .toSQL(); + + equal( + tableSql[0].sql, + 'alter table `users` add column `foo` varchar(255) default null' + ); + }); + + it('correctly escape singleQuotes passed to defaultTo()', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.string('foo').defaultTo("single 'quoted' value"); + }) + .toSQL(); + + equal( + tableSql[0].sql, + "alter table `users` add column `foo` varchar(255) default 'single ''quoted'' value'" + ); + }); + + it('chains notNull and defaultTo', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.string('foo', 100).notNull().defaultTo('bar'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + "alter table `users` add column `foo` varchar(100) not null default 'bar'" + ); + }); + + it('adding text', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.text('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` text'); + }); + + it('adding big integer', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.bigInteger('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` bigint'); + }); + + it('bigincrements works the same as increments for sqlite3', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.bigIncrements('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'alter table `users` add column `foo` integer not null primary key autoincrement' + ); + }); + + it('adding integer', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.integer('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` integer'); + }); + + it('adding autoincrements', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.increments('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + 'alter table `users` add column `foo` integer not null primary key autoincrement' + ); + }); + + it('adding medium integer', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.mediumint('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` integer'); + }); + + it('adding tiny integer', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.tinyint('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` tinyint'); + }); + + it('adding small integer', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.smallint('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` integer'); + }); + + it('adding float', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.float('foo', 5, 2); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` float'); + }); + + it('adding double', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.double('foo', 15, 8); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` float'); + }); + + it('adding decimal', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.decimal('foo', 5, 2); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` float'); + }); + + it('test adding decimal, no precision', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.decimal('foo', null); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` float'); + }); + + it('adding boolean', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.boolean('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` boolean'); + }); + + it('adding enum', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.enum('foo', ['bar', 'baz']); + }) + .toSQL(); + + equal(1, tableSql.length); + equal( + tableSql[0].sql, + "alter table `users` add column `foo` text check (`foo` in ('bar', 'baz'))" + ); + }); + + it('adding date', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.date('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` date'); + }); + + it('adding date time', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.dateTime('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` datetime'); + }); + + it('adding time', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.time('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` time'); + }); + + it('adding time stamp', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.timestamp('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` datetime'); + }); + + it('adding time stamps', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.timestamps(); + }) + .toSQL(); + + equal(2, tableSql.length); + const expected = [ + 'alter table `users` add column `created_at` datetime', + 'alter table `users` add column `updated_at` datetime', + ]; + deepEqual(expected, _.map(tableSql, 'sql')); + }); + + it('adding binary', function () { + tableSql = client + .schemaBuilder() + .table('users', function (table) { + table.binary('foo'); + }) + .toSQL(); + + equal(1, tableSql.length); + equal(tableSql[0].sql, 'alter table `users` add column `foo` blob'); + }); + + it('allows for on delete cascade with foreign keys, #166', function () { + tableSql = client + .schemaBuilder() + .createTable('users', function (table) { + table + .string('user_id', 36) + .index() + .references('id') + .inTable('user') + .onDelete('CASCADE'); + }) + .toSQL(); + + equal(2, tableSql.length); + equal( + tableSql[0].sql, + 'create table `users` (`user_id` varchar(36), foreign key(`user_id`) references `user`(`id`) on delete CASCADE)' + ); + }); + + describe('queryContext', function () { + let spy; + let originalWrapIdentifier; + + before(function () { + spy = sinon.spy(); + originalWrapIdentifier = client.config.wrapIdentifier; + client.config.wrapIdentifier = function (value, wrap, queryContext) { + spy(value, queryContext); + return wrap(value); + }; + }); + + beforeEach(function () { + spy.resetHistory(); + }); + + after(function () { + client.config.wrapIdentifier = originalWrapIdentifier; + }); + + it('SchemaCompiler passes queryContext to wrapIdentifier via TableCompiler', function () { + client + .schemaBuilder() + .queryContext('table context') + .createTable('users', function (table) { + table.increments('id'); + table.string('email'); + }) + .toSQL(); + + expect(spy.callCount).to.equal(3); + expect(spy.firstCall.args).to.deep.equal(['id', 'table context']); + expect(spy.secondCall.args).to.deep.equal(['email', 'table context']); + expect(spy.thirdCall.args).to.deep.equal(['users', 'table context']); + }); + + it('TableCompiler passes queryContext to wrapIdentifier', function () { + client + .schemaBuilder() + .createTable('users', function (table) { + table.increments('id').queryContext('id context'); + table.string('email').queryContext('email context'); + }) + .toSQL(); + + expect(spy.callCount).to.equal(3); + expect(spy.firstCall.args).to.deep.equal(['id', 'id context']); + expect(spy.secondCall.args).to.deep.equal(['email', 'email context']); + expect(spy.thirdCall.args).to.deep.equal(['users', undefined]); + }); + + it('TableCompiler allows overwriting queryContext from SchemaCompiler', function () { + client + .schemaBuilder() + .queryContext('schema context') + .createTable('users', function (table) { + table.queryContext('table context'); + table.increments('id'); + table.string('email'); + }) + .toSQL(); + + expect(spy.callCount).to.equal(3); + expect(spy.firstCall.args).to.deep.equal(['id', 'table context']); + expect(spy.secondCall.args).to.deep.equal(['email', 'table context']); + expect(spy.thirdCall.args).to.deep.equal(['users', 'table context']); + }); + + it('ColumnCompiler allows overwriting queryContext from TableCompiler', function () { + client + .schemaBuilder() + .queryContext('schema context') + .createTable('users', function (table) { + table.queryContext('table context'); + table.increments('id').queryContext('id context'); + table.string('email').queryContext('email context'); + }) + .toSQL(); + + expect(spy.callCount).to.equal(3); + expect(spy.firstCall.args).to.deep.equal(['id', 'id context']); + expect(spy.secondCall.args).to.deep.equal(['email', 'email context']); + expect(spy.thirdCall.args).to.deep.equal(['users', 'table context']); + }); + }); +}); + +describe('SQLite parser and compiler', function () { + const wrap = (v) => `"${v}"`; + + describe('create table', function () { + it('basic', function () { + const sql = 'CREATE TABLE "users" ("foo")'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('temporary', function () { + const sql = 'CREATE TEMP TABLE "users" ("foo")'; + const ast = { + temporary: true, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('if not exists', function () { + const sql = 'CREATE TABLE IF NOT EXISTS "users" ("foo")'; + const ast = { + temporary: false, + exists: true, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('schema', function () { + const sql = 'CREATE TABLE "schema"."users" ("foo")'; + const ast = { + temporary: false, + exists: false, + schema: 'schema', + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('rowid', function () { + const sql = 'CREATE TABLE "users" ("foo") WITHOUT ROWID'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: true, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition type', function () { + const sql = 'CREATE TABLE "users" ("foo" INTEGER)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'INTEGER', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition type one parameter', function () { + const sql = 'CREATE TABLE "users" ("foo" VARYING CHARACTER(255))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'VARYING CHARACTER(255)', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition type two parameters', function () { + const sql = 'CREATE TABLE "users" ("foo" DECIMAL(+10, -5))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'DECIMAL(+10, -5)', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition multiple type', function () { + const sql = 'CREATE TABLE "users" ("foo" FLOAT, "bar" DECIMAL(4, 2))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'FLOAT', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'bar', + type: 'DECIMAL(4, 2)', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition primary key', function () { + const sql = 'CREATE TABLE "users" ("foo" PRIMARY KEY)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: { + name: null, + order: null, + conflict: null, + autoincrement: false, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition primary key order', function () { + const sql = 'CREATE TABLE "users" ("foo" PRIMARY KEY ASC)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: { + name: null, + order: 'ASC', + conflict: null, + autoincrement: false, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition primary key conflict', function () { + const sql = 'CREATE TABLE "users" ("foo" PRIMARY KEY ON CONFLICT ABORT)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: { + name: null, + order: null, + conflict: 'ABORT', + autoincrement: false, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition primary key autoincrement', function () { + const sql = 'CREATE TABLE "users" ("foo" PRIMARY KEY AUTOINCREMENT)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: { + name: null, + order: null, + conflict: null, + autoincrement: true, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition primary key all', function () { + const sql = + 'CREATE TABLE "users" ("foo" PRIMARY KEY DESC ON CONFLICT FAIL AUTOINCREMENT)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: { + name: null, + order: 'DESC', + conflict: 'FAIL', + autoincrement: true, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition not null', function () { + const sql = 'CREATE TABLE "users" ("foo" NOT NULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: { + name: null, + conflict: null, + }, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition not null conflict', function () { + const sql = 'CREATE TABLE "users" ("foo" NOT NULL ON CONFLICT IGNORE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: { + name: null, + conflict: 'IGNORE', + }, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition null', function () { + const sql = 'CREATE TABLE "users" ("foo" NULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: { + name: null, + conflict: null, + }, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition null conflict', function () { + const sql = 'CREATE TABLE "users" ("foo" NULL ON CONFLICT ABORT)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: { + name: null, + conflict: 'ABORT', + }, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition unique', function () { + const sql = 'CREATE TABLE "users" ("foo" UNIQUE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: { + name: null, + conflict: null, + }, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition unique conflict', function () { + const sql = 'CREATE TABLE "users" ("foo" UNIQUE ON CONFLICT REPLACE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: { + name: null, + conflict: 'REPLACE', + }, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition check', function () { + const sql = 'CREATE TABLE "users" ("foo" CHECK ("foo" != 42))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: { + name: null, + expression: ['"foo"', '!=', '42'], + }, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition default', function () { + const sql = `CREATE TABLE "users" ("foo" DEFAULT 'bar')`; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: { + name: null, + value: `'bar'`, + expression: false, + }, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition default signed number', function () { + const sql = 'CREATE TABLE "users" ("foo" DEFAULT -42)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: { + name: null, + value: '-42', + expression: false, + }, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition default expression', function () { + const sql = 'CREATE TABLE "users" ("foo" DEFAULT (random()))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: { + name: null, + value: ['random', []], + expression: true, + }, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition collate', function () { + const sql = 'CREATE TABLE "users" ("foo" COLLATE RTRIM)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: { + name: null, + collation: 'RTRIM', + }, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references', function () { + const sql = 'CREATE TABLE "users" ("foo" REFERENCES "other")'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: [], + delete: null, + update: null, + match: null, + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references referenced column', function () { + const sql = 'CREATE TABLE "users" ("foo" REFERENCES "other" ("bar"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: ['bar'], + delete: null, + update: null, + match: null, + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references multiple columns', function () { + const sql = + 'CREATE TABLE "users" ("foo" REFERENCES "other" ("bar", "lar"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: ['bar', 'lar'], + delete: null, + update: null, + match: null, + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references on delete', function () { + const sql = + 'CREATE TABLE "users" ("foo" REFERENCES "other" ON DELETE SET NULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: [], + delete: 'SET NULL', + update: null, + match: null, + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references on update', function () { + const sql = + 'CREATE TABLE "users" ("foo" REFERENCES "other" ON UPDATE CASCADE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: [], + delete: null, + update: 'CASCADE', + match: null, + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references match', function () { + const sql = 'CREATE TABLE "users" ("foo" REFERENCES "other" MATCH FULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: [], + delete: null, + update: null, + match: 'FULL', + deferrable: null, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references deferrable', function () { + const sql = + 'CREATE TABLE "users" ("foo" REFERENCES "other" NOT DEFERRABLE INITIALLY DEFERRED)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: [], + delete: null, + update: null, + match: null, + deferrable: { + not: true, + initially: 'DEFERRED', + }, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition references all', function () { + const sql = + 'CREATE TABLE "users" ("foo" REFERENCES "other" ("bar") ON DELETE RESTRICT ON UPDATE NO ACTION MATCH PARTIAL DEFERRABLE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: ['bar'], + delete: 'RESTRICT', + update: 'NO ACTION', + match: 'PARTIAL', + deferrable: { + not: false, + initially: null, + }, + }, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition as', function () { + const sql = 'CREATE TABLE "users" ("foo" AS (count(*)))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: { + name: null, + generated: false, + expression: ['count', ['*']], + mode: null, + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition as generated always', function () { + const sql = + 'CREATE TABLE "users" ("foo" GENERATED ALWAYS AS (length(foo)))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: { + name: null, + generated: true, + expression: ['length', ['foo']], + mode: null, + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition as mode', function () { + const sql = 'CREATE TABLE "users" ("foo" AS (length(foo)) VIRTUAL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: { + name: null, + generated: false, + expression: ['length', ['foo']], + mode: 'VIRTUAL', + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition all', function () { + const sql = + 'CREATE TABLE "users" ("foo" TEXT PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT NOT NULL ON CONFLICT FAIL NULL ON CONFLICT IGNORE UNIQUE ON CONFLICT REPLACE CHECK ("foo" != 42) DEFAULT NULL COLLATE BINARY REFERENCES "other" ("baz", "bar", "lar") ON UPDATE SET NULL MATCH SIMPLE NOT DEFERRABLE GENERATED ALWAYS AS (count(*)) STORED)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'TEXT', + constraints: { + primary: { + name: null, + order: 'DESC', + conflict: 'ROLLBACK', + autoincrement: true, + }, + notnull: { + name: null, + conflict: 'FAIL', + }, + null: { + name: null, + conflict: 'IGNORE', + }, + unique: { + name: null, + conflict: 'REPLACE', + }, + check: { + name: null, + expression: ['"foo"', '!=', '42'], + }, + default: { + name: null, + value: 'NULL', + expression: false, + }, + collate: { + name: null, + collation: 'BINARY', + }, + references: { + name: null, + table: 'other', + columns: ['baz', 'bar', 'lar'], + delete: null, + update: 'SET NULL', + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + as: { + name: null, + generated: true, + expression: ['count', ['*']], + mode: 'STORED', + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition named', function () { + const sql = + 'CREATE TABLE "users" ("foo" UNSIGNED BIG INT CONSTRAINT "primary_constraint" PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT CONSTRAINT "notnull_constraint" NOT NULL ON CONFLICT FAIL CONSTRAINT "null_constraint" NULL ON CONFLICT IGNORE CONSTRAINT "unique_constraint" UNIQUE ON CONFLICT REPLACE CONSTRAINT "check_constraint" CHECK ("foo" != 42) CONSTRAINT "default_constraint" DEFAULT NULL CONSTRAINT "collate_constraint" COLLATE BINARY CONSTRAINT "references_constraint" REFERENCES "other" ("baz", "bar", "lar") ON UPDATE SET NULL MATCH SIMPLE NOT DEFERRABLE CONSTRAINT "as_constraint" GENERATED ALWAYS AS (count(*)) STORED)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: 'UNSIGNED BIG INT', + constraints: { + primary: { + name: 'primary_constraint', + order: 'DESC', + conflict: 'ROLLBACK', + autoincrement: true, + }, + notnull: { + name: 'notnull_constraint', + conflict: 'FAIL', + }, + null: { + name: 'null_constraint', + conflict: 'IGNORE', + }, + unique: { + name: 'unique_constraint', + conflict: 'REPLACE', + }, + check: { + name: 'check_constraint', + expression: ['"foo"', '!=', '42'], + }, + default: { + name: 'default_constraint', + value: 'NULL', + expression: false, + }, + collate: { + name: 'collate_constraint', + collation: 'BINARY', + }, + references: { + name: 'references_constraint', + table: 'other', + columns: ['baz', 'bar', 'lar'], + delete: null, + update: 'SET NULL', + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + as: { + name: 'as_constraint', + generated: true, + expression: ['count', ['*']], + mode: 'STORED', + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition multiple', function () { + const sql = + 'CREATE TABLE "users" ("primary_column" BLOB CONSTRAINT "primary_constraint" PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT, "notnull_column" DOUBLE PRECISION NOT NULL ON CONFLICT FAIL, "null_column" NATIVE CHARACTER(70) NULL ON CONFLICT IGNORE, "unique_column" CONSTRAINT "unique_constraint" UNIQUE ON CONFLICT REPLACE, "check_column" CHECK ("foo" != 42), "default_column" INT8 DEFAULT NULL, "collate_column" CONSTRAINT "collate_constraint" COLLATE BINARY, "references_column" NUMERIC REFERENCES "other" ("baz", "bar", "lar") ON UPDATE SET NULL MATCH SIMPLE NOT DEFERRABLE, "as_column" CONSTRAINT "as_constraint" GENERATED ALWAYS AS (count(*)) STORED)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'primary_column', + type: 'BLOB', + constraints: { + primary: { + name: 'primary_constraint', + order: 'DESC', + conflict: 'ROLLBACK', + autoincrement: true, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'notnull_column', + type: 'DOUBLE PRECISION', + constraints: { + primary: null, + notnull: { + name: null, + conflict: 'FAIL', + }, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'null_column', + type: 'NATIVE CHARACTER(70)', + constraints: { + primary: null, + notnull: null, + null: { + name: null, + conflict: 'IGNORE', + }, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'unique_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: { + name: 'unique_constraint', + conflict: 'REPLACE', + }, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'check_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: { + name: null, + expression: ['"foo"', '!=', '42'], + }, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'default_column', + type: 'INT8', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: { + name: null, + value: 'NULL', + expression: false, + }, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'collate_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: { + name: 'collate_constraint', + collation: 'BINARY', + }, + references: null, + as: null, + }, + }, + { + name: 'references_column', + type: 'NUMERIC', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: ['baz', 'bar', 'lar'], + delete: null, + update: 'SET NULL', + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + as: null, + }, + }, + { + name: 'as_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: { + name: 'as_constraint', + generated: true, + expression: ['count', ['*']], + mode: 'STORED', + }, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint primary key', function () { + const sql = 'CREATE TABLE "users" ("foo", PRIMARY KEY ("foo"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + ], + conflict: null, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint primary key conflict', function () { + const sql = + 'CREATE TABLE "users" ("foo", PRIMARY KEY ("foo") ON CONFLICT ROLLBACK)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + ], + conflict: 'ROLLBACK', + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint primary key column collation and order', function () { + const sql = + 'CREATE TABLE "users" ("foo", PRIMARY KEY ("foo", "baz" COLLATE BINARY, "bar" ASC, "lar" COLLATE NOCASE DESC))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + { + name: 'baz', + expression: false, + collation: 'BINARY', + order: null, + }, + { + name: 'bar', + expression: false, + collation: null, + order: 'ASC', + }, + { + name: 'lar', + expression: false, + collation: 'NOCASE', + order: 'DESC', + }, + ], + conflict: null, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint primary key column expression', function () { + const sql = + 'CREATE TABLE "users" ("foo", PRIMARY KEY (abs(foo), random() COLLATE RTRIM, baz + bar ASC))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: ['abs', ['foo']], + expression: true, + collation: null, + order: null, + }, + { + name: ['random', []], + expression: true, + collation: 'RTRIM', + order: null, + }, + { + name: ['baz', '+', 'bar'], + expression: true, + collation: null, + order: 'ASC', + }, + ], + conflict: null, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint unique', function () { + const sql = 'CREATE TABLE "users" ("foo", UNIQUE ("foo"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'UNIQUE', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + ], + conflict: null, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint check', function () { + const sql = 'CREATE TABLE "users" ("foo", CHECK ("foo" IS NULL))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'CHECK', + name: null, + expression: ['"foo"', 'IS', 'NULL'], + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other")'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: [], + delete: null, + update: null, + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key referenced column', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" ("bar"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: ['bar'], + delete: null, + update: null, + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key multiple columns', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo", "baz") REFERENCES "other" ("bar", "lar"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo', 'baz'], + references: { + table: 'other', + columns: ['bar', 'lar'], + delete: null, + update: null, + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key on delete', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" ON DELETE SET NULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: [], + delete: 'SET NULL', + update: null, + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key on update', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" ON UPDATE CASCADE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: [], + delete: null, + update: 'CASCADE', + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key match', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" MATCH FULL)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: [], + delete: null, + update: null, + match: 'FULL', + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key deferrable', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" NOT DEFERRABLE INITIALLY DEFERRED)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: [], + delete: null, + update: null, + match: null, + deferrable: { + not: true, + initially: 'DEFERRED', + }, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint foreign key all', function () { + const sql = + 'CREATE TABLE "users" ("foo", FOREIGN KEY ("foo") REFERENCES "other" ("bar") ON DELETE RESTRICT ON UPDATE NO ACTION MATCH PARTIAL DEFERRABLE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: null, + columns: ['foo'], + references: { + table: 'other', + columns: ['bar'], + delete: 'RESTRICT', + update: 'NO ACTION', + match: 'PARTIAL', + deferrable: { + not: false, + initially: null, + }, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint named', function () { + const sql = + 'CREATE TABLE "users" ("foo", CONSTRAINT "primary_constraint" PRIMARY KEY ("foo"))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: 'primary_constraint', + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + ], + conflict: null, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('table constraint multiple', function () { + const sql = + 'CREATE TABLE "users" ("foo", PRIMARY KEY ("foo", "baz" DESC) ON CONFLICT IGNORE, CHECK ("foo" IS NULL), CONSTRAINT "check_42" CHECK (count(foo) < 42 AND bar = 42), CONSTRAINT "unique_bar" UNIQUE ("bar"), FOREIGN KEY ("bar") REFERENCES "other" ("foo") ON DELETE SET DEFAULT ON UPDATE SET DEFAULT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT "foreign_other_multiple" FOREIGN KEY ("bar", "lar") REFERENCES "other" MATCH SIMPLE NOT DEFERRABLE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + { + name: 'baz', + expression: false, + collation: null, + order: 'DESC', + }, + ], + conflict: 'IGNORE', + }, + { + type: 'CHECK', + name: null, + expression: ['"foo"', 'IS', 'NULL'], + }, + { + type: 'CHECK', + name: 'check_42', + expression: ['count', ['foo'], '<', '42', 'AND', 'bar', '=', '42'], + }, + { + type: 'UNIQUE', + name: 'unique_bar', + columns: [ + { + name: 'bar', + expression: false, + collation: null, + order: null, + }, + ], + conflict: null, + }, + { + type: 'FOREIGN KEY', + name: null, + columns: ['bar'], + references: { + table: 'other', + columns: ['foo'], + delete: 'SET DEFAULT', + update: 'SET DEFAULT', + match: null, + deferrable: { + not: false, + initially: 'IMMEDIATE', + }, + }, + }, + { + type: 'FOREIGN KEY', + name: 'foreign_other_multiple', + columns: ['bar', 'lar'], + references: { + table: 'other', + columns: [], + delete: null, + update: null, + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column definition multiple table constraint multiple', function () { + const sql = + 'CREATE TABLE "users" ("primary_column" BLOB CONSTRAINT "primary_constraint" PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT, "notnull_column" DOUBLE PRECISION NOT NULL ON CONFLICT FAIL, "null_column" NATIVE CHARACTER(70) NULL ON CONFLICT IGNORE, "unique_column" CONSTRAINT "unique_constraint" UNIQUE ON CONFLICT REPLACE, "check_column" CHECK ("foo" != 42), "default_column" TEXT DEFAULT NULL, "collate_column" CONSTRAINT "collate_constraint" COLLATE BINARY, "references_column" NUMERIC REFERENCES "other" ("baz", "bar", "lar") ON UPDATE SET NULL MATCH SIMPLE NOT DEFERRABLE, "as_column" CONSTRAINT "as_constraint" GENERATED ALWAYS AS (count(*)) STORED, PRIMARY KEY ("foo", "baz" DESC) ON CONFLICT IGNORE, CHECK ("foo" IS NULL), CONSTRAINT "check_42" CHECK (count(foo) < 42 AND bar = 42), CONSTRAINT "unique_bar" UNIQUE ("bar"), FOREIGN KEY ("bar") REFERENCES "other" ("foo") ON DELETE SET DEFAULT ON UPDATE SET DEFAULT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT "foreign_other_multiple" FOREIGN KEY ("bar", "lar") REFERENCES "other" MATCH SIMPLE NOT DEFERRABLE)'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'primary_column', + type: 'BLOB', + constraints: { + primary: { + name: 'primary_constraint', + order: 'DESC', + conflict: 'ROLLBACK', + autoincrement: true, + }, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'notnull_column', + type: 'DOUBLE PRECISION', + constraints: { + primary: null, + notnull: { + name: null, + conflict: 'FAIL', + }, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'null_column', + type: 'NATIVE CHARACTER(70)', + constraints: { + primary: null, + notnull: null, + null: { + name: null, + conflict: 'IGNORE', + }, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'unique_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: { + name: 'unique_constraint', + conflict: 'REPLACE', + }, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'check_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: { + name: null, + expression: ['"foo"', '!=', '42'], + }, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'default_column', + type: 'TEXT', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: { + name: null, + value: 'NULL', + expression: false, + }, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'collate_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: { + name: 'collate_constraint', + collation: 'BINARY', + }, + references: null, + as: null, + }, + }, + { + name: 'references_column', + type: 'NUMERIC', + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: { + name: null, + table: 'other', + columns: ['baz', 'bar', 'lar'], + delete: null, + update: 'SET NULL', + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + as: null, + }, + }, + { + name: 'as_column', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: { + name: 'as_constraint', + generated: true, + expression: ['count', ['*']], + mode: 'STORED', + }, + }, + }, + ], + constraints: [ + { + type: 'PRIMARY KEY', + name: null, + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + { + name: 'baz', + expression: false, + collation: null, + order: 'DESC', + }, + ], + conflict: 'IGNORE', + }, + { + type: 'CHECK', + name: null, + expression: ['"foo"', 'IS', 'NULL'], + }, + { + type: 'CHECK', + name: 'check_42', + expression: ['count', ['foo'], '<', '42', 'AND', 'bar', '=', '42'], + }, + { + type: 'UNIQUE', + name: 'unique_bar', + columns: [ + { + name: 'bar', + expression: false, + collation: null, + order: null, + }, + ], + conflict: null, + }, + { + type: 'FOREIGN KEY', + name: null, + columns: ['bar'], + references: { + table: 'other', + columns: ['foo'], + delete: 'SET DEFAULT', + update: 'SET DEFAULT', + match: null, + deferrable: { + not: false, + initially: 'IMMEDIATE', + }, + }, + }, + { + type: 'FOREIGN KEY', + name: 'foreign_other_multiple', + columns: ['bar', 'lar'], + references: { + table: 'other', + columns: [], + delete: null, + update: null, + match: 'SIMPLE', + deferrable: { + not: true, + initially: null, + }, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('special character identifier', function () { + const sql = + 'CREATE TABLE "$chem@"."users table" (" ( ", "[`""foo""`]", " INTEGER ", " PRIMARY KEY ", " ) WITHOUT ROWID")'; + const ast = { + temporary: false, + exists: false, + schema: '$chem@', + table: 'users table', + columns: [ + { + name: ' ( ', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: '[`""foo""`]', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: ' INTEGER ', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: ' PRIMARY KEY ', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: ' ) WITHOUT ROWID', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('no wrap', function () { + const sql = + 'CREATE TABLE users (note, foo, CONSTRAINT foreign_constraint FOREIGN KEY (note, foo) REFERENCES other (baz, bar))'; + const ast = { + temporary: false, + exists: false, + schema: null, + table: 'users', + columns: [ + { + name: 'note', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + { + name: 'foo', + type: null, + constraints: { + primary: null, + notnull: null, + null: null, + unique: null, + check: null, + default: null, + collate: null, + references: null, + as: null, + }, + }, + ], + constraints: [ + { + type: 'FOREIGN KEY', + name: 'foreign_constraint', + columns: ['note', 'foo'], + references: { + table: 'other', + columns: ['baz', 'bar'], + delete: null, + update: null, + match: null, + deferrable: null, + }, + }, + ], + rowid: false, + }; + + const parsed = parseCreateTable(sql); + const compiled = compileCreateTable(ast); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('ordering', function () { + const sql = + 'CREATE TABLE "users" ("foo" TEXT REFERENCES "other" ("baz", "bar", "lar") MATCH PARTIAL ON UPDATE NO ACTION ON DELETE RESTRICT DEFERRABLE CHECK ("foo" != 42) GENERATED ALWAYS AS (count(*)) STORED UNIQUE ON CONFLICT REPLACE NOT NULL ON CONFLICT FAIL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT DEFAULT NULL COLLATE BINARY NULL ON CONFLICT IGNORE)'; + const newSql = + 'CREATE TABLE "users" ("foo" TEXT PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT NOT NULL ON CONFLICT FAIL NULL ON CONFLICT IGNORE UNIQUE ON CONFLICT REPLACE CHECK ("foo" != 42) DEFAULT NULL COLLATE BINARY REFERENCES "other" ("baz", "bar", "lar") ON DELETE RESTRICT ON UPDATE NO ACTION MATCH PARTIAL DEFERRABLE GENERATED ALWAYS AS (count(*)) STORED)'; + + const parsedSql = compileCreateTable(parseCreateTable(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + + it('lowercase', function () { + const sql = + 'create table "users" ("primary_column" blob constraint "primary_constraint" primary key desc on conflict rollback autoincrement, "notnull_column" double precision not null on conflict fail, "null_column" native character(70) null on conflict ignore, "unique_column" constraint "unique_constraint" unique on conflict replace, "check_column" check ("foo" != 42), "default_column" text default null, "collate_column" constraint "collate_constraint" collate binary, "references_column" numeric references "other" ("baz", "bar", "lar") on update set null match simple not deferrable, "as_column" constraint "as_constraint" generated always as (count(*)) stored, primary key ("foo", "baz" desc) on conflict ignore, check ("foo" is null), constraint "check_42" check (count(foo) < 42 AND bar = 42), constraint "unique_bar" unique ("bar"), foreign key ("bar") references "other" ("foo") on delete set default on update set default deferrable initially immediate, constraint "foreign_other_multiple" foreign key ("bar", "lar") references "other" match simple not deferrable)'; + const newSql = + 'CREATE TABLE "users" ("primary_column" blob CONSTRAINT "primary_constraint" PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT, "notnull_column" double precision NOT NULL ON CONFLICT FAIL, "null_column" native character(70) NULL ON CONFLICT IGNORE, "unique_column" CONSTRAINT "unique_constraint" UNIQUE ON CONFLICT REPLACE, "check_column" CHECK ("foo" != 42), "default_column" text DEFAULT null, "collate_column" CONSTRAINT "collate_constraint" COLLATE binary, "references_column" numeric REFERENCES "other" ("baz", "bar", "lar") ON UPDATE SET NULL MATCH simple NOT DEFERRABLE, "as_column" CONSTRAINT "as_constraint" GENERATED ALWAYS AS (count(*)) STORED, PRIMARY KEY ("foo", "baz" DESC) ON CONFLICT IGNORE, CHECK ("foo" is null), CONSTRAINT "check_42" CHECK (count(foo) < 42 AND bar = 42), CONSTRAINT "unique_bar" UNIQUE ("bar"), FOREIGN KEY ("bar") REFERENCES "other" ("foo") ON DELETE SET DEFAULT ON UPDATE SET DEFAULT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT "foreign_other_multiple" FOREIGN KEY ("bar", "lar") REFERENCES "other" MATCH simple NOT DEFERRABLE)'; + + const parsedSql = compileCreateTable(parseCreateTable(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + + it('whitespaces', function () { + const sql = + 'CREATE TABLE "users table"("foo",\t"bar"CHECK("foo"\n!=\t42), CONSTRAINT\r\n"foreign_constraint" \t FOREIGN \n KEY("foo","bar")REFERENCES \r\n "other"("baz","bar"))'; + const newSql = + 'CREATE TABLE "users table" ("foo", "bar" CHECK ("foo" != 42), CONSTRAINT "foreign_constraint" FOREIGN KEY ("foo", "bar") REFERENCES "other" ("baz", "bar"))'; + + const parsedSql = compileCreateTable(parseCreateTable(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + + it('wrap', function () { + const sql = + 'CREATE TABLE "schema".users (`foo`, [bar], \'baz\' DEFAULT "lar", CONSTRAINT [foreign_constraint] FOREIGN KEY (foo, `bar`) REFERENCES other ([baz], "bar"))'; + const newSql = + 'CREATE TABLE "schema"."users" ("foo", "bar", "baz" DEFAULT "lar", CONSTRAINT "foreign_constraint" FOREIGN KEY ("foo", "bar") REFERENCES "other" ("baz", "bar"))'; + + const parsedSql = compileCreateTable(parseCreateTable(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + }); + + describe('create index', function () { + it('basic', function () { + const sql = 'CREATE INDEX "users_index" on "users" ("foo")'; + const ast = { + unique: false, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { name: 'foo', expression: false, collation: null, order: null }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('unique', function () { + const sql = 'CREATE UNIQUE INDEX "users_index" on "users" ("foo")'; + const ast = { + unique: true, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { name: 'foo', expression: false, collation: null, order: null }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('if not exists', function () { + const sql = 'CREATE INDEX IF NOT EXISTS "users_index" on "users" ("foo")'; + const ast = { + unique: false, + exists: true, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { name: 'foo', expression: false, collation: null, order: null }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('schema', function () { + const sql = 'CREATE INDEX "schema"."users_index" on "users" ("foo")'; + const ast = { + unique: false, + exists: false, + schema: 'schema', + index: 'users_index', + table: 'users', + columns: [ + { name: 'foo', expression: false, collation: null, order: null }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('where', function () { + const sql = + 'CREATE INDEX "users_index" on "users" ("foo") where foo IS NOT NULL AND bar = 42'; + const ast = { + unique: false, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { name: 'foo', expression: false, collation: null, order: null }, + ], + where: ['foo', 'IS', 'NOT', 'NULL', 'AND', 'bar', '=', '42'], + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column collation and order', function () { + const sql = + 'CREATE INDEX "users_index" on "users" ("foo", "baz" COLLATE BINARY, "bar" ASC, "lar" COLLATE NOCASE DESC)'; + const ast = { + unique: false, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { + name: 'foo', + expression: false, + collation: null, + order: null, + }, + { + name: 'baz', + expression: false, + collation: 'BINARY', + order: null, + }, + { + name: 'bar', + expression: false, + collation: null, + order: 'ASC', + }, + { + name: 'lar', + expression: false, + collation: 'NOCASE', + order: 'DESC', + }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('column expression', function () { + const sql = + 'CREATE INDEX "users_index" on "users" (abs(foo), random() COLLATE RTRIM, baz + bar ASC)'; + const ast = { + unique: false, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { + name: ['abs', ['foo']], + expression: true, + collation: null, + order: null, + }, + { + name: ['random', []], + expression: true, + collation: 'RTRIM', + order: null, + }, + { + name: ['baz', '+', 'bar'], + expression: true, + collation: null, + order: 'ASC', + }, + ], + where: null, + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('special character identifier', function () { + const sql = + 'CREATE INDEX "$chema"."users index" on "use-rs" (" ( ", " COLLATE ", " ""foo"" " ASC, "``b a z``" DESC, "[[``""bar""``]]") where foo IS NOT NULL'; + const ast = { + unique: false, + exists: false, + schema: '$chema', + index: 'users index', + table: 'use-rs', + columns: [ + { + name: ' ( ', + expression: false, + collation: null, + order: null, + }, + { + name: ' COLLATE ', + expression: false, + collation: null, + order: null, + }, + { + name: ' ""foo"" ', + expression: false, + collation: null, + order: 'ASC', + }, + { + name: '``b a z``', + expression: false, + collation: null, + order: 'DESC', + }, + { + name: '[[``""bar""``]]', + expression: false, + collation: null, + order: null, + }, + ], + where: ['foo', 'IS', 'NOT', 'NULL'], + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast, wrap); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('no wrap', function () { + const sql = + 'CREATE INDEX users_index on users (note COLLATE BINARY ASC) where foo IS NOT NULL'; + const ast = { + unique: false, + exists: false, + schema: null, + index: 'users_index', + table: 'users', + columns: [ + { + name: 'note', + expression: false, + collation: 'BINARY', + order: 'ASC', + }, + ], + where: ['foo', 'IS', 'NOT', 'NULL'], + }; + + const parsed = parseCreateIndex(sql); + const compiled = compileCreateIndex(ast); + + expect(parsed).to.deep.equal(ast); + expect(compiled).to.equal(sql); + }); + + it('lowercase', function () { + const sql = + 'create index "users_index" ON "users" ("foo" collate BINARY asc) WHERE "foo" is not null'; + const newSql = + 'CREATE INDEX "users_index" on "users" ("foo" COLLATE BINARY ASC) where "foo" is not null'; + + const parsedSql = compileCreateIndex(parseCreateIndex(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + + it('whitespaces', function () { + const sql = + 'CREATE INDEX "users index"\non\t"users"("foo" COLLATE\tBINARY\nASC)\r\nwhere "foo"\n IS\tNOT\r\nNULL'; + const newSql = + 'CREATE INDEX "users index" on "users" ("foo" COLLATE BINARY ASC) where "foo" IS NOT NULL'; + + const parsedSql = compileCreateIndex(parseCreateIndex(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + + it('wrap', function () { + const sql = + 'CREATE INDEX "schema".[users index] on `users` ("foo " ASC, [b a z] DESC, ` bar`, \'lar\')'; + const newSql = + 'CREATE INDEX "schema"."users index" on "users" ("foo " ASC, "b a z" DESC, " bar", "lar")'; + + const parsedSql = compileCreateIndex(parseCreateIndex(sql), wrap); + + expect(parsedSql).to.equal(newSql); + }); + }); +}); diff --git a/test/util/db-helpers.js b/test/util/db-helpers.js index fe159bc158..93edd99332 100644 --- a/test/util/db-helpers.js +++ b/test/util/db-helpers.js @@ -52,17 +52,20 @@ function isRedshift(knex) { } function isSQLite(knex) { - return getDriverName(knex) === drivers.SQLite; + return getDriverName(knex) === drivers.SQLite || isBetterSQLite3(knex); } function isCockroachDB(knex) { return getDriverName(knex) === drivers.CockroachDB; } +function isBetterSQLite3(knex) { + return getDriverName(knex) === drivers.BetterSQLite3; +} /** * * @param knex - * @param {('pg'|'pgnative'|'pg-redshift'|'oracledb'|'mysql'|'mysql2'|'mssql'|'sqlite3'|'cockroachdb')[]} supportedDbs - supported DB values in DRIVER_NAMES from lib/constants. + * @param {('pg'|'pgnative'|'pg-redshift'|'oracledb'|'mysql'|'mysql2'|'mssql'|'sqlite3'|'cockroachdb'|'better-sqlite3')[]} supportedDbs - supported DB values in DRIVER_NAMES from lib/constants. * @returns {*} */ function isOneOfDbs(knex, supportedDbs) { @@ -82,4 +85,5 @@ module.exports = { isPgBasedDriverName, isRedshift, isSQLite, + isBetterSQLite3, };