diff --git a/apps/sim/app/api/table/utils.ts b/apps/sim/app/api/table/utils.ts index 7424258ad0e..28f08241a76 100644 --- a/apps/sim/app/api/table/utils.ts +++ b/apps/sim/app/api/table/utils.ts @@ -156,7 +156,10 @@ async function checkTableAccess(tableId: string, userId: string): Promise { it('handles nested $or and $and', () => { const out = render( buildFilterClause( - { $or: [{ $and: [{ status: 'active' }, { verified: true }] }, { role: 'admin' }] }, + { + $or: [{ $and: [{ status: 'active' }, { verified: true }] }, { role: 'admin' }], + }, TABLE, NO_COLUMNS ) @@ -297,7 +299,9 @@ describe('SQL Builder', () => { it('propagates date cast through nested $and', () => { const out = render( buildFilterClause( - { $and: [{ birthDate: { $gte: '2024-01-01' } }, { birthDate: { $lt: '2025-01-01' } }] }, + { + $and: [{ birthDate: { $gte: '2024-01-01' } }, { birthDate: { $lt: '2025-01-01' } }], + }, TABLE, dateCols ) @@ -309,7 +313,9 @@ describe('SQL Builder', () => { it('propagates date cast through nested $or', () => { const out = render( buildFilterClause( - { $or: [{ birthDate: { $lt: '2000-01-01' } }, { birthDate: { $gt: '2024-01-01' } }] }, + { + $or: [{ birthDate: { $lt: '2000-01-01' } }, { birthDate: { $gt: '2024-01-01' } }], + }, TABLE, dateCols ) @@ -399,6 +405,26 @@ describe('SQL Builder', () => { ) }) + it('uses direct timestamp column for created_at comparisons', () => { + const out = render( + buildFilterClause({ created_at: { $gt: '2026-07-20' } }, TABLE, NO_COLUMNS) + ) + + expect(out).toContain(`${TABLE}.created_at`) + expect(out).not.toContain(`data->>'created_at'`) + expect(out).toContain('::timestamptz') + }) + + it('uses direct column equality for created_at', () => { + const out = render( + buildFilterClause({ created_at: { $eq: '2026-07-20' } }, TABLE, NO_COLUMNS) + ) + + expect(out).toContain(`${TABLE}.created_at`) + expect(out).toContain('=') + expect(out).not.toContain(`${TABLE}.data @>`) + }) + it('combines multiple sort fields with commas', () => { const cols: ColumnDefinition[] = [ { name: 'name', type: 'string' }, diff --git a/apps/sim/lib/table/sql.ts b/apps/sim/lib/table/sql.ts index 5e695d93195..61c035c02ba 100644 --- a/apps/sim/lib/table/sql.ts +++ b/apps/sim/lib/table/sql.ts @@ -17,6 +17,7 @@ import type { JsonValue, Sort, } from '@/lib/table/types' +import { isBuiltInDateField } from '@/app/api/table/utils.ts' /** * Error thrown when caller-supplied filter or sort input is malformed. @@ -380,7 +381,9 @@ function buildFieldCondition( case '$ncontains': conditions.push( - buildLikeClause(tableName, field, value as string, 'contains', { negate: true }) + buildLikeClause(tableName, field, value as string, 'contains', { + negate: true, + }) ) break @@ -458,8 +461,15 @@ function buildLogicalClause( /** Builds JSONB containment clause: `data @> '{"field": value}'::jsonb` (uses GIN index) */ function buildContainmentClause(tableName: string, field: string, value: JsonValue): SQL { + if (isBuiltInDateField(field)) { + return sql`${sql.raw(`${tableName}.${field}`)} = ${value}::timestamptz` + } + const jsonObj = JSON.stringify({ [field]: value }) return sql`${sql.raw(`${tableName}.data`)} @> ${jsonObj}::jsonb` + + // const jsonObj = JSON.stringify({ [field]: value }) + // return sql`${sql.raw(`${tableName}.data`)} @> ${jsonObj}::jsonb` } /** @@ -485,10 +495,17 @@ function buildComparisonClause( value: number | string, columnType: ColumnType | undefined ): SQL { + if (isBuiltInDateField(field)) { + columnType = 'date' + } + const escapedField = field.replace(/'/g, "''") const cast = jsonbCastForType(columnType) ?? 'numeric' validateComparisonValue(field, columnType, cast, value) - const cell = sql.raw(`(${tableName}.data->>'${escapedField}')::${cast}`) + + const cell = isBuiltInDateField(field) + ? sql.raw(`${tableName}.${field}`) + : sql.raw(`(${tableName}.data->>'${escapedField}')::${cast}`) return cast === 'timestamptz' ? sql`${cell} ${sql.raw(operator)} ${value}::timestamptz` : sql`${cell} ${sql.raw(operator)} ${value}`