From 1816d143bc55fcc125223ede42c83c0766164777 Mon Sep 17 00:00:00 2001 From: Mickael Coquer Date: Tue, 31 Mar 2026 08:53:04 +0200 Subject: [PATCH 1/3] fix: [igo-db] remove implicit LIKE on strings containing % (security) Strings with % are no longer auto-converted to LIKE queries. Use the explicit $like operator instead: { column: { $like: 'value%' } } Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guide/models.md | 5 +---- package.json | 2 +- src/db/OperatorCompiler.js | 11 +---------- src/db/PaginatedOptimizedSql.js | 2 +- test/db/PaginatedOptimizedQueryTest.js | 24 +++++++----------------- test/db/SqlTest.js | 16 ---------------- 6 files changed, 11 insertions(+), 49 deletions(-) diff --git a/docs/guide/models.md b/docs/guide/models.md index 96789e4..63be3a4 100644 --- a/docs/guide/models.md +++ b/docs/guide/models.md @@ -296,10 +296,7 @@ const users = await User.where({ status: ['active', 'pending'] }).list(); // IS NULL const users = await User.where({ deleted_at: null }).list(); -// LIKE (auto-detected with %) -const users = await User.where({ last_name: 'Dup%' }).list(); - -// $like - explicit LIKE +// $like - LIKE const users = await User.where({ last_name: { $like: 'Dup%' } }).list(); // $between - range diff --git a/package.json b/package.json index e20d1df..b271210 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "igo", - "version": "5.5.0", + "version": "5.5.1", "homepage": "https://github.com/igocreate/igo", "description": "Igo is a Node.js Web Framework based on Express", "main": "index.js", diff --git a/src/db/OperatorCompiler.js b/src/db/OperatorCompiler.js index 50f43f9..ba4cba1 100644 --- a/src/db/OperatorCompiler.js +++ b/src/db/OperatorCompiler.js @@ -5,7 +5,7 @@ const _ = require('lodash'); * Compile une condition unitaire (colonne + valeur) en SQL * * Supporte : null (IS NULL), array (IN), $like, $between, $gte, $lte, $gt, $lt, - * string avec % (LIKE implicite), et égalité par défaut. + * et égalité par défaut. * * @param {string} columnRef - Référence qualifiée de la colonne (ex: `table`.`col`) * @param {any} value - Valeur ou objet opérateur @@ -53,10 +53,6 @@ const compileCondition = (columnRef, value, dialect, i) => { } } - if (_.isString(value) && value.includes('%')) { - return { sql: `${columnRef} LIKE ${dialect.param(i++)}`, params: [value], i }; - } - return { sql: `${columnRef} = ${dialect.param(i++)}`, params: [value], i }; }; @@ -110,11 +106,6 @@ const compileNotCondition = (columnRef, value, dialect, i) => { } } - // String avec % → NOT LIKE implicite - if (_.isString(value) && value.includes('%')) { - return { sql: `${columnRef} NOT LIKE ${dialect.param(i++)}`, params: [value], i }; - } - return { sql: `${columnRef} != ${dialect.param(i++)}`, params: [value], i }; }; diff --git a/src/db/PaginatedOptimizedSql.js b/src/db/PaginatedOptimizedSql.js index 87a6f60..f0dcb6a 100644 --- a/src/db/PaginatedOptimizedSql.js +++ b/src/db/PaginatedOptimizedSql.js @@ -1022,7 +1022,7 @@ module.exports = class PaginatedOptimizedSql extends Sql { * - Égalité : { status: 'ACTIVE' } * - IN : { status: ['ACTIVE', 'PENDING'] } * - IS NULL : { email: null } - * - LIKE : { last_name: 'Dupont%' } + * - LIKE : { last_name: { $like: 'Dupont%' } } * - BETWEEN : { created_at: { $between: ['2024-01-01', '2024-12-31'] } } * - >= : { created_at: { $gte: '2024-01-01' } } * - <= : { created_at: { $lte: '2024-12-31' } } diff --git a/test/db/PaginatedOptimizedQueryTest.js b/test/db/PaginatedOptimizedQueryTest.js index ee30f1f..8913e70 100644 --- a/test/db/PaginatedOptimizedQueryTest.js +++ b/test/db/PaginatedOptimizedQueryTest.js @@ -238,16 +238,6 @@ describe('db.PaginatedOptimizedQuery', function() { assert.deepStrictEqual(params, []); }); - it('should generate implicit LIKE for string with %', () => { - const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); - query.query.verb = 'count'; - query.where({ status: 'TRANS%' }); - const { sql, params } = query.toSQL(); - - assert.ok(sql.includes('WHERE `folders`.`status` LIKE ?')); - assert.deepStrictEqual(params, ['TRANS%']); - }); - it('should generate explicit $like', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; @@ -395,7 +385,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should generate 1-level EXISTS', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; - query.where({ status: 'SUBMITTED', 'applicant.last_name': 'Dupont%' }); + query.where({ status: 'SUBMITTED', 'applicant.last_name': { $like: 'Dupont%' } }); const { sql, params } = query.toSQL(); assert.ok(sql.includes('WHERE `folders`.`status` = ?')); @@ -406,7 +396,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should group conditions on the same table into one EXISTS', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; - query.where({ 'applicant.last_name': 'Dupont%', 'applicant.email': 'test@test.com' }); + query.where({ 'applicant.last_name': { $like: 'Dupont%' }, 'applicant.email': 'test@test.com' }); const { sql, params } = query.toSQL(); assert.ok(sql.includes('EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? AND `applicants`.`email` = ? )')); @@ -417,7 +407,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should generate 2 separate EXISTS for 2 different joined tables', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; - query.where({ 'applicant.last_name': 'Dupont%', 'pme_folder.status': 'ACTIVE' }); + query.where({ 'applicant.last_name': { $like: 'Dupont%' }, 'pme_folder.status': 'ACTIVE' }); const { sql, params } = query.toSQL(); assert.ok(sql.includes('EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? )')); @@ -447,7 +437,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.query.verb = 'count'; query.where({ $or: [ - { 'applicant.last_name': 'Dupont%' }, + { 'applicant.last_name': { $like: 'Dupont%' } }, { 'pme_folder.status': 'ACTIVE' } ] }); @@ -461,7 +451,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should include extraWhere in EXISTS clause', () => { const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere)); query.query.verb = 'count'; - query.where({ 'library.title': 'Test%' }); + query.where({ 'library.title': { $like: 'Test%' } }); const { sql, params } = query.toSQL(); assert.ok(sql.includes('EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ?')); @@ -694,7 +684,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should apply extraWhere in both EXISTS and LEFT JOIN when combined', () => { const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere)); query.query.verb = 'select_ids'; - query.where({ code: 'ABC', 'library.title': 'Test%' }).order('library.title ASC').limit(50); + query.where({ code: 'ABC', 'library.title': { $like: 'Test%' } }).order('library.title ASC').limit(50); const { sql, params } = query.toSQL(); assert.ok(sql.includes('EXISTS')); @@ -716,7 +706,7 @@ describe('db.PaginatedOptimizedQuery', function() { it('should apply multiple extraWhere conditions', () => { const query = mockGetDb(new PaginatedOptimizedQuery(BookWithMultipleExtraWhere)); query.query.verb = 'count'; - query.where({ 'library.title': 'Test%' }); + query.where({ 'library.title': { $like: 'Test%' } }); const { sql, params } = query.toSQL(); assert.ok(sql.includes('`libraries`.`collection` = ?')); diff --git a/test/db/SqlTest.js b/test/db/SqlTest.js index d88e1b9..b7a64d2 100644 --- a/test/db/SqlTest.js +++ b/test/db/SqlTest.js @@ -304,14 +304,6 @@ describe('db.Sql', function() { assert.strictEqual('WHERE `books`.`price` NOT BETWEEN ? AND ? ', sql); assert.deepStrictEqual([10, 50], params); }); - - it('should use NOT LIKE in whereNot for implicit %', function() { - var params = []; - var query = freshQuery({ where: [], whereNot: [{ title: 'Draft%' }] }); - var sql = new Sql(query, dialect).whereNotSQL(params); - assert.strictEqual('WHERE `books`.`title` NOT LIKE ? ', sql); - assert.deepStrictEqual(['Draft%'], params); - }); }); // @@ -392,14 +384,6 @@ describe('db.Sql', function() { assert.deepStrictEqual(['Node%'], params); }); - it('should support implicit LIKE with %', function() { - var params = []; - var query = freshQuery({ where: [{ title: 'Node%' }] }); - var sql = new Sql(query, dialect).whereSQL(params); - assert.strictEqual('WHERE `books`.`title` LIKE ? ', sql); - assert.deepStrictEqual(['Node%'], params); - }); - it('should support $gte operator', function() { var params = []; var query = freshQuery({ where: [{ price: { $gte: 10 } }] }); From 367ca1ea28b309c6935a2d927394e0c950ed52e0 Mon Sep 17 00:00:00 2001 From: Mickael Coquer Date: Tue, 31 Mar 2026 09:07:17 +0200 Subject: [PATCH 2/3] fix: [igo-db] update missed tests and stale JSDoc after implicit LIKE removal - Use explicit $like in COUNT/IDS optimization tests and assert LIKE in SQL - Update JSDoc examples in PaginatedOptimizedSql to match new behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- src/db/PaginatedOptimizedSql.js | 4 ++-- test/db/PaginatedOptimizedQueryTest.js | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/db/PaginatedOptimizedSql.js b/src/db/PaginatedOptimizedSql.js index f0dcb6a..0aa0a82 100644 --- a/src/db/PaginatedOptimizedSql.js +++ b/src/db/PaginatedOptimizedSql.js @@ -910,8 +910,8 @@ module.exports = class PaginatedOptimizedSql extends Sql { * * Exemple : * Input: [ - * { 'applicant.last_name': 'Dupont%' }, - * { 'applicant.first_name': 'Jean%' }, + * { 'applicant.last_name': { $like: 'Dupont%' } }, + * { 'applicant.first_name': { $like: 'Jean%' } }, * { 'beneficiary.email': 'test@test.com' } * ] * Output: ( diff --git a/test/db/PaginatedOptimizedQueryTest.js b/test/db/PaginatedOptimizedQueryTest.js index 8913e70..e028973 100644 --- a/test/db/PaginatedOptimizedQueryTest.js +++ b/test/db/PaginatedOptimizedQueryTest.js @@ -467,31 +467,24 @@ describe('db.PaginatedOptimizedQuery', function() { it('COUNT should have no LEFT JOIN, no ORDER BY, and use EXISTS for filters', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; - query.where({ type: 'agp', 'applicant.last_name': 'Dupont%' }) + query.where({ type: 'agp', 'applicant.last_name': { $like: 'Dupont%' } }) .order('applicants.last_name ASC') .join('applicant'); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('SELECT COUNT(0)')); - assert.ok(sql.includes('EXISTS')); - assert.ok(!sql.includes('LEFT JOIN')); - assert.ok(!sql.includes('ORDER BY')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`type` = ? AND EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? )'); assert.deepStrictEqual(params, ['agp', 'Dupont%']); }); it('IDS should SELECT only id, use EXISTS, ORDER BY + LIMIT, and no LEFT JOIN when sort is on main table', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'select_ids'; - query.where({ type: 'agp', 'applicant.last_name': 'Dupont%' }) + query.where({ type: 'agp', 'applicant.last_name': { $like: 'Dupont%' } }) .order('folders.created_at DESC') .limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('SELECT `folders`.`id`')); - assert.ok(sql.includes('EXISTS (SELECT 1 FROM `applicants`')); - assert.ok(sql.includes('ORDER BY folders.created_at DESC')); - assert.ok(sql.includes('LIMIT ?, ?')); - assert.ok(!sql.includes('LEFT JOIN')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` WHERE `folders`.`type` = ? AND EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? ) ORDER BY folders.created_at DESC LIMIT ?, ?'); assert.deepStrictEqual(params, ['agp', 'Dupont%', 0, 50]); }); }); From d8ba0ecbb0aab06b7e5e80f6305e44a6bbb599d4 Mon Sep 17 00:00:00 2001 From: Mickael Coquer Date: Tue, 31 Mar 2026 09:23:26 +0200 Subject: [PATCH 3/3] refactor: [tests] use strictEqual on full SQL in PaginatedOptimizedQueryTest Replace all assert.ok(sql.includes(...)) with assert.strictEqual on the complete generated SQL string. This makes tests more readable and catches any unexpected change in the generated queries. Co-Authored-By: Claude Opus 4.6 (1M context) --- test/db/PaginatedOptimizedQueryTest.js | 159 +++++++++++-------------- 1 file changed, 71 insertions(+), 88 deletions(-) diff --git a/test/db/PaginatedOptimizedQueryTest.js b/test/db/PaginatedOptimizedQueryTest.js index e028973..fde7ac2 100644 --- a/test/db/PaginatedOptimizedQueryTest.js +++ b/test/db/PaginatedOptimizedQueryTest.js @@ -205,7 +205,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: 'SUBMITTED' }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`status` = ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` = ?'); assert.deepStrictEqual(params, ['SUBMITTED']); }); @@ -213,9 +213,10 @@ describe('db.PaginatedOptimizedQuery', function() { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'count'; query.where({ applicant_id: null }); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('`folders`.`applicant_id` IS NULL')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`applicant_id` IS NULL'); + assert.deepStrictEqual(params, []); }); it('should generate IN for array values', () => { @@ -224,7 +225,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: ['SUBMITTED', 'VALIDATED'] }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`status` IN (?)')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` IN (?)'); assert.deepStrictEqual(params, [['SUBMITTED', 'VALIDATED']]); }); @@ -234,7 +235,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: [] }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE FALSE')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE FALSE'); assert.deepStrictEqual(params, []); }); @@ -244,7 +245,17 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: { $like: 'TRANS%' } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`status` LIKE ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` LIKE ?'); + assert.deepStrictEqual(params, ['TRANS%']); + }); + + it('should treat string with % as equality (no implicit LIKE)', () => { + const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); + query.query.verb = 'count'; + query.where({ status: 'TRANS%' }); + const { sql, params } = query.toSQL(); + + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` = ?'); assert.deepStrictEqual(params, ['TRANS%']); }); @@ -254,7 +265,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ created_at: { $between: ['2024-01-01', '2024-12-31'] } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`created_at` BETWEEN ? AND ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`created_at` BETWEEN ? AND ?'); assert.deepStrictEqual(params, ['2024-01-01', '2024-12-31']); }); @@ -272,7 +283,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ created_at: { [op]: '2024-01-01' } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes(`WHERE \`folders\`.\`created_at\` ${symbol} ?`), `${op} should produce ${symbol}`); + assert.strictEqual(sql, `SELECT COUNT(0) as \`count\` FROM \`folders\` WHERE \`folders\`.\`created_at\` ${symbol} ?`); assert.deepStrictEqual(params, ['2024-01-01']); } }); @@ -288,7 +299,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: 'SUBMITTED', type: 'agp' }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`status` = ? AND `folders`.`type` = ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` = ? AND `folders`.`type` = ?'); assert.deepStrictEqual(params, ['SUBMITTED', 'agp']); }); @@ -298,7 +309,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ $and: [{ status: 'SUBMITTED' }, { type: 'agp' }] }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('(`folders`.`status` = ? AND `folders`.`type` = ?)')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (`folders`.`status` = ? AND `folders`.`type` = ?)'); assert.deepStrictEqual(params, ['SUBMITTED', 'agp']); }); @@ -308,7 +319,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ $or: [{ applicant_id: null }, { pme_folder_id: null }] }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE (`folders`.`applicant_id` IS NULL OR `folders`.`pme_folder_id` IS NULL)')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (`folders`.`applicant_id` IS NULL OR `folders`.`pme_folder_id` IS NULL)'); assert.deepStrictEqual(params, []); }); @@ -321,7 +332,7 @@ describe('db.PaginatedOptimizedQuery', function() { }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE (`folders`.`applicant_id` IS NULL OR `folders`.`pme_folder_id` IS NULL) AND `folders`.`status` = ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (`folders`.`applicant_id` IS NULL OR `folders`.`pme_folder_id` IS NULL) AND `folders`.`status` = ?'); assert.deepStrictEqual(params, ['TRANSMIS']); }); @@ -336,7 +347,7 @@ describe('db.PaginatedOptimizedQuery', function() { }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE (`folders`.`applicant_id` IS NULL OR (`folders`.`pme_folder_id` IS NULL AND `folders`.`type` = ?))')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (`folders`.`applicant_id` IS NULL OR (`folders`.`pme_folder_id` IS NULL AND `folders`.`type` = ?))'); assert.deepStrictEqual(params, ['agp']); }); @@ -360,9 +371,7 @@ describe('db.PaginatedOptimizedQuery', function() { }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes( - 'WHERE (`folders`.`applicant_id` IS NULL OR (`folders`.`pme_folder_id` IS NULL AND (`folders`.`status` LIKE ? OR `folders`.`status` = ?))) AND `folders`.`type` = ?' - )); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (`folders`.`applicant_id` IS NULL OR (`folders`.`pme_folder_id` IS NULL AND (`folders`.`status` LIKE ? OR `folders`.`status` = ?))) AND `folders`.`type` = ?'); assert.deepStrictEqual(params, ['TRANS%', 'DRAFT', 'agp']); }); @@ -372,8 +381,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({}); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('SELECT COUNT(0)')); - assert.ok(!sql.includes('WHERE')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders`'); assert.deepStrictEqual(params, []); }); }); @@ -388,8 +396,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ status: 'SUBMITTED', 'applicant.last_name': { $like: 'Dupont%' } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('WHERE `folders`.`status` = ?')); - assert.ok(sql.includes('AND EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? )')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE `folders`.`status` = ? AND EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? )'); assert.deepStrictEqual(params, ['SUBMITTED', 'Dupont%']); }); @@ -399,8 +406,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ 'applicant.last_name': { $like: 'Dupont%' }, 'applicant.email': 'test@test.com' }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? AND `applicants`.`email` = ? )')); - assert.strictEqual((sql.match(/EXISTS/g) || []).length, 1); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? AND `applicants`.`email` = ? )'); assert.deepStrictEqual(params, ['Dupont%', 'test@test.com']); }); @@ -410,9 +416,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ 'applicant.last_name': { $like: 'Dupont%' }, 'pme_folder.status': 'ACTIVE' }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? )')); - assert.ok(sql.includes('EXISTS (SELECT 1 FROM `pme_folders` WHERE `pme_folders`.`id` = `folders`.`pme_folder_id` AND `pme_folders`.`status` = ? )')); - assert.strictEqual((sql.match(/EXISTS/g) || []).length, 2); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ? ) AND EXISTS (SELECT 1 FROM `pme_folders` WHERE `pme_folders`.`id` = `folders`.`pme_folder_id` AND `pme_folders`.`status` = ? )'); assert.deepStrictEqual(params, ['Dupont%', 'ACTIVE']); }); @@ -422,13 +426,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ 'pme_folder.company.country.code': 'FR' }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes( - 'EXISTS (SELECT 1 FROM `pme_folders` WHERE `pme_folders`.`id` = `folders`.`pme_folder_id` ' + - 'AND EXISTS (SELECT 1 FROM `companies` WHERE `companies`.`id` = `pme_folders`.`company_id` ' + - 'AND EXISTS (SELECT 1 FROM `countries` WHERE `countries`.`id` = `companies`.`country_id` ' + - 'AND `countries`.`code` = ? ) ) )' - )); - assert.strictEqual((sql.match(/EXISTS/g) || []).length, 3); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE EXISTS (SELECT 1 FROM `pme_folders` WHERE `pme_folders`.`id` = `folders`.`pme_folder_id` AND EXISTS (SELECT 1 FROM `companies` WHERE `companies`.`id` = `pme_folders`.`company_id` AND EXISTS (SELECT 1 FROM `countries` WHERE `countries`.`id` = `companies`.`country_id` AND `countries`.`code` = ? ) ) )'); assert.deepStrictEqual(params, ['FR']); }); @@ -443,8 +441,7 @@ describe('db.PaginatedOptimizedQuery', function() { }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('(EXISTS (SELECT 1 FROM `applicants`')); - assert.ok(sql.includes('OR EXISTS (SELECT 1 FROM `pme_folders`')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `folders` WHERE (EXISTS (SELECT 1 FROM `applicants` WHERE `applicants`.`id` = `folders`.`applicant_id` AND `applicants`.`last_name` LIKE ?) OR EXISTS (SELECT 1 FROM `pme_folders` WHERE `pme_folders`.`id` = `folders`.`pme_folder_id` AND `pme_folders`.`status` = ?))'); assert.deepStrictEqual(params, ['Dupont%', 'ACTIVE']); }); @@ -454,8 +451,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ 'library.title': { $like: 'Test%' } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ?')); - assert.ok(sql.includes('`libraries`.`title` LIKE ?')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `books` WHERE EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ? AND `libraries`.`title` LIKE ? )'); assert.deepStrictEqual(params, ['A', 'Test%']); }); }); @@ -499,9 +495,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ type: 'agp' }).order('applicants.last_name ASC').limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `applicants` ON `applicants`.`id` = `folders`.`applicant_id`')); - assert.ok(sql.includes('ORDER BY applicants.last_name ASC')); - assert.ok(!sql.includes('INNER JOIN')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `applicants` ON `applicants`.`id` = `folders`.`applicant_id` WHERE `folders`.`type` = ? ORDER BY applicants.last_name ASC LIMIT ?, ?'); assert.deepStrictEqual(params, ['agp', 0, 50]); }); @@ -511,10 +505,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ type: 'pme' }).order('pme_folder.company.name ASC').limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `pme_folders` ON `pme_folders`.`id` = `folders`.`pme_folder_id`')); - assert.ok(sql.includes('LEFT JOIN `companies` ON `companies`.`id` = `pme_folders`.`company_id`')); - assert.ok(sql.includes('ORDER BY companies.name ASC')); - assert.strictEqual((sql.match(/LEFT JOIN/g) || []).length, 2); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `pme_folders` ON `pme_folders`.`id` = `folders`.`pme_folder_id` LEFT JOIN `companies` ON `companies`.`id` = `pme_folders`.`company_id` WHERE `folders`.`type` = ? ORDER BY companies.name ASC LIMIT ?, ?'); assert.deepStrictEqual(params, ['pme', 0, 50]); }); @@ -522,21 +513,20 @@ describe('db.PaginatedOptimizedQuery', function() { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'select_ids'; query.where({ type: 'agp' }).order('folders.created_at DESC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(!sql.includes('LEFT JOIN')); - assert.ok(!sql.includes('INNER JOIN')); - assert.ok(sql.includes('ORDER BY folders.created_at DESC')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` WHERE `folders`.`type` = ? ORDER BY folders.created_at DESC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['agp', 0, 50]); }); it('should transform association name to table name in ORDER BY', () => { const query = mockGetDb(new PaginatedOptimizedQuery(Folder)); query.query.verb = 'select_ids'; query.where({ type: 'agp' }).order('pme_folder.company.name ASC').join('pme_folder.company').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('ORDER BY companies.name ASC')); - assert.ok(!sql.includes('company.name')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `pme_folders` ON `pme_folders`.`id` = `folders`.`pme_folder_id` LEFT JOIN `companies` ON `companies`.`id` = `pme_folders`.`company_id` WHERE `folders`.`type` = ? ORDER BY companies.name ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['agp', 0, 50]); }); it('should preserve SQL functions (COALESCE, CONCAT, IFNULL) in ORDER BY', () => { @@ -545,10 +535,10 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ type: 'agp' }).join('applicant') .order('COALESCE(`applicant`.`last_name`, `applicant`.`first_name`) ASC') .limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `applicants`')); - assert.ok(sql.includes('ORDER BY COALESCE(applicants.last_name, applicants.first_name) ASC')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `applicants` ON `applicants`.`id` = `folders`.`applicant_id` WHERE `folders`.`type` = ? ORDER BY COALESCE(applicants.last_name, applicants.first_name) ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['agp', 0, 50]); }); it('should be idempotent when transforming paths (_transformSinglePath)', () => { @@ -574,8 +564,7 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ is_initial: true }).order('studies_year DESC').limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders_with_blocks`.`block_studies_id`')); - assert.ok(sql.includes('ORDER BY block_studies.studies_year DESC')); + assert.strictEqual(sql, 'SELECT `pme_folders_with_blocks`.`id` FROM `pme_folders_with_blocks` LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders_with_blocks`.`block_studies_id` WHERE `pme_folders_with_blocks`.`is_initial` = ? ORDER BY block_studies.studies_year DESC LIMIT ?, ?'); assert.deepStrictEqual(params, [true, 0, 50]); }); @@ -583,31 +572,30 @@ describe('db.PaginatedOptimizedQuery', function() { const query = mockGetDb(new PaginatedOptimizedQuery(PMEFolderWithBlocks)); query.query.verb = 'select_ids'; query.where({ is_initial: true }).order('studies_year DESC').order('bac_year ASC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.strictEqual((sql.match(/LEFT JOIN `block_studies`/g) || []).length, 1); - assert.ok(sql.includes('ORDER BY block_studies.studies_year DESC, block_studies.bac_year ASC')); + assert.strictEqual(sql, 'SELECT `pme_folders_with_blocks`.`id` FROM `pme_folders_with_blocks` LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders_with_blocks`.`block_studies_id` WHERE `pme_folders_with_blocks`.`is_initial` = ? ORDER BY block_studies.studies_year DESC, block_studies.bac_year ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, [true, 0, 50]); }); it('should add separate LEFT JOINs for columns from different blocks', () => { const query = mockGetDb(new PaginatedOptimizedQuery(PMEFolderWithBlocks)); query.query.verb = 'select_ids'; query.where({ is_initial: true }).order('studies_year DESC').order('destination ASC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `block_studies`')); - assert.ok(sql.includes('LEFT JOIN `block_travel_wishes`')); - assert.ok(sql.includes('ORDER BY block_studies.studies_year DESC, block_travel_wishes.destination ASC')); + assert.strictEqual(sql, 'SELECT `pme_folders_with_blocks`.`id` FROM `pme_folders_with_blocks` LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders_with_blocks`.`block_studies_id` LEFT JOIN `block_travel_wishes` ON `block_travel_wishes`.`id` = `pme_folders_with_blocks`.`block_travel_wishes_id` WHERE `pme_folders_with_blocks`.`is_initial` = ? ORDER BY block_studies.studies_year DESC, block_travel_wishes.destination ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, [true, 0, 50]); }); it('should not add JOIN when sorting on a main table column', () => { const query = mockGetDb(new PaginatedOptimizedQuery(PMEFolderWithBlocks)); query.query.verb = 'select_ids'; query.where({ is_initial: true }).order('professional_activity DESC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(!sql.includes('LEFT JOIN')); - assert.ok(sql.includes('ORDER BY professional_activity DESC')); + assert.strictEqual(sql, 'SELECT `pme_folders_with_blocks`.`id` FROM `pme_folders_with_blocks` WHERE `pme_folders_with_blocks`.`is_initial` = ? ORDER BY professional_activity DESC LIMIT ?, ?'); + assert.deepStrictEqual(params, [true, 0, 50]); }); it('should add LEFT JOIN for unprefixed column found in explicitly joined table', () => { @@ -615,21 +603,20 @@ describe('db.PaginatedOptimizedQuery', function() { query.query.verb = 'select_ids'; query.join('applicant'); query.where({ type: 'agp' }).order('first_name ASC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `applicants`'), 'should add LEFT JOIN for the joined table'); - assert.ok(sql.includes('ORDER BY'), 'should have ORDER BY'); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `applicants` ON `applicants`.`id` = `folders`.`applicant_id` WHERE `folders`.`type` = ? ORDER BY applicants.first_name ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['agp', 0, 50]); }); it('should handle nested path 3 levels (folder -> pme_folder -> block_study)', () => { const query = mockGetDb(new PaginatedOptimizedQuery(FolderWithNestedBlocks)); query.query.verb = 'select_ids'; query.where({ type: ['agp', 'avt'] }).order('pme_folder.block_study.bac_year DESC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `pme_folders` ON `pme_folders`.`id` = `folders`.`pme_folder_id`')); - assert.ok(sql.includes('LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders`.`block_studies_id`')); - assert.ok(sql.includes('ORDER BY block_studies.bac_year DESC')); + assert.strictEqual(sql, 'SELECT `folders`.`id` FROM `folders` LEFT JOIN `pme_folders` ON `pme_folders`.`id` = `folders`.`pme_folder_id` LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders`.`block_studies_id` WHERE `folders`.`type` IN (?) ORDER BY block_studies.bac_year DESC LIMIT ?, ?'); + assert.deepStrictEqual(params, [['agp', 'avt'], 0, 50]); }); it('should transform nested path correctly for FULL phase (alias vs table name)', () => { @@ -653,10 +640,10 @@ describe('db.PaginatedOptimizedQuery', function() { const query = mockGetDb(new PaginatedOptimizedQuery(PMEFolderWithBlocks)); query.query.verb = 'select_ids'; query.where({ is_initial: true }).order('COALESCE(`studies_year`, "N/A") DESC').limit(50); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `block_studies`')); - assert.ok(sql.includes('COALESCE(block_studies.studies_year')); + assert.strictEqual(sql, 'SELECT `pme_folders_with_blocks`.`id` FROM `pme_folders_with_blocks` LEFT JOIN `block_studies` ON `block_studies`.`id` = `pme_folders_with_blocks`.`block_studies_id` WHERE `pme_folders_with_blocks`.`is_initial` = ? ORDER BY COALESCE(block_studies.studies_year, "N/A") DESC LIMIT ?, ?'); + assert.deepStrictEqual(params, [true, 0, 50]); }); }); @@ -670,8 +657,8 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ code: 'ABC' }).order('library.title ASC').limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('LEFT JOIN `libraries` ON `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ?')); - assert.ok(params.includes('A')); + assert.strictEqual(sql, 'SELECT `books`.`id` FROM `books` LEFT JOIN `libraries` ON `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ? WHERE `books`.`code` = ? ORDER BY libraries.title ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['A', 'ABC', 0, 50]); }); it('should apply extraWhere in both EXISTS and LEFT JOIN when combined', () => { @@ -680,20 +667,18 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ code: 'ABC', 'library.title': { $like: 'Test%' } }).order('library.title ASC').limit(50); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('EXISTS')); - assert.ok(sql.includes('LEFT JOIN')); - assert.strictEqual((sql.match(/`libraries`\.`collection`/g) || []).length, 2); - assert.strictEqual(params.filter(p => p === 'A').length, 2); + assert.strictEqual(sql, 'SELECT `books`.`id` FROM `books` LEFT JOIN `libraries` ON `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ? WHERE `books`.`code` = ? AND EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ? AND `libraries`.`title` LIKE ? ) ORDER BY libraries.title ASC LIMIT ?, ?'); + assert.deepStrictEqual(params, ['A', 'ABC', 'A', 'Test%', 0, 50]); }); it('COUNT should not include LEFT JOIN even with extraWhere sort', () => { const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere)); query.query.verb = 'count'; query.where({ code: 'ABC' }).order('library.title ASC'); - const { sql } = query.toSQL(); + const { sql, params } = query.toSQL(); - assert.ok(!sql.includes('LEFT JOIN')); - assert.ok(!sql.includes('ORDER BY')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `books` WHERE `books`.`code` = ?'); + assert.deepStrictEqual(params, ['ABC']); }); it('should apply multiple extraWhere conditions', () => { @@ -702,10 +687,8 @@ describe('db.PaginatedOptimizedQuery', function() { query.where({ 'library.title': { $like: 'Test%' } }); const { sql, params } = query.toSQL(); - assert.ok(sql.includes('`libraries`.`collection` = ?')); - assert.ok(sql.includes('`libraries`.`title`')); - assert.ok(params.includes('A')); - assert.ok(params.includes('Main')); + assert.strictEqual(sql, 'SELECT COUNT(0) as `count` FROM `books_multi` WHERE EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books_multi`.`library_id` AND `libraries`.`collection` = ? AND `libraries`.`title` = ? AND `libraries`.`title` LIKE ? )'); + assert.deepStrictEqual(params, ['A', 'Main', 'Test%']); }); }); });