diff --git a/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts b/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts index 24579d30b616..26709ec56bc4 100644 --- a/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts +++ b/packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts @@ -196,7 +196,17 @@ function renderLimitOffset( pim: ParamIndexMap, ): string { if (value === undefined) return ''; - if (typeof value === 'number') return `${keyword} ${value}`; + if (typeof value === 'number') { + // NaN/±Infinity stringify into SQL identifiers (`LIMIT NaN`) and crash the engine. + if (!Number.isFinite(value)) { + throw adapterError( + 'RUNTIME.AST_INVALID', + `${keyword} requires a finite number, got ${String(value)}`, + { meta: { node: 'select', clause: keyword } }, + ); + } + return `${keyword} ${value}`; + } return `${keyword} ${renderExpr(value, contract, pim)}`; } diff --git a/packages/3-targets/6-adapters/postgres/test/adapter-errors.test.ts b/packages/3-targets/6-adapters/postgres/test/adapter-errors.test.ts index 06daac81f43c..6e3cc8234afe 100644 --- a/packages/3-targets/6-adapters/postgres/test/adapter-errors.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/adapter-errors.test.ts @@ -142,6 +142,22 @@ describe('adapter-postgres structured error codes', () => { ); }); + it('raises RUNTIME.AST_INVALID for a non-finite LIMIT or OFFSET', () => { + const base = () => + SelectAst.from(TableSource.named('user', undefined, 'public')).withProjection([ + ProjectionItem.of('id', ColumnRef.of('user', 'id')), + ]); + for (const ast of [ + base().withLimit(Number.NaN), + base().withLimit(Number.POSITIVE_INFINITY), + base().withOffset(Number.NaN), + ]) { + expect(structuredCodeOf(() => renderLoweredSql(ast, contract, codecLookup))).toBe( + 'RUNTIME.AST_INVALID', + ); + } + }); + it('raises RUNTIME.AST_INVALID for an INSERT with zero rows', () => { const ast = InsertAst.into(TableSource.named('user', undefined, 'public')).withRows([]); expect(structuredCodeOf(() => renderLoweredSql(ast, contract, codecLookup))).toBe( diff --git a/packages/3-targets/6-adapters/sqlite/src/core/adapter.ts b/packages/3-targets/6-adapters/sqlite/src/core/adapter.ts index 81f2cb537219..7bb31e975afa 100644 --- a/packages/3-targets/6-adapters/sqlite/src/core/adapter.ts +++ b/packages/3-targets/6-adapters/sqlite/src/core/adapter.ts @@ -228,7 +228,17 @@ function renderLimitOffset( ctx: SqliteRenderContext, ): string { if (value === undefined) return ''; - if (typeof value === 'number') return `${keyword} ${value}`; + if (typeof value === 'number') { + // NaN/±Infinity stringify into SQL identifiers (`LIMIT NaN`) and crash the engine. + if (!Number.isFinite(value)) { + throw structuredError( + 'RUNTIME.AST_INVALID', + `${keyword} requires a finite number, got ${String(value)}`, + { meta: { node: 'select', clause: keyword } }, + ); + } + return `${keyword} ${value}`; + } return `${keyword} ${renderExpr(value, ctx)}`; } diff --git a/packages/3-targets/6-adapters/sqlite/test/adapter.test.ts b/packages/3-targets/6-adapters/sqlite/test/adapter.test.ts index 9024b0144409..9667e319692c 100644 --- a/packages/3-targets/6-adapters/sqlite/test/adapter.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/adapter.test.ts @@ -156,6 +156,21 @@ describe('SQLite adapter', () => { ); }); + it('throws RUNTIME.AST_INVALID for non-finite LIMIT / OFFSET', () => { + const base = () => + SelectAst.from(TableSource.named('user')).withProjection([ + ProjectionItem.of('id', ColumnRef.of('user', 'id')), + ]); + for (const ast of [ + base().withLimit(Number.NaN), + base().withLimit(Number.POSITIVE_INFINITY), + base().withOffset(Number.NaN), + base().withLimit(1).withOffset(Number.NEGATIVE_INFINITY), + ]) { + expect(() => adapter.lower(ast, { contract })).toThrow(/requires a finite number/); + } + }); + it('renders DISTINCT', () => { const ast = SelectAst.from(TableSource.named('user')) .withProjection([ProjectionItem.of('email', ColumnRef.of('user', 'email'))]) diff --git a/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts b/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts index 0fe5db9b350f..3201d0ecba5e 100644 --- a/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts @@ -99,6 +99,22 @@ describe('structured error codes', () => { }); }); + it('RUNTIME.AST_INVALID on a non-finite LIMIT', () => { + const err = catchError(() => + runtimeAdapter.lower( + SelectAst.from(TableSource.named('user')) + .withProjection([ProjectionItem.of('id', LiteralExpr.of(1))]) + .withLimit(Number.NaN), + { contract }, + ), + ); + expect(isStructuredError(err)).toBe(true); + expect(err).toMatchObject({ + code: 'RUNTIME.AST_INVALID', + meta: { node: 'select', clause: 'LIMIT' }, + }); + }); + it('RUNTIME.AST_UNSUPPORTED on DEFAULT as an INSERT value', () => { const ast = InsertAst.into(TableSource.named('user')).withRows([ { email: new DefaultValueExpr() },