diff --git a/src/core/query-builder.ts b/src/core/query-builder.ts index 3b1e70c..af7bc6d 100644 --- a/src/core/query-builder.ts +++ b/src/core/query-builder.ts @@ -31,23 +31,9 @@ export function buildSelectQuery(table: string, options: TableQueryOptions): { s const whereClauses: string[] = []; const params: any[] = []; - // Column filters - for (const filter of filters) { - if (filter.value) { - whereClauses.push(`${escapeIdentifier(filter.column)} LIKE ?`); - params.push(`%${filter.value}%`); - } - } - - // Global filter - if (globalFilter) { - const globalConditions = columns.map(col => `${escapeIdentifier(col)} LIKE ?`).join(' OR '); - whereClauses.push(`(${globalConditions})`); - // Add param for each column in the OR clause - for (let i = 0; i < columns.length; i++) { - params.push(`%${globalFilter}%`); - } - } + const { conditions, params: filterParams } = buildFilterConditions(filters, globalFilter, columns); + whereClauses.push(...conditions); + params.push(...filterParams); if (whereClauses.length > 0) { sql += ` WHERE ${whereClauses.join(' AND ')}`; @@ -79,26 +65,47 @@ export function buildCountQuery(table: string, options: TableCountOptions): { sq const whereClauses: string[] = []; const params: any[] = []; + const { conditions, params: filterParams } = buildFilterConditions(filters, globalFilter, columns); + whereClauses.push(...conditions); + params.push(...filterParams); + + if (whereClauses.length > 0) { + sql += ` WHERE ${whereClauses.join(' AND ')}`; + } + + return { sql, params }; +} + +/** + * Helper to build WHERE conditions for column filters and global search. + */ +function buildFilterConditions( + filters: { column: string; value: string }[] = [], + globalFilter: string | undefined, + searchColumns: string[] +): { conditions: string[]; params: any[] } { + const conditions: string[] = []; + const params: any[] = []; + // Column filters for (const filter of filters) { if (filter.value) { - whereClauses.push(`${escapeIdentifier(filter.column)} LIKE ?`); + conditions.push(`${escapeIdentifier(filter.column)} LIKE ?`); params.push(`%${filter.value}%`); } } // Global filter - if (globalFilter && columns.length > 0) { - const globalConditions = columns.map(col => `${escapeIdentifier(col)} LIKE ?`).join(' OR '); - whereClauses.push(`(${globalConditions})`); - for (let i = 0; i < columns.length; i++) { + if (globalFilter && searchColumns.length > 0) { + const globalConditions = searchColumns + .map(col => `${escapeIdentifier(col)} LIKE ?`) + .join(' OR '); + + conditions.push(`(${globalConditions})`); + for (let i = 0; i < searchColumns.length; i++) { params.push(`%${globalFilter}%`); } } - if (whereClauses.length > 0) { - sql += ` WHERE ${whereClauses.join(' AND ')}`; - } - - return { sql, params }; + return { conditions, params }; } diff --git a/tests/unit/query-builder.test.ts b/tests/unit/query-builder.test.ts index 5c9ab12..9acd43e 100644 --- a/tests/unit/query-builder.test.ts +++ b/tests/unit/query-builder.test.ts @@ -34,5 +34,64 @@ describe('Query Builder', () => { const { sql } = buildSelectQuery('logs', options); assert.strictEqual(sql, 'SELECT * FROM "logs" ORDER BY "created_at" DESC LIMIT 10 OFFSET 20'); }); + + it('should handle global filter with explicit columns', () => { + const options = { + columns: ['name', 'description'], + globalFilter: 'test' + }; + const { sql, params } = buildSelectQuery('products', options); + assert.strictEqual(sql, 'SELECT "name", "description" FROM "products" WHERE ("name" LIKE ? OR "description" LIKE ?)'); + assert.deepStrictEqual(params, ['%test%', '%test%']); + }); + + it('should handle global filter with default columns (edge case)', () => { + // This documents current behavior where default ['*'] results in " * " LIKE ? + const options = { + globalFilter: 'test' + }; + const { sql, params } = buildSelectQuery('products', options); + assert.strictEqual(sql, 'SELECT * FROM "products" WHERE ("*" LIKE ?)'); + assert.deepStrictEqual(params, ['%test%']); + }); + + it('should handle global filter with empty columns (safe behavior)', () => { + const options = { + columns: [], + globalFilter: 'test' + }; + const { sql, params } = buildSelectQuery('products', options); + // Implementation should avoid generating WHERE () + assert.strictEqual(sql, 'SELECT FROM "products"'); + assert.deepStrictEqual(params, []); + }); + }); + + describe('buildCountQuery', () => { + it('should build simple count', () => { + const { sql, params } = buildCountQuery('my_table', {}); + assert.strictEqual(sql, 'SELECT COUNT(*) as count FROM "my_table"'); + assert.deepStrictEqual(params, []); + }); + + it('should handle global filter with explicit columns', () => { + const options = { + columns: ['name', 'description'], + globalFilter: 'test' + }; + const { sql, params } = buildCountQuery('products', options); + assert.strictEqual(sql, 'SELECT COUNT(*) as count FROM "products" WHERE ("name" LIKE ? OR "description" LIKE ?)'); + assert.deepStrictEqual(params, ['%test%', '%test%']); + }); + + it('should handle global filter with empty columns (safe behavior)', () => { + const options = { + columns: [], + globalFilter: 'test' + }; + const { sql, params } = buildCountQuery('products', options); + assert.strictEqual(sql, 'SELECT COUNT(*) as count FROM "products"'); + assert.deepStrictEqual(params, []); + }); }); });