diff --git a/lib/client.js b/lib/client.js index 00ee2893a9..5b1bd9d31d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -194,6 +194,7 @@ class Client extends EventEmitter { } initializeDriver() { + if (this.driver) return; try { this.driver = this._driver(); } catch (e) { diff --git a/lib/dialects/sqlite3/index.js b/lib/dialects/sqlite3/index.js index 06fb9b11f0..4cf727c27f 100644 --- a/lib/dialects/sqlite3/index.js +++ b/lib/dialects/sqlite3/index.js @@ -16,7 +16,7 @@ const ViewCompiler = require('./schema/sqlite-viewcompiler'); const SQLite3_DDL = require('./schema/ddl'); const Formatter = require('../../formatter'); const QueryBuilder = require('./query/sqlite-querybuilder'); -const { parseVersion, compareVersions } = require('../../util/version'); +const { parseVersion, satisfiesVersion } = require('../../util/version'); class Client_SQLite3 extends Client { constructor(config) { @@ -34,7 +34,7 @@ class Client_SQLite3 extends Client { this.logger.warn( 'sqlite does not support inserting default values. Set the ' + '`useNullAsDefault` flag to hide this warning. ' + - '(see docs http://knexjs.org/#Builder-insert).' + '(see docs https://knexjs.org/guide/query-builder.html#insert).' ); } } @@ -44,12 +44,13 @@ class Client_SQLite3 extends Client { } /** - * Get `Version` from client or return `[0, 0, 0]` + * Get `Version` from client or return `[0, 0, 0]` on failure. * * @returns {[number, number, number]} */ _clientVersion() { - const version = parseVersion(this.client.VERSION); + this.initializeDriver(); + const version = parseVersion(this.driver.VERSION); if (!version) return [0, 0, 0]; return version; } @@ -62,7 +63,7 @@ class Client_SQLite3 extends Client { * @returns {boolean} */ _satisfiesVersion(minVersion, maxVersion) { - return compareVersions(this._clientVersion(), minVersion, maxVersion); + return satisfiesVersion(this._clientVersion(), minVersion, maxVersion); } schemaCompiler() { diff --git a/lib/dialects/sqlite3/query/sqlite-querycompiler.js b/lib/dialects/sqlite3/query/sqlite-querycompiler.js index 528231eae8..de98d301ce 100644 --- a/lib/dialects/sqlite3/query/sqlite-querycompiler.js +++ b/lib/dialects/sqlite3/query/sqlite-querycompiler.js @@ -104,7 +104,7 @@ class QueryCompiler_SQLite3 extends QueryCompiler { parametersArray.push(`(${parameter})`); } // 'insert into TABLE_NAME (column1, column2, ...) values (v1, v2, ...), (v3, v4, ...), ...' - sql += ` values (${parametersArray.join(', ')})`; + sql += ` values ${parametersArray.join(', ')}`; const { onConflict, ignore, merge } = this.single; if (onConflict && ignore) sql += this._ignore(onConflict); diff --git a/lib/util/version.ts b/lib/util/version.ts index dc2fedd72d..7cc39aa49f 100644 --- a/lib/util/version.ts +++ b/lib/util/version.ts @@ -2,7 +2,7 @@ export type Version = [number, number, number]; /** Helper function to check `x` a integer where `x >= 0` */ -function isNonNegInt(x: unknown): x is number { +export function isNonNegInt(x: unknown): x is number { return typeof x === 'number' && Number.isInteger(x) && x >= 0; } @@ -18,7 +18,9 @@ export function isVersion(x: unknown): x is Version { /** Parses given string into `Version` or returns `undefined` */ export function parseVersion(x: string): Version | undefined { const versionRegex = /^(\d+)\.(\d+)\.(\d+)/m; - const versionNumbers = (versionRegex.exec(x) ?? []).slice(1, 4); + const versionNumbers = (versionRegex.exec(x) ?? []) + .slice(1, 4) + .map((x) => parseInt(x)); if (!isVersion(versionNumbers)) return undefined; return versionNumbers; } @@ -57,7 +59,7 @@ export function compareVersions(v1: Version, v2: Version): 1 | 0 | -1 { } /** - * Returns `boolean` for if a given `version` satisfies the given `min` and `max`. + * Returns `boolean` for if a given `version` satisfies the given `min` (inclusive) and `max` (exclusive). * * This will throw an error if: * diff --git a/test/cli/knexfile-test.spec.js b/test/cli/knexfile-test.spec.js index 37b6ef1799..81c9c10b52 100644 --- a/test/cli/knexfile-test.spec.js +++ b/test/cli/knexfile-test.spec.js @@ -4,7 +4,6 @@ const path = require('path'); const tildify = require('tildify'); const { FileTestHelper, execCommand } = require('cli-testlab'); -const color = require('colorette'); const KNEX = path.normalize(__dirname + '/../../bin/cli.js'); @@ -101,9 +100,7 @@ module.exports = { return execCommand( `node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile.js --knexpath=../knex.js`, { - expectedOutput: `Working directory changed to ${color.magenta( - expectedCWD - )}`, + expectedOutput: `Working directory changed to ${expectedCWD}`, } ); }); @@ -122,9 +119,7 @@ module.exports = { return execCommand( `node ${KNEX} migrate:latest --knexfile=test/jake-util/knexfile-relative/knexfile-with-resolve.js --knexpath=../knex.js`, { - expectedOutput: `Working directory changed to ${color.magenta( - expectedCWD - )}`, + expectedOutput: `Working directory changed to ${expectedCWD}`, } ); }); diff --git a/test/db-less-test-suite.js b/test/db-less-test-suite.js index 8f2dc9357e..c49d6a9a62 100644 --- a/test/db-less-test-suite.js +++ b/test/db-less-test-suite.js @@ -11,6 +11,7 @@ describe('Util Tests', function () { require('./unit/util/save-async-stack'); require('./unit/util/comma-no-paren-regex'); require('./unit/util/security'); + require('./unit/util/version'); }); describe('Query Building Tests', function () { diff --git a/test/integration2/query/insert/inserts.spec.js b/test/integration2/query/insert/inserts.spec.js index 31ecdab97f..ee6bef5e49 100644 --- a/test/integration2/query/insert/inserts.spec.js +++ b/test/integration2/query/insert/inserts.spec.js @@ -256,7 +256,7 @@ describe('Inserts', function () { ); tester( 'sqlite3', - 'insert into `accounts` (`about`, `created_at`, `email`, `first_name`, `last_name`, `logins`, `updated_at`) select ? as `about`, ? as `created_at`, ? as `email`, ? as `first_name`, ? as `last_name`, ? as `logins`, ? as `updated_at` union all select ? as `about`, ? as `created_at`, ? as `email`, ? as `first_name`, ? as `last_name`, ? as `logins`, ? as `updated_at` returning `id`', + 'insert into `accounts` (`about`, `created_at`, `email`, `first_name`, `last_name`, `logins`, `updated_at`) values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?) returning `id`', [ 'Lorem ipsum Dolore labore incididunt enim.', TEST_TIMESTAMP, @@ -474,7 +474,7 @@ describe('Inserts', function () { ); tester( 'sqlite3', - 'insert into `accounts` (`about`, `created_at`, `email`, `first_name`, `last_name`, `logins`, `updated_at`) select ? as `about`, ? as `created_at`, ? as `email`, ? as `first_name`, ? as `last_name`, ? as `logins`, ? as `updated_at` union all select ? as `about`, ? as `created_at`, ? as `email`, ? as `first_name`, ? as `last_name`, ? as `logins`, ? as `updated_at` returning `id`', + 'insert into `accounts` (`about`, `created_at`, `email`, `first_name`, `last_name`, `logins`, `updated_at`) values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?) returning `id`', [ 'Lorem ipsum Dolore labore incididunt enim.', TEST_TIMESTAMP, @@ -1974,7 +1974,7 @@ describe('Inserts', function () { ); tester( 'sqlite3', - 'insert into `upsert_tests` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name` where true on conflict (`email`) do update set `email` = excluded.`email`, `name` = excluded.`name` returning `email`', + 'insert into `upsert_tests` (`email`, `name`) values (?, ?), (?, ?) on conflict (`email`) do update set `email` = excluded.`email`, `name` = excluded.`name` returning `email`', ['two@example.com', 'AFTER', 'three@example.com', 'AFTER'] ); }); @@ -2055,8 +2055,7 @@ describe('Inserts', function () { ); tester( 'sqlite3', - 'insert into `upsert_tests` (`email`, `name`, `type`) select ? as `email`, ? as `name`, ? as `type` union all select ? as `email`, ? as `name`, ? as `type` union all select ? as `email`, ? as `name`, ? as `type` where true ' + - "on conflict (email) where type = 'type1' do update set `email` = excluded.`email`, `name` = excluded.`name`, `type` = excluded.`type`", + "insert into `upsert_tests` (`email`, `name`, `type`) values (?, ?, ?), (?, ?, ?), (?, ?, ?) on conflict (email) where type = 'type1' do update set `email` = excluded.`email`, `name` = excluded.`name`, `type` = excluded.`type`", [ 'one@example.com', 'AFTER', @@ -2315,6 +2314,72 @@ describe('Inserts', function () { assertJsonEquals(result[0].content, arrayOfObject); }); }); + + describe(`#5769 sqlite multi-insert uses standard syntax and supports JSON`, function () { + it(`should JSON.stringify Sqlite multi-insert of Objects`, async function () { + if (!isSQLite(knex)) { + return this.skip(); + } + + knex(tableName) + .insert([{ data: { a: 1 } }, { data: { b: 2 } }]) + .testSql(function (tester) { + tester( + 'sqlite3', + `insert into \`${tableName}\` (\`data\`) values (?), (?)`, + ['{"a":1}', '{"b":2}'], + [] + ); + }); + }); + + it(`should throw error if doing Sqlite multi-insert on pre-3.7.11 version`, async function () { + if (!isSQLite(knex)) { + return this.skip(); + } + + const realClientVersion = knex.client._clientVersion; + knex.client._clientVersion = () => [3, 7, 10]; + + try { + expect(() => { + knex(tableName) + .insert([{ a: 1 }, { a: 2 }]) + .testSql(function (tester) { + tester('sqlite3', 'NA', [], []); + }); + }).to.throw('Requires Sqlite 3.7.11 or newer to do multi-insert'); + } finally { + knex.client._clientVersion = realClientVersion; + expect(knex.client._clientVersion()).to.not.equal([3, 7, 10]); + } + }); + + it(`should NOT throw error if doing Sqlite multi-insert on 3.7.11 version`, async function () { + if (!isSQLite(knex)) { + return this.skip(); + } + + const realClientVersion = knex.client._clientVersion; + knex.client._clientVersion = () => [3, 7, 11]; + + try { + knex(tableName) + .insert([{ id: 1 }, { id: 2 }], 'id') + .testSql(function (tester) { + tester( + 'sqlite3', + `insert into \`${tableName}\` (\`id\`) values (?), (?) returning \`id\``, + [1, 2], + [] + ); + }); + } finally { + knex.client._clientVersion = realClientVersion; + expect(knex.client._clientVersion()).to.not.equal([3, 7, 11]); + } + }); + }); }); }); }); diff --git a/test/jake-util/knexfile-relative/knexfile-with-resolve.js b/test/jake-util/knexfile-relative/knexfile-with-resolve.js index 52c4322529..c9ebf98a46 100644 --- a/test/jake-util/knexfile-relative/knexfile-with-resolve.js +++ b/test/jake-util/knexfile-relative/knexfile-with-resolve.js @@ -15,6 +15,7 @@ module.exports = { connection: { filename: __dirname + '/../test.sqlite3', }, + useNullAsDefault: true, migrations: { directory: MIGRATIONS_DIR, }, diff --git a/test/jake-util/knexfile-relative/knexfile.js b/test/jake-util/knexfile-relative/knexfile.js index 96b211eb2a..d561d72805 100644 --- a/test/jake-util/knexfile-relative/knexfile.js +++ b/test/jake-util/knexfile-relative/knexfile.js @@ -3,6 +3,7 @@ module.exports = { connection: { filename: __dirname + '/../test.sqlite3', }, + useNullAsDefault: true, migrations: { directory: './knexfile_migrations', }, diff --git a/test/unit/query/builder.js b/test/unit/query/builder.js index cc3b160d01..a4a2a18e19 100644 --- a/test/unit/query/builder.js +++ b/test/unit/query/builder.js @@ -208,7 +208,8 @@ describe('Custom identifier wrapping', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users_wrapper_was_here` (`email_wrapper_was_here`, `name_wrapper_was_here`) select ? as `email_wrapper_was_here`, ? as `name_wrapper_was_here` union all select ? as `email_wrapper_was_here`, ? as `name_wrapper_was_here` returning `id_wrapper_was_here`', + sql: 'insert into `users_wrapper_was_here` (`email_wrapper_was_here`, `name_wrapper_was_here`) values (?, ?), (?, ?) returning `id_wrapper_was_here`', + bindings: ['foo', 'taylor', 'bar', 'dayle'], }, pg: { sql: 'insert into "users_wrapper_was_here" ("email_wrapper_was_here", "name_wrapper_was_here") values (?, ?), (?, ?) returning "id_wrapper_was_here"', @@ -260,7 +261,7 @@ describe('Custom identifier wrapping', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users_wrapper_was_here` (`email_wrapper_was_here`, `name_wrapper_was_here`) select ? as `email_wrapper_was_here`, ? as `name_wrapper_was_here` union all select ? as `email_wrapper_was_here`, ? as `name_wrapper_was_here` returning `id_wrapper_was_here`, `name_wrapper_was_here`', + sql: 'insert into `users_wrapper_was_here` (`email_wrapper_was_here`, `name_wrapper_was_here`) values (?, ?), (?, ?) returning `id_wrapper_was_here`, `name_wrapper_was_here`', bindings: ['foo', 'taylor', 'bar', 'dayle'], }, pg: { @@ -5692,7 +5693,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name`', + sql: 'insert into `users` (`email`, `name`) values (?, ?), (?, ?)', bindings: ['foo', 'taylor', 'bar', 'dayle'], }, mssql: { @@ -5724,7 +5725,7 @@ describe('QueryBuilder', () => { mysql: "insert into `users` (`email`, `name`) values ('foo', 'taylor'), (NULL, 'dayle')", sqlite3: - "insert into `users` (`email`, `name`) select 'foo' as `email`, 'taylor' as `name` union all select NULL as `email`, 'dayle' as `name`", + "insert into `users` (`email`, `name`) values ('foo', 'taylor'), (NULL, 'dayle')", mssql: "insert into [users] ([email], [name]) values ('foo', 'taylor'), (NULL, 'dayle')", oracledb: @@ -5788,7 +5789,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name` returning `id`', + sql: 'insert into `users` (`email`, `name`) values (?, ?), (?, ?) returning `id`', }, pg: { sql: 'insert into "users" ("email", "name") values (?, ?), (?, ?) returning "id"', @@ -5839,7 +5840,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name` returning `id`, `name`', + sql: 'insert into `users` (`email`, `name`) values (?, ?), (?, ?) returning `id`, `name`', bindings: ['foo', 'taylor', 'bar', 'dayle'], }, pg: { @@ -5921,18 +5922,8 @@ describe('QueryBuilder', () => { bindings: [1, 2, 2, 3], }, sqlite3: { - sql: 'insert into `table` (`a`, `b`, `c`) select ? as `a`, ? as `b`, ? as `c` union all select ? as `a`, ? as `b`, ? as `c` union all select ? as `a`, ? as `b`, ? as `c`', - bindings: [ - 1, - undefined, - undefined, - undefined, - 2, - undefined, - 2, - undefined, - 3, - ], + sql: 'insert into `table` (`a`, `b`, `c`) values (?, ?, ?), (?, ?, ?), (?, ?, ?)', + bindings: [1, null, null, null, 2, null, 2, null, 3], }, mssql: { sql: 'insert into [table] ([a], [b], [c]) values (?, DEFAULT, DEFAULT), (DEFAULT, ?, DEFAULT), (?, DEFAULT, ?)', @@ -6560,7 +6551,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'bar'], }, sqlite3: { - sql: 'insert into `users` (`email`) select ? as `email` union all select ? as `email` where true on conflict (`email`) do nothing', + sql: 'insert into `users` (`email`) values (?), (?) on conflict (`email`) do nothing', bindings: ['foo', 'bar'], }, } @@ -6584,7 +6575,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'bar'], }, sqlite3: { - sql: 'insert into `users` (`email`) select ? as `email` union all select ? as `email` where true on conflict (value) WHERE deleted_at IS NULL do nothing', + sql: 'insert into `users` (`email`) values (?), (?) on conflict (value) WHERE deleted_at IS NULL do nothing', bindings: ['foo', 'bar'], }, } @@ -6631,7 +6622,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'taylor', 'bar', 'dayle', 'overidden'], }, sqlite3: { - sql: 'insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name` where true on conflict (`email`) do update set `name` = ?', + sql: 'insert into `users` (`email`, `name`) values (?, ?), (?, ?) on conflict (`email`) do update set `name` = ?', bindings: ['foo', 'taylor', 'bar', 'dayle', 'overidden'], }, pg: { @@ -6658,7 +6649,7 @@ describe('QueryBuilder', () => { bindings: ['foo', 'taylor', 'bar', 'dayle'], }, sqlite3: { - sql: 'insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name` where true on conflict (`email`) do update set `email` = excluded.`email`, `name` = excluded.`name`', + sql: 'insert into `users` (`email`, `name`) values (?, ?), (?, ?) on conflict (`email`) do update set `email` = excluded.`email`, `name` = excluded.`name`', bindings: ['foo', 'taylor', 'bar', 'dayle'], }, pg: { @@ -10057,7 +10048,7 @@ describe('QueryBuilder', () => { bindings: ['bob', 'thisMail', 'sam', 'thatMail', 'jack'], }, sqlite3: { - sql: 'with `withClause` as (select `foo` from `users` where `name` = ?) insert into `users` (`email`, `name`) select ? as `email`, ? as `name` union all select ? as `email`, ? as `name`', + sql: 'with `withClause` as (select `foo` from `users` where `name` = ?) insert into `users` (`email`, `name`) values (?, ?), (?, ?)', bindings: ['bob', 'thisMail', 'sam', 'thatMail', 'jack'], }, pg: { diff --git a/test/unit/util/version.js b/test/unit/util/version.js new file mode 100644 index 0000000000..6cb7fd94cc --- /dev/null +++ b/test/unit/util/version.js @@ -0,0 +1,372 @@ +const { + isNonNegInt, + isVersion, + parseVersion, + parseVersionOrError, + compareVersions, + satisfiesVersion, +} = require('../../../lib/util/version'); +const { expect } = require('chai'); + +describe('utils/version', () => { + describe('isNonNegInt', () => { + const scenarioMatrix = [ + { input: 0, result: true }, + { input: 1, result: true }, + { input: Number.MAX_SAFE_INTEGER, result: true }, + { input: -1, result: false }, + { input: 1.5, result: false }, + { input: -Number.MAX_SAFE_INTEGER, result: false }, + { input: -Number.MIN_VALUE, result: false }, + ]; + + scenarioMatrix.forEach(({ input, result }) => { + it(`returns ${result} for ${input}`, () => { + expect(isNonNegInt(input)).to.equal(result); + }); + }); + }); + + describe('isVersion', () => { + const scenarioMatrix = [ + { input: [0, 0, 0], result: true }, + { input: [3, 2, 1], result: true }, + { + input: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + result: true, + }, + // Non-arrays + { input: undefined, result: false }, + { input: null, result: false }, + { input: {}, result: false }, + { input: 'a', result: false }, + { input: true, result: false }, + // Non-three element arrays + { input: [], result: false }, + { input: [0], result: false }, + { input: [0, 0], result: false }, + { input: [0, 0, 0, 0], result: false }, + // Three element arrays with non-numbers + { input: [null, 0, 0], result: false }, + { input: [0, 0, null], result: false }, + { input: [undefined, 0, 0], result: false }, + { input: [0, 0, undefined], result: false }, + { input: [{}, 0, 0], result: false }, + { input: [0, 0, {}], result: false }, + { input: ['a', 0, 0], result: false }, + { input: [0, 0, 'a'], result: false }, + { input: [true, 0, 0], result: false }, + { input: [0, 0, true], result: false }, + // Three element arrays with invalid numbers + { input: [-1, 0, 0], result: false }, + { input: [0, 0, -1], result: false }, + { input: [1.5, 0, 0], result: false }, + { input: [0, 0, 1.5], result: false }, + { input: [-Number.MAX_SAFE_INTEGER, 0, 0], result: false }, + { input: [0, 0, -Number.MAX_SAFE_INTEGER], result: false }, + { input: [-Number.MIN_VALUE, 0, 0], result: false }, + { input: [0, 0, -Number.MIN_VALUE], result: false }, + ]; + + scenarioMatrix.forEach(({ input, result }) => { + it(`returns ${result} for ${JSON.stringify(input)}`, () => { + expect(isVersion(input)).to.equal(result); + }); + }); + }); + + const parseVersionScenarioMatrix = [ + { input: '0.0.0', result: [0, 0, 0] }, + { input: '3.2.1', result: [3, 2, 1] }, + { + input: `${Number.MAX_SAFE_INTEGER}.${Number.MAX_SAFE_INTEGER}.${Number.MAX_SAFE_INTEGER}`, + result: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + }, + { input: '3.2.1.0', result: [3, 2, 1] }, + { input: '3.2.1a', result: [3, 2, 1] }, + { input: '3.2.1-a', result: [3, 2, 1] }, + // Not three digits + { input: '', result: undefined }, + { input: 'a', result: undefined }, + { input: 'a.0.0', result: undefined }, + { input: '0.0.a', result: undefined }, + { input: 'a.0.0.0', result: undefined }, + { input: '0.0.a.0', result: undefined }, + // Three element arrays with invalid numbers + { input: '-1.0.0', result: undefined }, + { input: '0.0.-1', result: undefined }, + ]; + + describe('parseVersion', () => { + parseVersionScenarioMatrix.forEach(({ input, result }) => { + it(`returns ${JSON.stringify(result)} for '${input}'`, () => { + expect(parseVersion(input)).to.deep.equal(result); + }); + }); + }); + + describe('parseVersionOrError', () => { + parseVersionScenarioMatrix.forEach(({ input, result }) => { + const doesThrow = result === undefined; + const testText = doesThrow + ? 'throws error' + : `returns ${JSON.stringify(result)}`; + it(`${testText} for '${input}'`, () => { + if (doesThrow) { + expect(() => parseVersionOrError(input)).to.throw(); + } else { + expect(parseVersionOrError(input)).to.deep.equal(result); + } + }); + }); + }); + + describe('compareVersions', () => { + const scenarioMatrix = [ + // Equality + { v1: [0, 0, 0], v2: [0, 0, 0], result: 0 }, + { v1: [3, 2, 1], v2: [3, 2, 1], result: 0 }, + { + v1: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + v2: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + result: 0, + }, + // Major version difference + { v1: [4, 0, 0], v2: [3, 2, 1], result: 1 }, + { v1: [3, 2, 1], v2: [4, 0, 0], result: -1 }, + // Minor version difference + { v1: [3, 3, 0], v2: [3, 2, 1], result: 1 }, + { v1: [3, 2, 1], v2: [3, 3, 0], result: -1 }, + // Patch version difference + { v1: [3, 2, 2], v2: [3, 2, 1], result: 1 }, + { v1: [3, 2, 1], v2: [3, 2, 2], result: -1 }, + ]; + + scenarioMatrix.forEach(({ v1, v2, result }) => { + it(`returns ${result} for ${JSON.stringify({ v1, v2 })}`, () => { + expect(compareVersions(v1, v2)).to.equal(result); + }); + }); + }); + + describe('satisfiesVersion', () => { + const scenarioMatrix = [ + // Invalid inputs + { version: null, min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: 'a', min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: 1, min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: {}, min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: [], min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: [1], min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: [1, 2], min: [3, 2, 1], max: [3, 2, 1], throws: true }, + { version: [1, 2, 3, 4], min: [3, 2, 1], max: [3, 2, 1], throws: true }, + + // Neither min nor max + { version: [0, 0, 0], min: undefined, max: undefined, throws: true }, + + // Min only + // Equal to min + { version: [0, 0, 0], min: [0, 0, 0], max: undefined, result: true }, + { version: [3, 2, 1], min: [3, 2, 1], max: undefined, result: true }, + { + version: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + min: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + max: undefined, + result: true, + }, + // Exceeds min, major + { version: [4, 0, 0], min: [3, 2, 1], max: undefined, result: true }, + { + version: [Number.MAX_SAFE_INTEGER, 0, 0], + min: [3, 2, 1], + max: undefined, + result: true, + }, + // Exceeds min, minor + { version: [3, 3, 0], min: [3, 2, 1], max: undefined, result: true }, + { + version: [3, Number.MAX_SAFE_INTEGER, 0], + min: [3, 2, 1], + max: undefined, + result: true, + }, + // Exceeds min, patch + { version: [3, 2, 2], min: [3, 2, 1], max: undefined, result: true }, + { + version: [3, 2, Number.MAX_SAFE_INTEGER], + min: [3, 2, 1], + max: undefined, + result: true, + }, + // Below min, major + { version: [2, 2, 1], min: [3, 2, 1], max: undefined, result: false }, + { + version: [2, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], + min: [3, 2, 1], + max: undefined, + result: false, + }, + // Below min, minor + { version: [3, 1, 1], min: [3, 2, 1], max: undefined, result: false }, + { + version: [3, 1, Number.MAX_SAFE_INTEGER], + min: [3, 2, 1], + max: undefined, + result: false, + }, + // Below min, patch + { version: [3, 2, 0], min: [3, 2, 1], max: undefined, result: false }, + + // Max only + // Equal to max + { version: [0, 0, 0], min: undefined, max: [0, 0, 0], result: false }, + { version: [3, 2, 1], min: undefined, max: [3, 2, 1], result: false }, + { + version: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + min: undefined, + max: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + result: false, + }, + // Exceeds max, major + { version: [4, 0, 0], min: undefined, max: [3, 2, 1], result: false }, + { + version: [Number.MAX_SAFE_INTEGER, 0, 0], + min: undefined, + max: [3, 2, 1], + result: false, + }, + // Exceeds max, minor + { version: [3, 3, 0], min: undefined, max: [3, 2, 1], result: false }, + { + version: [3, Number.MAX_SAFE_INTEGER, 0], + min: undefined, + max: [3, 2, 1], + result: false, + }, + // Exceeds max, patch + { version: [3, 2, 2], min: undefined, max: [3, 2, 1], result: false }, + { + version: [3, 2, Number.MAX_SAFE_INTEGER], + min: undefined, + max: [3, 2, 1], + result: false, + }, + // Below max, major + { version: [2, 2, 1], min: undefined, max: [3, 2, 1], result: true }, + { + version: [2, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], + min: undefined, + max: [3, 2, 1], + result: true, + }, + // Below max, minor + { version: [3, 1, 1], min: undefined, max: [3, 2, 1], result: true }, + { + version: [3, 1, Number.MAX_SAFE_INTEGER], + min: undefined, + max: [3, 2, 1], + result: true, + }, + // Below max, patch + { version: [3, 2, 0], min: undefined, max: [3, 2, 1], result: true }, + + // Min and max + // Equal to min and max + { version: [0, 0, 0], min: [0, 0, 0], max: [0, 0, 0], result: false }, + { version: [3, 2, 1], min: [3, 2, 1], max: [3, 2, 1], result: false }, + { + version: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + min: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + max: [ + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + Number.MAX_SAFE_INTEGER, + ], + result: false, + }, + // Equal to min and below max + { version: [3, 2, 1], min: [3, 2, 1], max: [4, 0, 0], result: true }, + { version: [3, 2, 1], min: [3, 2, 1], max: [3, 3, 0], result: true }, + { version: [3, 2, 1], min: [3, 2, 1], max: [3, 2, 2], result: true }, + // Equal to min and above max + { version: [3, 2, 1], min: [3, 2, 1], max: [2, 9, 9], result: false }, + { version: [3, 2, 1], min: [3, 2, 1], max: [3, 1, 9], result: false }, + { version: [3, 2, 1], min: [3, 2, 1], max: [3, 2, 0], result: false }, + // Above min and equal to max + { version: [3, 2, 1], min: [2, 9, 9], max: [3, 2, 1], result: false }, + { version: [3, 2, 1], min: [3, 1, 9], max: [3, 2, 1], result: false }, + { version: [3, 2, 1], min: [3, 2, 0], max: [3, 2, 1], result: false }, + // Above min and below max + { version: [3, 2, 1], min: [2, 9, 9], max: [4, 0, 0], result: true }, + { version: [3, 2, 1], min: [3, 1, 9], max: [3, 3, 0], result: true }, + { version: [3, 2, 1], min: [3, 2, 0], max: [3, 2, 2], result: true }, + // Above min and above max + { version: [3, 2, 1], min: [2, 9, 9], max: [2, 9, 9], result: false }, + { version: [3, 2, 1], min: [3, 1, 9], max: [3, 1, 9], result: false }, + { version: [3, 2, 1], min: [3, 2, 0], max: [3, 2, 0], result: false }, + // Below min and equal to max + { version: [3, 2, 1], min: [4, 0, 0], max: [3, 2, 1], result: false }, + { version: [3, 2, 1], min: [3, 3, 0], max: [3, 2, 1], result: false }, + { version: [3, 2, 1], min: [3, 2, 2], max: [3, 2, 1], result: false }, + // Below min and below max + { version: [3, 2, 1], min: [4, 0, 0], max: [4, 0, 0], result: false }, + { version: [3, 2, 1], min: [3, 3, 0], max: [3, 3, 0], result: false }, + { version: [3, 2, 1], min: [3, 2, 2], max: [3, 2, 2], result: false }, + // Below min and above max + { version: [3, 2, 1], min: [4, 0, 0], max: [2, 9, 9], result: false }, + { version: [3, 2, 1], min: [3, 3, 0], max: [3, 1, 9], result: false }, + { version: [3, 2, 1], min: [3, 2, 2], max: [3, 2, 0], result: false }, + ]; + + scenarioMatrix.forEach(({ version, min, max, result, throws }) => { + const testText = throws ? 'throws error' : `returns ${result}`; + it(`${testText} for '${JSON.stringify({ version, min, max })}'`, () => { + if (throws) { + expect(() => satisfiesVersion(version, min, max)).to.throw(); + } else { + expect(satisfiesVersion(version, min, max)).to.equal(result); + } + }); + }); + }); +});