/*eslint no-var:0, max-len:0 */ 'use strict'; const chai = require('chai'); chai.use(require('chai-as-promised')); const expect = chai.expect; const Knex = require('../../../../knex'); const _ = require('lodash'); const delay = require('../../../../lib/execution/internal/delay'); const { isPostgreSQL, isOracle, isRedshift, isSQLite, isMysql, isMssql, isPgBased, isPgNative, isCockroachDB, } = require('../../../util/db-helpers'); const { DRIVER_NAMES: drivers } = require('../../../util/constants'); const { dropTables, createAccounts, createTestTableTwo, } = require('../../../util/tableCreatorHelper'); const { getAllDbs, getKnexForDb, } = require('../../util/knex-instance-provider'); const logger = require('../../../integration/logger'); const { insertAccounts } = require('../../../util/dataInsertHelper'); describe('Additional', function () { getAllDbs().forEach((db) => { describe(db, () => { let knex; before(async () => { knex = logger( getKnexForDb(db, { pool: { destroyTimeoutMillis: 1500, }, }) ); await dropTables(knex); await createAccounts(knex, false, false); await createTestTableTwo(knex); }); beforeEach(async () => { await knex('accounts').truncate(); await insertAccounts(knex); }); after(async () => { await knex.destroy(); }); describe('Custom response processing', () => { before('setup custom response handler', () => { knex.client.config.postProcessResponse = (response, queryContext) => { response.callCount = response.callCount ? response.callCount + 1 : 1; response.queryContext = queryContext; return response; }; }); after('restore client configuration', () => { knex.client.config.postProcessResponse = null; }); it('should process normal response', () => { return knex('accounts') .limit(1) .then((res) => { expect(res.callCount).to.equal(1); }); }); it('should pass query context to the custom handler', () => { return knex('accounts') .queryContext('the context') .limit(1) .then((res) => { expect(res.queryContext).to.equal('the context'); }); }); it('should process raw response', () => { return knex.raw('select * from ??', ['accounts']).then((res) => { expect(res.callCount).to.equal(1); }); }); it('should pass query context for raw responses', () => { return knex .raw('select * from ??', ['accounts']) .queryContext('the context') .then((res) => { expect(res.queryContext).to.equal('the context'); }); }); it('should process response done in transaction', () => { return knex .transaction((trx) => { return trx('accounts') .limit(1) .then((res) => { expect(res.callCount).to.equal(1); return res; }); }) .then((res) => { expect(res.callCount).to.equal(1); }); }); it('should pass query context for responses from transactions', () => { return knex .transaction((trx) => { return trx('accounts') .queryContext('the context') .limit(1) .then((res) => { expect(res.queryContext).to.equal('the context'); return res; }); }) .then((res) => { expect(res.queryContext).to.equal('the context'); }); }); it('should handle error correctly in a stream', (done) => { const stream = knex('wrongtable').limit(1).stream(); stream.on('error', () => { done(); }); }); it('should process response done through a stream', (done) => { let response; const stream = knex('accounts').limit(1).stream(); stream.on('data', (res) => { response = res; }); stream.on('finish', () => { expect(response.callCount).to.equal(1); done(); }); }); it('should pass query context for responses through a stream', (done) => { let response; const stream = knex('accounts') .queryContext('the context') .limit(1) .stream(); stream.on('data', (res) => { response = res; }); stream.on('finish', () => { expect(response.queryContext).to.equal('the context'); done(); }); }); it('should process response for each row done through a stream', (done) => { const stream = knex('accounts').limit(5).stream(); let count = 0; stream.on('data', () => count++); stream.on('finish', () => { expect(count).to.equal(5); done(); }); }); }); describe('columnInfo with wrapIdentifier and postProcessResponse', () => { before('setup hooks', () => { knex.client.config.postProcessResponse = (response) => { return _.mapKeys(response, (val, key) => { return _.camelCase(key); }); }; knex.client.config.wrapIdentifier = (id, origImpl) => { return origImpl(_.snakeCase(id)); }; }); after('restore client configuration', () => { knex.client.config.postProcessResponse = null; knex.client.config.wrapIdentifier = null; }); it('should work using camelCased table name', () => { return knex('testTableTwo') .columnInfo() .then((res) => { expect(Object.keys(res)).to.have.all.members([ 'id', 'accountId', 'details', 'status', ]); }); }); it('should work using snake_cased table name', () => { return knex('test_table_two') .columnInfo() .then((res) => { expect(Object.keys(res)).to.have.all.members([ 'id', 'accountId', 'details', 'status', ]); }); }); }); describe('returning with wrapIdentifier and postProcessResponse` (TODO: fix to work on all possible dialects)', function () { const origHooks = {}; if (!isPostgreSQL(knex) || !isMssql(knex)) { return; } before('setup custom hooks', () => { origHooks.postProcessResponse = knex.client.config.postProcessResponse; origHooks.wrapIdentifier = knex.client.config.wrapIdentifier; // Add `_foo` to each identifier. knex.client.config.postProcessResponse = (res) => { if (Array.isArray(res)) { return res.map((it) => { if (typeof it === 'object') { return _.mapKeys(it, (value, key) => { return key + '_foo'; }); } else { return it; } }); } else { return res; } }; // Remove `_foo` from the end of each identifier. knex.client.config.wrapIdentifier = (id) => { return id.substring(0, id.length - 4); }; }); after('restore hooks', () => { knex.client.config.postProcessResponse = origHooks.postProcessResponse; knex.client.config.wrapIdentifier = origHooks.wrapIdentifier; }); it('should return the correct column when a single property is given to returning', () => { return knex('accounts_foo') .insert({ balance_foo: 123 }) .returning('balance_foo') .then((res) => { expect(res).to.eql([123]); }); }); it('should return the correct columns when multiple properties are given to returning', () => { return knex('accounts_foo') .insert({ balance_foo: 123, email_foo: 'foo@bar.com' }) .returning(['balance_foo', 'email_foo']) .then((res) => { expect(res).to.eql([ { balance_foo: 123, email_foo: 'foo@bar.com' }, ]); }); }); }); it('should truncate a table with truncate', function () { return knex('test_table_two') .truncate() .testSql(function (tester) { tester('mysql', 'truncate `test_table_two`'); tester('pg', 'truncate "test_table_two" restart identity'); tester('pgnative', 'truncate "test_table_two" restart identity'); tester('pg-redshift', 'truncate "test_table_two"'); tester('sqlite3', 'delete from `test_table_two`'); tester('oracledb', 'truncate table "test_table_two"'); tester('mssql', 'truncate table [test_table_two]'); }) .then(() => { return knex('test_table_two') .select('*') .then((resp) => { expect(resp).to.have.length(0); }); }) .then(() => { // Insert new data after truncate and make sure ids restart at 1. // This doesn't currently work on oracle, where the created sequence // needs to be manually reset. // On redshift, one would need to create an entirely new table and do // `insert into ... (select ...); alter table rename...` if (isOracle(knex) || isRedshift(knex)) { return; } return knex('test_table_two') .insert({ status: 1 }) .then((res) => { return knex('test_table_two') .select('id') .first() .then((res) => { expect(res).to.be.an('object'); if (!isCockroachDB(knex)) { expect(res.id).to.equal(1); } }); }); }); }); it('should allow raw queries directly with `knex.raw`', function () { const tables = { [drivers.MySQL]: 'SHOW TABLES', [drivers.MySQL2]: 'SHOW TABLES', [drivers.CockroachDB]: "SELECT table_name FROM information_schema.tables WHERE table_schema='public'", [drivers.PostgreSQL]: "SELECT table_name FROM information_schema.tables WHERE table_schema='public'", [drivers.PgNative]: "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.SQLite]: "SELECT name FROM sqlite_master WHERE type='table';", [drivers.Oracle]: 'select TABLE_NAME from USER_TABLES', [drivers.MsSQL]: "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='dbo'", }; return knex .raw(tables[knex.client.driverName]) .testSql(function (tester) { tester(knex.client.driverName, tables[knex.client.driverName]); }); }); it('should allow using the primary table as a raw statement', function () { expect(knex(knex.raw('raw_table_name')).toQuery()).to.equal( 'select * from raw_table_name' ); }); it('should allow using .fn-methods to create raw statements', function () { expect(knex.fn.now().prototype === knex.raw().prototype); expect(knex.fn.now().toQuery()).to.equal('CURRENT_TIMESTAMP'); expect(knex.fn.now(6).toQuery()).to.equal('CURRENT_TIMESTAMP(6)'); }); it('#2184 - should properly escape table name for SQLite columnInfo', function () { if (!isSQLite(knex)) { return this.skip(); } return knex.schema .dropTableIfExists('group') .then(function () { return knex.schema.createTable('group', function (table) { table.integer('foo'); }); }) .then(function () { return knex('group').columnInfo(); }) .then(function (columnInfo) { expect(columnInfo).to.deep.equal({ foo: { type: 'integer', maxLength: null, nullable: true, defaultValue: null, }, }); }); }); if (isOracle(knex)) { const oracledb = require('oracledb'); describe('test oracle stored procedures', function () { it('create stored procedure', function () { return knex .raw( ` CREATE OR REPLACE PROCEDURE SYSTEM.multiply (X IN NUMBER, Y IN NUMBER, OUTPUT OUT NUMBER) IS BEGIN OUTPUT := X * Y; END;` ) .then(function (result) { expect(result).to.be.an('array'); }); }); it('get outbound values from stored procedure', function () { const bindVars = { x: 6, y: 7, output: { dir: oracledb.BIND_OUT, }, }; return knex .raw('BEGIN SYSTEM.MULTIPLY(:x, :y, :output); END;', bindVars) .then(function (result) { expect(result[0]).to.be.ok; expect(result[0]).to.equal('42'); }); }); it('drop stored procedure', function () { const bindVars = { x: 6, y: 7 }; return knex .raw('drop procedure SYSTEM.MULTIPLY', bindVars) .then(function (result) { expect(result).to.be.ok; expect(result).to.be.an('array'); }); }); }); } it('should allow renaming a column', function () { let countColumn; if (isOracle(knex)) { countColumn = 'COUNT(*)'; } else if (isMssql(knex)) { countColumn = ''; } else { countColumn = 'count(*)'; } let count; const inserts = []; _.times(40, function (i) { inserts.push({ email: 'email' + i, first_name: 'Test', last_name: 'Data', }); }); return knex('accounts') .insert(inserts) .then(function () { return knex.count('*').from('accounts'); }) .then(function (resp) { count = resp[0][countColumn]; return knex.schema .table('accounts', function (t) { t.renameColumn('about', 'about_col'); }) .testSql(function (tester) { tester('mysql', [ 'show fields from `accounts` where field = ?', ]); tester('pg', [ 'alter table "accounts" rename "about" to "about_col"', ]); tester('pgnative', [ 'alter table "accounts" rename "about" to "about_col"', ]); tester('pg-redshift', [ 'alter table "accounts" rename "about" to "about_col"', ]); tester('sqlite3', [ 'alter table `accounts` rename `about` to `about_col`', ]); tester('oracledb', [ 'DECLARE PK_NAME VARCHAR(200); IS_AUTOINC NUMBER := 0; BEGIN EXECUTE IMMEDIATE (\'ALTER TABLE "accounts" RENAME COLUMN "about" TO "about_col"\'); SELECT COUNT(*) INTO IS_AUTOINC from "USER_TRIGGERS" where trigger_name = \'accounts_autoinc_trg\'; IF (IS_AUTOINC > 0) THEN SELECT cols.column_name INTO PK_NAME FROM all_constraints cons, all_cons_columns cols WHERE cons.constraint_type = \'P\' AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner AND cols.table_name = \'accounts\'; IF (\'about_col\' = PK_NAME) THEN EXECUTE IMMEDIATE (\'DROP TRIGGER "accounts_autoinc_trg"\'); EXECUTE IMMEDIATE (\'create or replace trigger "accounts_autoinc_trg" BEFORE INSERT on "accounts" for each row declare checking number := 1; begin if (:new."about_col" is null) then while checking >= 1 loop select "accounts_seq".nextval into :new."about_col" from dual; select count("about_col") into checking from "accounts" where "about_col" = :new."about_col"; end loop; end if; end;\'); end if; end if;END;', ]); tester('mssql', ["exec sp_rename ?, ?, 'COLUMN'"]); }); }) .then(function () { return knex.count('*').from('accounts'); }) .then(function (resp) { expect(resp[0][countColumn]).to.equal(count); }) .then(function () { return knex('accounts').select('about_col'); }) .then(function () { return knex.schema.table('accounts', function (t) { t.renameColumn('about_col', 'about'); }); }) .then(function () { return knex.count('*').from('accounts'); }) .then(function (resp) { expect(resp[0][countColumn]).to.equal(count); }); }); it('should allow dropping a column', async function () { let countColumn; if (isOracle(knex)) { countColumn = 'COUNT(*)'; } else if (isMssql(knex)) { countColumn = ''; } else { countColumn = 'count(*)'; } let count; await knex .count('*') .from('accounts') .then(function (resp) { count = resp[0][countColumn]; }) .then(function () { return knex.schema .table('accounts', function (t) { t.dropColumn('first_name'); }) .testSql(function (tester) { tester('mysql', ['alter table `accounts` drop `first_name`']); tester('pg', [ 'alter table "accounts" drop column "first_name"', ]); tester('pgnative', [ 'alter table "accounts" drop column "first_name"', ]); tester('pg-redshift', [ 'alter table "accounts" drop column "first_name"', ]); tester('sqlite3', ['PRAGMA table_info(`accounts`)']); tester('oracledb', [ 'alter table "accounts" drop ("first_name")', ]); //tester('oracledb', ['alter table "accounts" drop ("first_name")']); tester('mssql', [ "\n DECLARE @constraint varchar(100) = (SELECT default_constraints.name\n FROM sys.all_columns\n INNER JOIN sys.tables\n ON all_columns.object_id = tables.object_id\n INNER JOIN sys.schemas\n ON tables.schema_id = schemas.schema_id\n INNER JOIN sys.default_constraints\n ON all_columns.default_object_id = default_constraints.object_id\n WHERE schemas.name = 'dbo'\n AND tables.name = 'accounts'\n AND all_columns.name = 'first_name')\n\n IF @constraint IS NOT NULL EXEC('ALTER TABLE accounts DROP CONSTRAINT ' + @constraint)", 'ALTER TABLE [accounts] DROP COLUMN [first_name]', ]); }); }) .then(function () { return knex.select('*').from('accounts').first(); }) .then(function (resp) { expect(_.keys(resp).sort()).to.eql([ 'about', 'balance', 'created_at', 'email', 'id', 'last_name', 'logins', 'phone', 'updated_at', ]); }) .then(function () { return knex.count('*').from('accounts'); }) .then(function (resp) { expect(resp[0][countColumn]).to.equal(count); }); await dropTables(knex); await createAccounts(knex, false, false); await createTestTableTwo(knex); }); it('.timeout() should throw TimeoutError', async function () { const driverName = knex.client.driverName; if (isSQLite(knex)) { return this.skip(); } //TODO -- No built-in support for sleeps if (isRedshift(knex)) { return this.skip(); } const testQueries = { [drivers.CockroachDB]: function () { return knex.raw('SELECT pg_sleep(1)'); }, [drivers.PostgreSQL]: function () { return knex.raw('SELECT pg_sleep(1)'); }, [drivers.PgNative]: function () { return knex.raw('SELECT pg_sleep(1)'); }, [drivers.MySQL]: function () { return knex.raw('SELECT SLEEP(1)'); }, [drivers.MySQL2]: function () { return knex.raw('SELECT SLEEP(1)'); }, [drivers.MsSQL]: function () { return knex.raw("WAITFOR DELAY '00:00:01'"); }, [drivers.Oracle]: function () { return knex.raw('begin dbms_lock.sleep(1); end;'); }, }; if (!Object.prototype.hasOwnProperty.call(testQueries, driverName)) { throw new Error('Missing test query for driver: ' + driverName); } const query = testQueries[driverName](); await query .timeout(200) .then(function () { expect(true).to.equal(false); }) .catch(function (error) { expect(_.pick(error, 'timeout', 'name', 'message')).to.deep.equal({ timeout: 200, name: 'KnexTimeoutError', message: 'Defined query timeout of 200ms exceeded when running query.', }); }); }); it('.timeout(ms, {cancel: true}) should throw TimeoutError and cancel slow query', async function () { if (isSQLite(knex)) { return this.skip(); } //TODO -- No built-in support for sleeps if (isRedshift(knex)) { return this.skip(); } // There's unexpected behavior caused by knex releasing a connection back // to the pool because of a timeout when a long query is still running. // A subsequent query will acquire the connection (still in-use) and hang // until the first query finishes. Setting a sleep time longer than the // mocha timeout exposes this behavior. const testQueries = { [drivers.PostgreSQL]: function () { return knex.raw('SELECT pg_sleep(10)'); }, [drivers.CockroachDB]: function () { return knex.raw('SELECT pg_sleep(10)'); }, [drivers.PgNative]: function () { return knex.raw('SELECT pg_sleep(10)'); }, [drivers.MySQL]: function () { return knex.raw('SELECT SLEEP(10)'); }, [drivers.MySQL2]: function () { return knex.raw('SELECT SLEEP(10)'); }, [drivers.MsSQL]: function () { return knex.raw("WAITFOR DELAY '00:00:10'"); }, [drivers.Oracle]: function () { return knex.raw('begin dbms_lock.sleep(10); end;'); }, }; const driverName = knex.client.driverName; if (!Object.prototype.hasOwnProperty.call(testQueries, driverName)) { throw new Error('Missing test query for driverName: ' + driverName); } const query = testQueries[driverName](); function addTimeout() { return query.timeout(200, { cancel: true }); } // Only mysql/postgres query cancelling supported for now if (!isMysql(knex) && !isPgBased(knex)) { expect(addTimeout).to.throw( 'Query cancelling not supported for this dialect' ); return; // TODO: Use `this.skip()` here? } const getProcessesQueries = { [drivers.CockroachDB]: function () { return knex.raw('SELECT * FROM [SHOW CLUSTER STATEMENTS]'); }, [drivers.PostgreSQL]: function () { return knex.raw('SELECT * from pg_stat_activity'); }, [drivers.PgNative]: function () { return knex.raw('SELECT * from pg_stat_activity'); }, [drivers.MySQL]: function () { return knex.raw('SHOW PROCESSLIST'); }, [drivers.MySQL2]: function () { return knex.raw('SHOW PROCESSLIST'); }, }; if ( !Object.prototype.hasOwnProperty.call(getProcessesQueries, driverName) ) { throw new Error('Missing test query for driverName: ' + driverName); } const getProcessesQuery = getProcessesQueries[driverName](); try { await addTimeout(); expect(true).to.equal(false); } catch (error) { expect(_.pick(error, 'timeout', 'name', 'message')).to.deep.equal({ timeout: 200, name: 'KnexTimeoutError', message: 'Defined query timeout of 200ms exceeded when running query.', }); // Ensure sleep command is removed. // This query will hang if a connection gets released back to the pool // too early. // 50ms delay since killing query doesn't seem to have immediate effect to the process listing await delay(50); const results = await getProcessesQuery; let processes; let sleepProcess; if (isPgBased(knex)) { processes = results.rows; sleepProcess = _.find(processes, { query: query.toString() }); } else { processes = results[0]; sleepProcess = _.find(processes, { Info: 'SELECT SLEEP(10)', }); } expect(sleepProcess).to.equal(undefined); } }); it('.timeout(ms, {cancel: true}) should throw TimeoutError and cancel slow query in transaction', async function () { const driverName = knex.client.driverName; if (driverName === 'sqlite3') { return this.skip(); } //TODO -- No built-in support for sleeps if (/redshift/.test(driverName)) { return this.skip(); } // There's unexpected behavior caused by knex releasing a connection back // to the pool because of a timeout when a long query is still running. // A subsequent query will acquire the connection (still in-use) and hang // until the first query finishes. Setting a sleep time longer than the // mocha timeout exposes this behavior. const testQueries = { [drivers.CockroachDB]: function () { return 'SELECT pg_sleep(10)'; }, [drivers.PostgreSQL]: function () { return 'SELECT pg_sleep(10)'; }, [drivers.PgNative]: function () { return 'SELECT pg_sleep(10)'; }, [drivers.MySQL]: function () { return 'SELECT SLEEP(10)'; }, [drivers.MySQL2]: function () { return 'SELECT SLEEP(10)'; }, [drivers.MsSQL]: function () { return "WAITFOR DELAY '00:00:10'"; }, [drivers.Oracle]: function () { return 'begin dbms_lock.sleep(10); end;'; }, }; if (!Object.prototype.hasOwnProperty.call(testQueries, driverName)) { throw new Error('Missing test query for driverName: ' + driverName); } const query = testQueries[driverName](); function addTimeout() { return knex.raw(query).timeout(200, { cancel: true }); } // Only mysql/postgres query cancelling supported for now if (!isMysql(knex) && !isPgBased(knex)) { expect(addTimeout).to.throw( 'Query cancelling not supported for this dialect' ); return; // TODO: Use `this.skip()` here? } const getProcessesQueries = { [drivers.CockroachDB]: function () { return knex.raw('SELECT * FROM [SHOW CLUSTER STATEMENTS]'); }, [drivers.PostgreSQL]: function () { return knex.raw('SELECT * from pg_stat_activity'); }, [drivers.PgNative]: function () { return knex.raw('SELECT * from pg_stat_activity'); }, [drivers.MySQL]: function () { return knex.raw('SHOW PROCESSLIST'); }, [drivers.MySQL2]: function () { return knex.raw('SHOW PROCESSLIST'); }, }; if ( !Object.prototype.hasOwnProperty.call(getProcessesQueries, driverName) ) { throw new Error('Missing test query for driverName: ' + driverName); } const getProcessesQuery = getProcessesQueries[driverName](); let trx; try { // ToDo I have no idea why this works differently for Cockroach... if (!isCockroachDB(knex)) { await knex.transaction((trx) => addTimeout().transacting(trx)); } else { trx = await knex.transaction(); await trx.raw(query).timeout(200, { cancel: true }); } expect(true).to.equal(false); } catch (error) { if (isCockroachDB(knex)) { await trx.rollback(); } expect(_.pick(error, 'timeout', 'name', 'message')).to.deep.equal({ timeout: 200, name: 'KnexTimeoutError', message: 'Defined query timeout of 200ms exceeded when running query.', }); // Ensure sleep command is removed. // This query will hang if a connection gets released back to the pool // too early. // 50ms delay since killing query doesn't seem to have immediate effect to the process listing await delay(50); const results = await getProcessesQuery; let processes; let sleepProcess; if (_.startsWith(driverName, 'pg')) { processes = results.rows; sleepProcess = _.find(processes, { query: query.toString() }); } else { processes = results[0]; sleepProcess = _.find(processes, { Info: 'SELECT SLEEP(10)', }); } expect(sleepProcess).to.equal(undefined); } }); it('.timeout(ms, {cancel: true}) should cancel slow query even if connection pool is exhausted', async function () { // Only mysql/postgres query cancelling supported for now if (!isMysql(knex) && !isPostgreSQL(knex)) { return this.skip(); } // To make this test easier, I'm changing the pool settings to max 1. // Also setting acquireTimeoutMillis to lower as not to wait the default time const knexConfig = _.cloneDeep(knex.client.config); knexConfig.pool.min = 0; knexConfig.pool.max = 1; knexConfig.pool.acquireTimeoutMillis = 100; const knexDb = new Knex(knexConfig); const testQueries = { [drivers.CockroachDB]: function () { return knexDb.raw('SELECT pg_sleep(10)'); }, [drivers.PostgreSQL]: function () { return knexDb.raw('SELECT pg_sleep(10)'); }, [drivers.PgNative]: function () { return knexDb.raw('SELECT pg_sleep(10)'); }, [drivers.MySQL]: function () { return knexDb.raw('SELECT SLEEP(10)'); }, [drivers.MySQL2]: function () { return knexDb.raw('SELECT SLEEP(10)'); }, [drivers.MsSQL]: function () { return knexDb.raw("WAITFOR DELAY '00:00:10'"); }, [drivers.Oracle]: function () { return knexDb.raw('begin dbms_lock.sleep(10); end;'); }, }; const driverName = knex.client.driverName; if (!Object.prototype.hasOwnProperty.call(testQueries, driverName)) { throw new Error('Missing test query for dialect: ' + driverName); } const query = testQueries[driverName](); // We must use the original knex instance without the exhausted pool to list running queries const getProcessesForDriver = { [drivers.CockroachDB]: async () => { const results = await knex.raw( 'SELECT * FROM [SHOW CLUSTER STATEMENTS]' ); return _.map(_.filter(results.rows, { state: 'active' }), 'query'); }, [drivers.PostgreSQL]: async () => { const results = await knex.raw('SELECT * from pg_stat_activity'); return _.map(_.filter(results.rows, { state: 'active' }), 'query'); }, [drivers.PgNative]: async () => { const results = await knex.raw('SELECT * from pg_stat_activity'); return _.map(_.filter(results.rows, { state: 'active' }), 'query'); }, [drivers.MySQL]: async () => { const results = await knex.raw('SHOW PROCESSLIST'); return _.map(results[0], 'Info'); }, [drivers.MySQL2]: async () => { const results = await knex.raw('SHOW PROCESSLIST'); return _.map(results[0], 'Info'); }, }; if ( !Object.prototype.hasOwnProperty.call( getProcessesForDriver, driverName ) ) { throw new Error('Missing test query for driverName: ' + driverName); } const getProcesses = getProcessesForDriver[driverName]; try { const promise = query.timeout(50, { cancel: true }).then(_.identity); await delay(15); const processesBeforeTimeout = await getProcesses(); expect(processesBeforeTimeout).to.include(query.toString()); await expect(promise).to.eventually.be.rejected.and.to.deep.include({ timeout: 50, name: 'KnexTimeoutError', message: 'Defined query timeout of 50ms exceeded when running query.', }); const processesAfterTimeout = await getProcesses(); expect(processesAfterTimeout).to.not.include(query.toString()); } finally { await knexDb.destroy(); } }); it('.timeout(ms, {cancel: true}) should release connections after failing if connection cancellation throws an error', async function () { // Only mysql/postgres query cancelling supported for now if (!isPgBased(knex) && !isMysql(knex)) { return this.skip(); } // To make this test easier, I'm changing the pool settings to max 1. // Also setting acquireTimeoutMillis to lower as not to wait the default time const knexConfig = _.cloneDeep(knex.client.config); knexConfig.pool.min = 0; knexConfig.pool.max = 2; knexConfig.pool.acquireTimeoutMillis = 100; const rawTestQueries = { [drivers.CockroachDB]: (sleepSeconds) => `SELECT pg_sleep(${sleepSeconds})`, [drivers.PostgreSQL]: (sleepSeconds) => `SELECT pg_sleep(${sleepSeconds})`, [drivers.PgNative]: (sleepSeconds) => `SELECT pg_sleep(${sleepSeconds})`, [drivers.MySQL]: (sleepSeconds) => `SELECT SLEEP(${sleepSeconds})`, [drivers.MySQL2]: (sleepSeconds) => `SELECT SLEEP(${sleepSeconds})`, }; const driverName = knex.client.driverName; if (!Object.prototype.hasOwnProperty.call(rawTestQueries, driverName)) { throw new Error('Missing test query for driverName: ' + driverName); } const knexDb = new Knex(knexConfig); const getTestQuery = (sleepSeconds = 10) => { const rawTestQuery = rawTestQueries[driverName](sleepSeconds); return knexDb.raw(rawTestQuery); }; const knexPrototype = Object.getPrototypeOf(knexDb.client); const originalWrappedCancelQueryCall = knexPrototype._wrappedCancelQueryCall; knexPrototype._wrappedCancelQueryCall = (conn, connectionToKill) => { if (isPgNative(knex) || isCockroachDB(knex) || isMysql(knex)) { throw new Error('END THIS'); } else { return knexPrototype.query(conn, { method: 'raw', sql: 'TestError', }); } }; const queryTimeout = 10; const secondQueryTimeout = 11; try { await expect( getTestQuery().timeout(queryTimeout, { cancel: true }) ).to.be.eventually.rejected.and.deep.include({ timeout: queryTimeout, name: isCockroachDB(knex) || isMysql(knex) || isPgNative(knex) ? 'Error' : 'error', message: `After query timeout of ${queryTimeout}ms exceeded, cancelling of query failed.`, }); knexPrototype._wrappedCancelQueryCall = originalWrappedCancelQueryCall; try { await getTestQuery().timeout(secondQueryTimeout, { cancel: true }); } catch (err) { console.log(err); } // ToDo figure out why CockroachDB throws `connect EISCONN` here if (!isCockroachDB(knex)) { await expect( getTestQuery().timeout(secondQueryTimeout, { cancel: true }) ).to.be.eventually.rejected.and.deep.include({ timeout: secondQueryTimeout, name: 'KnexTimeoutError', message: `Defined query timeout of ${secondQueryTimeout}ms exceeded when running query.`, }); } } finally { await knexDb.destroy(); } }); it('Event: query-response', function () { let queryCount = 0; const onQueryResponse = function (response, obj, builder) { queryCount++; expect(response).to.be.an('array'); expect(obj).to.be.an('object'); expect(obj.__knexUid).to.be.a('string'); expect(obj.__knexQueryUid).to.be.a('string'); expect(builder).to.be.an('object'); }; knex.on('query-response', onQueryResponse); return knex('accounts') .select() .on('query-response', onQueryResponse) .then(function () { return knex.transaction(function (tr) { return tr('accounts') .select() .on('query-response', onQueryResponse); //Transactions should emit the event as well }); }) .then(function () { knex.removeListener('query-response', onQueryResponse); expect(queryCount).to.equal(4); }); }); it('Event: preserves listeners on a copy with user params', function () { let queryCount = 0; const onQueryResponse = function (response, obj, builder) { queryCount++; expect(response).to.be.an('array'); expect(obj).to.be.an('object'); expect(obj.__knexUid).to.be.a('string'); expect(obj.__knexQueryUid).to.be.a('string'); expect(builder).to.be.an('object'); }; knex.on('query-response', onQueryResponse); const knexCopy = knex.withUserParams({}); return knexCopy('accounts') .select() .on('query-response', onQueryResponse) .then(function () { return knexCopy.transaction(function (tr) { return tr('accounts') .select() .on('query-response', onQueryResponse); //Transactions should emit the event as well }); }) .then(function () { expect(Object.keys(knex._events).length).to.equal(1); expect(Object.keys(knexCopy._events).length).to.equal(1); knex.removeListener('query-response', onQueryResponse); expect(Object.keys(knex._events).length).to.equal(0); expect(queryCount).to.equal(4); }); }); it('Event: query-error', function () { let queryCountKnex = 0; let queryCountBuilder = 0; const onQueryErrorKnex = function (error, obj) { queryCountKnex++; expect(obj).to.be.an('object'); expect(obj.__knexUid).to.be.a('string'); expect(obj.__knexQueryUid).to.be.a('string'); expect(error).to.be.an('error'); }; const onQueryErrorBuilder = function (error, obj) { queryCountBuilder++; expect(obj).to.be.an('object'); expect(obj.__knexUid).to.be.a('string'); expect(obj.__knexQueryUid).to.be.a('string'); expect(error).to.be.an('error'); }; knex.on('query-error', onQueryErrorKnex); return knex .raw('Broken query') .on('query-error', onQueryErrorBuilder) .then(function () { expect(true).to.equal(false); //Should not be resolved }) .catch(function () { knex.removeListener('query-error', onQueryErrorKnex); knex.removeListener('query-error', onQueryErrorBuilder); expect(queryCountBuilder).to.equal(1); expect(queryCountKnex).to.equal(1); }); }); it('Event: start', function () { return knex('accounts') .insert({ last_name: 'Start event test' }) .then(function () { const queryBuilder = knex('accounts').select(); queryBuilder.on('start', function (builder) { //Alter builder prior to compilation //Select only one row builder.where('last_name', 'Start event test').first(); }); return queryBuilder; }) .then(function (row) { expect(row).to.exist; expect(row.last_name).to.equal('Start event test'); }); }); it("Event 'query' should not emit native sql string", function () { const builder = knex('accounts').where('id', 1).select(); builder.on('query', function (obj) { const native = builder.toSQL().toNative().sql; const sql = builder.toSQL().sql; //Only assert if they diff to begin with. //IE Maria does not diff if (native !== sql) { expect(obj.sql).to.not.equal(builder.toSQL().toNative().sql); expect(obj.sql).to.equal(builder.toSQL().sql); } }); return builder; }); describe('async stack traces', function () { before(() => { knex.client.config.asyncStackTraces = true; }); after(() => { delete knex.client.config.asyncStackTraces; }); it('should capture stack trace on raw query', () => { return knex .raw('select * from some_nonexisten_table') .catch((err) => { expect(err.stack.split('\n')[2]).to.match(/at Object\.raw \(/); // the index 2 might need adjustment if the code is refactored expect(typeof err.originalStack).to.equal('string'); }); }); it('should capture stack trace on schema builder', () => { return knex.schema .renameTable('some_nonexisten_table', 'whatever') .catch((err) => { expect(err.stack.split('\n')[1]).to.match( /client\.schemaBuilder/ ); // the index 1 might need adjustment if the code is refactored expect(typeof err.originalStack).to.equal('string'); }); }); }); it('Overwrite knex.logger functions using config', () => { const knexConfig = _.clone(knex.client.config); let callCount = 0; const assertCall = function (expectedMessage, message) { expect(message).to.equal(expectedMessage); callCount++; }; knexConfig.log = { warn: assertCall.bind(null, 'test'), error: assertCall.bind(null, 'test'), debug: assertCall.bind(null, 'test'), deprecate: assertCall.bind( null, 'test is deprecated, please use test2' ), }; //Sqlite warning message knexConfig.useNullAsDefault = true; const knexDb = new Knex(knexConfig); knexDb.client.logger.warn('test'); knexDb.client.logger.error('test'); knexDb.client.logger.debug('test'); knexDb.client.logger.deprecate('test', 'test2'); expect(callCount).to.equal(4); }); }); }); });