From 490c7ad75f9a31ed54ce31a3542e988abef3860d Mon Sep 17 00:00:00 2001 From: Tiago Lauer Date: Thu, 20 Aug 2026 13:58:02 -0300 Subject: [PATCH 01/15] refactor: move lexical string primitives --- src/case.ts | 2 +- src/compiler/next/compile-select.ts | 2 +- src/compiler/next/infer-expression.ts | 2 +- src/compiler/next/infer-params.ts | 2 +- src/compiler/semantics/functions.ts | 2 +- src/cte.ts | 2 +- src/from.ts | 2 +- src/language/dialect/common.ts | 2 +- src/language/dml/parse-delete.ts | 2 +- src/language/dml/parse-insert.ts | 2 +- src/language/dml/parse-merge.ts | 2 +- src/language/dml/parse-update.ts | 2 +- src/language/lexical/statement.ts | 2 +- src/{ => language/lexical}/string.ts | 4 +++- src/language/select/parse-from.ts | 2 +- src/language/select/parse-predicate.ts | 2 +- src/language/select/parse-projection.ts | 2 +- src/language/select/parse-select.ts | 2 +- src/language/with/parse-with.ts | 2 +- src/params.ts | 2 +- src/parse.ts | 4 ++-- src/where.ts | 2 +- 22 files changed, 25 insertions(+), 23 deletions(-) rename src/{ => language/lexical}/string.ts (99%) diff --git a/src/case.ts b/src/case.ts index 2c228cb..2b693fe 100644 --- a/src/case.ts +++ b/src/case.ts @@ -1,4 +1,4 @@ -import type { Trim, FirstWord, DropFirstWord, IsKeyword, Unquote } from './string.js'; +import type { Trim, FirstWord, DropFirstWord, IsKeyword, Unquote } from './language/lexical/string.js'; import type { SchemaLike, Source, ResolveColumnType } from './parse.js'; // A nested CASE is often written wrapped in parens (`(case ... end)`), which diff --git a/src/compiler/next/compile-select.ts b/src/compiler/next/compile-select.ts index 22da625..83baa17 100644 --- a/src/compiler/next/compile-select.ts +++ b/src/compiler/next/compile-select.ts @@ -1,5 +1,5 @@ import type { PredicateIR } from '../../language/ir/predicate.js'; -import type { IsKeyword, Trim } from '../../string.js'; +import type { IsKeyword, Trim } from '../../language/lexical/string.js'; import type { SelectQueryIR, SetOperationIR, diff --git a/src/compiler/next/infer-expression.ts b/src/compiler/next/infer-expression.ts index cae7beb..f700c16 100644 --- a/src/compiler/next/infer-expression.ts +++ b/src/compiler/next/infer-expression.ts @@ -8,7 +8,7 @@ import type { StripQualifier, Trim, Unquote, -} from '../../string.js'; +} from '../../language/lexical/string.js'; import type { FunctionReturnType, IsFunctionCall, diff --git a/src/compiler/next/infer-params.ts b/src/compiler/next/infer-params.ts index e748557..a200b9d 100644 --- a/src/compiler/next/infer-params.ts +++ b/src/compiler/next/infer-params.ts @@ -7,7 +7,7 @@ import type { StripQualifier, Trim, Unquote, -} from '../../string.js'; +} from '../../language/lexical/string.js'; import type { PredicateIR } from '../../language/ir/predicate.js'; import type { ProjectionIR, diff --git a/src/compiler/semantics/functions.ts b/src/compiler/semantics/functions.ts index e2dfffd..0a19fe9 100644 --- a/src/compiler/semantics/functions.ts +++ b/src/compiler/semantics/functions.ts @@ -1,4 +1,4 @@ -import type { Trim } from '../../string.js'; +import type { Trim } from '../../language/lexical/string.js'; export interface FunctionReturnTypes { count: number; diff --git a/src/cte.ts b/src/cte.ts index dfa0ece..148b92f 100644 --- a/src/cte.ts +++ b/src/cte.ts @@ -5,7 +5,7 @@ import type { IsKeyword, ExtractParenGroup, SplitColumnList, -} from './string.js'; +} from './language/lexical/string.js'; import type { SchemaLike, Flatten, QueryTypeError, SelectColumnKeys, ShadowedBy } from './parse.js'; import type { InferRowWith } from './parse.js'; diff --git a/src/from.ts b/src/from.ts index 56a0994..9395b22 100644 --- a/src/from.ts +++ b/src/from.ts @@ -8,7 +8,7 @@ import type { ExtractParenGroup, ApplyParenDelta, SplitColumnList, -} from './string.js'; +} from './language/lexical/string.js'; export interface Source { table: string; diff --git a/src/language/dialect/common.ts b/src/language/dialect/common.ts index ed10bf1..629d4aa 100644 --- a/src/language/dialect/common.ts +++ b/src/language/dialect/common.ts @@ -4,7 +4,7 @@ import type { FirstWord, IsKeyword, Trim, -} from '../../string.js'; +} from '../lexical/string.js'; export interface DialectCapabilities { top: boolean; diff --git a/src/language/dml/parse-delete.ts b/src/language/dml/parse-delete.ts index 23410db..115ca45 100644 --- a/src/language/dml/parse-delete.ts +++ b/src/language/dml/parse-delete.ts @@ -7,7 +7,7 @@ import type { SplitAtTopLevelKeyword, TakeUntilTopLevelKeyword, Trim, -} from '../../string.js'; +} from '../lexical/string.js'; import type { PredicateIR } from '../ir/predicate.js'; import type { DeleteQueryIR } from '../ir/query.js'; import type { SourceIR } from '../ir/source.js'; diff --git a/src/language/dml/parse-insert.ts b/src/language/dml/parse-insert.ts index be100b7..bc28237 100644 --- a/src/language/dml/parse-insert.ts +++ b/src/language/dml/parse-insert.ts @@ -12,7 +12,7 @@ import type { TakeUntilTopLevelKeyword, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { InsertDefaultValuesIR, InsertQueryIR, diff --git a/src/language/dml/parse-merge.ts b/src/language/dml/parse-merge.ts index e676b30..3d7ac94 100644 --- a/src/language/dml/parse-merge.ts +++ b/src/language/dml/parse-merge.ts @@ -10,7 +10,7 @@ import type { TakeUntilTopLevelKeyword, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { PredicateIR } from '../ir/predicate.js'; import type { MergeQueryIR } from '../ir/query.js'; import type { DerivedSourceIR, SourceIR } from '../ir/source.js'; diff --git a/src/language/dml/parse-update.ts b/src/language/dml/parse-update.ts index 86a0ee5..49c65fc 100644 --- a/src/language/dml/parse-update.ts +++ b/src/language/dml/parse-update.ts @@ -8,7 +8,7 @@ import type { TakeUntilTopLevelKeyword, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { PredicateIR } from '../ir/predicate.js'; import type { UpdateQueryIR } from '../ir/query.js'; import type { AssignmentIR, WriteTargetIR } from '../ir/write.js'; diff --git a/src/language/lexical/statement.ts b/src/language/lexical/statement.ts index 639bc86..0c271ec 100644 --- a/src/language/lexical/statement.ts +++ b/src/language/lexical/statement.ts @@ -1,4 +1,4 @@ -import type { FirstWord, IsKeyword, Normalize, Trim } from '../../string.js'; +import type { FirstWord, IsKeyword, Normalize, Trim } from './string.js'; type StatementKindName = | 'select' diff --git a/src/string.ts b/src/language/lexical/string.ts similarity index 99% rename from src/string.ts rename to src/language/lexical/string.ts index 749f65e..9cc60b5 100644 --- a/src/string.ts +++ b/src/language/lexical/string.ts @@ -1,6 +1,8 @@ export type NonSpaceWhitespace = '\t' | '\n' | '\r' | '\f' | '\v'; -export type Whitespace = ' ' | NonSpaceWhitespace; +export type Whitespace = + | ' ' + | NonSpaceWhitespace; export type TrimLeft = S extends `${Whitespace}${infer Rest}` ? TrimLeft diff --git a/src/language/select/parse-from.ts b/src/language/select/parse-from.ts index 9b555a5..01ad0b2 100644 --- a/src/language/select/parse-from.ts +++ b/src/language/select/parse-from.ts @@ -9,7 +9,7 @@ import type { StripQualifier, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { PredicateIR } from '../ir/predicate.js'; import type { SelectQueryIR } from '../ir/query.js'; import type { diff --git a/src/language/select/parse-predicate.ts b/src/language/select/parse-predicate.ts index 9ef192b..bf923dd 100644 --- a/src/language/select/parse-predicate.ts +++ b/src/language/select/parse-predicate.ts @@ -4,7 +4,7 @@ import type { FirstWord, IsKeyword, Trim, -} from '../../string.js'; +} from '../lexical/string.js'; import type { PredicateIR } from '../ir/predicate.js'; import type { SelectClausesIR } from '../ir/query.js'; diff --git a/src/language/select/parse-projection.ts b/src/language/select/parse-projection.ts index 43a5b76..2c54c8d 100644 --- a/src/language/select/parse-projection.ts +++ b/src/language/select/parse-projection.ts @@ -8,7 +8,7 @@ import type { StripQualifier, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { ColumnProjectionIR, ExpressionProjectionIR, diff --git a/src/language/select/parse-select.ts b/src/language/select/parse-select.ts index e381bf9..4aca571 100644 --- a/src/language/select/parse-select.ts +++ b/src/language/select/parse-select.ts @@ -8,7 +8,7 @@ import type { StripQualifier, Trim, Unquote, -} from '../../string.js'; +} from '../lexical/string.js'; import type { SelectClausesIR, SelectQueryIR, diff --git a/src/language/with/parse-with.ts b/src/language/with/parse-with.ts index 20fe01b..a822089 100644 --- a/src/language/with/parse-with.ts +++ b/src/language/with/parse-with.ts @@ -6,7 +6,7 @@ import type { Normalize, SplitColumnList, Trim, -} from '../../string.js'; +} from '../lexical/string.js'; import type { AnyCteIR, CteIR } from '../ir/cte.js'; import type { SelectQueryIR } from '../ir/query.js'; import type { ParseSelectIR } from '../select/parse-select.js'; diff --git a/src/params.ts b/src/params.ts index eef3144..1a23415 100644 --- a/src/params.ts +++ b/src/params.ts @@ -9,7 +9,7 @@ import type { HasNonTrailingSemicolon, MaskQuotedIdentifiers, StartsWithIdentifierChar, -} from './string.js'; +} from './language/lexical/string.js'; import type { Source, SchemaLike, diff --git a/src/parse.ts b/src/parse.ts index 1c5c285..5cfe9cd 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -12,7 +12,7 @@ import type { ApplyParenDelta, SplitColumnList, HasNonTrailingSemicolon, -} from './string.js'; +} from './language/lexical/string.js'; import type { IsFunctionCall, FunctionOutputName, @@ -534,7 +534,7 @@ export type ParseStatement = ParseStatementNormalized = ParseSelectBody>; -export type { SplitColumnList } from './string.js'; +export type { SplitColumnList } from './language/lexical/string.js'; type OutputName = IsFunctionCall extends true ? FunctionOutputName diff --git a/src/where.ts b/src/where.ts index b819712..efd504a 100644 --- a/src/where.ts +++ b/src/where.ts @@ -1,4 +1,4 @@ -import type { Trim, FirstWord, DropFirstWord, IsKeyword, ExtractParenGroup } from './string.js'; +import type { Trim, FirstWord, DropFirstWord, IsKeyword, ExtractParenGroup } from './language/lexical/string.js'; import type { TakeUntilClauseBoundary } from './from.js'; import type { SplitAtTopLevelKeyword, From 84606b54a513b2bc53c0cf36437dc65846c7c99a Mon Sep 17 00:00:00 2001 From: Tiago Lauer Date: Thu, 20 Aug 2026 13:59:01 -0300 Subject: [PATCH 02/15] refactor: move surviving compiler primitives --- src/compiler/legacy.ts | 3 +- src/functions.ts | 7 ---- src/index.ts | 2 +- src/language/lexical/placeholders.ts | 39 +++++++++++++++++++ src/params.ts | 56 +--------------------------- src/parse.ts | 2 +- src/public/client.ts | 6 +-- src/public/schema.ts | 2 +- tests/pg-operators.test-d.ts | 2 +- 9 files changed, 47 insertions(+), 72 deletions(-) delete mode 100644 src/functions.ts create mode 100644 src/language/lexical/placeholders.ts diff --git a/src/compiler/legacy.ts b/src/compiler/legacy.ts index 490f61d..64cc821 100644 --- a/src/compiler/legacy.ts +++ b/src/compiler/legacy.ts @@ -14,7 +14,6 @@ export type { export type { InferParams as LegacyInferParams, - UsedPlaceholderStyles, } from '../params.js'; -export type { FunctionReturnTypes } from '../functions.js'; +export type { FunctionReturnTypes } from './semantics/functions.js'; diff --git a/src/functions.ts b/src/functions.ts deleted file mode 100644 index aad437d..0000000 --- a/src/functions.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type { - FunctionName, - FunctionOutputName, - FunctionReturnType, - FunctionReturnTypes, - IsFunctionCall, -} from './compiler/semantics/functions.js'; diff --git a/src/index.ts b/src/index.ts index 64bef8d..113a8ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,6 @@ export type { ParseStatement, ParsedStatement, Source, -} from './compiler/legacy.js'; +} from './parse.js'; export { defineSchema } from './public/schema.js'; diff --git a/src/language/lexical/placeholders.ts b/src/language/lexical/placeholders.ts new file mode 100644 index 0000000..805d3c2 --- /dev/null +++ b/src/language/lexical/placeholders.ts @@ -0,0 +1,39 @@ +import type { + MaskQuotedIdentifiers, + Normalize, + StartsWithIdentifierChar, +} from './string.js'; + +type StripDoubledAt = S extends `${infer Before}@@${infer After}` + ? StripDoubledAt<`${Before}${After}`> + : S; + +type StripDollarAction = S extends `${infer Before}$action${infer After}` + ? StartsWithIdentifierChar extends true + ? `${Before}$action${StripDollarAction}` + : `${Before}${StripDollarAction}` + : S; + +type HasPrefixedPlaceholder< + S extends string, + Prefix extends string, +> = S extends `${string}${Prefix}${infer After}` + ? StartsWithIdentifierChar extends true + ? true + : HasPrefixedPlaceholder + : false; + +type HasQuestionPlaceholder = S extends `${string}?${infer After}` + ? After extends `|${string}` | `&${string}` + ? HasQuestionPlaceholder + : true + : false; + +export type UsedPlaceholderStyles = Lowercase< + MaskQuotedIdentifiers> +> extends infer Text extends string + ? + | (HasQuestionPlaceholder extends true ? 'question' : never) + | (HasPrefixedPlaceholder, '$'> extends true ? 'dollar' : never) + | (HasPrefixedPlaceholder, '@'> extends true ? 'at' : never) + : never; diff --git a/src/params.ts b/src/params.ts index 1a23415..01e99aa 100644 --- a/src/params.ts +++ b/src/params.ts @@ -7,7 +7,6 @@ import type { ExtractParenGroup, Digit, HasNonTrailingSemicolon, - MaskQuotedIdentifiers, StartsWithIdentifierChar, } from './language/lexical/string.js'; import type { @@ -23,7 +22,7 @@ import type { QueryTypeError, } from './parse.js'; import type { ParseWithClause } from './cte.js'; -import type { FunctionName } from './functions.js'; +import type { FunctionName } from './compiler/semantics/functions.js'; // `@>` and `<@` compare an array or a jsonb value against another of the same // type, so the bound value carries the column's own type - the same thing `=` @@ -628,59 +627,6 @@ type CteBodyParamScan< : CteBodyParamScan : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames }; -type StripDoubledAt = S extends `${infer Before}@@${infer After}` - ? StripDoubledAt<`${Before}${After}`> - : S; - -// Whole-token, because `$actionType` is a name that merely starts with the -// pseudo-column's letters. Stripping the substring turned it into `type`, the -// dollar scan then found nothing, and the query reported no placeholder style -// at all - so it passed the brand check for every dialect while Params still -// demanded a value for it, which is the guaranteed runtime failure the brand -// exists to prevent (issue #298). IsPlaceholder already excluded only the -// exact token; this is the same rule on the other side. -type StripDollarAction = S extends `${infer Before}$action${infer After}` - ? StartsWithIdentifierChar extends true - ? `${Before}$action${StripDollarAction}` - : `${Before}${StripDollarAction}` - : S; - -// A prefix only counts when a name or an index follows it, the same rule -// IsPlaceholder applies per token - otherwise `where tags @> $1` reported the -// `at` style and was rejected against a dollar executor (issue #249). -type HasPrefixedPlaceholder< - S extends string, - Prefix extends string, -> = S extends `${string}${Prefix}${infer After}` - ? StartsWithIdentifierChar extends true - ? true - : HasPrefixedPlaceholder - : false; - -// `?|` and `?&` are Postgres jsonb operators, not placeholders. A bare `?` is -// a placeholder, since that is what it is in MySQL and SQLite. -type HasQuestionPlaceholder = S extends `${string}?${infer After}` - ? After extends `|${string}` | `&${string}` - ? HasQuestionPlaceholder - : true - : false; - -// Quoted identifiers survive Normalize by design (the parser needs the name), -// so their bodies are masked here before the scan - a column legally named -// "user@id" is not a parameter style. -// -// Lowercased so the `$action` strip is case-insensitive, matching how -// IsMergeActionPseudoColumn resolves it. Case is irrelevant to the three -// characters this scans for, so nothing else is affected. -export type UsedPlaceholderStyles = Lowercase< - MaskQuotedIdentifiers> -> extends infer Text extends string - ? - | (HasQuestionPlaceholder extends true ? 'question' : never) - | (HasPrefixedPlaceholder, '$'> extends true ? 'dollar' : never) - | (HasPrefixedPlaceholder, '@'> extends true ? 'at' : never) - : never; - type OuterAndCteParams< DB extends SchemaLike, Q extends string, diff --git a/src/parse.ts b/src/parse.ts index 5cfe9cd..8c4fe2d 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -17,7 +17,7 @@ import type { IsFunctionCall, FunctionOutputName, FunctionReturnType, -} from './functions.js'; +} from './compiler/semantics/functions.js'; import type { Source, ParseFromClause, diff --git a/src/public/client.ts b/src/public/client.ts index 38dc279..93dc647 100644 --- a/src/public/client.ts +++ b/src/public/client.ts @@ -1,7 +1,5 @@ -import type { - QueryTypeError, - UsedPlaceholderStyles, -} from '../compiler/legacy.js'; +import type { QueryTypeError } from '../compiler/contracts/public-error.js'; +import type { UsedPlaceholderStyles } from '../language/lexical/placeholders.js'; import type { InferParamsViaGateway, InferStrictViaGateway, diff --git a/src/public/schema.ts b/src/public/schema.ts index 8cae3a1..8a3eb6e 100644 --- a/src/public/schema.ts +++ b/src/public/schema.ts @@ -5,7 +5,7 @@ export type { SchemaLike, } from '../compiler/schema/model.js'; -export type { FunctionReturnTypes } from '../compiler/legacy.js'; +export type { FunctionReturnTypes } from '../compiler/semantics/functions.js'; export type { QueryTypeError } from '../compiler/contracts/public-error.js'; diff --git a/tests/pg-operators.test-d.ts b/tests/pg-operators.test-d.ts index 93c1659..ec363b2 100644 --- a/tests/pg-operators.test-d.ts +++ b/tests/pg-operators.test-d.ts @@ -1,5 +1,5 @@ import type { Params } from '../src/index.js'; -import type { UsedPlaceholderStyles } from '../src/params.js'; +import type { UsedPlaceholderStyles } from '../src/language/lexical/placeholders.js'; type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; From d25bcdf6e042fa3fabbdf31ea1014f56971ebbea Mon Sep 17 00:00:00 2001 From: Tiago Lauer Date: Thu, 20 Aug 2026 14:06:18 -0300 Subject: [PATCH 03/15] refactor: remove legacy compiler --- scripts/check-architecture.mjs | 7 - src/compiler/gateway.ts | 111 +------- src/compiler/legacy.ts | 19 -- src/compiler/next/compile-delete.ts | 18 +- src/compiler/next/compile-insert.ts | 16 ++ src/compiler/next/compile-merge.ts | 35 ++- src/compiler/next/compile-update.ts | 19 +- src/compiler/next/compile-with.ts | 93 +++++-- src/compiler/next/index.ts | 43 ++- src/language/ir/query.ts | 7 + src/language/lexical/statement.ts | 8 +- src/language/select/parse-select.ts | 52 ++-- src/language/with/parse-with.ts | 25 +- tests/architecture/dependencies.test.mjs | 18 -- tests/next/cte-parity.test-d.ts | 112 -------- tests/next/delete-parity.test-d.ts | 113 -------- tests/next/insert-parity.test-d.ts | 239 ----------------- tests/next/merge-parity.test-d.ts | 93 ------- tests/next/select-parity.test-d.ts | 326 ----------------------- tests/next/update-parity.test-d.ts | 134 ---------- 20 files changed, 229 insertions(+), 1259 deletions(-) delete mode 100644 src/compiler/legacy.ts delete mode 100644 tests/next/cte-parity.test-d.ts delete mode 100644 tests/next/delete-parity.test-d.ts delete mode 100644 tests/next/insert-parity.test-d.ts delete mode 100644 tests/next/merge-parity.test-d.ts delete mode 100644 tests/next/select-parity.test-d.ts delete mode 100644 tests/next/update-parity.test-d.ts diff --git a/scripts/check-architecture.mjs b/scripts/check-architecture.mjs index 930f3cf..a22da17 100644 --- a/scripts/check-architecture.mjs +++ b/scripts/check-architecture.mjs @@ -35,19 +35,12 @@ const RULES = [ { from: 'src/compiler/', forbidden: ['src/runtime/'], - allow: ['src/compiler/legacy.ts'], name: 'compiler isolation', }, - { - from: 'src/compiler/next/', - forbidden: ['src/compiler/legacy.ts'], - name: 'next compiler isolation', - }, { from: 'src/tooling/', forbidden: [ 'src/compiler/next/', - 'src/compiler/legacy.ts', 'src/language/', 'src/runtime/', 'src/public/', diff --git a/src/compiler/gateway.ts b/src/compiler/gateway.ts index 0296b0f..479f386 100644 --- a/src/compiler/gateway.ts +++ b/src/compiler/gateway.ts @@ -1,112 +1,23 @@ -import type { StatementKind } from '../language/lexical/statement.js'; -import type { ParseWithIR } from '../language/with/parse-with.js'; -import type { ApplyLoosePolicy, ApplyStrictPolicy } from './contracts/compilation.js'; import type { SchemaLike } from './schema/model.js'; import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, - LegacyInferRow, - LegacyInferRowStrict, -} from './legacy.js'; -import type { CompileInsert, InferInsertParams } from './next/compile-insert.js'; -import type { CompileDelete, InferDeleteParams } from './next/compile-delete.js'; -import type { CompileMerge, InferMergeParams } from './next/compile-merge.js'; -import type { CompileSelect } from './next/compile-select.js'; -import type { CompileUpdate, InferUpdateParams } from './next/compile-update.js'; -import type { CompileWith, InferWithParams } from './next/compile-with.js'; -import type { InferNextParams } from './next/infer-params.js'; - -type NextCompilation< - DB, - Q extends string, - ValidatePredicates extends boolean, -> = Q extends `select ${string}` - ? CompileSelect - : GatewayKind extends 'select' - ? CompileSelect - : GatewayKind extends 'with' - ? CompileWith - : GatewayKind extends 'insert' - ? CompileInsert - : GatewayKind extends 'update' - ? CompileUpdate - : GatewayKind extends 'delete' - ? CompileDelete - : CompileMerge; - -type NextQuery = - ApplyLoosePolicy>; - -type NextStrictQuery = - ApplyStrictPolicy>; - -type NextRow = NextQuery extends infer Result - ? Result extends readonly (infer Row)[] - ? Row - : Result - : never; - -type NextStrictRow = - NextStrictQuery extends infer Result - ? Result extends readonly (infer Row)[] - ? Row - : Result - : never; - -type GatewayKind = Q extends `select ${string}` - ? 'select' - : StatementKind; - -type UsesNext = GatewayKind extends 'select' - ? true - : GatewayKind extends 'with' - ? ParseWithIR extends { kind: 'ok' } - ? true - : false - : GatewayKind extends 'insert' - ? true - : GatewayKind extends 'update' - ? true - : GatewayKind extends 'delete' - ? true - : GatewayKind extends 'merge' - ? true - : false; + NextParams, + NextQuery, + NextRow, + NextStrictQuery, + NextStrictRow, +} from './next/index.js'; export type InferViaGateway = - UsesNext extends true - ? NextQuery - : LegacyInferResult; + NextQuery; export type InferRowViaGateway = - UsesNext extends true - ? NextRow - : LegacyInferRow; + NextRow; export type InferStrictViaGateway = - UsesNext extends true - ? NextStrictQuery - : LegacyInferResultStrict; + NextStrictQuery; export type InferStrictRowViaGateway = - UsesNext extends true - ? NextStrictRow - : LegacyInferRowStrict; + NextStrictRow; export type InferParamsViaGateway = - GatewayKind extends 'select' - ? InferNextParams - : GatewayKind extends 'with' - ? UsesNext extends true - ? InferWithParams - : LegacyInferParams - : GatewayKind extends 'insert' - ? InferInsertParams - : GatewayKind extends 'update' - ? InferUpdateParams - : GatewayKind extends 'delete' - ? InferDeleteParams - : GatewayKind extends 'merge' - ? InferMergeParams - : LegacyInferParams; + NextParams; diff --git a/src/compiler/legacy.ts b/src/compiler/legacy.ts deleted file mode 100644 index 64cc821..0000000 --- a/src/compiler/legacy.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type { - Schema, - SchemaLike, - InferResult as LegacyInferResult, - InferRow as LegacyInferRow, - InferResultStrict as LegacyInferResultStrict, - InferRowStrict as LegacyInferRowStrict, - QueryTypeError, - ParseSelect, - ParseStatement, - ParsedStatement, - Source, -} from '../parse.js'; - -export type { - InferParams as LegacyInferParams, -} from '../params.js'; - -export type { FunctionReturnTypes } from './semantics/functions.js'; diff --git a/src/compiler/next/compile-delete.ts b/src/compiler/next/compile-delete.ts index 32dee2b..9f6d8a9 100644 --- a/src/compiler/next/compile-delete.ts +++ b/src/compiler/next/compile-delete.ts @@ -94,15 +94,21 @@ export type CompileDelete< : never : never; -export type InferDeleteParamsFromIR = +export type InferDeleteParamStateFromIR< + DB, + IR extends DeleteQueryIR, + State extends AnyParamState = EmptyParamState, + ParentScope = null, +> = ScanPredicateParams< DB, - Scope<[TargetSource, ...IR['sources']]>, + Scope<[TargetSource, ...IR['sources']], ParentScope>, IR['predicates'], - EmptyParamState - > extends infer State extends AnyParamState - ? ParamValues - : unknown[]; + State + >; + +export type InferDeleteParamsFromIR = + ParamValues>; export type InferDeleteParams = ParseDeleteIR extends { diff --git a/src/compiler/next/compile-insert.ts b/src/compiler/next/compile-insert.ts index 35fd157..4eab594 100644 --- a/src/compiler/next/compile-insert.ts +++ b/src/compiler/next/compile-insert.ts @@ -183,6 +183,22 @@ export type InferInsertParamsFromIR< ? unknown[] : []; +export type InferInsertParamStateFromIR< + DB, + IR extends InsertQueryIR, + State extends AnyParamState = EmptyParamState, + ParentScope = null, +> = IR['source'] extends InsertValuesIR + ? ScanRows extends infer ValuesState extends AnyParamState + ? ScanParamFragment< + DB, + Scope<[TargetSource], ParentScope>, + Tail, + ValuesState + > + : State + : State; + export type InferInsertParams = ParseInsertIR extends { kind: 'ok'; diff --git a/src/compiler/next/compile-merge.ts b/src/compiler/next/compile-merge.ts index 353b17a..20bd7d6 100644 --- a/src/compiler/next/compile-merge.ts +++ b/src/compiler/next/compile-merge.ts @@ -95,15 +95,17 @@ type AssignmentColumns< ? AssignmentColumns : Result; -type MergeScope = Scope< - [TargetSource, ...IR['sources']] +type MergeScope = Scope< + [TargetSource, ...IR['sources']], + ParentScope >; -type MergeOutputScope = Scope< +type MergeOutputScope = Scope< [ TargetSource, DerivedSourceIR<'$merge', { $action: MergeActionValue }>, - ] + ], + ParentScope >; export type CompileMergeIR< @@ -111,7 +113,7 @@ export type CompileMergeIR< IR extends MergeQueryIR, ParentScope = null, Validate extends boolean = true, -> = InferOutput, IR['output']> extends CompileOk< +> = InferOutput, IR['output']> extends CompileOk< infer Rows, infer OutputDiagnostics > @@ -121,7 +123,7 @@ export type CompileMergeIR< ...TargetDiagnostics, ...SourceDiagnostics, ...(Validate extends true - ? AnalyzePredicates, IR['predicates']> + ? AnalyzePredicates, IR['predicates']> : []), ...OutputDiagnostics, ] @@ -186,19 +188,24 @@ type ScanActions< : ScanActions : State; -export type InferMergeParamsFromIR = - ScanFragments extends infer SourceState extends AnyParamState +export type InferMergeParamStateFromIR< + DB, + IR extends MergeQueryIR, + State extends AnyParamState = EmptyParamState, + ParentScope = null, +> = ScanFragments extends infer SourceState extends AnyParamState ? ScanPredicateParams< DB, - MergeScope, + MergeScope, IR['predicates'], SourceState > extends infer PredicateState extends AnyParamState - ? ScanActions extends infer FinalState extends AnyParamState - ? ParamValues - : unknown[] - : unknown[] - : unknown[]; + ? ScanActions + : State + : State; + +export type InferMergeParamsFromIR = + ParamValues>; export type InferMergeParams = ParseMergeIR extends { diff --git a/src/compiler/next/compile-update.ts b/src/compiler/next/compile-update.ts index debfbbb..7ab65bd 100644 --- a/src/compiler/next/compile-update.ts +++ b/src/compiler/next/compile-update.ts @@ -155,17 +155,22 @@ type ScanAssignments< : never : State; -export type InferUpdateParamsFromIR = - ScanAssignments extends infer AssignmentState extends AnyParamState +export type InferUpdateParamStateFromIR< + DB, + IR extends UpdateQueryIR, + State extends AnyParamState = EmptyParamState, + ParentScope = null, +> = ScanAssignments extends infer AssignmentState extends AnyParamState ? ScanPredicateParams< DB, - Scope<[TargetSource, ...IR['sources']]>, + Scope<[TargetSource, ...IR['sources']], ParentScope>, IR['predicates'], AssignmentState - > extends infer FinalState extends AnyParamState - ? ParamValues - : unknown[] - : unknown[]; + > + : State; + +export type InferUpdateParamsFromIR = + ParamValues>; export type InferUpdateParams = ParseUpdateIR extends { diff --git a/src/compiler/next/compile-with.ts b/src/compiler/next/compile-with.ts index 90e8f75..6828e01 100644 --- a/src/compiler/next/compile-with.ts +++ b/src/compiler/next/compile-with.ts @@ -4,8 +4,14 @@ import type { ColumnProjectionIR, } from '../../language/ir/projection.js'; import type { + AnyQueryIR, + DeleteQueryIR, + InsertQueryIR, + InsertSelectIR, + MergeQueryIR, ProjectionIR, SelectQueryIR, + UpdateQueryIR, } from '../../language/ir/query.js'; import type { CteSourceIR } from '../../language/ir/source.js'; import type { @@ -22,6 +28,22 @@ import type { CompileSelectIR, ResolveSelectSources, } from './compile-select.js'; +import type { + CompileDeleteIR, + InferDeleteParamStateFromIR, +} from './compile-delete.js'; +import type { + CompileInsertIR, + InferInsertParamStateFromIR, +} from './compile-insert.js'; +import type { + CompileMergeIR, + InferMergeParamStateFromIR, +} from './compile-merge.js'; +import type { + CompileUpdateIR, + InferUpdateParamStateFromIR, +} from './compile-update.js'; import type { AnyParamState, EmptyParamState, @@ -66,6 +88,23 @@ type RenameKeys< type Flatten = { [Key in keyof Value]: Value[Key] }; +type CompileMain< + DB, + Query extends AnyQueryIR, + ParentScope, + ValidatePredicates extends boolean, +> = Query extends SelectQueryIR + ? CompileSelectIR + : Query extends InsertQueryIR + ? CompileInsertIR + : Query extends UpdateQueryIR + ? CompileUpdateIR + : Query extends DeleteQueryIR + ? CompileDeleteIR + : Query extends MergeQueryIR + ? CompileMergeIR + : never; + export type CteOutput< Row, Query extends SelectQueryIR, @@ -84,7 +123,7 @@ type CteBindingRow< type CompileCtes< DB, Ctes extends readonly AnyCteIR[], - Query extends SelectQueryIR, + Query extends AnyQueryIR, ParentScope, ValidatePredicates extends boolean, Sources extends readonly CteSourceIR[] = [], @@ -132,7 +171,7 @@ type CompileCtes< ? CompileFatal : never : never - : CompileSelectIR< + : CompileMain< DB, Query, Scope, @@ -173,7 +212,7 @@ export type CompileWith< type InferCteParams< DB, Ctes extends readonly AnyCteIR[], - Query extends SelectQueryIR, + Query extends AnyQueryIR, ParentScope, Sources extends readonly CteSourceIR[] = [], State extends AnyParamState = EmptyParamState, @@ -220,30 +259,42 @@ type InferCteParams< > : never : State - : InferParamsFromIR< - DB, - Query, - State, - Scope< - ResolveSelectSources>, - Scope + : Query extends SelectQueryIR + ? InferParamsFromIR< + DB, + Query, + State, + Scope< + ResolveSelectSources>, + Scope + > > - >; + : Query extends InsertQueryIR + ? InferInsertParamStateFromIR> + : Query extends UpdateQueryIR + ? InferUpdateParamStateFromIR> + : Query extends DeleteQueryIR + ? InferDeleteParamStateFromIR> + : Query extends MergeQueryIR + ? InferMergeParamStateFromIR> + : State; export type InferWithParams = ParseWithIR extends infer Parsed ? Parsed extends { kind: 'ok'; - value: infer IR extends WithQueryIR; - } - ? InferCteParams< - DB, - IR['ctes'], - IR['query'], - ParentScope - > extends infer State extends AnyParamState - ? ParamValues - : unknown[] + value: infer IR extends WithQueryIR; + } + ? IR['query'] extends { kind: 'insert'; source: InsertSelectIR } + ? unknown[] + : InferCteParams< + DB, + IR['ctes'], + IR['query'], + ParentScope + > extends infer State extends AnyParamState + ? ParamValues + : unknown[] : Parsed extends { kind: 'fatal'; diagnostics: readonly [ diff --git a/src/compiler/next/index.ts b/src/compiler/next/index.ts index f0765d7..bd3dc40 100644 --- a/src/compiler/next/index.ts +++ b/src/compiler/next/index.ts @@ -2,7 +2,7 @@ import type { StatementKind } from '../../language/lexical/statement.js'; import type { ApplyLoosePolicy, ApplyStrictPolicy, - CompileFatal, + CompileOk, } from '../contracts/compilation.js'; import type { Diagnostic } from '../contracts/diagnostic.js'; import type { CompileSelect } from './compile-select.js'; @@ -15,8 +15,8 @@ import type { InferNextParams } from './infer-params.js'; type UnsupportedStatement = Diagnostic< 'UNSUPPORTED_STATEMENT', - 'unsupported statement', - 'fatal', + 'unsupported or unrecognized statement', + 'error', 'statement', Sql >; @@ -24,27 +24,28 @@ type UnsupportedStatement = Diagnostic< export type CompileNext< DB, Sql extends string, + ValidatePredicates extends boolean = true, > = Sql extends `select ${string}` - ? CompileSelect + ? CompileSelect : StatementKind extends 'select' - ? CompileSelect + ? CompileSelect : StatementKind extends 'insert' - ? CompileInsert + ? CompileInsert : StatementKind extends 'update' - ? CompileUpdate + ? CompileUpdate : StatementKind extends 'delete' - ? CompileDelete + ? CompileDelete : StatementKind extends 'merge' - ? CompileMerge + ? CompileMerge : StatementKind extends 'with' - ? CompileWith - : CompileFatal]>; + ? CompileWith + : CompileOk]>; export type NextQuery = - ApplyLoosePolicy>; + ApplyLoosePolicy>; export type NextStrictQuery = - ApplyStrictPolicy>; + ApplyStrictPolicy>; export type NextWithQuery = ApplyLoosePolicy>; @@ -161,3 +162,19 @@ export type NextStrictRow = export type NextInferParams = InferNextParams; export type NextWithInferParams = InferWithParams; + +export type NextParams = Sql extends `select ${string}` + ? InferNextParams + : StatementKind extends 'select' + ? InferNextParams + : StatementKind extends 'with' + ? InferWithParams + : StatementKind extends 'insert' + ? InferInsertParams + : StatementKind extends 'update' + ? InferUpdateParams + : StatementKind extends 'delete' + ? InferDeleteParams + : StatementKind extends 'merge' + ? InferMergeParams + : unknown[]; diff --git a/src/language/ir/query.ts b/src/language/ir/query.ts index 19e75d3..ef95baf 100644 --- a/src/language/ir/query.ts +++ b/src/language/ir/query.ts @@ -171,3 +171,10 @@ export interface MergeQueryIR< output: Output; sourceValues: SourceValues; } + +export type AnyQueryIR = + | SelectQueryIR + | InsertQueryIR + | UpdateQueryIR + | DeleteQueryIR + | MergeQueryIR; diff --git a/src/language/lexical/statement.ts b/src/language/lexical/statement.ts index 0c271ec..80db023 100644 --- a/src/language/lexical/statement.ts +++ b/src/language/lexical/statement.ts @@ -18,9 +18,13 @@ type KindOf = : IsKeyword extends true ? 'merge' : 'unknown'; -export type StatementKind = KindOf>> extends infer Direct extends StatementKindName +type StripLeadingParens = Token extends `(${infer Rest}` + ? StripLeadingParens + : Token; + +export type StatementKind = KindOf>>> extends infer Direct extends StatementKindName ? Direct extends 'unknown' - ? KindOf>> + ? KindOf>>> : Direct : never; diff --git a/src/language/select/parse-select.ts b/src/language/select/parse-select.ts index 4aca571..b89d8f9 100644 --- a/src/language/select/parse-select.ts +++ b/src/language/select/parse-select.ts @@ -260,38 +260,28 @@ type AddSetOperation< : Branch : Parsed; -type HasSetOperation = Lowercase extends - | `${string} union ${string}` - | `${string} intersect ${string}` - | `${string} except ${string}` - ? true - : false; - -type ParseSetBody = - HasSetOperation extends false - ? ParseBody - : SplitAtSet extends infer Set - ? [Set] extends [never] - ? ParseBody - : Set extends { - primary: infer Primary extends string; - kind: infer Kind extends SetOperationIR['kind']; - branch: infer Branch extends string; - } - ? AddSetOperation< - ParseBody, - Kind, - ParseNormalized - > - : ParseFatal - : never; - type ParseNormalized = - Normalized extends `${infer Select} ${infer Body}` - ? IsKeyword extends true - ? ParseSetBody, Sql> - : ParseFatal - : ParseFatal; + SplitAtSet extends infer Set + ? [Set] extends [never] + ? Trim extends `(${infer Inner})` + ? ParseNormalized, Sql> + : Normalized extends `${infer Select} ${infer Body}` + ? IsKeyword extends true + ? ParseBody, Sql> + : ParseFatal + : ParseFatal + : Set extends { + primary: infer Primary extends string; + kind: infer Kind extends SetOperationIR['kind']; + branch: infer Branch extends string; + } + ? AddSetOperation< + ParseNormalized, + Kind, + ParseNormalized + > + : ParseFatal + : never; export type ParseSelectIR = HasNonTrailingSemicolon extends true ? ParseMultipleStatements diff --git a/src/language/with/parse-with.ts b/src/language/with/parse-with.ts index a822089..44ad326 100644 --- a/src/language/with/parse-with.ts +++ b/src/language/with/parse-with.ts @@ -7,13 +7,18 @@ import type { SplitColumnList, Trim, } from '../lexical/string.js'; +import type { StatementKind } from '../lexical/statement.js'; import type { AnyCteIR, CteIR } from '../ir/cte.js'; -import type { SelectQueryIR } from '../ir/query.js'; +import type { AnyQueryIR, SelectQueryIR } from '../ir/query.js'; +import type { ParseDeleteIR } from '../dml/parse-delete.js'; +import type { ParseInsertIR } from '../dml/parse-insert.js'; +import type { ParseMergeIR } from '../dml/parse-merge.js'; +import type { ParseUpdateIR } from '../dml/parse-update.js'; import type { ParseSelectIR } from '../select/parse-select.js'; export interface WithQueryIR< Ctes extends readonly AnyCteIR[] = readonly AnyCteIR[], - Query extends SelectQueryIR = SelectQueryIR, + Query extends AnyQueryIR = AnyQueryIR, > { kind: 'with'; ctes: Ctes; @@ -28,6 +33,18 @@ type MalformedWith = { reference: Sql; }; +type ParseMain = StatementKind extends 'select' + ? ParseSelectIR + : StatementKind extends 'insert' + ? ParseInsertIR + : StatementKind extends 'update' + ? ParseUpdateIR + : StatementKind extends 'delete' + ? ParseDeleteIR + : StatementKind extends 'merge' + ? ParseMergeIR + : ParseFatal; + type ParseFatal = { kind: 'fatal'; readonly __value?: WithQueryIR; @@ -113,10 +130,10 @@ type ParseCteList< } ? Rest extends `,${infer Tail}` ? ParseCteList, Recursive, Whole, [...Ctes, Cte]> - : ParseSelectIR extends infer Main + : ParseMain extends infer Main ? Main extends { kind: 'ok'; - value: infer Query extends SelectQueryIR; + value: infer Query extends AnyQueryIR; } ? { kind: 'ok'; diff --git a/tests/architecture/dependencies.test.mjs b/tests/architecture/dependencies.test.mjs index 69c3877..21f2b9c 100644 --- a/tests/architecture/dependencies.test.mjs +++ b/tests/architecture/dependencies.test.mjs @@ -27,24 +27,6 @@ describe('architecture dependencies', () => { } }); - it('reports Next compiler imports from the Legacy compiler', () => { - const root = mkdtempSync(join(tmpdir(), 'owlsql-architecture-')); - mkdirSync(join(root, 'src', 'compiler', 'next'), { recursive: true }); - writeFileSync(join(root, 'src', 'compiler', 'legacy.ts'), 'export type Legacy = string;\n'); - writeFileSync( - join(root, 'src', 'compiler', 'next', 'invalid.ts'), - "import type { Legacy } from '../legacy.js';\n", - ); - - try { - expect(checkArchitecture(root)).toEqual([ - 'src/compiler/next/invalid.ts -> src/compiler/legacy.ts violates next compiler isolation', - ]); - } finally { - rmSync(root, { recursive: true, force: true }); - } - }); - it('reports tooling imports from compiler implementation internals', () => { const root = mkdtempSync(join(tmpdir(), 'owlsql-architecture-')); mkdirSync(join(root, 'src', 'tooling'), { recursive: true }); diff --git a/tests/next/cte-parity.test-d.ts b/tests/next/cte-parity.test-d.ts deleted file mode 100644 index 9ecbd0e..0000000 --- a/tests/next/cte-parity.test-d.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { - InferParamsViaGateway, - InferViaGateway, -} from '../../src/compiler/gateway.js'; -import type { - NextWithInferParams, - NextWithQuery, - NextWithStrictQuery, -} from '../../src/compiler/next/index.js'; -import type { ParseWithIR } from '../../src/language/with/parse-with.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = Value; - -type DB = { - users: { id: number; name: string; active: boolean }; - posts: { id: number; user_id: number; title: string; views: number }; -}; - -type _structural = Expect< - ParseWithIR<'with recursive popular(post_id) as materialized (select id from posts) select post_id from popular'> extends { - kind: 'ok'; - value: { - ctes: readonly [{ - name: 'popular'; - columns: readonly ['post_id']; - query: { kind: 'select' }; - recursive: true; - }]; - query: { kind: 'select' }; - }; - } - ? true - : false ->; - -type _single = Expect 100) select id, title from popular'>, - NextWithQuery 100) select id, title from popular'> ->>; - -type _chained = Expect, - NextWithQuery ->>; - - -type _aliases = Expect, - NextWithQuery ->>; - -type _shadowing = Expect, - NextWithStrictQuery ->>; - -type _params = Expect @minimum) select id from popular where id = @id'>, - NextWithInferParams @minimum) select id from popular where id = @id'> ->>; - -type _nestedCase = Expect, - NextWithQuery ->>; - -type _materialized = Expect, - NextWithQuery ->>; - -type _notMaterialized = Expect, - NextWithQuery ->>; - -type _recursiveKeyword = Expect, - NextWithQuery ->>; - -type _diagnosticPropagation = Expect, - NextWithStrictQuery ->>; - -type _scalarSubqueryScope = Expect, - NextWithQuery ->>; - -type WithLedUpdate = `with targets as (select id from users) -update users set name = @name where id in (select id from targets)`; - -type _withLedDmlResultStaysLegacy = Expect, - LegacyInferResult ->>; - -type _withLedDmlParamsStayLegacy = Expect, - LegacyInferParams ->>; diff --git a/tests/next/delete-parity.test-d.ts b/tests/next/delete-parity.test-d.ts deleted file mode 100644 index f7236f7..0000000 --- a/tests/next/delete-parity.test-d.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { - NextDeleteInferParams, - NextDeleteQuery, - NextDeleteStrictQuery, -} from '../../src/compiler/next/index.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = T; - -type DB = { - users: { - id: number; - name: string; - }; - accounts: { - id: number; - user_id: number; - balance: number; - }; - refunds: { - id: number; - account_id: number; - }; -}; - -type Parity = Equal< - LegacyInferResult, - NextDeleteQuery ->; - -type StrictParity = Equal< - LegacyInferResultStrict, - NextDeleteStrictQuery ->; - -type ParamsParity = Equal< - LegacyInferParams, - NextDeleteInferParams ->; - -type Simple = Expect>; -type NoPredicate = Expect>; -type Returning = Expect>; -type Output = Expect>; -type Alias = Expect>; -type BareAlias = Expect>; -type SchemaQualified = Expect>; -type UnknownTarget = Expect>; -type UnknownWhere = Expect>; -type UnknownReturning = Expect>; -type DeleteUsing = Expect>; -type UnknownUsingAlias = Expect>; -type UsingJoin = Expect>; -type UnknownJoinOn = Expect>; -type Params = Expect>; -type NamedParams = Expect>; -type UsingParams = Expect>; -type LooseUnknownColumn = Expect>; - -export type DeleteParityLock = [ - Simple, - NoPredicate, - Returning, - Output, - Alias, - BareAlias, - SchemaQualified, - UnknownTarget, - UnknownWhere, - UnknownReturning, - DeleteUsing, - UnknownUsingAlias, - UsingJoin, - UnknownJoinOn, - Params, - NamedParams, - UsingParams, - LooseUnknownColumn, -]; diff --git a/tests/next/insert-parity.test-d.ts b/tests/next/insert-parity.test-d.ts deleted file mode 100644 index 3d4d23b..0000000 --- a/tests/next/insert-parity.test-d.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { ApplyLoosePolicy } from '../../src/compiler/contracts/compilation.js'; -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { InferOutput } from '../../src/compiler/next/infer-output.js'; -import type { - ResolveWriteColumn, - ResolveWriteTarget, -} from '../../src/compiler/next/resolve-write-target.js'; -import type { RootScope } from '../../src/compiler/next/scope.js'; -import type { - NextInsertInferParams, - NextInsertQuery, - NextInsertStrictQuery, -} from '../../src/compiler/next/index.js'; -import type { ColumnProjectionIR } from '../../src/language/ir/projection.js'; -import type { - AssignmentIR, - OutputIR, - WriteTargetIR, -} from '../../src/language/ir/write.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = T; - -type DB = { - users: { - id: number; - name: string; - email: string | null; - }; -}; - -type Target = WriteTargetIR<'users', 'u'>; -type TargetSource = ResolveWriteTarget; -type TargetScope = TargetSource extends { kind: 'ok'; value: infer Source } - ? RootScope<[Source & { kind: 'table'; name: 'users'; alias: 'u'; join: 'root'; nullable: false; mergedColumns: [] }]> - : never; - -type TargetContract = Expect< - Equal ->; -type InsertColumnsContract = Expect< - Equal ->; -type AssignmentContract = Expect< - Equal, { target: 'name'; value: '$1' }> ->; -type ReturningContract = Expect< - Equal< - OutputIR<'returning', [ColumnProjectionIR]>, - { mode: 'returning'; projections: [ColumnProjectionIR] } - > ->; -type OutputContract = Expect< - Equal< - OutputIR<'output', [ColumnProjectionIR]>, - { mode: 'output'; projections: [ColumnProjectionIR] } - > ->; -type NoOutputContract = Expect>; -type ResolveTargetContract = Expect< - Equal ->; -type ResolveColumnContract = Expect< - Equal, { kind: 'ok'; value: string }> ->; -type UnknownColumnContract = Expect< - Equal['kind'], 'error'> ->; -type ReturningInference = Expect< - Equal< - ApplyLoosePolicy< - InferOutput< - DB, - TargetScope, - OutputIR<'returning', [ColumnProjectionIR]> - > - >, - { id: number }[] - > ->; -type NoOutputInference = Expect< - Equal< - ApplyLoosePolicy>, - Record[] - > ->; - -type InsertParity = Equal< - LegacyInferResult, - NextInsertQuery ->; - -type StrictInsertParity = Equal< - LegacyInferResultStrict, - NextInsertStrictQuery ->; - -type InsertParamsParity = Equal< - LegacyInferParams, - NextInsertInferParams ->; - -type ValuesReturning = Expect>; -type ValuesWithoutOutput = Expect>; -type OutputBeforeValues = Expect>; -type GluedColumns = Expect>; -type TargetAlias = Expect>; -type SchemaQualifiedTarget = Expect>; -type QuotedTarget = Expect>; -type UnknownTarget = Expect>; -type UnknownColumn = Expect>; -type UnknownLaterColumn = Expect>; -type InsertSelect = Expect>; -type InsertSelectUnknownColumn = Expect>; -type InsertSelectUnknownTable = Expect>; -type InsertSelectUnknownPredicate = Expect>; -type PositionalParams = Expect>; -type MultipleRows = Expect>; -type MixedLiterals = Expect>; -type CallParams = Expect>; -type ConflictParams = Expect>; -type InsertSelectParams = Expect>; -type NullableParams = Expect>; -type NamedCallParams = Expect>; -type NestedCallParams = Expect>; -type NoColumnListParams = Expect>; -type DefaultValues = Expect>; -type InsertSelectReturning = Expect>; -type UnknownReturningColumn = Expect>; -type LooseUnknownColumn = Expect>; -type UppercaseInsert = Expect>; - -export type InsertContractLock = [ - TargetContract, - InsertColumnsContract, - AssignmentContract, - ReturningContract, - OutputContract, - NoOutputContract, - ResolveTargetContract, - ResolveColumnContract, - UnknownColumnContract, - ReturningInference, - NoOutputInference, - ValuesReturning, - ValuesWithoutOutput, - OutputBeforeValues, - GluedColumns, - TargetAlias, - SchemaQualifiedTarget, - QuotedTarget, - UnknownTarget, - UnknownColumn, - UnknownLaterColumn, - InsertSelect, - InsertSelectUnknownColumn, - InsertSelectUnknownTable, - InsertSelectUnknownPredicate, - PositionalParams, - MultipleRows, - MixedLiterals, - CallParams, - ConflictParams, - InsertSelectParams, - NullableParams, - NamedCallParams, - NestedCallParams, - NoColumnListParams, - DefaultValues, - InsertSelectReturning, - UnknownReturningColumn, - LooseUnknownColumn, - UppercaseInsert, -]; diff --git a/tests/next/merge-parity.test-d.ts b/tests/next/merge-parity.test-d.ts deleted file mode 100644 index 86cd4bd..0000000 --- a/tests/next/merge-parity.test-d.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { - NextMergeInferParams, - NextMergeQuery, - NextMergeStrictQuery, -} from '../../src/compiler/next/index.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = T; - -type DB = { - users: { - id: number; - name: string; - email: string; - }; -}; - -type Parity = Equal< - LegacyInferResult, - NextMergeQuery ->; - -type StrictParity = Equal< - LegacyInferResultStrict, - NextMergeStrictQuery ->; - -type ParamsParity = Equal< - LegacyInferParams, - NextMergeInferParams ->; - -type FullMerge = - 'merge into users as target using (values (@id, @name)) as source (id, name) on target.id = source.id when matched then update set target.name = source.name when not matched then insert (id, name) values (source.id, source.name) output inserted.id, inserted.name'; - -type ActionMerge = - 'merge into users as target using (values (@id, @name)) as source (id, name) on target.id = source.id when matched then update set target.name = source.name output $action, inserted.id'; - -type NoAliasMerge = - 'merge into users using (values (@id)) as source (id) on users.id = source.id when not matched then insert (id) values (source.id) output inserted.id'; - -type NoOutputMerge = - 'merge into users as target using (values (@id, @name)) as source (id, name) on target.id = source.id when matched then update set target.name = source.name'; - -type Output = Expect>; -type ActionOutput = Expect>; -type ActionParams = Expect>; -type WithoutAlias = Expect>; -type WithoutOutput = Expect>; -type UnknownOutput = Expect>; -type UnknownTarget = Expect>; -type Uppercase = Expect>; -type UnknownActionColumn = Expect>; -type InsertActionColumn = Expect>; -type ActionPlaceholder = Expect>; -type InsertPlaceholder = Expect>; - -export type MergeParityLock = [ - Output, - ActionOutput, - ActionParams, - WithoutAlias, - WithoutOutput, - UnknownOutput, - UnknownTarget, - Uppercase, - UnknownActionColumn, - InsertActionColumn, - ActionPlaceholder, - InsertPlaceholder, -]; diff --git a/tests/next/select-parity.test-d.ts b/tests/next/select-parity.test-d.ts deleted file mode 100644 index 4697e90..0000000 --- a/tests/next/select-parity.test-d.ts +++ /dev/null @@ -1,326 +0,0 @@ -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { - NextQuery, - NextInferParams, - NextRow, - NextStrictQuery, -} from '../../src/compiler/next/index.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = T; - -type DB = { - users: { - id: number; - name: string; - active: boolean; - age: number; - }; - posts: { - id: number; - user_id: number; - title: string; - }; -}; - -type _simple = Expect, - NextQuery ->>; - -type _aliased = Expect, - NextQuery ->>; - -type _qualified = Expect, - NextQuery ->>; - -type _renamed = Expect, - NextQuery ->>; - -type _unknownLoose = Expect, - NextQuery ->>; - -type _unknownStrict = Expect, - NextStrictQuery ->>; - -type _innerJoin = Expect, - NextQuery ->>; - -type _leftJoin = Expect, - NextQuery ->>; - -type _rightJoin = Expect, - NextQuery ->>; - -type _fullJoin = Expect, - NextQuery ->>; - -type _using = Expect, - NextStrictQuery ->>; - -type _onAmbiguity = Expect, - NextStrictQuery ->>; - -type _derived = Expect, - NextQuery ->>; - -type _star = Expect, - NextQuery ->>; - -type _qualifiedStar = Expect, - NextQuery ->>; - -type _aggregates = Expect, - NextQuery ->>; - -type _function = Expect, - NextQuery ->>; - -type _case = Expect, - NextQuery ->>; - -type _cast = Expect, - NextQuery ->>; - -type _literal = Expect, - NextQuery ->>; - -type _unsupportedExpression = Expect, - NextQuery ->>; - -type _strictFunctionError = Expect, - NextStrictQuery ->>; - -type _commonFunctions = Expect, - NextQuery ->>; - -type _unaliasedFunction = Expect, - NextQuery ->>; - -type _nestedCase = Expect, - NextQuery ->>; - -type _parenthesizedColumn = Expect, - NextQuery ->>; - -type _window = Expect, - NextQuery ->>; - -type _strictCastError = Expect, - NextStrictQuery ->>; - -type _leftJoinStar = Expect, - NextQuery ->>; - -type _where = Expect 18 and active = true'>, - NextStrictQuery 18 and active = true'> ->>; - -type _whereTypo = Expect, - NextStrictQuery ->>; - -type _comparisonParams = Expect, - NextInferParams ->>; - -type _predicateParams = Expect, - NextInferParams ->>; - -type _betweenParams = Expect, - NextInferParams ->>; - -type _arrayParam = Expect, - NextInferParams ->>; - -type _namedParams = Expect, - NextInferParams ->>; - -type _questionParams = Expect, - NextInferParams ->>; - -type _conflictingParams = Expect, - NextInferParams ->>; - -type _limitOffsetParams = Expect, - NextInferParams ->>; - -type _groupHaving = Expect 1 order by active'>, - NextQuery 1 order by active'> ->>; - -type _havingParams = Expect $1'>, - NextInferParams $1'> ->>; - -type _union = Expect, - NextQuery ->>; - -type _literalSetOperations = Expect, - NextQuery ->>; - -type _setRow = Expect, - { value: number } ->>; - -type _scalarSubquery = Expect, - NextQuery ->>; - -type _scalarNullable = Expect, - NextQuery ->>; - -type _lateralCorrelated = Expect, - NextQuery ->>; - -type _derivedCorrelated = Expect, - NextQuery ->>; - -type _scalarInnerTypo = Expect, - NextStrictQuery ->>; - -type _derivedInnerTypo = Expect, - NextStrictQuery ->>; - -type _noOuterScope = Expect, - NextStrictQuery ->>; - -type _uncorrelatedScalar = Expect, - NextStrictQuery ->>; - -type _invalidScalar = Expect, - NextStrictQuery ->>; - -type _postgresDistinctOn = Expect, - NextQuery ->>; - -type _postgresCastParam = Expect, - NextInferParams ->>; - -type _mssqlTop = Expect, - NextQuery ->>; - -type _mssqlBrackets = Expect, - NextQuery ->>; - -type _mysqlQuotes = Expect, - NextQuery ->>; - -type _questionPlaceholder = Expect, - NextInferParams ->>; diff --git a/tests/next/update-parity.test-d.ts b/tests/next/update-parity.test-d.ts deleted file mode 100644 index bed7a09..0000000 --- a/tests/next/update-parity.test-d.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { - LegacyInferParams, - LegacyInferResult, - LegacyInferResultStrict, -} from '../../src/compiler/legacy.js'; -import type { - NextUpdateInferParams, - NextUpdateQuery, - NextUpdateStrictQuery, -} from '../../src/compiler/next/index.js'; - -type Equal = - (() => T extends A ? 1 : 2) extends - (() => T extends B ? 1 : 2) ? true : false; - -type Expect = T; - -type DB = { - users: { - id: number; - name: string; - email: string | null; - }; - accounts: { - id: number; - user_id: number; - balance: number; - }; - refunds: { - id: number; - account_id: number; - }; -}; - -type Parity = Equal< - LegacyInferResult, - NextUpdateQuery ->; - -type StrictParity = Equal< - LegacyInferResultStrict, - NextUpdateStrictQuery ->; - -type ParamsParity = Equal< - LegacyInferParams, - NextUpdateInferParams ->; - -type Simple = Expect>; -type Returning = Expect>; -type ReturningStar = Expect>; -type Output = Expect>; -type OutputBeforeFrom = Expect>; -type Alias = Expect>; -type BareAlias = Expect>; -type SchemaQualified = Expect>; -type UnknownTarget = Expect>; -type UnknownAssignment = Expect>; -type UnknownLaterAssignment = Expect>; -type UnknownWhere = Expect>; -type UnknownReturning = Expect>; -type UpdateFrom = Expect>; -type UnknownFromAlias = Expect>; -type JoinOn = Expect>; -type UnknownJoinOn = Expect>; -type ScalarAssignment = Expect>; -type ScalarAssignmentTypo = Expect>; -type AssignmentParams = Expect>; -type NamedParams = Expect>; -type FromParams = Expect $2' ->>; -type LooseUnknownAssignment = Expect>; - -export type UpdateParityLock = [ - Simple, - Returning, - ReturningStar, - Output, - OutputBeforeFrom, - Alias, - BareAlias, - SchemaQualified, - UnknownTarget, - UnknownAssignment, - UnknownLaterAssignment, - UnknownWhere, - UnknownReturning, - UpdateFrom, - UnknownFromAlias, - JoinOn, - UnknownJoinOn, - ScalarAssignment, - ScalarAssignmentTypo, - AssignmentParams, - NamedParams, - FromParams, - LooseUnknownAssignment, -]; From cd2e0aa091d70678324e0ff064318e7e8c5b0162 Mon Sep 17 00:00:00 2001 From: Tiago Lauer Date: Thu, 20 Aug 2026 14:08:24 -0300 Subject: [PATCH 04/15] refactor: finalize v1 public api surface --- CHANGELOG.md | 4 + README.md | 2 - VERSIONING.md | 1 - docs/architecture/public-api-v1.md | 42 + scripts/package-smoke.mjs | 17 + src/case.ts | 106 -- src/cte.ts | 127 -- src/from.ts | 291 ----- src/index.ts | 8 - src/params.ts | 683 ----------- src/parse.ts | 1412 ---------------------- src/public/schema.ts | 2 - src/where.ts | 156 --- tests/architecture/dependencies.test.mjs | 9 +- tests/contracts/public-api.test-d.ts | 5 - tests/public-api.test-d.ts | 8 +- 16 files changed, 69 insertions(+), 2804 deletions(-) create mode 100644 docs/architecture/public-api-v1.md delete mode 100644 src/case.ts delete mode 100644 src/cte.ts delete mode 100644 src/from.ts delete mode 100644 src/params.ts delete mode 100644 src/parse.ts delete mode 100644 src/where.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c94f0..015bf75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes to this project, following [Keep a Changelog](https://keepachang ## [Unreleased] +### Changed + +- **Breaking (pre-v1):** removed the accidental advanced exports `ParseSelect`, `ParseStatement`, `ParsedStatement`, `Source`, and `FunctionReturnTypes`. They exposed the retired parser and compiler internals and have no supported replacement. Use `Query`, `Row`, `StrictQuery`, `StrictRow`, and `Params` for the public type-inference contract. + ### Fixed - `WITH t AS MATERIALIZED (...)` and its `NOT MATERIALIZED` twin parse again. The parser wanted the body's opening paren directly after `as`, so the Postgres 12 planner hint took the whole WITH clause down with it and the query degraded into an index signature row instead of reporting anything ([#283](https://github.com/tiagolauer/OwlSQL/issues/283)). diff --git a/README.md b/README.md index 92523c9..8912855 100644 --- a/README.md +++ b/README.md @@ -878,7 +878,6 @@ only once the query is finished. | `InferResult` / `InferRow` | type | Underlying aliases of `Query` / `Row` (plus `InferResultStrict` / `InferRowStrict`). | | `Params` / `InferParams` | type | Inferred parameter tuple for query `Q`. | | `QueryTypeError` | type | Branded compile-time error carrying `Message`. | -| `FunctionReturnTypes` | interface | SQL-function → return-type registry. | | `Result` | type | `Ok \| Err` discriminated union (`Ok` / `Err` are exported too). | | `ResultStatus` | enum | `Ok` / `Error`. | | `ok` / `err` | function | Construct a success / error result. | @@ -887,7 +886,6 @@ only once the query is finished. | `QueryErrorKind` | enum | `EMPTY_QUERY` / `EXECUTOR_FAILED`. | | `Schema` / `SchemaLike` | type | Ideal schema shape (`table → column → type`) / the loosest accepted shape. | | `defineSchema(obj)` | function | Optional identity helper (see below). | -| `ParseSelect` / `ParseStatement` / `ParsedStatement` / `Source` | type | Parser internals, exported for advanced tooling; not needed for normal use and more likely to change between minor versions. | **Driver adapters** (each on its own subpath, so no unused peer dependency is ever required): diff --git a/VERSIONING.md b/VERSIONING.md index 7e35307..f16c5d9 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -72,7 +72,6 @@ The editor plugin is a separate package, [`@owlsql/ts-plugin`](ts-plugin/README. No compatibility guarantee, changeable in any release: - The message text inside `QueryTypeError<...>`. Match on the presence of the error, never on its wording. -- The parser internals exported for advanced tooling: `ParseSelect`, `ParseStatement`, `ParsedStatement`, `Source`, `FunctionReturnTypes`. - CLI human-readable output — progress lines, error phrasing, help text. Exit codes *are* covered. - Compile-time cost. The [type-instantiation budget](CONTRIBUTING.md#the-type-instantiation-budget) exists to keep this honest, but a release may legitimately raise it. diff --git a/docs/architecture/public-api-v1.md b/docs/architecture/public-api-v1.md new file mode 100644 index 0000000..b6947aa --- /dev/null +++ b/docs/architecture/public-api-v1.md @@ -0,0 +1,42 @@ +# OwlSQL v1 public API + +This inventory records the supported `@owlsql/core` surface after building and inspecting `dist/index.d.ts` and `package.json#exports`. + +## Root export + +`@owlsql/core` exports these runtime values: + +- `createTypedDb` +- `defineSchema` +- `QueryErrorKind` +- `ResultStatus` +- `ok` +- `err` +- `isOk` +- `isErr` + +It exports these types: + +- `Query`, `Row`, `StrictQuery`, `StrictRow`, `Params` +- `InferResult`, `InferRow`, `InferResultStrict`, `InferRowStrict`, `InferParams` +- `Schema`, `SchemaLike`, `QueryTypeError` +- `Executor`, `ExecutorResult`, `DialectExecutor`, `PlaceholderStyle` +- `QueryError`, `TypedDb`, `TypedDbOptions` +- `Result`, `Ok`, `Err`, `QueryMeta` + +## Adapter subpaths + +| Subpath | Public exports | +| --- | --- | +| `@owlsql/core/pg` | `createPgExecutor`, `createPgTransaction`, `PgQueryable` | +| `@owlsql/core/mysql2` | `createMysql2Executor`, `createMysql2Transaction`, `Mysql2Queryable` | +| `@owlsql/core/postgres` | `createPostgresJsExecutor`, `createPostgresJsTransaction` | +| `@owlsql/core/node-sqlite` | `createNodeSqliteExecutor` | +| `@owlsql/core/mssql` | `createMssqlExecutor`, `createMssqlTransaction`, `MssqlQueryable` | +| `@owlsql/core/kysely` | `createKyselyExecutor` | + +The package also exposes the `owlsql` binary from `dist/cli/index.js`. + +## Removed before 1.0 + +`ParseSelect`, `ParseStatement`, `ParsedStatement`, `Source`, and `FunctionReturnTypes` were accidental compiler-internal exports. They are removed without replacement. Language IR, compiler contracts, schema generation internals, and tooling modules are not public package subpaths. diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 221a12b..5017b2b 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -14,6 +14,13 @@ const CONSUMER_PACKAGES = [ 'pg', 'postgres', ]; +const REMOVED_EXPORTS = [ + 'FunctionReturnTypes', + 'ParseSelect', + 'ParseStatement', + 'ParsedStatement', + 'Source', +]; function runNpm(args, options) { const npmCli = process.env.npm_execpath; @@ -53,6 +60,15 @@ function pack(destination) { return join(destination, result[0].filename); } +function assertPublicDeclarations() { + const declarations = readFileSync(join(ROOT, 'dist', 'index.d.ts'), 'utf8'); + for (const name of REMOVED_EXPORTS) { + if (new RegExp(`\\b${name}\\b`).test(declarations)) { + throw new Error(`Removed export is still public: ${name}.`); + } + } +} + function writeConsumer(consumer) { writeJson(join(consumer, 'package.json'), { name: 'owlsql-package-smoke', @@ -117,6 +133,7 @@ function main() { try { const packageJson = readJson(join(ROOT, 'package.json')); + assertPublicDeclarations(); const tarball = pack(temporary); mkdirSync(consumer); writeConsumer(consumer); diff --git a/src/case.ts b/src/case.ts deleted file mode 100644 index 2b693fe..0000000 --- a/src/case.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { Trim, FirstWord, DropFirstWord, IsKeyword, Unquote } from './language/lexical/string.js'; -import type { SchemaLike, Source, ResolveColumnType } from './parse.js'; - -// A nested CASE is often written wrapped in parens (`(case ... end)`), which -// glues the paren onto the adjacent keyword token ("(case", "end)") since -// there's no space between them. Stripping the attached paren before the -// keyword comparison - without touching what gets accumulated into the body -// text - lets `case`/`end` depth-tracking see through the wrapping the same -// way it already does for a bare nested `case ... end`. -type StripLeadingParens = S extends `(${infer Rest}` ? StripLeadingParens : S; -type StripTrailingParens = S extends `${infer Rest})` ? StripTrailingParens : S; - -type IsCaseToken = IsKeyword, 'case'>; -type IsEndToken = IsKeyword, 'end'>; - -export type IsCaseExpression = IsCaseToken>>; - -type FindEnd< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? IsCaseToken extends true - ? FindEnd - : IsEndToken extends true - ? Depth extends [unknown, ...infer DepthRest extends unknown[]] - ? FindEnd - : { body: Trim; rest: Trim } - : FindEnd - : IsEndToken extends true - ? Depth extends [] - ? { body: Trim; rest: '' } - : never - : never; - -export type SplitCaseExpression = DropFirstWord> extends infer AfterCase extends string - ? FindEnd extends { body: infer Body extends string; rest: infer Rest extends string } - ? Rest extends '' - ? { body: Body; alias: 'case' } - : IsKeyword, 'as'> extends true - ? { body: Body; alias: Unquote>> } - : { body: Body; alias: Unquote> } - : never - : never; - -interface CaseSegment { - kind: 'when' | 'then' | 'else'; - text: string; -} - -type ScanCaseSegments< - S extends string, - CurrentKind extends 'when' | 'then' | 'else', - CurrentText extends string, - Accumulated extends CaseSegment[], - Depth extends unknown[] = [], -> = S extends `${infer Head} ${infer Tail}` - ? IsCaseToken extends true - ? ScanCaseSegments - : IsEndToken extends true - ? Depth extends [unknown, ...infer DepthRest extends unknown[]] - ? ScanCaseSegments - : ScanCaseSegments - : Depth extends [] - ? IsKeyword extends true - ? ScanCaseSegments }], Depth> - : IsKeyword extends true - ? ScanCaseSegments }], Depth> - : IsKeyword extends true - ? ScanCaseSegments }], Depth> - : ScanCaseSegments - : ScanCaseSegments - : [...Accumulated, { kind: CurrentKind; text: Trim }]; - -type HasElseBranch = Segments extends [ - infer Head extends CaseSegment, - ...infer Tail extends CaseSegment[], -] - ? Head['kind'] extends 'else' - ? true - : HasElseBranch - : false; - -type BranchUnion< - DB extends SchemaLike, - Sources extends Source[], - Segments extends CaseSegment[], - Strict extends boolean, -> = Segments extends [infer Head extends CaseSegment, ...infer Tail extends CaseSegment[]] - ? Head['kind'] extends 'then' | 'else' - ? ResolveColumnType, Strict> | BranchUnion - : BranchUnion - : never; - -export type CaseExpressionType< - DB extends SchemaLike, - Sources extends Source[], - Body extends string, - Strict extends boolean, -> = ScanCaseSegments>, 'when', '', []> extends infer Segments extends CaseSegment[] - ? BranchUnion extends infer Union - ? HasElseBranch extends true - ? Union - : Union | null - : never - : never; diff --git a/src/cte.ts b/src/cte.ts deleted file mode 100644 index 148b92f..0000000 --- a/src/cte.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { - Trim, - FirstWord, - DropFirstWord, - IsKeyword, - ExtractParenGroup, - SplitColumnList, -} from './language/lexical/string.js'; -import type { SchemaLike, Flatten, QueryTypeError, SelectColumnKeys, ShadowedBy } from './parse.js'; -import type { InferRowWith } from './parse.js'; - -type CteEntry = [name: string, query: string, columns: string[] | null]; - -type CteNameAndRest = S extends `${infer NamePart}(${infer AfterOpen}` - ? NamePart extends `${string} ${string}` - ? { name: FirstWord; columns: null; rest: Trim> } - : ExtractParenGroup extends { - inner: infer Cols extends string; - rest: infer Rest extends string; - } - ? { name: Trim; columns: SplitColumnList; rest: Trim } - : never - : { name: FirstWord; columns: null; rest: Trim> }; - -// `AS MATERIALIZED (...)` and `AS NOT MATERIALIZED (...)` are Postgres 12+ -// planner hints sitting between `as` and the body. Requiring the paren to -// follow `as` directly rejected the whole WITH clause over them (issue #283). -// Nothing else reads the hint: it changes how the CTE is executed, not what -// it returns. -type SkipMaterializedKeyword = IsKeyword, 'materialized'> extends true - ? Trim> - : IsKeyword, 'not'> extends true - ? IsKeyword>>, 'materialized'> extends true - ? Trim>>> - : S - : S; - -type ParseCteEntry = CteNameAndRest> extends { - name: infer Name extends string; - columns: infer Columns extends string[] | null; - rest: infer Rest extends string; -} - ? IsKeyword, 'as'> extends true - ? SkipMaterializedKeyword>> extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer SubQuery extends string; rest: infer AfterQuery extends string } - ? { name: Name; columns: Columns; query: Trim; rest: Trim } - : never - : never - : never - : never; - -type ParseCteList = - ParseCteEntry extends { - name: infer Name extends string; - columns: infer Columns extends string[] | null; - query: infer Query extends string; - rest: infer Rest extends string; - } - ? Rest extends `,${infer After}` - ? ParseCteList, [...Accumulated, [Name, Query, Columns]]> - : { ctes: [...Accumulated, [Name, Query, Columns]]; rest: Rest } - : never; - -type SkipRecursiveKeyword = IsKeyword, 'recursive'> extends true - ? Trim> - : S; - -// The [never] guard is load-bearing: ParseCteList resolves to never for a WITH -// clause it cannot read, and `never extends { ctes: infer C extends -// CteEntry[]; rest: infer R extends string }` passes with both infers falling -// back to their constraints. That handed ResolveCteContext a `rest` of -// `string` instead of a clean failure, and the query degraded into an index -// signature row rather than reporting anything (issue #283). -export type ParseWithClause = IsKeyword, 'with'> extends true - ? [ParseCteList>>>] extends [never] - ? never - : ParseCteList>>> extends { - ctes: infer Ctes extends CteEntry[]; - rest: infer Rest extends string; - } - ? { ctes: Ctes; rest: Rest } - : never - : never; - -type RenameKeys = NewNames extends [ - infer NewName extends string, - ...infer NewTail extends string[], -] - ? Keys extends [infer OldKey extends string, ...infer OldTail extends string[]] - ? { [Key in NewName]: OldKey extends keyof Row ? Row[OldKey] : unknown } & RenameKeys< - Row, - OldTail, - NewTail - > - : Record - : Record; - -type CteRow< - DB extends SchemaLike, - Query extends string, - Columns extends string[] | null, - Strict extends boolean, -> = Flatten> extends infer Row - ? Columns extends string[] - ? Row extends QueryTypeError - ? Row - : SelectColumnKeys extends infer Keys extends string[] - ? [Keys] extends [never] - ? Row - : Flatten> - : Row - : Row - : never; - -export type BuildCteMap< - DB extends SchemaLike, - Ctes extends CteEntry[], - Strict extends boolean, - Accumulated extends Record = Record, -> = Ctes extends [infer Head extends CteEntry, ...infer Tail extends CteEntry[]] - ? BuildCteMap< - DB, - Tail, - Strict, - Accumulated & { [Key in Head[0]]: CteRow, Head[1], Head[2], Strict> } - > - : Accumulated; diff --git a/src/from.ts b/src/from.ts deleted file mode 100644 index 9395b22..0000000 --- a/src/from.ts +++ /dev/null @@ -1,291 +0,0 @@ -import type { - Trim, - FirstWord, - DropFirstWord, - IsKeyword, - Unquote, - StripQualifier, - ExtractParenGroup, - ApplyParenDelta, - SplitColumnList, -} from './language/lexical/string.js'; - -export interface Source { - table: string; - alias: string; - nullable: boolean; - derivedQuery?: string; - // The columns a `JOIN ... USING (...)` merges. USING makes one column out of - // the pair, so the name is not ambiguous the way the same name coming from - // two independent tables is - it is carried here because the count that - // decides ambiguity sees only the sources (issue #284). - mergedColumns?: string; -} - -type ClauseBoundary = - | 'where' - | 'group' - | 'order' - | 'limit' - | 'having' - | 'offset' - | 'fetch' - | 'window' - | 'union' - | 'except' - | 'intersect' - | 'for' - | 'returning' - | 'output'; - -type IsBoundary = Lowercase extends ClauseBoundary - ? true - : false; - -type SplitFromClauseBoundary< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? IsBoundary extends true - ? { clause: Trim; rest: Trim<`${Head} ${Tail}`> } - : SplitFromClauseBoundary, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : SplitFromClauseBoundary, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : Depth extends [] - ? IsBoundary extends true - ? { clause: Trim; rest: Trim } - : { clause: Trim; rest: '' } - : { clause: Trim; rest: '' }; - -export type TakeFromClause = SplitFromClauseBoundary['clause']; - -type IsJoinPhraseWord = Lowercase extends - | 'join' - | 'inner' - | 'left' - | 'right' - | 'full' - | 'outer' - | 'cross' - | 'lateral' - ? true - : false; - -// Collects every top-level `ON` condition in a FROM clause into one text, with -// the groups joined by `and` so the WHERE scanner can validate them in a -// single pass - the operands are ordinary bare or qualified column references, -// exactly what ValidateWhereOperand already handles. Depth-tracked, so an `ON` -// belonging to a derived table's own inner query is left to that query's own -// parse. -type ScanJoinOnText< - S extends string, - Depth extends unknown[] = [], - Collecting extends boolean = false, - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? IsKeyword extends true - ? ScanJoinOnText< - Tail, - ApplyParenDelta, - true, - Accumulated extends '' ? '' : `${Accumulated} and` - > - : IsJoinPhraseWord extends true - ? ScanJoinOnText, false, Accumulated> - : Collecting extends true - ? ScanJoinOnText< - Tail, - ApplyParenDelta, - true, - Accumulated extends '' ? Head : `${Accumulated} ${Head}` - > - : ScanJoinOnText, false, Accumulated> - : ScanJoinOnText, Collecting, Accumulated> - : Depth extends [] - ? Collecting extends true - ? IsJoinPhraseWord extends true - ? Accumulated - : Accumulated extends '' - ? S - : `${Accumulated} ${S}` - : Accumulated - : Accumulated; - -export type ExtractJoinOnText = ScanJoinOnText>; - -export type RestAfterFromClause = SplitFromClauseBoundary['rest']; - -export type TakeUntilClauseBoundary = SplitFromClauseBoundary['clause']; - -type JoinAfterOuter< - Tail extends string, - Joined extends boolean, - Prev extends boolean, -> = IsKeyword, 'join'> extends true - ? { joined: Joined; prev: Prev; rest: DropFirstWord } - : IsKeyword, 'outer'> extends true - ? IsKeyword>, 'join'> extends true - ? { joined: Joined; prev: Prev; rest: DropFirstWord> } - : never - : never; - -type JoinPhrase = - IsKeyword extends true - ? { joined: false; prev: false; rest: Tail } - : IsKeyword extends true - ? IsKeyword, 'join'> extends true - ? { joined: false; prev: false; rest: DropFirstWord } - : never - : IsKeyword extends true - ? IsKeyword, 'join'> extends true - ? { joined: false; prev: false; rest: DropFirstWord } - : never - : IsKeyword extends true - ? JoinAfterOuter - : IsKeyword extends true - ? JoinAfterOuter - : IsKeyword extends true - ? JoinAfterOuter - : never; - -type SplitAtFirstJoin< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? JoinPhrase extends infer Phrase - ? [Phrase] extends [never] - ? SplitAtFirstJoin, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : Phrase extends { - joined: infer Joined extends boolean; - prev: infer Prev extends boolean; - rest: infer Rest extends string; - } - ? { before: Trim; joined: Joined; prev: Prev; after: Rest } - : never - : never - : SplitAtFirstJoin, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : never; - -type AliasOf = - DropFirstWord extends '' - ? Table - : FirstWord> extends infer Next extends string - ? IsKeyword extends true - ? Table - : IsKeyword extends true - ? FirstWord>> extends infer Aliased extends string - ? Aliased extends '' - ? Table - : Aliased - : Table - : Next - : Table; - -type CleanIdentifier = Unquote>; - -type DerivedAlias = Trim extends '' - ? never - : IsKeyword>, 'as'> extends true - ? FirstWord>> - : IsKeyword>, 'on'> extends true - ? never - : FirstWord>; - -type DerivedSegmentToSource = Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer SubQuery extends string; rest: infer Rest extends string } - ? [DerivedAlias] extends [never] - ? never - : { - table: DerivedAlias; - alias: DerivedAlias; - nullable: Nullable; - derivedQuery: Trim; - } - : never - : never; - -// `LATERAL` attaches directly to a derived-table subquery ("join lateral -// (select ...) alias on ..."), so it has to be stripped before the "does -// this segment start with a paren" check below - otherwise the keyword -// itself gets read as the table name and the real alias is lost. -type StripLateral = Trim extends `${infer Head} ${infer Rest}` - ? IsKeyword extends true - ? Trim - : Trim - : Trim; - -// The USING list of the join this segment belongs to. Only the spaced form is -// read; `using(id)` glued to its paren keeps the old behaviour of no merge, -// which errs on the side of the existing report rather than a silent one. -type UsingColumnsOf = Segment extends `${infer Head} ${infer Tail}` - ? IsKeyword extends true - ? Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string } - ? Inner - : '' - : '' - : UsingColumnsOf - : ''; - -type SegmentToSource = StripLateral extends `(${string}` - ? DerivedSegmentToSource, Nullable> - : CleanIdentifier> extends infer Table extends string - ? { - table: Table; - alias: Unquote>; - nullable: Nullable; - mergedColumns: UsingColumnsOf; - } - : never; - -type PartsToSources = Parts extends [ - infer Head extends string, - ...infer Tail extends string[], -] - ? [SegmentToSource, ...PartsToSources] - : []; - -type SegmentToSources = PartsToSources< - SplitColumnList, - Nullable ->; - -type MarkNullable = { - [Index in keyof Sources]: Sources[Index] extends { derivedQuery: infer Q extends string } - ? { table: Sources[Index]['table']; alias: Sources[Index]['alias']; nullable: true; derivedQuery: Q } - : Sources[Index] extends { mergedColumns: infer M extends string } - ? { table: Sources[Index]['table']; alias: Sources[Index]['alias']; nullable: true; mergedColumns: M } - : { table: Sources[Index]['table']; alias: Sources[Index]['alias']; nullable: true }; -}; - -type CollectSources< - S extends string, - Nullable extends boolean, - Accumulated extends Source[] = [], -> = SplitAtFirstJoin extends infer Split - ? [Split] extends [never] - ? [...Accumulated, ...SegmentToSources] - : Split extends { - before: infer Before extends string; - joined: infer Joined extends boolean; - prev: infer Prev extends boolean; - after: infer After extends string; - } - ? Prev extends true - ? CollectSources< - After, - Joined, - [...MarkNullable, ...SegmentToSources] - > - : CollectSources]> - : [...Accumulated, ...SegmentToSources] - : never; - -export type ParseFromClause = CollectSources< - TakeFromClause, - false ->; diff --git a/src/index.ts b/src/index.ts index 113a8ca..f5aa32a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ export type { Schema, SchemaLike, QueryTypeError, - FunctionReturnTypes, } from './public/schema.js'; export type { @@ -48,11 +47,4 @@ export { isErr, } from './public/result.js'; -export type { - ParseSelect, - ParseStatement, - ParsedStatement, - Source, -} from './parse.js'; - export { defineSchema } from './public/schema.js'; diff --git a/src/params.ts b/src/params.ts deleted file mode 100644 index 01e99aa..0000000 --- a/src/params.ts +++ /dev/null @@ -1,683 +0,0 @@ -import type { - Normalize, - FirstWord, - DropFirstWord, - Trim, - IsKeyword, - ExtractParenGroup, - Digit, - HasNonTrailingSemicolon, - StartsWithIdentifierChar, -} from './language/lexical/string.js'; -import type { - Source, - SchemaLike, - ParseStatement, - ResolveColumnLoose, - ResolveCteContext, - ResolveKey, - AfterKeyword, - SplitColumnList, - MultipleStatementsError, - QueryTypeError, -} from './parse.js'; -import type { ParseWithClause } from './cte.js'; -import type { FunctionName } from './compiler/semantics/functions.js'; - -// `@>` and `<@` compare an array or a jsonb value against another of the same -// type, so the bound value carries the column's own type - the same thing `=` -// does. They are listed here so the parameter beside them is typed rather -// than left `unknown` now that they are no longer misread as placeholders. -type Operator = '=' | '<>' | '!=' | '<' | '>' | '<=' | '>=' | '@>' | '<@'; - -type WordOperator = 'like' | 'ilike' | 'in' | 'between' | 'distinct'; - -type ForcedNumberKeyword = 'limit' | 'offset'; - -type IsOperator = Token extends Operator - ? true - : Lowercase extends WordOperator - ? true - : false; - -type IsTransparentToken = Token extends '(' | ')' | ',' - ? true - : Lowercase extends 'and' | 'or' | 'not' | 'is' | 'from' - ? true - : false; - -type StripLeadingParens = S extends `(${infer Rest}` - ? StripLeadingParens - : S; - -type StripTrailingListPunctuation = S extends `${infer Rest})` - ? StripTrailingListPunctuation - : S extends `${infer Rest},` - ? StripTrailingListPunctuation - : S; - -// `$1::int` is the placeholder `$1` carrying a Postgres cast, not a name - -// without stripping the cast the index is unreadable and the token stops -// binding by position (issue #228). -type StripCast = S extends `${infer Before}::${string}` ? Before : S; - -type AfterLastOpenParen = S extends `${string}(${infer After}` - ? After extends `${string}(${string}` - ? AfterLastOpenParen - : After - : S; - -// A placeholder is routinely written inside a call - `lower($1)`, `any($1)`, -// `coalesce($1, 0)`. The scan splits on spaces, so the call is one token and -// stripping only the trailing `)` left `lower($1`, which matches nothing: the -// placeholder vanished and the tuple had no slot for a value the driver still -// demands (issue #244). -// -// Two placeholders inside a single token (`f($1,$2)`, written without the -// space) cannot both get a slot from a one-token scan, so that token is left -// alone rather than handed a made-up single slot - the same "write it with -// spaces" rule the README already states for `where id=$1`. -type StripCallWrapper = S extends `${string}(${string}` - ? AfterLastOpenParen extends infer Inner extends string - ? Inner extends `${string},${string}` - ? S - : Inner - : S - : S; - -export type CleanScanToken = StripCast< - StripCallWrapper>> ->; - -export type CleanColumnToken = StripLeadingParens extends infer Stripped extends string - ? Stripped extends `${string}(${string}` - ? Stripped - : StripTrailingListPunctuation - : never; - -// The body test is what keeps an operator out: `@>` and `?|` start with a -// placeholder prefix but carry no name, so they used to be counted as -// parameters and to report the query as using a placeholder style the -// executor doesn't accept (issue #249). A bare `?` stays a placeholder - -// that is exactly what it is in MySQL and SQLite, and no amount of text -// alone can tell it apart from Postgres's jsonb existence operator. -export type IsPlaceholder = CleanScanToken extends `${string},${string}` - ? // Two placeholders glued into one space-delimited token (`in ($1,$2)`). - // CleanScanToken has already dropped a *trailing* comma, so a comma left - // inside the token means it holds more than one thing, and a one-token - // scan cannot hand out two slots. `$1,$2` used to pass the `$` body test - // (the body starts with a digit), fail DigitsToCounter, and fall through - // to the named branch as a single slot called `$1,$2` - one phantom - // parameter for two real ones, which pg rejects at bind time (issue - // #291). Not a placeholder, matching what the `?` spelling already did - // and the "write the comma with a space" rule the README states. - false - : CleanScanToken extends '?' - ? true - : // MERGE's `$action` is a pseudo-column, not a placeholder - ResolveColumnType - // already types it as the branch that fired, so counting it here handed the - // caller an extra parameter slot (issue #231). - Lowercase> extends '$action' - ? false - : CleanScanToken extends `$$${string}` | `@@${string}` - ? false - : CleanScanToken extends `$${infer Body}` - ? StartsWithIdentifierChar - : CleanScanToken extends `@${infer Body}` - ? StartsWithIdentifierChar - : // `:name` is the third prefix the node:sqlite adapter binds, and it - // was the only one the type layer didn't know about (issue #238). - // A `::` cast never reaches here - CleanScanToken strips it. - CleanScanToken extends `:${infer Body}` - ? StartsWithIdentifierChar - : false; - -interface DigitCounters { - '0': []; - '1': [unknown]; - '2': [unknown, unknown]; - '3': [unknown, unknown, unknown]; - '4': [unknown, unknown, unknown, unknown]; - '5': [unknown, unknown, unknown, unknown, unknown]; - '6': [unknown, unknown, unknown, unknown, unknown, unknown]; - '7': [unknown, unknown, unknown, unknown, unknown, unknown, unknown]; - '8': [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]; - '9': [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]; -} - -type TimesTen = [ - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, - ...Counter, -]; - -type DigitsToCounter = - S extends `${infer Head}${infer Rest}` - ? Head extends Digit - ? DigitsToCounter, ...DigitCounters[Head & keyof DigitCounters]]> - : never - : Accumulated; - -// The [never] guard is load-bearing: DigitsToCounter resolves to never for -// anything that isn't all digits (`$1::int`, `$id`), and `never extends -// [unknown, ...infer Position]` passes, which routed those tokens into the -// numbered bucket at a bogus position instead of letting them fall through to -// the named branch below (issue #228). -type PlaceholderPosition = - CleanScanToken extends `$${infer Digits}` - ? [DigitsToCounter] extends [never] - ? never - : DigitsToCounter extends [unknown, ...infer Position extends unknown[]] - ? Position - : never - : never; - -// A bare `?` is never named - each occurrence is a distinct positional slot. -// Anything else that reaches here (a non-numeric `$name`/`@name`) is a real -// name, used as the dedup key: a repeated `@id` must bind once (README), not -// grow a new tuple slot per occurrence. -type PlaceholderName = CleanScanToken extends '?' - ? never - : CleanScanToken; - -type FindNameIndex< - Names extends string[], - Name extends string, - Position extends unknown[] = [], -> = Names extends [infer Head extends string, ...infer Tail extends string[]] - ? Head extends Name - ? Position - : FindNameIndex - : never; - -// A repeated placeholder intersects the two types it was read with, and -// `number & string` is `never`: the tuple became uncallable with any argument -// list at all, including the empty one, behind an opaque "not assignable to -// never" chain (issue #302). Rejecting the query is right - pg refuses -// inconsistent deduced parameter types too - so this keeps the rejection and -// adds the diagnosis. An earlier conflict is kept as it is rather than -// intersected again, so the first placeholder that conflicts is the one named. -type MergeSlot = Head extends QueryTypeError - ? Head - : [Head & Type] extends [never] - ? QueryTypeError<`conflicting types for ${Label}`> - : Head & Type; - -type SetSlot< - Tuple extends unknown[], - Position extends unknown[], - Type, - Label extends string, -> = Position extends [unknown, ...infer PositionRest extends unknown[]] - ? Tuple extends [infer Head, ...infer TupleRest extends unknown[]] - ? [Head, ...SetSlot] - : [unknown, ...SetSlot<[], PositionRest, Type, Label>] - : Tuple extends [infer Head, ...infer TupleRest extends unknown[]] - ? [MergeSlot, ...TupleRest] - : [Type]; - -// `= any($1)` / `= all($1)` compare the column against a whole array, so the -// bound value is an array of the column's type, not one of them. Without this -// the fix for #244 would hand the caller a confidently wrong element type for -// the idiomatic Postgres way to bind a list. -type ArrayWrappingFunction = 'any' | 'all' | 'some'; - -type WrapArrayArgument = Token extends `${string}(${string}` - ? Lowercase> extends ArrayWrappingFunction - ? T[] - : T - : T; - -// `= NULL` never matches a row, so a comparison parameter that accepts null -// types a query that silently returns nothing (issue #299). The `| null` on a -// column belongs to the result side - it says a row can come back with that -// column empty, including the one a LEFT JOIN invents - not to the value you -// compare against. -// -// `is distinct from` is the exception, and the reason this is keyed on the -// operator: comparing against null is the entire point of it. -type NullTolerantOperator = 'distinct'; - -// `unknown extends T` holds only for `unknown` itself, and the guard matters: -// NonNullable is `{}`, which would quietly reject the null a column -// this scan could not resolve is still allowed to take. -type ComparisonValue = Lowercase extends NullTolerantOperator - ? T - : unknown extends T - ? T - : NonNullable; - -type ParamType< - DB extends SchemaLike, - Sources extends Source[], - Column extends string, - Op extends string, - Token extends string = '', - InAssignment extends boolean = false, -> = Lowercase extends ForcedNumberKeyword - ? number - : IsOperator extends true - ? WrapArrayArgument< - InAssignment extends true - ? ResolveColumnLoose - : ComparisonValue, Op>, - Token - > - : unknown; - -type AddParam< - Token extends string, - Type, - Indexed extends unknown[], - Sequential extends unknown[], - SequentialNames extends string[], -> = PlaceholderPosition extends infer Position - ? [Position] extends [never] - ? [PlaceholderName] extends [never] - ? // A bare `?` still needs a same-length filler pushed onto - // SequentialNames, or a later named placeholder's FindNameIndex - // result (an index into SequentialNames) would no longer line up - // with that same placeholder's real slot in Sequential. '?' itself - // is a safe filler: PlaceholderName never resolves to '?', so it can - // never be produced as a search target and accidentally matched. - { indexed: Indexed; sequential: [...Sequential, Type]; sequentialNames: [...SequentialNames, '?'] } - : FindNameIndex> extends infer Existing - ? [Existing] extends [never] - ? { - indexed: Indexed; - sequential: [...Sequential, Type]; - sequentialNames: [...SequentialNames, PlaceholderName]; - } - : Existing extends unknown[] - ? { - indexed: Indexed; - sequential: SetSlot>; - sequentialNames: SequentialNames; - } - : never - : never - : Position extends unknown[] - ? { - indexed: SetSlot>; - sequential: Sequential; - sequentialNames: SequentialNames; - } - : never - : never; - -// Which side of the statement the scan is on. A `=` in a SET assignment or a -// VALUES tuple is a write, where null is a legitimate value to bind; a `=` in -// a WHERE or an ON is a comparison, where it can never match. The scan is -// linear and keeps only the two previous tokens, so the region has to be -// carried along - `set a = $1, b = $2` gives the second placeholder no local -// hint that it is still inside the SET clause. -type AssignmentKeyword = 'set' | 'values'; - -type ComparisonKeyword = 'where' | 'on' | 'having' | 'using' | 'join' | 'returning' | 'output'; - -type NextAssignmentRegion = - Lowercase extends AssignmentKeyword - ? true - : Lowercase extends ComparisonKeyword - ? false - : Current; - -type ScanParamsRaw< - S extends string, - DB extends SchemaLike, - Sources extends Source[], - PrevPrev extends string = '', - Prev extends string = '', - Indexed extends unknown[] = [], - Sequential extends unknown[] = [], - SequentialNames extends string[] = [], - InAssignment extends boolean = false, -> = S extends `${infer Head} ${infer Tail}` - ? IsPlaceholder extends true - ? AddParam< - Head, - ParamType, - Indexed, - Sequential, - SequentialNames - > extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? ScanParamsRaw< - Tail, - DB, - Sources, - PrevPrev, - Prev, - NextIndexed, - NextSequential, - NextSequentialNames, - InAssignment - > - : never - : IsTransparentToken extends true - ? ScanParamsRaw< - Tail, - DB, - Sources, - PrevPrev, - Prev, - Indexed, - Sequential, - SequentialNames, - InAssignment - > - : ScanParamsRaw< - Tail, - DB, - Sources, - Prev, - CleanColumnToken, - Indexed, - Sequential, - SequentialNames, - NextAssignmentRegion - > - : S extends '' - ? { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames } - : IsPlaceholder extends true - ? AddParam< - S, - ParamType, - Indexed, - Sequential, - SequentialNames - > - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames }; - -type ScanParams< - S extends string, - DB extends SchemaLike, - Sources extends Source[], - PrevPrev extends string = '', - Prev extends string = '', - Indexed extends unknown[] = [], - Sequential extends unknown[] = [], - SequentialNames extends string[] = [], -> = ScanParamsRaw extends { - indexed: infer FinalIndexed extends unknown[]; - sequential: infer FinalSequential extends unknown[]; -} - ? [...FinalIndexed, ...FinalSequential] - : never; - -// The column list starts right after the target name, which is not always a -// word boundary: `insert into users(id, name)` glues it to the table, so -// dropping the first word ate the first column too (issue #245). -type RestAfterTarget = FirstWord> extends `${string}(${string}` - ? Trim extends `${string}(${infer AfterOpen}` - ? `(${AfterOpen}` - : Trim>> - : Trim>>; - -type InsertColumnList = AfterKeyword extends infer AfterInto extends string - ? RestAfterTarget extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Cols extends string; rest: infer Rest extends string } - ? { columns: SplitColumnList; rest: Trim } - : never - : never - : never; - -type ColumnTypeAt = [ - ResolveKey, -] extends [never] - ? unknown - : ResolveKey extends infer TableKey extends keyof DB - ? [ResolveKey] extends [never] - ? unknown - : ResolveKey extends infer ColumnKey extends keyof DB[TableKey] - ? DB[TableKey][ColumnKey] - : unknown - : unknown; - -// A VALUES entry is one token to this matcher, and StripCallWrapper gives up -// on a call carrying an inner comma - so `coalesce(?, 0)` registered no -// placeholder at all and the tuple came up a slot short. With `@name` that is -// worse than a compile error: the caller can only pass one value, the runtime -// scanner binds it to the first name it meets and binds the second to null, and -// the INSERT succeeds having written values into the wrong columns (issue #269). -// -// Splitting the call's own argument list is enough: each argument is a token -// again, and a single-argument call around a placeholder (`lower(?)`) is -// something CleanScanToken already unwraps. -type CallArguments = Entry extends `${string}(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string } - ? SplitColumnList - : [] - : []; - -type AddCallArgumentParams< - Arguments extends string[], - Type, - Indexed extends unknown[], - Sequential extends unknown[], - SequentialNames extends string[], -> = Arguments extends [infer Head extends string, ...infer Tail extends string[]] - ? IsPlaceholder> extends true - ? AddParam, Type, Indexed, Sequential, SequentialNames> extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? AddCallArgumentParams - : never - : AddCallArgumentParams - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames }; - -type MatchInsertValues< - DB extends SchemaLike, - Table extends string, - Columns extends string[], - Values extends string[], - Indexed extends unknown[], - Sequential extends unknown[], - SequentialNames extends string[], -> = Values extends [infer Head extends string, ...infer ValuesTail extends string[]] - ? Columns extends [infer ColumnHead extends string, ...infer ColumnsTail extends string[]] - ? IsPlaceholder> extends true - ? AddParam< - Trim, - ColumnTypeAt, - Indexed, - Sequential, - SequentialNames - > extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? MatchInsertValues< - DB, - Table, - ColumnsTail, - ValuesTail, - NextIndexed, - NextSequential, - NextSequentialNames - > - : never - : AddCallArgumentParams< - CallArguments>, - ColumnTypeAt, - Indexed, - Sequential, - SequentialNames - > extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? MatchInsertValues< - DB, - Table, - ColumnsTail, - ValuesTail, - NextIndexed, - NextSequential, - NextSequentialNames - > - : never - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames } - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames }; - -type ScanValuesGroups< - S extends string, - DB extends SchemaLike, - Table extends string, - Columns extends string[], - Indexed extends unknown[], - Sequential extends unknown[], - SequentialNames extends string[], -> = Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Vals extends string; rest: infer Rest extends string } - ? MatchInsertValues< - DB, - Table, - Columns, - SplitColumnList, - Indexed, - Sequential, - SequentialNames - > extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? Trim extends `,${infer NextGroup}` - ? ScanValuesGroups - : { - indexed: NextIndexed; - sequential: NextSequential; - sequentialNames: NextSequentialNames; - rest: Trim; - } - : never - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames; rest: Trim } - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames; rest: Trim }; - -type InsertParamTypes = ParseStatement extends { - sources: [infer Src extends Source]; -} - ? [InsertColumnList] extends [never] - ? unknown[] - : InsertColumnList extends { columns: infer Columns extends string[]; rest: infer AfterColumns extends string } - ? // `AfterValues extends string` is distributive (AfterValues is a naked - // type parameter), so an INSERT with no VALUES clause - INSERT ... - // SELECT - collapsed the whole conditional to `never` instead of - // reaching the unknown[] fallback, leaving a rest parameter that no - // call can satisfy (issue #230). - [AfterKeyword] extends [never] - ? unknown[] - : AfterKeyword extends infer AfterValues - ? AfterValues extends string - ? ScanValuesGroups extends { - indexed: infer Indexed extends unknown[]; - sequential: infer Sequential extends unknown[]; - sequentialNames: infer SequentialNames extends string[]; - rest: infer Rest extends string; - } - ? ScanParams - : unknown[] - : unknown[] - : unknown[] - : unknown[] - : unknown[]; - -type CteScanEntry = [name: string, query: string, columns: string[] | null]; - -// The registry threads through every CTE body and on into the outer query, -// rather than each scan starting a fresh one and the results being -// concatenated. A name registry is what dedups a repeated placeholder to one -// slot, so a `@since` written in a CTE body and again outside it used to get -// two slots while the adapters - which dedupe by name over the whole -// statement - bound one value. Every parameter after the duplicate then -// shifted by one: on mssql the next `@name` silently received the duplicated -// value, on node:sqlite the query matched nothing (issue #268). -type CteBodyParamScan< - DB extends SchemaLike, - Ctes extends CteScanEntry[], - Indexed extends unknown[] = [], - Sequential extends unknown[] = [], - SequentialNames extends string[] = [], -> = Ctes extends [infer Head extends CteScanEntry, ...infer Tail extends CteScanEntry[]] - ? [ParseStatement] extends [never] - ? CteBodyParamScan - : ParseStatement extends { sources: infer Sources extends Source[] } - ? ScanParamsRaw extends { - indexed: infer NextIndexed extends unknown[]; - sequential: infer NextSequential extends unknown[]; - sequentialNames: infer NextSequentialNames extends string[]; - } - ? CteBodyParamScan - : never - : CteBodyParamScan - : { indexed: Indexed; sequential: Sequential; sequentialNames: SequentialNames }; - -type OuterAndCteParams< - DB extends SchemaLike, - Q extends string, - CteDB extends SchemaLike, - EffectiveQuery extends string, - Sources extends Source[], -> = [ParseWithClause>] extends [never] - ? ScanParams - : ParseWithClause> extends { ctes: infer Ctes extends CteScanEntry[] } - ? CteBodyParamScan extends { - indexed: infer CteIndexed extends unknown[]; - sequential: infer CteSequential extends unknown[]; - sequentialNames: infer CteSequentialNames extends string[]; - } - ? // Seeding the outer scan with what the CTE bodies produced replaces the - // old concatenation: the numbered slots merge at their own positions - // the way SetSlot already merges a repeat, the sequential ones come - // out CTE-first in textual order, and a name already registered by a - // CTE body now resolves to its existing slot instead of a second one. - ScanParams< - EffectiveQuery, - CteDB, - Sources, - '', - '', - CteIndexed, - CteSequential, - CteSequentialNames - > - : unknown[] - : unknown[]; - -// Same guard InferRowWith applies (issue #206): the parameter tuple is -// derived from the merged text too, so a stacked statement would otherwise -// hand the caller a normal-looking signature covering placeholders from two -// different statements. The error tuple makes the call site fail instead. -export type InferParams = - HasNonTrailingSemicolon extends true - ? [MultipleStatementsError] - : InferParamsChecked; - -type InferParamsChecked = - IsKeyword>, 'insert'> extends true - ? InsertParamTypes> - : ResolveCteContext extends { - db: infer CteDB extends SchemaLike; - query: infer EffectiveQuery extends string; - } - ? [ParseStatement] extends [never] - ? unknown[] - : ParseStatement extends { sources: infer Sources extends Source[] } - ? OuterAndCteParams - : unknown[] - : unknown[]; diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index 8c4fe2d..0000000 --- a/src/parse.ts +++ /dev/null @@ -1,1412 +0,0 @@ -import type { - Normalize, - Trim, - FirstWord, - DropFirstWord, - StripQualifier, - BeforeParen, - Qualifier, - IsKeyword, - Unquote, - ExtractParenGroup, - ApplyParenDelta, - SplitColumnList, - HasNonTrailingSemicolon, -} from './language/lexical/string.js'; -import type { - IsFunctionCall, - FunctionOutputName, - FunctionReturnType, -} from './compiler/semantics/functions.js'; -import type { - Source, - ParseFromClause, - RestAfterFromClause, - TakeFromClause, - TakeUntilClauseBoundary, - ExtractJoinOnText, -} from './from.js'; -import type { IsCaseExpression, SplitCaseExpression, CaseExpressionType } from './case.js'; -import type { ParseWithClause, BuildCteMap } from './cte.js'; -import type { ExtractSelectWhereText, ExtractUpdateDeleteWhereText, WhereClauseError } from './where.js'; - -export type Schema = Record>; - -export type SchemaLike = object; - -export type { Source } from './from.js'; - -export type QueryTypeError = { - readonly __sqlTypeError: Message; -}; - -type CaseInsensitiveKey = { - [Key in keyof T]: Key extends string - ? Lowercase extends Lowercase - ? Key - : never - : never; -}[keyof T]; - -export type ResolveKey = Name extends keyof T - ? Name - : CaseInsensitiveKey; - -type StatementAfterSelect = S extends `${infer Keyword} ${infer Rest}` - ? IsKeyword extends true - ? Rest - : never - : never; - -type StripTopCount = Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { rest: infer Rest extends string } - ? Trim - : Trim - : DropFirstWord>; - -type StripWithTies = IsKeyword, 'with'> extends true - ? IsKeyword>, 'ties'> extends true - ? Trim>> - : S - : S; - -type HasTopCount = Trim extends `(${string}` - ? true - : FirstWord> extends `${number}` - ? true - : false; - -type StripTopClause = IsKeyword>, 'top'> extends true - ? HasTopCount>> extends true - ? StripTopCount>> extends infer AfterCount extends string - ? IsKeyword, 'percent'> extends true - ? StripWithTies>> - : StripWithTies - : S - : S - : S; - -// Postgres accepts `distinct on (a)` and `distinct on(a)` alike. The glued -// spelling makes the first word `on(a)`, which fails a whole-word compare, so -// the ON group leaked into the column list and `on(team_id)` was read as a -// call to a function named `on` aliased to the column beside it (issue #289). -type AfterDistinctOnKeyword = IsKeyword, 'on'> extends true - ? Trim> - : Trim extends `${infer Head}(${infer AfterOpen}` - ? IsKeyword extends true - ? `(${AfterOpen}` - : never - : never; - -type StripDistinctOn = [AfterDistinctOnKeyword] extends [never] - ? S - : AfterDistinctOnKeyword extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { rest: infer Rest extends string } - ? Trim - : S - : S; - -type StripDistinctClause = IsKeyword>, 'distinct'> extends true - ? StripDistinctOn>>> - : IsKeyword>, 'all'> extends true - ? Trim>> - : S; - -// A branch of a set operation ends the column list just as surely as FROM -// does. Only FROM stopped the scan, so a branch written without one - the -// anchor of a recursive CTE is usually `select 1` - ran straight past the -// operator and swallowed the next branch: `select 1 union select 2` came back -// keyed `'union select 2'` (issue #274). Depth-tracked, so a `union` inside a -// parenthesized subquery in the select list is left alone. -type IsSetOperator = Lowercase extends - | 'union' - | 'intersect' - | 'except' - ? true - : false; - -type ColumnsBeforeFrom< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? IsKeyword extends true - ? { columns: Trim; afterFrom: Tail } - : IsSetOperator extends true - ? { columns: Trim; afterFrom: null } - : ColumnsBeforeFrom, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : ColumnsBeforeFrom, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : Depth extends [] - ? IsKeyword extends true - ? { columns: Trim; afterFrom: '' } - : { columns: Trim; afterFrom: null } - : { columns: Trim; afterFrom: null }; - -export type AfterKeyword = - S extends `${infer Head} ${infer Tail}` - ? IsKeyword extends true - ? Tail - : AfterKeyword - : never; - -type ReturningColumns = AfterKeyword extends infer Rest - ? Rest extends string - ? Rest - : '' - : ''; - -type AccumulateUntil< - S extends string, - StopKeyword extends string, - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? IsKeyword extends true - ? Trim - : AccumulateUntil - : IsKeyword extends true - ? Trim - : Trim; - -type OutputClauseColumns = AfterKeyword< - S, - 'output' -> extends infer Rest - ? Rest extends string - ? AccumulateUntil - : '' - : ''; - -// MERGE's OUTPUT clause is always the last clause in the statement (nothing -// meaningful follows it), so unlike INSERT/UPDATE/DELETE's OUTPUT it needs no -// stop keyword to truncate at - it runs to the end of the (already -// semicolon-stripped) string. -type OutputClauseColumnsToEnd = AfterKeyword extends infer Rest - ? Rest extends string - ? Trim - : '' - : ''; - -type StripPseudoTableEntry = Lowercase< - Qualifier> -> extends 'inserted' | 'deleted' - ? StripQualifier> - : Entry; - -type StripPseudoTableQualifiers = S extends `${infer Head},${infer Tail}` - ? `${StripPseudoTableEntry},${StripPseudoTableQualifiers}` - : StripPseudoTableEntry; - -type ReturningOrOutputColumns = ReturningColumns extends '' - ? StripPseudoTableQualifiers> - : ReturningColumns; - -type CleanTargetIdentifier = Unquote>>; - -// The word after a write target is an alias unless it opens the next clause. -// `UPDATE t alias`, `DELETE FROM t AS alias` and `MERGE INTO t AS alias` are -// all legal - the README's own MERGE example uses `as target` - and the alias -// used to be dropped, so every reference through it failed to resolve -// (issue #246). -type WriteTargetBoundary = - | 'set' - | 'where' - | 'values' - | 'value' - | 'returning' - | 'output' - | 'using' - | 'from' - | 'select' - | 'on' - | 'default' - | 'with' - | 'union' - | 'order' - | 'group' - | 'having' - | 'limit' - | 'offset' - | 'when'; - -type IsWriteTargetBoundary = Lowercase extends WriteTargetBoundary - ? true - : false; - -// CleanTargetIdentifier resolves a token that opens a column list (`(id,`) to -// the empty string, which is exactly the "no alias here" answer this needs. -type AliasAfterTarget = Trim extends '' - ? '' - : FirstWord> extends infer Next extends string - ? IsKeyword extends true - ? CleanTargetIdentifier>>>> - : IsWriteTargetBoundary extends true - ? '' - : CleanTargetIdentifier - : ''; - -type SingleSource = CleanTargetIdentifier< - FirstWord> -> extends infer Table extends string - ? FirstWord> extends `${string}(${string}` - ? // A column list glued to the target (`users(id, name)`) leaves no room - // for an alias: Postgres spells that form `insert into t AS u (id)`. - [{ table: Table; alias: Table; nullable: false }] - : AliasAfterTarget>> extends infer Alias extends string - ? [{ table: Table; alias: Alias extends '' ? Table : Alias; nullable: false }] - : [{ table: Table; alias: Table; nullable: false }] - : never; - -// AfterKeyword resolves to never when the keyword is absent; SingleSource -// needs a string either way. -type RestAfterKeyword = AfterKeyword< - S, - Keyword -> extends infer Rest - ? Rest extends string - ? Rest - : '' - : ''; - -// Postgres/SQLite's `UPDATE t SET ... FROM other WHERE ...` and Postgres's -// `DELETE FROM t USING other WHERE ...` both introduce an extra table that -// isn't the statement's own single target. Depth-tracked (unlike -// AfterKeyword) so a "from"/"using" inside a subquery in the SET list isn't -// mistaken for the clause introducer. -export type SplitAtTopLevelKeyword< - S extends string, - Keyword extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? IsKeyword extends true - ? { before: Trim; after: Tail } - : SplitAtTopLevelKeyword, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : SplitAtTopLevelKeyword, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : Depth extends [] - ? IsKeyword extends true - ? { before: Trim; after: '' } - : never - : never; - -// The [never] guard is load-bearing: SplitAtTopLevelKeyword resolves to never -// when the statement has no such clause, and `never extends { after: infer X -// extends string }` passes with X inferred as `string`, which would hand -// ParseFromClause a wildcard table name matching every table in the schema -// (issue #229). -type ExtraSourcesAfterKeyword = [ - SplitAtTopLevelKeyword, -] extends [never] - ? [] - : SplitAtTopLevelKeyword extends { after: infer AfterClause extends string } - ? ParseFromClause - : []; - -// The FROM/USING clause text of a write, for the same JOIN ... ON check a -// SELECT gets. Both branches hardcoded an empty string, so a mistyped column -// in `update users set ... from orders o join refunds r on r.nope = o.id` was -// accepted silently even though the joined sources themselves were registered -// (issue #281). Mirrors ParseSelectBody, which keeps the clause as raw text -// and lets ExtractJoinOnText scan it only in strict mode. -type ExtraFromTextAfterKeyword = [ - SplitAtTopLevelKeyword, -] extends [never] - ? '' - : SplitAtTopLevelKeyword extends { after: infer AfterClause extends string } - ? TakeFromClause - : ''; - -// Nothing scanned the INSERT column list or the SET assignment targets against -// the schema, so `insert into users (naem) values ($1)` and -// `update users set naem = $1` both passed strict mode - guaranteed runtime -// errors on every engine, and the kind of typo the mode exists to catch. The -// documented list of unchecked clauses covers GROUP BY, HAVING and ORDER BY; -// column names in write statements were never on it (issue #282). -type AssignmentTarget = Trim extends `${infer Before}=${string}` - ? Trim - : Trim; - -// A row assignment (`set (a, b) = (1, 2)`) and anything else that is not a -// plain name are left alone: this check reports a name the schema does not -// have, and it must not invent a report for a shape it cannot read. -type IsPlainColumnName = Column extends '' | `${string}(${string}` | `${string} ${string}` - ? false - : true; - -type FirstUnknownWriteColumn< - DB extends SchemaLike, - Sources extends Source[], - Entries extends string[], -> = Entries extends [infer Head extends string, ...infer Tail extends string[]] - ? AssignmentTarget extends infer Column extends string - ? IsPlainColumnName extends false - ? FirstUnknownWriteColumn - : ResolveColumnType extends QueryTypeError - ? QueryTypeError - : FirstUnknownWriteColumn - : never - : never; - -type ApplyWriteColumnCheck< - DB extends SchemaLike, - Sources extends Source[], - ColumnsText extends string, - Strict extends boolean, - Row, -> = ColumnsText extends '' - ? // Cheapest test first: every SELECT carries an empty string here, and the - // structural `Row extends QueryTypeError` check below is not free on a - // wide row. - Row - : Strict extends true - ? Row extends QueryTypeError - ? Row - : [FirstUnknownWriteColumn>>] extends [never] - ? Row - : FirstUnknownWriteColumn>> - : Row; - -// The column list of an INSERT, which is the first parenthesized group before -// VALUES or the SELECT half - never the VALUES tuple itself. -type FirstParenGroupInner = S extends `${string}(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string } - ? Inner - : '' - : ''; - -type BeforeTopLevelKeyword = [ - SplitAtTopLevelKeyword, -] extends [never] - ? '' - : SplitAtTopLevelKeyword extends { before: infer Before extends string } - ? Before - : ''; - -type InsertColumnsText = BeforeTopLevelKeyword extends '' - ? FirstParenGroupInner> - : FirstParenGroupInner>; - -// The SET clause runs to the first clause boundary. `from` is not one of them, -// so an `UPDATE ... FROM` keeps its FROM text in this string - harmless, -// because only the text before each `=` is read. -type UpdateSetText = [SplitAtTopLevelKeyword] extends [never] - ? '' - : SplitAtTopLevelKeyword extends { after: infer After extends string } - ? TakeUntilClauseBoundary - : ''; - -// The SELECT half of an `INSERT ... SELECT`. The INSERT branch reads the -// target and the RETURNING/OUTPUT clause and stops there, so the trailing -// SELECT was never parsed as a statement: its sources, its columns and its -// WHERE did not exist as far as validation was concerned, and -// `insert into users (name) select naem from ghosts where nope = 1` - three -// mistakes in one line - passed strict mode (issue #272). -type InsertSelectText = [SplitAtTopLevelKeyword] extends [never] - ? '' - : SplitAtTopLevelKeyword extends { after: infer After extends string } - ? `select ${After}` - : ''; - -// Reported as the row, the way every other strict failure is. The nested query -// resolves through the same entry point, so its own CTEs, joins and clause -// checks all apply. -type ApplyNestedSelectCheck< - DB extends SchemaLike, - QueryText extends string, - Strict extends boolean, - Row, -> = QueryText extends '' - ? Row - : Strict extends true - ? Row extends QueryTypeError - ? Row - : InferRowWith extends QueryTypeError - ? QueryTypeError - : Row - : Row; - -export interface ParsedStatement { - columns: string; - sources: Source[]; - whereText: string; - fromText: string; - // Empty for everything but the two write branches that carry a column list: - // INSERT's, and UPDATE's SET assignment targets. - writeColumnsText: string; - // Empty unless the statement is an `INSERT ... SELECT`, whose SELECT half is - // a statement of its own. - nestedSelect: string; -} - -type ParseSelectBody = StatementAfterSelect extends infer Body - ? Body extends string - ? ColumnsBeforeFrom>> extends { - columns: infer Columns extends string; - afterFrom: infer AfterFrom; - } - ? AfterFrom extends string - ? { - columns: Columns; - sources: ParseFromClause; - whereText: ExtractSelectWhereText>; - writeColumnsText: ''; - nestedSelect: ''; - // Kept as raw text rather than pre-extracted ON conditions: the - // JOIN ON check only runs in strict mode, and this way the scan - // is never instantiated for a non-strict query. - fromText: TakeFromClause; - } - : { columns: Columns; sources: []; whereText: ''; fromText: ''; writeColumnsText: ''; nestedSelect: '' } - : never - : never - : never; - -// A branch of a set operation may be parenthesized - that is the standard way -// to give one its own ORDER BY or LIMIT - and the statement then opens with -// `(` instead of a keyword, which matched nothing (issue #275). Unwrapping and -// parsing what is inside keeps the documented rule that the row shape comes -// from the first branch: whatever follows the group is another branch, and -// branches have to be column-compatible. -type ParseStatementNormalized = Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string } - ? ParseStatementNormalized> - : never - : FirstWord extends infer Keyword extends string - ? IsKeyword extends true - ? ParseSelectBody - : IsKeyword extends true - ? { - columns: ReturningOrOutputColumns; - sources: SingleSource>; - whereText: ''; - fromText: ''; - writeColumnsText: InsertColumnsText; - nestedSelect: InsertSelectText; - } - : IsKeyword extends true - ? { - // `from` stops the OUTPUT list as well as `where`: T-SQL writes - // `UPDATE t SET ... OUTPUT inserted.id FROM t JOIN ...`, and with - // `where` as the only boundary the whole FROM clause was - // accumulated into the column list, where `inserted.id from users` - // parsed as one entry with `from users` as its alias (issue #300). - columns: ReturningOrOutputColumns; - sources: [ - ...SingleSource>, - ...ExtraSourcesAfterKeyword, - ]; - whereText: ExtractUpdateDeleteWhereText; - fromText: ExtraFromTextAfterKeyword; - writeColumnsText: UpdateSetText; - nestedSelect: ''; - } - : IsKeyword extends true - ? { - columns: ReturningOrOutputColumns; - sources: [ - ...SingleSource>, - ...ExtraSourcesAfterKeyword, - ]; - whereText: ExtractUpdateDeleteWhereText; - fromText: ExtraFromTextAfterKeyword; - writeColumnsText: ''; - nestedSelect: ''; - } - : IsKeyword extends true - ? { - // The USING/WHEN branches aren't modeled - only the target - // table (for OUTPUT column resolution) and the trailing - // OUTPUT clause itself, mirroring how OUTPUT already works - // for INSERT/UPDATE/DELETE. Requires an explicit INTO; MERGE - // without it (legal but rare in practice) isn't recognized. - columns: StripPseudoTableQualifiers>; - sources: SingleSource>; - whereText: ''; - fromText: ''; - writeColumnsText: ''; - nestedSelect: ''; - } - : never - : never; - -export type ParseStatement = ParseStatementNormalized>; - -export type ParseSelect = ParseSelectBody>; - -export type { SplitColumnList } from './language/lexical/string.js'; - -type OutputName = IsFunctionCall extends true - ? FunctionOutputName - : Unquote>; - -type OverAttachedParen = Token extends `${infer Word}(${infer AfterOpen}` - ? IsKeyword extends true - ? AfterOpen - : never - : never; - -type FindOverKeyword = - S extends `${infer Head} ${infer Tail}` - ? IsKeyword extends true - ? IsFunctionCall> extends true - ? { expr: Trim; rest: Tail } - : FindOverKeyword - : OverAttachedParen extends infer AfterOpen extends string - ? [AfterOpen] extends [never] - ? FindOverKeyword - : IsFunctionCall> extends true - ? { expr: Trim; rest: `(${AfterOpen} ${Tail}` } - : FindOverKeyword - : never - : OverAttachedParen extends infer AfterOpen extends string - ? [AfterOpen] extends [never] - ? never - : IsFunctionCall> extends true - ? { expr: Trim; rest: `(${AfterOpen}` } - : never - : never; - -type StripLeadingOpenParen = Trim extends `(${infer Rest}` ? Rest : never; - -// The [never] guard is load-bearing now that the paren is no longer required: -// FindOverKeyword resolves to never for an entry with no OVER at all, and -// `never extends { expr: infer E extends string; rest: infer R extends string }` -// passes with both infers falling back to `string` - which the named-window -// branch below would then accept as a window name. -type SplitWindowExpression = [FindOverKeyword] extends [never] - ? never - : FindOverKeyword extends { - expr: infer Expr extends string; - rest: infer Rest extends string; - } - ? StripLeadingOpenParen extends infer AfterOpen extends string - ? [AfterOpen] extends [never] - ? // `over w`, referring to a window declared in a WINDOW clause, is the - // other half of the syntax: what follows OVER is a name rather than an - // inline definition. Requiring the paren meant the entry was not - // recognized as a window expression at all, so it fell to the bare-alias - // split and `over w` became the alias of `sum(salary)` (issue #301). - Trim extends '' - ? never - : { expr: Expr; after: Trim>> } - : ExtractParenGroup extends { rest: infer AfterClose extends string } - ? { expr: Expr; after: Trim } - : never - : never - : never; - -type IsWindowExpression = [SplitWindowExpression] extends [never] - ? false - : true; - -type SplitParenthesizedEntry = StripLeadingOpenParen extends infer AfterOpen extends string - ? [AfterOpen] extends [never] - ? never - : ExtractParenGroup extends { inner: infer Inner extends string; rest: infer AfterClose extends string } - ? { expr: `(${Inner})`; after: Trim } - : never - : never; - -type IsParenthesizedEntry = [SplitParenthesizedEntry] extends [never] - ? false - : true; - -type FindTopLevelAsKeyword< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? Depth extends [] - ? IsKeyword extends true - ? { expr: Trim; alias: Tail } - : FindTopLevelAsKeyword, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : FindTopLevelAsKeyword, Accumulated extends '' ? Head : `${Accumulated} ${Head}`> - : never; - -// The bare-alias fallback used to split the entry at its first space, with no -// paren tracking, so an unaliased call whose arguments carry one - `power(age, -// 2)`, `cast(id as text)`, `extract(epoch from created_at)` - was cut in half: -// loose mode keyed the column `2)` and strict mode reported `unknown column: -// power(age,` on valid SQL (issue #276). The split now happens at the first -// space that is not inside a call, so an entry that is one call from end to -// end has no alias to find. -type SplitAtTopLevelSpace< - S extends string, - Depth extends unknown[] = [], - Accumulated extends string = '', -> = S extends `${infer Head} ${infer Tail}` - ? ApplyParenDelta extends infer NextDepth extends unknown[] - ? NextDepth extends [] - ? { expr: Accumulated extends '' ? Head : `${Accumulated} ${Head}`; alias: Tail } - : SplitAtTopLevelSpace - : never - : never; - -type ParseColumnEntry = IsCaseExpression extends true - ? SplitCaseExpression extends { body: infer Body extends string; alias: infer Alias extends string } - ? [Alias, `case ${Body} end`] - : [OutputName>, Trim] - : IsWindowExpression extends true - ? SplitWindowExpression extends { expr: infer Expr extends string; after: infer After extends string } - ? After extends '' - ? [OutputName, Expr] - : IsKeyword, 'as'> extends true - ? [Unquote>>, Expr] - : [Unquote>, Expr] - : [OutputName>, Trim] - : IsParenthesizedEntry extends true - ? SplitParenthesizedEntry extends { expr: infer Expr extends string; after: infer After extends string } - ? After extends '' - ? [Trim, UnwrapRedundantParens] - : IsKeyword, 'as'> extends true - ? [Unquote>>, UnwrapRedundantParens] - : // What follows the group is not always an alias: an expression can - // continue past it (`(id + 1) * 2 as x`), and this branch used to - // take the whole tail as the name, so the real alias was lost - // inside a key reading `* 2 as x` (issue #290). The bare-entry - // branch below guards the same situation with IsOperatorExpression; - // this one had no such check. - IsOperatorExpression extends true - ? [FindTopLevelAsKeyword] extends [never] - ? [Trim, Trim] - : FindTopLevelAsKeyword extends { - expr: infer FullExpr extends string; - alias: infer FullAlias extends string; - } - ? [Unquote>, FullExpr] - : [Trim, Trim] - : [Unquote>, UnwrapRedundantParens] - : [Trim, Trim] - : [FindTopLevelAsKeyword] extends [never] - ? [SplitAtTopLevelSpace] extends [never] - ? [OutputName>, Trim] - : SplitAtTopLevelSpace extends { - expr: infer Expression extends string; - alias: infer Alias extends string; - } - ? IsOperatorExpression extends true - ? [Trim, Trim] - : [Unquote>, Trim] - : [OutputName>, Trim] - : FindTopLevelAsKeyword extends { expr: infer Expr extends string; alias: infer Alias extends string } - ? [Unquote>, Expr] - : [OutputName>, Trim]; - -type ParseColumnEntries = { - [Index in keyof Columns]: ParseColumnEntry; -}; - -type EntryKeyList = { - [Index in keyof Entries]: Entries[Index][0]; -}; - -export type SelectColumnKeys = ParseStatementNormalized> extends { - columns: infer Columns extends string; -} - ? EntryKeyList>> extends infer Keys extends string[] - ? Keys - : never - : never; - -type ApplyNull = Nullable extends true ? T | null : T; - -type SourceColumnType< - DB extends SchemaLike, - S extends Source, - Column extends string, -> = ResolveKey extends infer TableKey - ? [TableKey] extends [never] - ? never - : TableKey extends keyof DB - ? ResolveKey extends infer ColumnKey - ? [ColumnKey] extends [never] - ? never - : ColumnKey extends keyof DB[TableKey] - ? ApplyNull - : never - : never - : never - : never; - -type ResolveBareAcross< - DB extends SchemaLike, - Sources extends Source[], - Column extends string, -> = Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? SourceColumnType extends infer Type - ? [Type] extends [never] - ? ResolveBareAcross - : Type - : never - : never; - -type AnyKnownTable = - Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? [ResolveKey] extends [never] - ? AnyKnownTable - : true - : false; - -type FirstUnknownTable = - Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? [ResolveKey] extends [never] - ? Head['table'] - : FirstUnknownTable - : ''; - -type FirstSourceTable = Sources extends [ - infer Head extends Source, - ...Source[], -] - ? Head['table'] - : ''; - -// `a join b using (id)` produces one `id`, not two - that is what USING is for, -// and referencing it bare is the whole point of the syntax. The source the -// join merged carries the list, so its copy of the column does not count -// towards ambiguity (issue #284). -type ColumnListIncludes = - List extends `${infer Head},${infer Tail}` - ? IsKeyword, Column> extends true - ? true - : ColumnListIncludes - : IsKeyword, Column>; - -type IsMergedColumn = Head extends { - mergedColumns: infer Merged extends string; -} - ? Merged extends '' - ? false - : ColumnListIncludes - : false; - -type CountBareMatches< - DB extends SchemaLike, - Sources extends Source[], - Column extends string, - Count extends unknown[] = [], -> = Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? [SourceColumnType] extends [never] - ? CountBareMatches - : IsMergedColumn extends true - ? CountBareMatches - : CountBareMatches - : Count; - -type BareColumnType< - DB extends SchemaLike, - Sources extends Source[], - Column extends string, - Strict extends boolean, -> = ResolveBareAcross extends infer Type - ? [Type] extends [never] - ? Strict extends true - ? Sources extends [] - ? QueryTypeError<`no FROM clause: cannot resolve column "${Column}"`> - : AnyKnownTable extends true - ? QueryTypeError<`unknown column: ${Column}`> - : QueryTypeError<`unknown table: ${FirstSourceTable}`> - : unknown - : Strict extends true - ? CountBareMatches extends [unknown, unknown, ...unknown[]] - ? QueryTypeError<`ambiguous column: ${Column}`> - : Type - : Type - : never; - -type FindSourceByName = - Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? IsKeyword extends true - ? Head - : IsKeyword extends true - ? Head - : FindSourceByName - : never; - -type QualifiedColumnType< - DB extends SchemaLike, - Sources extends Source[], - Name extends string, - Column extends string, - Strict extends boolean, -> = FindSourceByName extends infer Found - ? [Found] extends [never] - ? Strict extends true - ? QueryTypeError<`unknown alias: ${Name}`> - : unknown - : Found extends Source - ? [ResolveKey] extends [never] - ? Strict extends true - ? QueryTypeError<`unknown table: ${Found['table']}`> - : unknown - : ResolveKey extends infer TableKey extends keyof DB - ? [ResolveKey] extends [never] - ? Strict extends true - ? QueryTypeError<`unknown column: ${Column}`> - : unknown - : ResolveKey extends infer ColumnKey extends keyof DB[TableKey] - ? ApplyNull - : never - : never - : never - : never; - -// `(id)` is a column someone wrapped in parentheses, which every dialect -// accepts and people write around an expression they are editing. It used to -// resolve as a call to a function named `''` - `IsFunctionCall` matched it -// because its leading `${string}` also matches the empty string - and then as -// a literal column named `(id)` once that was tightened, so it stayed -// `unknown` either way, in strict mode too, after the column had already been -// validated (issue #288). -// -// Only a group holding no parens of its own is unwrapped, which is enough for -// a plain column and keeps `(a) + (b)` - where the outer match would be -// spurious - out of it. -type UnwrapRedundantParens = Expression extends `(${infer Inner})` - ? Inner extends `${string})${string}` - ? Expression - : IsKeyword>, 'select'> extends true - ? Expression - : Trim - : Expression; - -type ResolveColumnName< - DB extends SchemaLike, - Sources extends Source[], - Expression extends string, - Strict extends boolean, -> = Qualifier extends '' - ? BareColumnType>, Strict> - : QualifiedColumnType< - DB, - Sources, - Unquote>, - Unquote>, - Strict - >; - -type OperatorChar = '*' | '+' | '-' | '/' | '%' | '|' | '<' | '>' | '=' | '^'; - -type ContainsOperatorChar = S extends `${string}${OperatorChar}${string}` - ? true - : false; - -type IsOperatorExpression = S extends - | `${string}"${string}` - | `${string}[${string}` - | `${string}\`${string}` - ? false - : ContainsOperatorChar; - -export type LiteralType = Trim extends `'${string}'` - ? string - : Trim extends `${number}` - ? number - : Lowercase> extends 'true' | 'false' - ? boolean - : Lowercase> extends 'null' - ? null - : never; - -type ScalarSubqueryInner = Trim extends `(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string; rest: infer Rest extends string } - ? Trim extends '' - ? IsKeyword>, 'select'> extends true - ? Trim - : never - : never - : never - : never; - -type IsUnion = T extends U ? ([U] extends [T] ? false : true) : never; - -type IsCountSubquery = Lowercase< - FirstWord> -> extends `count(${string}` - ? true - : false; - -type ScalarSubqueryType< - DB extends SchemaLike, - Q extends string, - Strict extends boolean, - OuterSources extends Source[] = [], -> = InferRowWith extends infer Row - ? [Row] extends [never] - ? unknown - : Row extends QueryTypeError - ? Row - : IsUnion extends true - ? Strict extends true - ? QueryTypeError<'scalar subquery must select exactly one column'> - : unknown - : IsCountSubquery extends true - ? Row[keyof Row] - : Row[keyof Row] | null - : unknown; - -type StripDistinctPrefix = IsKeyword, 'distinct'> extends true - ? Trim> - : Arg; - -type FunctionArgError< - DB extends SchemaLike, - Sources extends Source[], - Arg extends string, -> = Arg extends '' | '*' | `${string} ${string}` | `$${string}` | `@${string}` | '?' - ? never - : StripQualifier extends '*' - ? never - : [LiteralType] extends [never] - ? IsFunctionCall extends true - ? FunctionCallError - : IsOperatorExpression extends true - ? never - : ResolveColumnType extends QueryTypeError - ? QueryTypeError - : never - : never; - -type FirstFunctionArgError< - DB extends SchemaLike, - Sources extends Source[], - Args extends string[], -> = Args extends [infer Head extends string, ...infer Tail extends string[]] - ? FunctionArgError>> extends infer Error - ? [Error] extends [never] - ? FirstFunctionArgError - : Error - : never - : never; - -type FunctionCallError< - DB extends SchemaLike, - Sources extends Source[], - Expression extends string, -> = Expression extends `${string}(${infer AfterOpen}` - ? ExtractParenGroup extends { inner: infer Inner extends string } - ? Trim extends '' - ? never - : FirstFunctionArgError> - : never - : never; - -type StrictFunctionType< - DB extends SchemaLike, - Sources extends Source[], - Expression extends string, -> = FunctionCallError extends infer Error - ? [Error] extends [never] - ? FunctionReturnType - : Error - : never; - -// MERGE's OUTPUT clause can select $action, a pseudo-column with no backing -// table that reports which branch fired for each row - not a real column on -// any source, so it's special-cased ahead of ordinary column resolution. -type IsMergeActionPseudoColumn = Lowercase extends '$action' - ? true - : false; - -// `id::text` is a column carrying a Postgres cast, not a column named -// `id::text`. Nothing stripped the suffix and `:` is not an operator -// character, so the whole token was looked up as a name and strict mode -// reported `unknown column: id::text` on ordinary SQL - one cast poisoning -// the whole row (issue #277). The operand is still resolved, so a typo in it -// is still caught; the result is `unknown` because the cast is what decides -// the type and this parser does not model SQL type names. -type CastExpressionType< - DB extends SchemaLike, - Sources extends Source[], - Operand extends string, - Strict extends boolean, -> = ResolveColumnType extends QueryTypeError - ? QueryTypeError - : unknown; - -export type ResolveColumnType< - DB extends SchemaLike, - Sources extends Source[], - Expression extends string, - Strict extends boolean, -> = IsMergeActionPseudoColumn extends true - ? 'INSERT' | 'UPDATE' | 'DELETE' - : IsCaseExpression extends true - ? SplitCaseExpression extends { body: infer Body extends string } - ? CaseExpressionType - : unknown - : [ScalarSubqueryInner] extends [never] - ? IsFunctionCall extends true - ? Strict extends true - ? StrictFunctionType - : FunctionReturnType - : [LiteralType] extends [never] - ? IsOperatorExpression extends true - ? unknown - : Expression extends `${infer CastOperand}::${string}` - ? CastExpressionType - : ResolveColumnName - : LiteralType - : ScalarSubqueryType, Strict, Sources>; - -export type ResolveColumnLoose< - DB extends SchemaLike, - Sources extends Source[], - Expression extends string, -> = ResolveColumnType; - -type CollectRowErrors = { - [Key in keyof Row]: Row[Key] extends QueryTypeError - ? QueryTypeError - : never; -}[keyof Row]; - -type SurfaceErrors = [CollectRowErrors] extends [never] ? Row : CollectRowErrors; - -type MergeSourceColumns = [ - ResolveKey, -] extends [never] - ? unknown - : ResolveKey extends infer TableKey extends keyof DB - ? { [Column in keyof DB[TableKey]]: ApplyNull } - : unknown; - -type MergedStarColumns = Sources extends [ - infer Head extends Source, - ...infer Tail extends Source[], -] - ? MergeSourceColumns & MergedStarColumns - : unknown; - -export type Flatten = { [Key in keyof T]: T[Key] }; - -type AllKnownTables = Sources extends [ - infer Head extends Source, - ...infer Tail extends Source[], -] - ? [ResolveKey] extends [never] - ? false - : AllKnownTables - : true; - -type StarRow< - DB extends SchemaLike, - Sources extends Source[], - Strict extends boolean, -> = Strict extends true - ? AllKnownTables extends true - ? Flatten> - : QueryTypeError<`unknown table: ${FirstUnknownTable}`> - : AnyKnownTable extends true - ? Flatten> - : unknown; - -type StarColumnsForAlias< - DB extends SchemaLike, - Sources extends Source[], - Name extends string, -> = FindSourceByName extends infer Found - ? [Found] extends [never] - ? unknown - : Found extends Source - ? MergeSourceColumns - : unknown - : unknown; - -type EntryContribution< - DB extends SchemaLike, - Sources extends Source[], - Entry extends [string, string], - Strict extends boolean, -> = Entry[1] extends '*' - ? MergedStarColumns - : StripQualifier extends '*' - ? Strict extends true - ? [FindSourceByName>>] extends [never] - ? { - [Key in Entry[1]]: QueryTypeError<`unknown alias: ${Unquote>}`>; - } - : StarColumnsForAlias>> - : StarColumnsForAlias>> - : { [Key in Entry[0]]: ResolveColumnType }; - -type BuildMixed< - DB extends SchemaLike, - Sources extends Source[], - Entries extends [string, string][], - Strict extends boolean, - Accumulated = unknown, -> = Entries extends [infer Head extends [string, string], ...infer Tail extends [string, string][]] - ? BuildMixed> - : Accumulated; - -type BuildSelection< - DB extends SchemaLike, - Sources extends Source[], - Entries extends [string, string][], - Strict extends boolean, -> = Strict extends true - ? SurfaceErrors>> - : Flatten>; - -type IsSelectAll = Trim extends '*' ? true : false; - -type EmptyRow = Record; - -// A write projecting nothing still names a table, and until now nothing looked -// it up: table existence was only ever checked as a side effect of resolving -// some column, in RETURNING or in WHERE. So `insert into ghosts (name) values -// ($1)` - the everyday INSERT, and the most common write typo there is - -// passed strict mode, while adding `returning id` to it failed properly -// (issue #271). -type EmptyRowChecked< - DB extends SchemaLike, - Sources extends Source[], - Strict extends boolean, -> = Strict extends true - ? AllKnownTables extends true - ? EmptyRow - : QueryTypeError<`unknown table: ${FirstUnknownTable}`> - : EmptyRow; - -// A CTE shadows a real table of the same name for the rest of the query - -// only its own projected columns stay visible. Merging the CTE map into DB -// with a plain intersection doesn't do that: object intersection is -// additive, so DB['users'] & CteRow<...> still exposes every field of the -// *original* users table, including ones the CTE's own query never -// selected. Omitting the shadowed keys first (case-insensitively, matching -// how every other name lookup in this codebase already resolves) before -// intersecting closes that gap. -// Every place a name can hide a table of the same name needs the omit-then- -// intersect pair, never a bare intersection: a CTE body seeing an earlier CTE -// (issue #285), and a derived table whose alias reuses a real table name -// (issue #286). Both used to intersect, which is additive, so the hidden -// table's other columns stayed in scope and strict mode typed a column the -// query cannot produce. -// Gated on there being an overlay at all: OmitShadowedTables is a mapped type -// over every table in the schema, and running it for the empty overlay a -// plain query carries cost 10% of the whole type budget. -export type ShadowedBy = [keyof Overlay] extends [never] - ? DB - : OmitShadowedTables & Overlay; - -type OmitShadowedTables = { - [Key in keyof DB as Key extends string - ? Lowercase extends Lowercase - ? never - : Key - : Key]: DB[Key]; -}; - -export type ResolveCteContext< - DB extends SchemaLike, - Q extends string, - Strict extends boolean, -> = [ParseWithClause>] extends [never] - ? { db: DB; query: Normalize } - : ParseWithClause> extends { - ctes: infer Ctes extends [string, string, string[] | null][]; - rest: infer Rest extends string; - } - ? { - db: OmitShadowedTables & BuildCteMap; - query: Rest; - } - : { db: DB; query: Normalize }; - -// A LATERAL subquery is correlated by definition, and an ordinary derived -// table may reference an outer source too; AllSources is the full FROM list of -// the query these were parsed from, passed as the fallback scope (issue #273). -type BuildDerivedSourceMap< - DB extends SchemaLike, - Sources extends Source[], - Strict extends boolean, - AllSources extends Source[] = [], - Accumulated extends Record = Record, -> = Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? Head extends { derivedQuery: infer Q extends string } - ? BuildDerivedSourceMap< - DB, - Tail, - Strict, - AllSources, - Accumulated & { [Key in Head['alias']]: Flatten> } - > - : BuildDerivedSourceMap - : Accumulated; - -// A derived table that fails to type replaces its own row with the error -// object, and the outer query then reports the alias's columns as unknown - -// pointing at the wrong thing entirely. Surfacing the inner error first keeps -// the message on the mistake that caused it (issue #273). -type FirstDerivedSourceError< - DB extends SchemaLike, - Sources extends Source[], - Strict extends boolean, - AllSources extends Source[], -> = Sources extends [infer Head extends Source, ...infer Tail extends Source[]] - ? Head extends { derivedQuery: infer Q extends string } - ? InferRowWith extends QueryTypeError - ? QueryTypeError - : FirstDerivedSourceError - : FirstDerivedSourceError - : never; - -type ApplyDerivedSourceCheck< - DB extends SchemaLike, - Sources extends Source[], - Strict extends boolean, - Row, -> = Strict extends true - ? [FirstDerivedSourceError] extends [never] - ? Row - : FirstDerivedSourceError - : Row; - -type ApplyClauseError< - DB extends SchemaLike, - Sources extends Source[], - ClauseText extends string, - Row, - OuterSources extends Source[] = [], -> = Trim extends '' - ? Row - : [WhereClauseError] extends [never] - ? Row - : WhereClauseError; - -// JOIN ON conditions are validated the same way the WHERE clause is: the -// operands are ordinary column references against the same sources, and a -// wrong side (`o.id` where `o.user_id` was meant) compiles, runs, and returns -// a wrong result set - exactly what strict mode exists to catch (issue #205). -// GROUP BY, HAVING and ORDER BY stay out: they have their own resolution -// rules (SELECT-list aliases, ordinals, aggregates), documented as a -// limitation in the README. -type ApplyWhereCheck< - DB extends SchemaLike, - Sources extends Source[], - WhereText extends string, - FromText extends string, - Strict extends boolean, - Row, - OuterSources extends Source[] = [], -> = Strict extends true - ? Row extends QueryTypeError - ? Row - : ApplyClauseError< - DB, - Sources, - ExtractJoinOnText, - ApplyClauseError, - OuterSources - > - : Row; - -// Checked in both modes, unlike the schema-driven checks that strict mode -// gates: how many statements a string holds is not a schema question, and -// Normalize strips every semicolon, so a stacked statement is folded into the -// first one and the inferred row describes something the driver will not do -// (issue #206). Non-strict inference is permissive - `unknown` where it can't -// tell - but never knowingly wrong. -export type MultipleStatementsError = - QueryTypeError<'multiple statements are not supported: found a semicolon before the end of the query'>; - -export type UnrecognizedStatementError = - QueryTypeError<'unsupported or unrecognized statement'>; - -export type InferRowWith< - DB extends SchemaLike, - Q extends string, - Strict extends boolean, - OuterSources extends Source[] = [], -> = HasNonTrailingSemicolon extends true - ? MultipleStatementsError - : InferRowWithChecked; - -type InferRowWithChecked< - DB extends SchemaLike, - Q extends string, - Strict extends boolean, - OuterSources extends Source[] = [], -> = ResolveCteContext extends { - db: infer CteDB extends SchemaLike; - query: infer EffectiveQuery extends string; -} - ? // The [never] guard is load-bearing, the same way it is in - // ExtraSourcesAfterKeyword: ParseStatementNormalized resolves to never for - // a statement it does not recognize, and `never extends { columns: infer C - // extends string, ... }` passes with every infer position resolving to its - // constraint, which built a row out of `string` keys instead of failing - // (issue #275). - [ParseStatementNormalized] extends [never] - ? // Strict mode says what happened. `never` is a correct answer - there is - // no row - but it explains nothing, and the message this used to produce - // was worse: `unknown table: ''`, which named no table and pointed a - // `selct` typo at the wrong layer entirely (issue #303). - Strict extends true - ? UnrecognizedStatementError - : never - : ParseStatementNormalized extends { - columns: infer Columns extends string; - sources: infer Sources extends Source[]; - whereText: infer WhereText extends string; - fromText: infer FromText extends string; - writeColumnsText: infer WriteColumnsText extends string; - nestedSelect: infer NestedSelect extends string; - } - ? ShadowedBy< - CteDB, - BuildDerivedSourceMap - > extends infer EffectiveDB extends SchemaLike - ? // The clause check wraps the empty row too: a write with no RETURNING - // projects nothing, but its WHERE clause is still a set of column - // references strict mode has to validate - and UPDATE/DELETE is where - // a wrong one costs the most (issue #233). - ApplyDerivedSourceCheck< - CteDB, - Sources, - Strict, - ApplyNestedSelectCheck< - DB, - NestedSelect, - Strict, - ApplyWriteColumnCheck< - EffectiveDB, - Sources, - WriteColumnsText, - Strict, - ApplyWhereCheck< - EffectiveDB, - Sources, - WhereText, - FromText, - Strict, - Trim extends '' - ? EmptyRowChecked - : IsSelectAll extends true - ? StarRow - : BuildSelection< - EffectiveDB, - Sources, - ParseColumnEntries> extends [string, string][] - ? ParseColumnEntries> - : [], - Strict - >, - OuterSources - > - > - > - > - : never - : never - : never; - -export type InferRow = InferRowWith; - -export type InferRowStrict = InferRowWith; - -export type InferResult = InferRow[]; - -export type InferResultStrict = InferRowStrict[]; diff --git a/src/public/schema.ts b/src/public/schema.ts index 8a3eb6e..840268b 100644 --- a/src/public/schema.ts +++ b/src/public/schema.ts @@ -5,8 +5,6 @@ export type { SchemaLike, } from '../compiler/schema/model.js'; -export type { FunctionReturnTypes } from '../compiler/semantics/functions.js'; - export type { QueryTypeError } from '../compiler/contracts/public-error.js'; export function defineSchema(schema: DB): DB { diff --git a/src/where.ts b/src/where.ts deleted file mode 100644 index efd504a..0000000 --- a/src/where.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { Trim, FirstWord, DropFirstWord, IsKeyword, ExtractParenGroup } from './language/lexical/string.js'; -import type { TakeUntilClauseBoundary } from './from.js'; -import type { - SplitAtTopLevelKeyword, - SchemaLike, - Source, - ResolveColumnType, - QueryTypeError, -} from './parse.js'; -import type { IsPlaceholder, CleanColumnToken } from './params.js'; - -export type ExtractSelectWhereText = Trim extends '' - ? '' - : IsKeyword>, 'where'> extends true - ? TakeUntilClauseBoundary>> - : ''; - -// Depth-tracked, because a SET assignment can hold a subquery with a WHERE of -// its own. Reading the first `where` in the string picked that one up: the -// subquery's columns were checked against the outer target and the real WHERE -// went unchecked, so a valid statement errored and a typo in the clause that -// matters passed (issue #280). The [never] guard is load-bearing the same way -// it is in ExtraSourcesAfterKeyword: SplitAtTopLevelKeyword resolves to never -// when there is no top-level WHERE, and `never extends { after: infer R -// extends string }` passes with R inferred as `string`. -export type ExtractUpdateDeleteWhereText = [ - SplitAtTopLevelKeyword, -] extends [never] - ? '' - : SplitAtTopLevelKeyword extends { after: infer Rest extends string } - ? TakeUntilClauseBoundary - : ''; - -type IsSymbolTriggerOperator = Token extends - | '=' - | '<>' - | '!=' - | '<' - | '>' - | '<=' - | '>=' - ? true - : false; - -type IsWordTriggerOperator = Lowercase extends - | 'like' - | 'ilike' - | 'in' - | 'between' - | 'is' - ? true - : false; - -type IsTriggerOperator = IsSymbolTriggerOperator extends true - ? true - : IsWordTriggerOperator; - -// Mirrors `IsTransparentToken` in params.ts (which lists `not`): `not` must not -// overwrite `Prev`, otherwise it clobbers the real column just before a word -// operator (`like`/`in`/`between`/`ilike`) fires, so the validator checks the -// literal word `not` instead of the column (`NOT LIKE` / `NOT IN` / `NOT BETWEEN`). -type IsTransparentToken = Lowercase extends 'not' ? true : false; - -// `and`/`or` separate comparisons in a WHERE clause. They are neither trigger -// operators nor transparent tokens, so without special handling the RHS operand -// of the comparison immediately before them (held in `Prev`) would be silently -// overwritten by the next token and never reach `ValidateWhereOperand` (issue -// #148). Treat them as validation boundaries: validate the accumulated `Prev` -// exactly the way the operator branch does, then reset scanning state so the -// next comparison's LHS operand starts fresh. `between`'s syntactic `and` -// (`x between 1 and 2`) is validated too, but its bounds are literals or real -// columns, so the check is harmless there. -type IsAndOr = Lowercase extends 'and' | 'or' ? true : false; - -type DropOneOpenParen = S extends `(${infer Rest}` ? Rest : S; - -type HeadStartsSubquery = Head extends `(${string}` - ? DropOneOpenParen extends '' - ? IsKeyword>, 'select'> extends true - ? true - : false - : IsKeyword>>, 'select'> extends true - ? true - : false - : false; - -// A correlated subquery references a column of the query it sits inside, which -// is ordinary SQL - the README's own scalar-subquery example is one. The inner -// scan only ever saw the inner query's sources, so every such reference read as -// unknown (issue #273). -// -// The outer sources are a fallback rather than an addition to the scope: SQL -// resolves the inner name first and only looks outward when it is not there, so -// merging the two lists would instead invent an `ambiguous column` for every -// name the two levels share. When the outer lookup fails too, the inner -// message is the one reported - the reference was meant for the inner query. -type ValidateWhereOperand< - DB extends SchemaLike, - Sources extends Source[], - Operand extends string, - OuterSources extends Source[] = [], -> = Operand extends '' ? never : IsPlaceholder extends true ? never : ResolveColumnType< - DB, - Sources, - Operand, - true -> extends QueryTypeError - ? OuterSources extends [] - ? QueryTypeError - : ResolveColumnType extends QueryTypeError - ? QueryTypeError - : never - : never; - -type WhereScan< - DB extends SchemaLike, - Sources extends Source[], - S extends string, - Prev extends string = '', - OuterSources extends Source[] = [], -> = S extends `${infer Head} ${infer Tail}` - ? HeadStartsSubquery extends true - ? ExtractParenGroup<`${DropOneOpenParen} ${Tail}`> extends { rest: infer Rest extends string } - ? WhereScan, '', OuterSources> - : never - : IsTriggerOperator extends true - ? ValidateWhereOperand extends infer Error - ? [Error] extends [never] - ? WhereScan, OuterSources> - : Error - : never - : IsAndOr extends true - ? ValidateWhereOperand extends infer Error - ? [Error] extends [never] - ? WhereScan - : Error - : never - : IsTransparentToken extends true - ? WhereScan - : WhereScan, OuterSources> - : // Terminal case: no trailing space left, so `S` is the final token of the - // clause. Earlier operands are validated by the operator *after* them, but - // the trailing operand has no following operator to trigger the check — so - // validate it here, mirroring the operator branch's `CleanColumnToken` - // handling (issue #128). `not` is transparent, and empty/placeholder - // operands are already short-circuited inside `ValidateWhereOperand`. - IsTransparentToken extends true - ? never - : ValidateWhereOperand, OuterSources>; - -export type WhereClauseError< - DB extends SchemaLike, - Sources extends Source[], - WhereText extends string, - OuterSources extends Source[] = [], -> = WhereScan, '', OuterSources>; diff --git a/tests/architecture/dependencies.test.mjs b/tests/architecture/dependencies.test.mjs index 21f2b9c..7db9093 100644 --- a/tests/architecture/dependencies.test.mjs +++ b/tests/architecture/dependencies.test.mjs @@ -9,18 +9,19 @@ describe('architecture dependencies', () => { expect(checkArchitecture(process.cwd())).toEqual([]); }); - it('reports runtime imports from legacy compiler files', () => { + it('reports runtime imports from compiler files', () => { const root = mkdtempSync(join(tmpdir(), 'owlsql-architecture-')); mkdirSync(join(root, 'src', 'runtime'), { recursive: true }); - writeFileSync(join(root, 'src', 'parse.ts'), 'export type Parsed = string;\n'); + mkdirSync(join(root, 'src', 'compiler'), { recursive: true }); + writeFileSync(join(root, 'src', 'compiler', 'gateway.ts'), 'export type Query = string;\n'); writeFileSync( join(root, 'src', 'runtime', 'invalid.ts'), - "import type { Parsed } from '../parse.js';\n", + "import type { Query } from '../compiler/gateway.js';\n", ); try { expect(checkArchitecture(root)).toEqual([ - 'src/runtime/invalid.ts -> src/parse.ts violates runtime isolation', + 'src/runtime/invalid.ts -> src/compiler/gateway.ts violates runtime isolation', ]); } finally { rmSync(root, { recursive: true, force: true }); diff --git a/tests/contracts/public-api.test-d.ts b/tests/contracts/public-api.test-d.ts index 126a718..6307864 100644 --- a/tests/contracts/public-api.test-d.ts +++ b/tests/contracts/public-api.test-d.ts @@ -3,7 +3,6 @@ import type { Err, Executor, ExecutorResult, - FunctionReturnTypes, InferParams, InferResult, InferResultStrict, @@ -11,9 +10,6 @@ import type { InferRowStrict, Ok, Params, - ParseSelect, - ParseStatement, - ParsedStatement, PlaceholderStyle, Query, QueryError, @@ -23,7 +19,6 @@ import type { Row, Schema, SchemaLike, - Source, StrictQuery, StrictRow, TypedDb, diff --git a/tests/public-api.test-d.ts b/tests/public-api.test-d.ts index 6d6cfe1..07d5a40 100644 --- a/tests/public-api.test-d.ts +++ b/tests/public-api.test-d.ts @@ -1,5 +1,5 @@ import { defineSchema } from '../src/index.js'; -import type { Row, StrictRow, FunctionReturnTypes, Query } from '../src/index.js'; +import type { Row, StrictRow, Query } from '../src/index.js'; type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) @@ -24,10 +24,6 @@ type StrictRowInfersSingleObject = Expect< Equal, { id: number }> >; -type FunctionRegistryTypesCount = Expect>; - -type FunctionRegistryTypesLower = Expect>; - const inlineSchema = defineSchema({ users: { id: 0 as number, name: '' as string } }); type DefineSchemaPreservesShape = Expect< @@ -41,7 +37,5 @@ export type Assertions = [ RowInfersSingleObject, RowMatchesQueryElement, StrictRowInfersSingleObject, - FunctionRegistryTypesCount, - FunctionRegistryTypesLower, DefineSchemaPreservesShape, ]; From 1f8917f97879ada4bf5fdcbc8ebbb78ae38a9771 Mon Sep 17 00:00:00 2001 From: Tiago Lauer Date: Thu, 20 Aug 2026 14:16:20 -0300 Subject: [PATCH 05/15] refactor: organize OwlSQL as modular monorepo --- .github/workflows/release.yml | 1 + .gitignore | 2 +- CHANGELOG.md | 2 +- COMPARISON.md | 11 +- CONTRIBUTING.md | 10 +- README.md | 1085 +--------------- VERSIONING.md | 2 +- .../008-keep-tooling-inside-core-package.md | 21 + examples/playground/README.md | 2 +- examples/ts-plugin-demo/README.md | 6 +- package-lock.json | 111 +- package.json | 133 +- packages/core/README.md | 1086 +++++++++++++++++ packages/core/package.json | 123 ++ {src => packages/core/src}/adapters/kysely.ts | 0 {src => packages/core/src}/adapters/mssql.ts | 0 {src => packages/core/src}/adapters/mysql2.ts | 0 .../core/src}/adapters/named-params.ts | 0 .../core/src}/adapters/node-sqlite.ts | 0 {src => packages/core/src}/adapters/pg.ts | 0 .../core/src}/adapters/postgres.ts | 0 .../core/src}/adapters/transaction.ts | 0 {src => packages/core/src}/cli/index.ts | 0 .../core/src}/compiler/analysis.ts | 0 .../src}/compiler/contracts/compilation.ts | 0 .../src}/compiler/contracts/diagnostic.ts | 0 .../core/src}/compiler/contracts/editor.ts | 0 .../src}/compiler/contracts/public-error.ts | 0 .../core/src}/compiler/gateway.ts | 0 .../core/src}/compiler/next/compile-delete.ts | 0 .../core/src}/compiler/next/compile-insert.ts | 0 .../core/src}/compiler/next/compile-merge.ts | 0 .../core/src}/compiler/next/compile-select.ts | 0 .../core/src}/compiler/next/compile-update.ts | 0 .../core/src}/compiler/next/compile-with.ts | 0 .../core/src}/compiler/next/index.ts | 0 .../src}/compiler/next/infer-expression.ts | 0 .../core/src}/compiler/next/infer-output.ts | 0 .../core/src}/compiler/next/infer-params.ts | 0 .../src}/compiler/next/infer-projection.ts | 0 .../core/src}/compiler/next/resolve-column.ts | 0 .../core/src}/compiler/next/resolve-source.ts | 0 .../compiler/next/resolve-write-target.ts | 0 .../core/src}/compiler/next/scope.ts | 0 .../core/src}/compiler/schema/model.ts | 0 .../core/src}/compiler/semantics/functions.ts | 0 {src => packages/core/src}/index.ts | 0 .../core/src}/language/dialect/common.ts | 0 .../core/src}/language/dialect/mssql.ts | 0 .../core/src}/language/dialect/mysql.ts | 0 .../core/src}/language/dialect/postgres.ts | 0 .../core/src}/language/dialect/sqlite.ts | 0 .../core/src}/language/dml/parse-delete.ts | 0 .../core/src}/language/dml/parse-insert.ts | 0 .../core/src}/language/dml/parse-merge.ts | 0 .../core/src}/language/dml/parse-update.ts | 0 {src => packages/core/src}/language/ir/cte.ts | 0 .../core/src}/language/ir/parameter.ts | 0 .../core/src}/language/ir/predicate.ts | 0 .../core/src}/language/ir/projection.ts | 0 .../core/src}/language/ir/query.ts | 0 .../core/src}/language/ir/source.ts | 0 .../core/src}/language/ir/write.ts | 0 .../src}/language/lexical/placeholders.ts | 0 .../core/src}/language/lexical/statement.ts | 0 .../core/src}/language/lexical/string.ts | 0 .../core/src}/language/select/parse-from.ts | 0 .../src}/language/select/parse-predicate.ts | 0 .../src}/language/select/parse-projection.ts | 0 .../core/src}/language/select/parse-select.ts | 0 .../core/src}/language/with/parse-with.ts | 0 {src => packages/core/src}/public/client.ts | 0 {src => packages/core/src}/public/query.ts | 0 {src => packages/core/src}/public/result.ts | 0 {src => packages/core/src}/public/schema.ts | 0 {src => packages/core/src}/result.ts | 0 {src => packages/core/src}/runtime/db.ts | 0 {src => packages/core/src}/runtime/errors.ts | 0 .../core/src}/runtime/executor.ts | 0 {src => packages/core/src}/runtime/result.ts | 0 .../core/src}/tooling/introspection/mssql.ts | 0 .../core/src}/tooling/introspection/mysql.ts | 0 .../src}/tooling/introspection/postgres.ts | 0 .../core/src}/tooling/introspection/redact.ts | 0 .../core/src}/tooling/introspection/sqlite.ts | 0 .../src}/tooling/schema-generator/codegen.ts | 0 .../src}/tooling/schema-generator/generate.ts | 0 .../src}/tooling/schema-generator/types.ts | 0 .../core/tests}/adapter-mssql.test.ts | 0 .../core/tests}/adapter-mysql2.test.ts | 0 .../core/tests}/adapter-node-sqlite.test.ts | 0 .../core/tests}/adapter-pg.test.ts | 0 .../core/tests}/adapter-postgres.test.ts | 0 .../tests}/adapter-transactions.test-d.ts | 0 .../core/tests}/adapters.test-d.ts | 0 .../tests/architecture/dependencies.test.mjs | 71 ++ .../core/tests}/case-insensitive.test-d.ts | 0 {tests => packages/core/tests}/case.test-d.ts | 0 .../core/tests}/cast-columns.test-d.ts | 0 .../core/tests}/clauses.test-d.ts | 0 .../core/tests}/cli-codegen-edge.test.ts | 0 .../core/tests}/cli-codegen.test.ts | 0 .../core/tests}/cli-flags.test.ts | 0 .../core/tests}/cli-generate.test.ts | 0 .../core/tests}/cli-mssql-introspect.test.ts | 0 .../core/tests}/cli-mssql-url.test.ts | 0 .../core/tests}/cli-mysql-introspect.test.ts | 0 .../core/tests}/cli-pg-introspect.test.ts | 0 .../core/tests}/cli-redact.test.ts | 0 .../core/tests}/cli-sqlite-introspect.test.ts | 0 .../core/tests}/cli-type-mapping.test.ts | 0 {tests => packages/core/tests}/cli-ux.test.ts | 0 .../core/tests}/comma-from.test-d.ts | 0 .../core/tests}/comments.test-d.ts | 0 .../core/tests}/conflicting-params.test-d.ts | 0 .../tests}/contracts/public-api.test-d.ts | 0 .../tests}/contracts/runtime-contract.test.ts | 0 .../core/tests}/correlated-subquery.test-d.ts | 0 .../core/tests}/cte-materialized.test-d.ts | 0 .../tests}/cte-named-param-dedup.test-d.ts | 0 {tests => packages/core/tests}/cte.test-d.ts | 0 .../core/tests}/depth.test-d.ts | 0 .../core/tests}/derived-table.test-d.ts | 0 .../core/tests}/dialect-brand.test-d.ts | 0 .../core/tests}/dialect-mssql.test-d.ts | 0 .../core/tests}/dialect-mysql.test-d.ts | 0 .../core/tests}/dialect-postgres.test-d.ts | 0 .../core/tests}/dialect-sqlite.test-d.ts | 0 .../core/tests}/distinct.test-d.ts | 0 .../core/tests}/dml-alias.test-d.ts | 0 .../core/tests}/dml-join-on-strict.test-d.ts | 0 .../core/tests}/dml-target.test-d.ts | 0 .../core/tests}/expression-columns.test-d.ts | 0 .../tests}/glued-placeholder-list.test-d.ts | 0 .../core/tests}/inference-edge.test-d.ts | 0 .../tests}/insert-select-strict.test-d.ts | 0 .../core/tests}/insert-target.test-d.ts | 0 .../insert-values-call-params.test-d.ts | 0 .../core/tests}/join-on-strict.test-d.ts | 0 .../core/tests}/join-using-merged.test-d.ts | 0 {tests => packages/core/tests}/join.test-d.ts | 0 .../core/tests}/literal-columns.test-d.ts | 0 .../core/tests}/multiarg-call-alias.test-d.ts | 0 .../multiarg-function-columns.test-d.ts | 0 .../core/tests}/multiword-alias.test-d.ts | 0 .../core/tests}/named-params.test.ts | 0 .../core/tests}/negative.test-d.ts | 0 .../core/tests}/next/diagnostics.test-d.ts | 0 .../tests}/next/editor-contract.test-d.ts | 2 +- .../tests}/next/language-select.test-d.ts | 0 .../core/tests}/next/scope.test-d.ts | 0 .../core/tests}/nullable-params.test-d.ts | 0 .../core/tests}/params-groups.test-d.ts | 0 .../core/tests}/params-in-call.test-d.ts | 0 .../core/tests}/params-index.test-d.ts | 0 .../core/tests}/params.test-d.ts | 0 .../core/tests}/paren-boundary.test-d.ts | 0 .../tests}/paren-expression-alias.test-d.ts | 0 .../tests}/parenthesized-column.test-d.ts | 0 .../tests}/parenthesized-set-ops.test-d.ts | 0 .../core/tests}/parser-edges.test-d.ts | 0 .../core/tests}/pg-operators.test-d.ts | 0 .../core/tests}/public-api.test-d.ts | 0 .../core/tests}/public-api.test.ts | 0 .../quoted-identifier-punctuation.test-d.ts | 0 .../tests}/quoted-identifier-spaces.test-d.ts | 0 .../core/tests}/runtime.test.ts | 0 .../core/tests}/scalar-subquery.test-d.ts | 0 .../core/tests}/select-without-from.test-d.ts | 0 .../core/tests}/semicolon.test-d.ts | 0 .../core/tests}/setop-without-from.test-d.ts | 0 .../core/tests}/shadowing.test-d.ts | 0 .../core/tests}/sqlite-availability.ts | 0 .../core/tests}/strict-blind-spots.test-d.ts | 0 .../core/tests}/strict.test-d.ts | 0 .../core/tests}/string-literal.test-d.ts | 0 .../core/tests}/tier2.test-d.ts | 0 .../core/tests}/tier3.test-d.ts | 0 .../tests}/tooling/schema-generator.test.ts | 0 .../core/tests}/tsql-output-from.test-d.ts | 0 .../core/tests}/types.test-d.ts | 0 .../core/tests}/union.test-d.ts | 0 .../tests}/unknown-write-target.test-d.ts | 0 .../tests}/unrecognized-statement.test-d.ts | 0 .../tests}/update-where-subquery.test-d.ts | 0 .../core/tests}/where-strict.test-d.ts | 0 .../core/tests}/where.test-d.ts | 0 .../core/tests}/whitespace.test-d.ts | 0 .../core/tests}/window.test-d.ts | 0 .../core/tests}/write-column-check.test-d.ts | 0 .../core/tsconfig.build.json | 0 .../core/tsconfig.compiler-api-tests.json | 0 tsconfig.json => packages/core/tsconfig.json | 4 +- .../core/vitest.config.ts | 0 {ts-plugin => packages/ts-plugin}/README.md | 4 +- .../ts-plugin}/package.json | 4 +- .../ts-plugin}/src/analysis-contract.cts | 0 .../ts-plugin}/src/detect.cts | 0 .../ts-plugin}/src/diagnostics.cts | 0 .../ts-plugin}/src/index.cts | 0 .../ts-plugin}/src/schema.cts | 0 .../ts-plugin}/src/sql-context.cts | 0 .../ts-plugin}/tests/completions.test.ts | 0 .../ts-plugin}/tests/detect.test.ts | 0 .../tests/diagnostic-contract.test.ts | 0 .../ts-plugin}/tests/diagnostics.test.ts | 0 .../ts-plugin}/tests/hover-crlf.test.ts | 0 .../ts-plugin}/tests/hover.test.ts | 0 .../ts-plugin}/tests/proxy.test.ts | 0 .../ts-plugin}/tests/schema.test.ts | 0 .../ts-plugin}/tests/sql-context.test.ts | 0 .../ts-plugin}/tests/test-helpers.ts | 6 +- .../ts-plugin}/tsconfig.json | 0 .../ts-plugin}/tsconfig.tests.json | 0 .../ts-plugin}/vitest.config.ts | 0 scripts/check-architecture.mjs | 57 +- scripts/package-smoke.mjs | 5 +- scripts/type-budget.mjs | 4 +- tests/architecture/dependencies.test.mjs | 68 -- tests/integration/cli-generate.test.ts | 4 +- tests/integration/databases.ts | 2 +- tests/integration/kysely.test.ts | 2 +- tests/integration/mssql.test.ts | 6 +- tests/integration/mysql2.test.ts | 4 +- tests/integration/pg.test.ts | 4 +- tests/integration/postgres-js.test.ts | 4 +- tests/{perf => performance}/baseline.json | 0 .../select-next-baseline.json | 0 .../select-next.test-d.ts | 2 +- tests/{perf => performance}/tsconfig.json | 0 .../{perf => performance}/tsconfig.next.json | 0 231 files changed, 1452 insertions(+), 1396 deletions(-) create mode 100644 docs/adr/008-keep-tooling-inside-core-package.md create mode 100644 packages/core/README.md create mode 100644 packages/core/package.json rename {src => packages/core/src}/adapters/kysely.ts (100%) rename {src => packages/core/src}/adapters/mssql.ts (100%) rename {src => packages/core/src}/adapters/mysql2.ts (100%) rename {src => packages/core/src}/adapters/named-params.ts (100%) rename {src => packages/core/src}/adapters/node-sqlite.ts (100%) rename {src => packages/core/src}/adapters/pg.ts (100%) rename {src => packages/core/src}/adapters/postgres.ts (100%) rename {src => packages/core/src}/adapters/transaction.ts (100%) rename {src => packages/core/src}/cli/index.ts (100%) rename {src => packages/core/src}/compiler/analysis.ts (100%) rename {src => packages/core/src}/compiler/contracts/compilation.ts (100%) rename {src => packages/core/src}/compiler/contracts/diagnostic.ts (100%) rename {src => packages/core/src}/compiler/contracts/editor.ts (100%) rename {src => packages/core/src}/compiler/contracts/public-error.ts (100%) rename {src => packages/core/src}/compiler/gateway.ts (100%) rename {src => packages/core/src}/compiler/next/compile-delete.ts (100%) rename {src => packages/core/src}/compiler/next/compile-insert.ts (100%) rename {src => packages/core/src}/compiler/next/compile-merge.ts (100%) rename {src => packages/core/src}/compiler/next/compile-select.ts (100%) rename {src => packages/core/src}/compiler/next/compile-update.ts (100%) rename {src => packages/core/src}/compiler/next/compile-with.ts (100%) rename {src => packages/core/src}/compiler/next/index.ts (100%) rename {src => packages/core/src}/compiler/next/infer-expression.ts (100%) rename {src => packages/core/src}/compiler/next/infer-output.ts (100%) rename {src => packages/core/src}/compiler/next/infer-params.ts (100%) rename {src => packages/core/src}/compiler/next/infer-projection.ts (100%) rename {src => packages/core/src}/compiler/next/resolve-column.ts (100%) rename {src => packages/core/src}/compiler/next/resolve-source.ts (100%) rename {src => packages/core/src}/compiler/next/resolve-write-target.ts (100%) rename {src => packages/core/src}/compiler/next/scope.ts (100%) rename {src => packages/core/src}/compiler/schema/model.ts (100%) rename {src => packages/core/src}/compiler/semantics/functions.ts (100%) rename {src => packages/core/src}/index.ts (100%) rename {src => packages/core/src}/language/dialect/common.ts (100%) rename {src => packages/core/src}/language/dialect/mssql.ts (100%) rename {src => packages/core/src}/language/dialect/mysql.ts (100%) rename {src => packages/core/src}/language/dialect/postgres.ts (100%) rename {src => packages/core/src}/language/dialect/sqlite.ts (100%) rename {src => packages/core/src}/language/dml/parse-delete.ts (100%) rename {src => packages/core/src}/language/dml/parse-insert.ts (100%) rename {src => packages/core/src}/language/dml/parse-merge.ts (100%) rename {src => packages/core/src}/language/dml/parse-update.ts (100%) rename {src => packages/core/src}/language/ir/cte.ts (100%) rename {src => packages/core/src}/language/ir/parameter.ts (100%) rename {src => packages/core/src}/language/ir/predicate.ts (100%) rename {src => packages/core/src}/language/ir/projection.ts (100%) rename {src => packages/core/src}/language/ir/query.ts (100%) rename {src => packages/core/src}/language/ir/source.ts (100%) rename {src => packages/core/src}/language/ir/write.ts (100%) rename {src => packages/core/src}/language/lexical/placeholders.ts (100%) rename {src => packages/core/src}/language/lexical/statement.ts (100%) rename {src => packages/core/src}/language/lexical/string.ts (100%) rename {src => packages/core/src}/language/select/parse-from.ts (100%) rename {src => packages/core/src}/language/select/parse-predicate.ts (100%) rename {src => packages/core/src}/language/select/parse-projection.ts (100%) rename {src => packages/core/src}/language/select/parse-select.ts (100%) rename {src => packages/core/src}/language/with/parse-with.ts (100%) rename {src => packages/core/src}/public/client.ts (100%) rename {src => packages/core/src}/public/query.ts (100%) rename {src => packages/core/src}/public/result.ts (100%) rename {src => packages/core/src}/public/schema.ts (100%) rename {src => packages/core/src}/result.ts (100%) rename {src => packages/core/src}/runtime/db.ts (100%) rename {src => packages/core/src}/runtime/errors.ts (100%) rename {src => packages/core/src}/runtime/executor.ts (100%) rename {src => packages/core/src}/runtime/result.ts (100%) rename {src => packages/core/src}/tooling/introspection/mssql.ts (100%) rename {src => packages/core/src}/tooling/introspection/mysql.ts (100%) rename {src => packages/core/src}/tooling/introspection/postgres.ts (100%) rename {src => packages/core/src}/tooling/introspection/redact.ts (100%) rename {src => packages/core/src}/tooling/introspection/sqlite.ts (100%) rename {src => packages/core/src}/tooling/schema-generator/codegen.ts (100%) rename {src => packages/core/src}/tooling/schema-generator/generate.ts (100%) rename {src => packages/core/src}/tooling/schema-generator/types.ts (100%) rename {tests => packages/core/tests}/adapter-mssql.test.ts (100%) rename {tests => packages/core/tests}/adapter-mysql2.test.ts (100%) rename {tests => packages/core/tests}/adapter-node-sqlite.test.ts (100%) rename {tests => packages/core/tests}/adapter-pg.test.ts (100%) rename {tests => packages/core/tests}/adapter-postgres.test.ts (100%) rename {tests => packages/core/tests}/adapter-transactions.test-d.ts (100%) rename {tests => packages/core/tests}/adapters.test-d.ts (100%) create mode 100644 packages/core/tests/architecture/dependencies.test.mjs rename {tests => packages/core/tests}/case-insensitive.test-d.ts (100%) rename {tests => packages/core/tests}/case.test-d.ts (100%) rename {tests => packages/core/tests}/cast-columns.test-d.ts (100%) rename {tests => packages/core/tests}/clauses.test-d.ts (100%) rename {tests => packages/core/tests}/cli-codegen-edge.test.ts (100%) rename {tests => packages/core/tests}/cli-codegen.test.ts (100%) rename {tests => packages/core/tests}/cli-flags.test.ts (100%) rename {tests => packages/core/tests}/cli-generate.test.ts (100%) rename {tests => packages/core/tests}/cli-mssql-introspect.test.ts (100%) rename {tests => packages/core/tests}/cli-mssql-url.test.ts (100%) rename {tests => packages/core/tests}/cli-mysql-introspect.test.ts (100%) rename {tests => packages/core/tests}/cli-pg-introspect.test.ts (100%) rename {tests => packages/core/tests}/cli-redact.test.ts (100%) rename {tests => packages/core/tests}/cli-sqlite-introspect.test.ts (100%) rename {tests => packages/core/tests}/cli-type-mapping.test.ts (100%) rename {tests => packages/core/tests}/cli-ux.test.ts (100%) rename {tests => packages/core/tests}/comma-from.test-d.ts (100%) rename {tests => packages/core/tests}/comments.test-d.ts (100%) rename {tests => packages/core/tests}/conflicting-params.test-d.ts (100%) rename {tests => packages/core/tests}/contracts/public-api.test-d.ts (100%) rename {tests => packages/core/tests}/contracts/runtime-contract.test.ts (100%) rename {tests => packages/core/tests}/correlated-subquery.test-d.ts (100%) rename {tests => packages/core/tests}/cte-materialized.test-d.ts (100%) rename {tests => packages/core/tests}/cte-named-param-dedup.test-d.ts (100%) rename {tests => packages/core/tests}/cte.test-d.ts (100%) rename {tests => packages/core/tests}/depth.test-d.ts (100%) rename {tests => packages/core/tests}/derived-table.test-d.ts (100%) rename {tests => packages/core/tests}/dialect-brand.test-d.ts (100%) rename {tests => packages/core/tests}/dialect-mssql.test-d.ts (100%) rename {tests => packages/core/tests}/dialect-mysql.test-d.ts (100%) rename {tests => packages/core/tests}/dialect-postgres.test-d.ts (100%) rename {tests => packages/core/tests}/dialect-sqlite.test-d.ts (100%) rename {tests => packages/core/tests}/distinct.test-d.ts (100%) rename {tests => packages/core/tests}/dml-alias.test-d.ts (100%) rename {tests => packages/core/tests}/dml-join-on-strict.test-d.ts (100%) rename {tests => packages/core/tests}/dml-target.test-d.ts (100%) rename {tests => packages/core/tests}/expression-columns.test-d.ts (100%) rename {tests => packages/core/tests}/glued-placeholder-list.test-d.ts (100%) rename {tests => packages/core/tests}/inference-edge.test-d.ts (100%) rename {tests => packages/core/tests}/insert-select-strict.test-d.ts (100%) rename {tests => packages/core/tests}/insert-target.test-d.ts (100%) rename {tests => packages/core/tests}/insert-values-call-params.test-d.ts (100%) rename {tests => packages/core/tests}/join-on-strict.test-d.ts (100%) rename {tests => packages/core/tests}/join-using-merged.test-d.ts (100%) rename {tests => packages/core/tests}/join.test-d.ts (100%) rename {tests => packages/core/tests}/literal-columns.test-d.ts (100%) rename {tests => packages/core/tests}/multiarg-call-alias.test-d.ts (100%) rename {tests => packages/core/tests}/multiarg-function-columns.test-d.ts (100%) rename {tests => packages/core/tests}/multiword-alias.test-d.ts (100%) rename {tests => packages/core/tests}/named-params.test.ts (100%) rename {tests => packages/core/tests}/negative.test-d.ts (100%) rename {tests => packages/core/tests}/next/diagnostics.test-d.ts (100%) rename {tests => packages/core/tests}/next/editor-contract.test-d.ts (96%) rename {tests => packages/core/tests}/next/language-select.test-d.ts (100%) rename {tests => packages/core/tests}/next/scope.test-d.ts (100%) rename {tests => packages/core/tests}/nullable-params.test-d.ts (100%) rename {tests => packages/core/tests}/params-groups.test-d.ts (100%) rename {tests => packages/core/tests}/params-in-call.test-d.ts (100%) rename {tests => packages/core/tests}/params-index.test-d.ts (100%) rename {tests => packages/core/tests}/params.test-d.ts (100%) rename {tests => packages/core/tests}/paren-boundary.test-d.ts (100%) rename {tests => packages/core/tests}/paren-expression-alias.test-d.ts (100%) rename {tests => packages/core/tests}/parenthesized-column.test-d.ts (100%) rename {tests => packages/core/tests}/parenthesized-set-ops.test-d.ts (100%) rename {tests => packages/core/tests}/parser-edges.test-d.ts (100%) rename {tests => packages/core/tests}/pg-operators.test-d.ts (100%) rename {tests => packages/core/tests}/public-api.test-d.ts (100%) rename {tests => packages/core/tests}/public-api.test.ts (100%) rename {tests => packages/core/tests}/quoted-identifier-punctuation.test-d.ts (100%) rename {tests => packages/core/tests}/quoted-identifier-spaces.test-d.ts (100%) rename {tests => packages/core/tests}/runtime.test.ts (100%) rename {tests => packages/core/tests}/scalar-subquery.test-d.ts (100%) rename {tests => packages/core/tests}/select-without-from.test-d.ts (100%) rename {tests => packages/core/tests}/semicolon.test-d.ts (100%) rename {tests => packages/core/tests}/setop-without-from.test-d.ts (100%) rename {tests => packages/core/tests}/shadowing.test-d.ts (100%) rename {tests => packages/core/tests}/sqlite-availability.ts (100%) rename {tests => packages/core/tests}/strict-blind-spots.test-d.ts (100%) rename {tests => packages/core/tests}/strict.test-d.ts (100%) rename {tests => packages/core/tests}/string-literal.test-d.ts (100%) rename {tests => packages/core/tests}/tier2.test-d.ts (100%) rename {tests => packages/core/tests}/tier3.test-d.ts (100%) rename {tests => packages/core/tests}/tooling/schema-generator.test.ts (100%) rename {tests => packages/core/tests}/tsql-output-from.test-d.ts (100%) rename {tests => packages/core/tests}/types.test-d.ts (100%) rename {tests => packages/core/tests}/union.test-d.ts (100%) rename {tests => packages/core/tests}/unknown-write-target.test-d.ts (100%) rename {tests => packages/core/tests}/unrecognized-statement.test-d.ts (100%) rename {tests => packages/core/tests}/update-where-subquery.test-d.ts (100%) rename {tests => packages/core/tests}/where-strict.test-d.ts (100%) rename {tests => packages/core/tests}/where.test-d.ts (100%) rename {tests => packages/core/tests}/whitespace.test-d.ts (100%) rename {tests => packages/core/tests}/window.test-d.ts (100%) rename {tests => packages/core/tests}/write-column-check.test-d.ts (100%) rename tsconfig.build.json => packages/core/tsconfig.build.json (100%) rename tsconfig.compiler-api-tests.json => packages/core/tsconfig.compiler-api-tests.json (100%) rename tsconfig.json => packages/core/tsconfig.json (73%) rename vitest.config.ts => packages/core/vitest.config.ts (100%) rename {ts-plugin => packages/ts-plugin}/README.md (88%) rename {ts-plugin => packages/ts-plugin}/package.json (95%) rename {ts-plugin => packages/ts-plugin}/src/analysis-contract.cts (100%) rename {ts-plugin => packages/ts-plugin}/src/detect.cts (100%) rename {ts-plugin => packages/ts-plugin}/src/diagnostics.cts (100%) rename {ts-plugin => packages/ts-plugin}/src/index.cts (100%) rename {ts-plugin => packages/ts-plugin}/src/schema.cts (100%) rename {ts-plugin => packages/ts-plugin}/src/sql-context.cts (100%) rename {ts-plugin => packages/ts-plugin}/tests/completions.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/detect.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/diagnostic-contract.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/diagnostics.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/hover-crlf.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/hover.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/proxy.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/schema.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/sql-context.test.ts (100%) rename {ts-plugin => packages/ts-plugin}/tests/test-helpers.ts (95%) rename {ts-plugin => packages/ts-plugin}/tsconfig.json (100%) rename {ts-plugin => packages/ts-plugin}/tsconfig.tests.json (100%) rename {ts-plugin => packages/ts-plugin}/vitest.config.ts (100%) delete mode 100644 tests/architecture/dependencies.test.mjs rename tests/{perf => performance}/baseline.json (100%) rename tests/{perf => performance}/select-next-baseline.json (100%) rename tests/{perf => performance}/select-next.test-d.ts (75%) rename tests/{perf => performance}/tsconfig.json (100%) rename tests/{perf => performance}/tsconfig.next.json (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2a9863..bbdf9dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,3 +40,4 @@ jobs: - run: npm ci - run: npm publish --loglevel verbose + working-directory: packages/core diff --git a/.gitignore b/.gitignore index 8042b37..4bd5663 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ dist/ -tests/perf/generated/ +tests/performance/generated/ *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 015bf75..7e6cbcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ Notable changes to this project, following [Keep a Changelog](https://keepachang ### Changed -- **Breaking:** the editor plugin moved out of this package into its own, [`@owlsql/ts-plugin`](ts-plugin/README.md). The `@owlsql/core/ts-plugin` subpath is gone. To migrate, install `@owlsql/ts-plugin` as a dev dependency and change the plugin name in your `tsconfig.json`: +- **Breaking:** the editor plugin moved out of this package into its own, [`@owlsql/ts-plugin`](packages/ts-plugin/README.md). The `@owlsql/core/ts-plugin` subpath is gone. To migrate, install `@owlsql/ts-plugin` as a dev dependency and change the plugin name in your `tsconfig.json`: ```json { diff --git a/COMPARISON.md b/COMPARISON.md index 9f54c34..3a53385 100644 --- a/COMPARISON.md +++ b/COMPARISON.md @@ -134,16 +134,17 @@ check the source link and open an issue. - **Bundle**: zero runtime dependencies (`package.json` has no `dependencies` field), and the runtime surface is `createTypedDb`, `defineSchema`, and the `Result` helpers — about 175 lines of source - across [`src/index.ts`](src/index.ts) and [`src/result.ts`](src/result.ts) + across [`packages/core/src/index.ts`](packages/core/src/index.ts) and + [`packages/core/src/runtime/result.ts`](packages/core/src/runtime/result.ts) combined, most of which is type declarations erased at compile time. The - parser itself (a few thousand lines across `src/parse.ts`/`src/from.ts`/ - etc.) is 100% types — it ships zero bytes to any runtime. + compiler under `packages/core/src/language` and `packages/core/src/compiler` + is 100% types — it ships zero bytes to any runtime. - **DX trade-off, stated plainly**: this is the smallest surface area of the five because it does the least. No migrations, no relation loading, no query builder ergonomics (autocomplete for chained methods) — you write SQL, you get a type back. If you want an ORM's feature set, this isn't - one; see the [Supported SQL subset](README.md#supported-sql-subset) and - [Limitations](README.md#limitations) for exactly where the parser's + one; see the [Supported SQL subset](packages/core/README.md#supported-sql-subset) and + [Limitations](packages/core/README.md#limitations) for exactly where the parser's coverage ends. ## Methodology notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbd62d9..5ced331 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ Keep a PR to one fix or one feature. A PR that touches three unrelated things is If the change alters what a query infers to, say so in the description and name the bump it implies under [VERSIONING.md](VERSIONING.md). A row shape that gains, loses, or retypes a key is a breaking change even when no runtime signature moved. -Every behavior change needs a test that would fail without the fix. If you're touching `src/parse.ts`, `src/where.ts`, or another type-level file, that usually means a `.test-d.ts` case with `@ts-expect-error` or an `Equal<>` assertion; runtime behavior (adapters, the CLI, the editor plugin) gets a `.test.ts` case instead. A PR without a regression test is a PR someone else will eventually re-break by accident. +Every behavior change needs a test that would fail without the fix. If you're touching `packages/core/src/language`, `packages/core/src/compiler`, or another type-level file, that usually means a `.test-d.ts` case with `@ts-expect-error` or an `Equal<>` assertion; runtime behavior (adapters, the CLI, the editor plugin) gets a `.test.ts` case instead. A PR without a regression test is a PR someone else will eventually re-break by accident. ### Architecture changes @@ -41,7 +41,7 @@ ADRs in `docs/adr/`. A change that supersedes one of those decisions needs a new You'll need Node 20 or later. The `node:sqlite` adapter and the CLI's SQLite introspection need Node 22.5+, since `node:sqlite` is newer than the rest of the runtime surface this library targets. -This repository holds two packages, as an npm workspace: `@owlsql/core` at the root, and the editor plugin in [`ts-plugin/`](ts-plugin/README.md). They are apart because they don't reach the same TypeScript versions — the library type-checks clean on TypeScript 7, while the plugin needs the classic compiler API, which TypeScript 7 does not ship at all. One package can only declare one peer range, and either choice would have been a lie about half the code. +This repository holds two npm workspaces: [`@owlsql/core`](packages/core) and the editor plugin in [`packages/ts-plugin/`](packages/ts-plugin/README.md). They are apart because they don't reach the same TypeScript versions — the library type-checks clean on TypeScript 7, while the plugin needs the classic compiler API, which TypeScript 7 does not ship at all. One package can only declare one peer range, and either choice would have been a lie about half the code. ```bash npm install @@ -70,7 +70,7 @@ Open an issue before writing the implementation if the feature touches the publi ### Design preferences - No runtime SQL parsing, ever. If a change needs to inspect the query string at runtime to work, it probably belongs in the ts-plugin (which already does its own lightweight runtime scanning for editor support), not in the core library. -- Adapters (`src/adapters/*.ts`) import the driver's types only, never the driver package itself as a value. This keeps `@owlsql/core/pg` usable without `pg` actually being installed, for anyone who only imports a different adapter. +- Adapters (`packages/core/src/adapters/*.ts`) import the driver's types only, never the driver package itself as a value. This keeps `@owlsql/core/pg` usable without `pg` actually being installed, for anyone who only imports a different adapter. - If you extend the SQL subset the parser accepts, update the "Supported SQL subset" and "Limitations" sections in the README in the same PR. A parser change nobody can discover from the docs is half a feature. - Prefer a documented scope boundary over a half-correct implementation. Several existing features (LATERAL correlation, WHERE-clause diagnostics with parens) deliberately do less than a full SQL engine would, and say so in the README, rather than guessing. @@ -78,8 +78,8 @@ Open an issue before writing the implementation if the feature touches the publi Three layers, and they test different things: -- **Type tests** (`tests/*.test-d.ts`) are pure type assertions. If they compile, the inference is correct; there's no runtime assertion to run. They cover column/alias projection, `@ts-expect-error` cases for queries that should fail to type, permissive-inference locks, and deep-recursion stress. -- **Runtime tests** (`tests/*.test.ts`) run under vitest and cover the executor/`Result` contract, adapter parameter handling, and the CLI. Drivers are faked here, so these prove the adapter's own logic, not what a real server sends back. The plugin's own tests live beside it in `ts-plugin/tests/`. +- **Type tests** (`packages/core/tests/*.test-d.ts`) are pure type assertions. If they compile, the inference is correct; there's no runtime assertion to run. They cover column/alias projection, `@ts-expect-error` cases for queries that should fail to type, permissive-inference locks, and deep-recursion stress. +- **Runtime tests** (`packages/core/tests/*.test.ts`) run under vitest and cover the executor/`Result` contract, adapter parameter handling, and the CLI. Drivers are faked here, so these prove the adapter's own logic, not what a real server sends back. The plugin's own tests live in `packages/ts-plugin/tests/`. - **Integration tests** (`tests/integration/*.test.ts`) run the adapters and the `generate` CLI against real PostgreSQL, MySQL, and SQL Server instances. They cover what a fake driver can't: how each driver actually decodes a column (`bigint`, `numeric`, `tinyint(1)`, `bit`), the metadata a real result carries, and whether a rolled-back transaction really left no rows behind. CI runs the type tests against a matrix of TypeScript versions, since a template-literal-type change that works on one TypeScript release can silently stop working (or start working differently) on another. diff --git a/README.md b/README.md index 8912855..28c9333 100644 --- a/README.md +++ b/README.md @@ -1,1086 +1,3 @@ # OwlSQL -> Write raw SQL. Get fully-typed results. No codegen, no ORM, no runtime parsing. - -OwlSQL (`@owlsql/core`) reads your SQL inside TypeScript's type system and -infers the row shape from the query string and your schema. It happens as you -type, in your editor. There is no build step. - -```ts -type DB = { - users: { id: number; name: string; email: string; active: boolean }; -}; - -const db = createTypedDb(createPgExecutor(pool)); - -const a = await db.query('select id from users'); -// a.value ^? { id: number }[] - -const b = await db.query('select name as handle, active from users'); -// b.value ^? { handle: string; active: boolean }[] - -const c = await db.query('select * from users'); -// c.value ^? { id: number; name: string; email: string; active: boolean }[] - -const d = await db.query('select id from users where id = $1', 7); -// ^ typed as number -``` - -Rename a column in the SQL, mistype a field, or select something that does not -exist, and the result type changes immediately, before you run a single line. -There is **no generated file to keep in sync** and **no SQL parser shipped to -production**: all the work happens during type checking. - -It is **not** an ORM and **not** a query builder. It does not connect to your -database. You keep writing the SQL you already know; this library only layers -compile-time result typing on top of whatever driver you use. - -**[Try it in your browser →](https://stackblitz.com/github/tiagolauer/OwlSQL/tree/master/examples/playground?file=index.ts)** -No install, no database — see [`examples/playground`](examples/playground). - ---- - -## Table of contents - -- [The problem](#the-problem) -- [How it works](#how-it-works) -- [Install](#install) -- [How it compares](#how-it-compares) -- [What it costs to compile](#what-it-costs-to-compile) -- [Tutorial](#tutorial) - - [1. Describe your schema](#1-describe-your-schema) - - [2. Create a typed client](#2-create-a-typed-client) - - [3. Run queries and handle the Result](#3-run-queries-and-handle-the-result) - - [4. Aliases, `*`, and qualified columns](#4-aliases--and-qualified-columns) - - [5. Type-only usage (no client)](#5-type-only-usage-no-client) - - [6. Aggregates and functions](#6-aggregates-and-functions) - - [7. INSERT / UPDATE / DELETE with RETURNING](#7-insert--update--delete-with-returning) - - [8. Strict mode — turn typos into type errors](#8-strict-mode--turn-typos-into-type-errors) - - [9. Joins](#9-joins) - - [10. Typed parameters](#10-typed-parameters) - - [11. Transactions](#11-transactions) -- [Driver recipes](#driver-recipes) -- [Database support](#database-support) -- [Editor autocomplete](#editor-autocomplete) -- [API reference](#api-reference) -- [Supported SQL subset](#supported-sql-subset) -- [Limitations](#limitations) -- [FAQ](#faq) -- [Contributing](#contributing) -- [License](#license) - ---- - -## The problem - -I was building a TypeScript backend and picked raw SQL over an ORM on purpose. -I wanted control over the queries and no layer of magic between my code and the -database. That part worked. - -The return types were the problem. Every query came back as `any[]` or -`unknown[]`, so I wrote an interface by hand for each one: - -```ts -interface UserListRow { id: number; name: string } -const rows = (await pool.query('select id, name from users')).rows as UserListRow[]; -``` - -Those interfaces drift. Someone adds `email` to the SQL, forgets the interface, -and the type quietly lies until it breaks in production. They are also -boilerplate: the interface restates the query in a second syntax, so you type -the same column list twice. - -The usual fixes each cost something. ORMs replace your SQL with their own DSL -and runtime, which was the thing I was trying to avoid. Codegen tools do give -you accurate types, but they bolt a generation step onto the build, so now you -have a watcher, a CLI, a database connection at build time, and generated files -in version control. - -The query string is already the source of truth, so the compiler may as well -read it. TypeScript's template literal types can parse a `SELECT` and map its -columns to a schema during type checking, which is what this library does. - -## How it works - -There is no runtime SQL parser and no build step. The entire parser is written -as recursive [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) -evaluated by `tsc`: - -1. **Normalize** — collapse newlines, tabs, and runs of spaces into a single - trimmed, single-spaced string. -2. **Parse** — strip the `SELECT` keyword, split on the first case-insensitive - `FROM`, and separate the column list from the table name. -3. **Resolve** — parse each column into `[outputName, sourceColumn]` (handling - `AS` aliases and `table.col` qualifiers), then look the column up in your - schema to get its TypeScript type. -4. **Assemble** — build `{ ...columns }[]`. - -The JavaScript that actually ships is a tiny passthrough: it forwards your SQL -to the driver you provide and wraps the rows in a `Result`. All the intelligence -lives in the `.d.ts` types. - -## Install - -```bash -npm install @owlsql/core -``` - -`typescript` is a peer dependency (**>= 5.4, < 8** — 5.4 is the oldest -version CI tests; the ts-plugin does not load on TS 7). You almost -certainly already have it. - -The package is **ESM-only** (`import` only — `require()` is not supported). -Node support: **>= 20** for the library and CLI; the `node:sqlite` adapter -and the CLI's SQLite introspection additionally need **Node >= 22.5** (they -fail with a clear error below that). - -## How it compares - -| | OwlSQL | Prisma | Kysely | pgTyped | Zapatos | -| --- | --- | --- | --- | --- | --- | -| You write | Raw SQL strings | Prisma's own query API | Builder method chains | Raw SQL in `.sql` files or tags | Helpers, or raw SQL via `db.sql` | -| Build step | No (opt-in `generate` for the schema only) | Yes, `prisma generate` | No (optional `kysely-codegen`) | Yes, a CLI run against a live database | No (opt-in schema generation) | -| Runtime query engine | None. Your string reaches the driver unchanged | Yes, a TypeScript query compiler | Yes, compiles the chain to SQL on every call | Minimal. Runs a query the CLI already extracted | Yes, builds SQL from helper calls | -| Bundle (min/gzip) | No dependencies; ~175 lines of glue, the parser costs 0 bytes | ~1.6 MB / ~600 KB | 189 KB / 38.7 KB | 399 KB / 85 KB | No bundled engine beyond thin helpers | - -Kysely is the closest of these in spirit: no magic, and inference that goes all -the way down. What differs is what you type. Its builder is a fluent API; here -you type SQL. If you want to paste a query straight out of `psql` or a -migration file and have it work, that is raw SQL, and that is the premise. - -This is not a runtime-speed comparison, on purpose. Your database and driver -dominate query execution, not the layer sitting on top of them, and these five -tools have architectures too different for a queries-per-second figure to say -anything. [COMPARISON.md](COMPARISON.md) has the long version, every number -sourced. - -## What it costs to compile - -Every tool in that table charges you something. Codegen tools charge a build -step, ORMs charge bundle size and a runtime engine, and this one charges -compile time. So here is the number. - -A fixture of 100 tables with 13 columns each, plus 32 queries covering joins, -`GROUP BY`, `CASE`, CTEs, `UNION`, strict mode and typed parameters, -type-checks in: - -| | | -| --- | --- | -| Type instantiations | 166,512 | -| Check time | ~0.4s | -| Runtime cost | 0. Nothing parses SQL at request time | - -Measured with `tsc --extendedDiagnostics` on TypeScript 5.9.3. The cost grows -linearly on top of a fixed overhead: about 96,000 instantiations go to the -schema itself, then roughly 2,200 per query. CI holds that number to a ceiling, -so a change that makes the parser work harder for the same answer fails the -build instead of quietly slowing down every editor that opens your project. - -## Tutorial - -### 1. Describe your schema - -A schema is just a type: table name → column name → TypeScript type. Use a -`type` or an `interface`, whichever you prefer. - -```ts -type DB = { - users: { - id: number; - name: string; - email: string; - active: boolean; - }; - posts: { - id: number; - title: string; - user_id: number; - published: boolean; - }; -}; -``` - -This type is the single source of truth for what your tables look like. It has -no runtime cost — it is erased during compilation. Mark nullable columns with -`| null` (e.g. `bio: string | null`) and that nullability flows straight into -your query results. - -**Optional: generate a starting point with `owlsql generate`.** -Writing that type by hand is fine for a handful of tables, but you can also -have it generated from a real database: - -``` -npx @owlsql/core generate --url postgres://user:pass@host/db --out schema.ts -``` - -This connects to your database, introspects the tables/columns/nullability, -and writes a `schema.ts` with `export interface DB { ... }` — the exact shape -from step 1 above. It's a **one-shot generator, not a codegen pipeline**: the -library still parses your queries entirely at the type level with zero -runtime codegen, same as always. The generated file is a normal `.ts` file — -commit it, edit it by hand afterward, rename fields, anything. Running -`generate` again just overwrites it with a fresh snapshot; nothing stays -"synced" automatically — unless you opt into checking for that in CI with -`--check` (below). - -| Flag | Required | Description | -| ---- | -------- | ----------- | -| `--url` | yes | Connection string (or a file path for SQLite). SQL Server accepts both `mssql://user:pass@host:1433/db` (translated to a driver config; `?encrypt=false` and `?trustServerCertificate=true` supported, and a named instance may be written as `host\INSTANCE`) and an ADO string (`Server=host;Database=db;User Id=u;Password=p`). | -| `--out` | no | Output file. Defaults to `./schema.ts`. | -| `--dialect` | no | `postgres` \| `mysql` \| `sqlite` \| `mssql`. Auto-detected from the URL scheme (`postgres://`/`postgresql://`, `mysql://`, `mssql://`/`sqlserver://`); an ADO `Server=...` string also routes to `mssql` — falls back to `sqlite` for a bare file path, so it's only needed when that's ambiguous. | -| `--schema` | no | Schema/database name to introspect. Defaults to `public` (Postgres), the connected database (MySQL), or `dbo` (SQL Server). Not used for SQLite. | -| `--table` | no | Comma-separated list (`--table users,posts`). Only introspect these tables, instead of every table in the schema. | -| `--exclude` | no | Comma-separated list. Skip these tables even if `--table` would otherwise include them. | -| `--check` | no | Don't write `--out` — introspect and render as usual, then compare against the existing file. Exits `0` with no output if they match, `1` with a message telling you where they first differ (or that the file doesn't exist yet) if they don't. `--table`/`--exclude`/`--schema` apply identically, so the comparison stays meaningful. Useful in CI to catch a migration that ran without anyone regenerating the committed schema. | - -```bash -# CI: fail the build if schema.ts has drifted from the real database -npx @owlsql/core generate --url "$DATABASE_URL" --out schema.ts --check -``` - -`generate` needs the matching driver installed as a real dependency (`pg`, -`mysql2`, or `mssql` — SQLite uses the `node:sqlite` builtin, Node ≥22.5). It -prints a clear error telling you which one to install if it's missing. - -**Type mapping follows each driver's defaults.** `pg` hands back `bigint`, -`numeric`/`decimal` and `money` as `string`; `mysql2` returns `decimal` as -`string` but `bigint` as a JS `number` (unless you enable -`supportBigNumbers`/`bigNumberStrings`); `mssql` (tedious) returns `bigint` as -`string` but parses `decimal`/`numeric`/`money` into JS `number` (with -precision loss beyond 2^53). SQLite has no column types, only affinities, so -the *declared* type drives the mapping: `INTEGER`/`REAL` and the numeric -names (`NUMERIC`, `DECIMAL(10,2)`, `MONEY`) become `number`, text-affinity -types and `JSON` become `string`, `BLOB` and an untyped column become -`Uint8Array`, and a declared type that says nothing about its contents -(`GEOMETRY`, a custom name) becomes `unknown` rather than a guess. The -generated types mirror exactly that. If your driver is configured -differently, just edit the generated field by hand; it's a plain type after -that point. - -### 2. Create a typed client - -The library never touches your database. You hand `createTypedDb` an -**executor**: a function that takes `(sql, params)`, runs it against your real -driver, and returns the raw rows. - -```ts -import { Pool } from 'pg'; -import { createTypedDb } from '@owlsql/core'; - -const pool = new Pool(); - -const db = createTypedDb(async (sql, params) => { - const res = await pool.query(sql, params as unknown[]); - return res.rows; -}); -``` - -`db` is now bound to your schema. Every query you run through it will be typed -against `DB`. - -### 3. Run queries and handle the Result - -`query` does not throw on failure. It returns a **`Result`** — a discriminated -union of success or error — so failures are values you handle explicitly. - -```ts -import { ResultStatus } from '@owlsql/core'; - -const result = await db.query('select id, name from users'); - -if (result.status === ResultStatus.Error) { - console.error(result.error.kind, result.error.message); - return; -} - -result.value; -// ^? { id: number; name: string }[] -for (const user of result.value) { - console.log(user.id, user.name); -} -``` - -Prefer a helper over the `status` field? `isOk` / `isErr` narrow the same way: - -```ts -import { isOk } from '@owlsql/core'; - -const result = await db.query('select id, email from users'); - -if (isOk(result)) { - result.value; - // ^? { id: number; email: string }[] -} -``` - -> ⚠️ **Pass the SQL as a string literal**, not a `string` variable. If the type -> widens to `string`, the compiler can no longer see the query and inference -> falls back to `unknown`. `db.query('select id from users')` ✅ — -> `const q: string = ...; db.query(q)` ❌. - -### 4. Aliases, `*`, and qualified columns - -```ts -const renamed = await db.query('select id, name as username from users'); -// renamed.value ^? { id: number; username: string }[] - -const implicit = await db.query('select name handle from users'); -// implicit.value ^? { handle: string }[] - -const qualified = await db.query('select u.id, u.name from users u'); -// qualified.value ^? { id: number; name: string }[] - -const everything = await db.query('select * from users'); -// everything.value ^? { id: number; name: string; email: string; active: boolean }[] -``` - -Trailing clauses are ignored for inference — they do not change the row shape: - -```ts -const recent = await db.query( - 'select id, title from posts where published = true order by id limit 10', -); -// recent.value ^? { id: number; title: string }[] -``` - -Keywords are case-insensitive and whitespace/newlines are tolerated, so -formatted multi-line queries work as-is: - -```ts -const r = await db.query(` - SELECT id, - title - FROM posts - WHERE published = true -`); -// r.value ^? { id: number; title: string }[] -``` - -### 5. Type-only usage (no client) - -Sometimes you only want the *type* of a query — for an API contract, a DTO, or a -function signature — without running anything. Use the `Query` type directly: - -```ts -import type { Query } from '@owlsql/core'; - -type UserListRow = Query; -// ^? { id: number; email: string }[] - -function renderUsers(rows: Query) { - // rows is { id: number; name: string }[] -} -``` - -`Row` gives the single-row object (without the surrounding array) if you -need it. - -### 6. Aggregates and functions - -Common SQL functions resolve to their return type, and the output column is -named after the function (or its alias): - -```ts -const stats = await db.query('select count(*) from users'); -// stats.value ^? { count: number }[] - -const named = await db.query('select count(*) as total, max(age) as oldest from users'); -// named.value ^? { total: number; oldest: number }[] - -const shout = await db.query('select id, upper(name) as name from users'); -// shout.value ^? { id: number; name: string }[] -``` - -Recognized: `count`, `sum`, `avg`, `min`, `max`, `length`, `char_length`, -`octet_length`, `abs`, `ceil`, `floor`, `round`, `power`, `mod`, `greatest`, -`least`, `row_number`, `rank`, `dense_rank`, `ntile`, `percent_rank`, -`cume_dist` → `number`; `lower`, `upper`, `trim`, `ltrim`, `rtrim`, `concat` → -`string`; `coalesce`, `nullif`, `lag`, `lead`, `first_value`, `last_value`, -`nth_value` → `unknown`; `now`, `current_timestamp`, `current_date` → `Date`. -Anything else resolves to `unknown`. This return-type table is -dialect-agnostic, which isn't always what the driver actually hands back for -`count`/`sum`/`avg` — see [Limitations](#limitations). - -### 7. INSERT / UPDATE / DELETE with RETURNING - -`RETURNING` is typed exactly like a `SELECT` projection against the target -table: - -```ts -const created = await db.query( - 'insert into users (name, email) values ($1, $2) returning id, name', -); -// created.value ^? { id: number; name: string }[] - -const updated = await db.query('update users set active = $1 where id = $2 returning *'); -// updated.value ^? { id: number; name: string; email: string; active: boolean }[] -``` - -A write without `RETURNING` resolves to `Record[]` (no row -columns). - -### 8. Strict mode — turn typos into type errors - -By default an unknown column or table resolves to `unknown` (permissive). Pass -`{ strict: true }` and the result instead becomes a `QueryTypeError` carrying a -human-readable message, so a typo is impossible to ignore: - -```ts -const db = createTypedDb(executor, { strict: true }); - -const ok = await db.query('select id, name from users'); -// ok.value ^? { id: number; name: string }[] - -const typo = await db.query('select naem from users'); -// typo.value ^? QueryTypeError<'unknown column: naem'>[] -``` - -The error type propagates wherever you use the rows, surfacing the message in -hovers and breaking any code that treats them as real data. - -Strict mode checks the `SELECT` list, the `WHERE` clause, and `JOIN ... ON` -conditions — including the `WHERE` of an `UPDATE`/`DELETE` that returns no -columns, where a typo is most expensive: - -```ts -const wrongSide = await db.query( - 'select u.id from users u join orders o on u.id = o.id', -); -// wrongSide.value ^? QueryTypeError<'unknown column: id'>[] (when orders has no such column) -``` - -`GROUP BY`, `HAVING`, and `ORDER BY` are **not** checked — they have their own -resolution rules (a `SELECT`-list alias, an ordinal, an aggregate), so a name -there is not necessarily a column of a source table. - -### 9. Joins - -`INNER`, `LEFT`, `RIGHT`, `FULL` (with optional `OUTER`), and `CROSS` joins are -supported, with table aliases and any number of joins. Qualified columns -(`alias.column`) resolve to the aliased table; unqualified columns are searched -across every joined table. `alias.*` expands one table; a bare `*` expands all. - -```ts -const rows = await db.query( - 'select u.name, p.title from users u join posts p on u.id = p.user_id', -); -// rows.value ^? { name: string; title: string }[] -``` - -An outer join makes the optional side's columns nullable: `LEFT` nulls the -right-hand table, `RIGHT` nulls the left-hand table, and `FULL` nulls both. - -```ts -const rows = await db.query( - 'select u.name, p.title from users u left join posts p on u.id = p.user_id', -); -// rows.value ^? { name: string; title: string | null }[] -``` - -`select *` across a join merges the columns of every table (applying join -nullability). In strict mode, an unknown alias becomes -`QueryTypeError<'unknown alias: x'>`. - -### 10. Typed parameters - -Placeholders in the query are typed from the column they're compared against, so -`query` checks the **number and types** of the arguments you pass: - -```ts -await db.query('select id from users where id = $1', 1); -// ^ inferred [number] - -await db.query('select id from users where id = $1 and name = $2', 1, 'ada'); -// inferred [number, string] - -// @ts-expect-error wrong type — id is a number -await db.query('select id from users where id = $1', 'oops'); - -// @ts-expect-error wrong count — one param expected -await db.query('select id from users where id = $1'); -``` - -Both numbered (`$1`, `$2`) and positional (`?`) placeholders work, including -across joins (`where p.views > $1` resolves against the aliased table). Use the -`Params` type to get the tuple on its own. - -For this to work, write the comparison **with spaces around the operator** -(`id = $1`, not `id=$1`) — that is what lets the compiler see the column, -operator, and placeholder as separate tokens. - -**Placeholder-style checking (opt-in).** The type layer accepts `$n`, `?` and -`@name` interchangeably, but each driver only understands its own style — `?` -with the pg adapter is a runtime syntax error. Declare the style your executor -expects and mismatches become compile errors: - -```ts -const db = createTypedDb(createPgExecutor(pool)); - -// @ts-expect-error '?' is not a pg placeholder — use $1 -await db.query('select id from users where id = ?', 1); -``` - -Styles: `'dollar'` (pg, postgres.js), `'question'` (mysql2), `'at'` (mssql). -`node:sqlite` accepts all three plus `:name`, so leave the option off there. -A `:name` placeholder is typed like any other but carries no style of its own, -so it is never checked against a declared dialect. - -**Write metadata.** Adapters report driver metadata alongside the rows: on a -successful `Result`, `result.meta?.rowCount` carries the affected-row count -and `result.meta?.lastInsertRowid` the generated id (where the driver -provides one), so an INSERT without `RETURNING` is no longer a black box. - -### 11. Transactions - -There is a footgun to know about: **never run `BEGIN`/`COMMIT` through an -executor bound to a pool.** Each `query()` may check out a *different* -connection, so `BEGIN` runs on connection A and `COMMIT` on connection B, -leaving an open transaction (and its locks) on a pooled connection that is -later handed to another caller. - -`pg`, `postgres.js`, `mysql2`, and `mssql` each ship a small transaction -helper that pins one connection for the whole callback and handles -begin/commit/rollback for you: - -```ts -import { Pool } from 'pg'; -import { createPgTransaction } from '@owlsql/core/pg'; - -const pool = new Pool(); - -async function transferFunds(from: number, to: number, amount: number) { - await createPgTransaction(pool)(async (tx) => { - await tx.query('update accounts set balance = balance - $1 where id = $2', amount, from); - await tx.query('update accounts set balance = balance + $1 where id = $2', amount, to); - }); -} -``` - -`createPgTransaction(pool)` returns the function that actually runs the -transaction — it's curried on `DB` because TypeScript can't partially infer -type arguments; a single `createPgTransaction(pool, fn)` call would -compile, but would silently stop inferring the callback's return type and -type it `unknown` instead. Splitting `DB` into its own call keeps the second -call (`(fn, options?)`) argument-only, so both the optional `options` and the -callback's return type infer normally. - -The callback's `tx` is a full `TypedDb`, typed exactly like the one -`createTypedDb` returns (pass `{ strict: true }` as the second argument to -the inner call the same way: `createPgTransaction(pool)(fn, { strict: -true })`). The transaction commits if the callback resolves and rolls back if -it throws — a rejected `tx.query()` result (the normal `Result` error path) -does *not* trigger a rollback by itself, only a thrown error does, same as -everywhere else this library never throws on a query failure. - -`createMysql2Transaction(pool)(fn, options?)` and -`createMssqlTransaction(pool)(fn, options?)` work the same way. -`createPostgresJsTransaction(sql)(fn, options?)` wraps postgres.js's own -`sql.begin(...)`, which already pins the connection and handles -commit/rollback itself. - -Kysely users should use Kysely's own `db.transaction()`. `node:sqlite` is a -single connection, so plain `begin`/`commit` statements are safe there and no -helper is provided. - -If the rollback *itself* fails, the helper throws an `AggregateError` whose -`errors[0]` is the original failure and `errors[1]` is the rollback failure — -the error that caused the transaction to be abandoned is never replaced by a -cleanup error. - -Under the hood, each helper does exactly what you'd otherwise write by hand: - -```ts -const client = await pool.connect(); -const tx = createTypedDb(createPgExecutor(client), { placeholders: 'dollar' }); - -try { - await client.query('begin'); - await tx.query('update accounts set balance = balance - $1 where id = $2', amount, from); - await tx.query('update accounts set balance = balance + $1 where id = $2', amount, to); - await client.query('commit'); -} catch (error) { - await client.query('rollback'); - throw error; -} finally { - client.release(); -} -``` - -## Driver recipes - -The executor is the only thing that touches your database, so any driver -works. For the most common drivers, OwlSQL ships a ready-made -adapter — import it from its own subpath and pass your existing client -straight in. No dependency is pulled in unless you import that specific -subpath (each driver is an optional peer dependency). - -**node-postgres (`pg`)** - -```ts -import { Pool } from 'pg'; -import { createPgExecutor } from '@owlsql/core/pg'; - -const db = createTypedDb(createPgExecutor(new Pool())); -``` - -**mysql2** - -```ts -import { createPool } from 'mysql2/promise'; -import { createMysql2Executor } from '@owlsql/core/mysql2'; - -const db = createTypedDb(createMysql2Executor(createPool({ /* ... */ }))); -``` - -**postgres.js** - -```ts -import postgres from 'postgres'; -import { createPostgresJsExecutor } from '@owlsql/core/postgres'; - -const db = createTypedDb(createPostgresJsExecutor(postgres())); -``` - -**node:sqlite** (Node's built-in SQLite module, no dependency to install — Node ≥22.5) - -```ts -import { DatabaseSync } from 'node:sqlite'; -import { createNodeSqliteExecutor } from '@owlsql/core/node-sqlite'; - -const db = createTypedDb(createNodeSqliteExecutor(new DatabaseSync('app.db'))); -``` - -**better-sqlite3** (synchronous driver wrapped in a promise — no dedicated -adapter, the same one-liner works with `node:sqlite`'s adapter since both -expose `prepare(sql).all(...params)`) - -```ts -import Database from 'better-sqlite3'; -const sqlite = new Database('app.db'); -const db = createTypedDb(async (sql, params) => sqlite.prepare(sql).all(...params)); -``` - -**Kysely** - -```ts -import { Kysely, PostgresDialect } from 'kysely'; -import { createKyselyExecutor } from '@owlsql/core/kysely'; - -const kysely = new Kysely({ dialect: new PostgresDialect({ /* ... */ }) }); -const db = createTypedDb(createKyselyExecutor(kysely)); -``` - -The adapter runs your query through `CompiledQuery.raw`, which forwards the -SQL text and parameters straight to the underlying driver with **no -placeholder translation** — the SQL you write still has to use whichever -placeholder syntax your configured dialect's own driver expects (`$1` for -`PostgresDialect`, `?` for `MysqlDialect`/`SqliteDialect`). What the adapter -does *not* care about is which Kysely dialect object you passed in; it just -relays whatever string you give it. - -If you want the same compile-time protection against using the wrong -placeholder style that the other adapters get, pass `placeholders` to -`createTypedDb` the same way you would for any of them — it's driven by -that option, not by which adapter produced the executor: - -```ts -const db = createTypedDb(createKyselyExecutor(kysely)); -``` - -**Drizzle (raw SQL)** - -Drizzle's own `sql.raw()` doesn't take a separate parameters array, so it -can't be wired directly into an `Executor`. Instead, reach through Drizzle to -the underlying driver client with [`db.$client`](https://orm.drizzle.team/docs/connect-overview) -and reuse the matching adapter above — one extra line over the plain driver: - -```ts -import { drizzle } from 'drizzle-orm/node-postgres'; -import { createPgExecutor } from '@owlsql/core/pg'; - -const drizzleDb = drizzle(process.env.DATABASE_URL!); -const db = createTypedDb(createPgExecutor(drizzleDb.$client)); -``` - -Swap `createPgExecutor` for `createMysql2Executor`/`createPostgresJsExecutor`/ -`createNodeSqliteExecutor` depending on which Drizzle driver you're using — -`$client` is always the native driver instance underneath. - -**mssql (SQL Server)** - -```ts -import sql from 'mssql'; -import { createMssqlExecutor } from '@owlsql/core/mssql'; - -const pool = await sql.connect({ /* ... */ }); -const db = createTypedDb(createMssqlExecutor(pool)); -``` - -The adapter scans the query for `@name` placeholders (skipping string -literals and `@@` system variables) and binds each one by name via -`request.input(...)`, in order of first appearance — matching how -`Params` types the positional tuple. A repeated `@name` binds once. - -## Database support - -The parser accepts the SQL used by each of the four major engines, without any -per-dialect configuration — it stays permissive and recognizes each dialect's -syntax by shape, not by a declared "mode". - -| Feature | PostgreSQL | MySQL | SQLite | SQL Server | -| ------- | ---------- | ----- | ------ | ---------- | -| Placeholders | `$1`, `$2`, ... | `?` | `?` | `@name`, `@p1` | -| Quoted identifiers | `"col"` | `` `col` `` | `"col"` | `[col]`, `"col"` | -| Row-returning writes | `RETURNING col` | *(not supported by the engine — `INSERT`/`UPDATE`/`DELETE` type as `Record[]`)* | `RETURNING col` | `OUTPUT inserted.col` / `OUTPUT deleted.col` | -| Pagination | `LIMIT n OFFSET m` | `LIMIT n OFFSET m` | `LIMIT n OFFSET m` | `TOP n`, `TOP (n) PERCENT`, or `OFFSET ... FETCH NEXT n ROWS ONLY` | -| `ILIKE` | ✓ | — | — | — | -| Joins, CTEs, `CASE`, window functions, subqueries in `FROM` | ✓ | ✓ | ✓ | ✓ (dialect-agnostic — see [Supported SQL subset](#supported-sql-subset)) | - -See [`tests/dialect-postgres.test-d.ts`](tests/dialect-postgres.test-d.ts), -[`tests/dialect-mysql.test-d.ts`](tests/dialect-mysql.test-d.ts), -[`tests/dialect-sqlite.test-d.ts`](tests/dialect-sqlite.test-d.ts), and -[`tests/dialect-mssql.test-d.ts`](tests/dialect-mssql.test-d.ts) for the exact -query shapes each engine is tested against. - -## Editor autocomplete - -```ts -db.query(` - select id, na -`) -// ^ autocomplete suggests `name` - -db.query(`select id, name from users`) -// ^ hovering shows (column) name: string - -db.query(`select id from users where na`) -// ^ autocomplete suggests `name` -``` - -Want to see it running for yourself before there's a recorded demo here? -[`examples/ts-plugin-demo`](examples/ts-plugin-demo) is a ready-to-open -VSCode project set up for exactly that. - -`@owlsql/ts-plugin` is a **TypeScript Language Service Plugin** — -it runs inside `tsserver`, the same process that already powers VSCode's -IntelliSense, and adds column-name completions while you're still typing the -query string. This is a genuinely different mechanism from the rest of the -library: everything else works by *type-checking* a finished query string; -this works by hooking into the editor's completion request for a string -that isn't even valid SQL yet. - -**Setup** — it ships as its own package, so install it first. - -> **Not on npm yet.** `@owlsql/ts-plugin` has not been published; the -> command below will be the install once it is. Until then, build it from a -> clone of this repository and install that folder: -> -> ```bash -> git clone https://github.com/tiagolauer/OwlSQL -> cd OwlSQL && npm install && npm run build --workspace @owlsql/ts-plugin -> cd /path/to/your/project -> npm install --save-dev /path/to/OwlSQL/ts-plugin -> ``` -> -> Everything below this box is the same either way. - -```bash -npm install --save-dev @owlsql/ts-plugin -``` - -Then add it to your `tsconfig.json`: - -```json -{ - "compilerOptions": { - "plugins": [{ "name": "@owlsql/ts-plugin" }] - } -} -``` - -Then, in VSCode, open the Command Palette and run **"TypeScript: Select -TypeScript Version" → "Use Workspace Version"**. This step is not optional — -VSCode's *bundled* TypeScript does not load workspace plugins, so skipping it -is the #1 reason this kind of plugin appears to do nothing. Other editors -that talk to `tsserver` (Cursor, some Neovim/Sublime LSP setups) generally -pick up `tsconfig.json` plugins automatically. - -**What it does:** suggests column names right after `SELECT`/a comma in the -column list or after `WHERE`/`AND`/`OR`, suggests table names right after -`FROM`/`JOIN` (or a comma in an old-style comma-joined `FROM` list), and -shows a column's resolved type on hover, for `db.query(...)` calls made -through a client built with `createTypedDb`. It is `JOIN`/alias-aware: -every table introduced by a `FROM` or `JOIN` in the same string is in scope, -and typing an alias qualifier (`u.` in `... from users u`) narrows -completions and hover to that one source. With no qualifier, completions/hover -union columns across all sources present so far — the deduplicated union of -every table in `DB` before any `FROM` is typed at all, exactly what covers -the example above. `WHERE`-position completions require a `FROM` to already -be present (there's no table to scope to otherwise); table-name completions -after `FROM`/`JOIN` suggest every table in `DB`, filtered by whatever prefix -you've typed. It also reports unknown columns, unknown tables, unknown -aliases, and ambiguous unqualified columns (present in more than one joined -table) as live editor diagnostics in the `SELECT` list, `FROM`/`JOIN` clause, -and simple `WHERE` comparisons (`where naem = 'x'` squiggles `naem` the -moment you type it) — the same checks strict mode (`{ strict: true }`) -applies at compile time, surfaced as a squiggle while you type instead of -only once the query is finished. - -**What it does not do** (documented scope, not bugs): - -- **`WHERE`-clause diagnostics cover simple comparisons only.** A column - token immediately before `=`/`<>`/`<`/`>`/`<=`/`>=`/`LIKE`/`ILIKE`/`IN`/ - `BETWEEN`/`IS`, or `AND`/`OR`, or the end of the clause, is checked exactly - like a `SELECT`-list column. The moment a `WHERE` clause contains any `(` - or `)` at all — a subquery, a function call, a parenthesized group — the - whole clause is skipped rather than risked: no diagnostics for it, never a - wrong one. `HAVING`/`ORDER BY`/`GROUP BY` aren't checked at all. -- The first `FROM ` is found with a regex, not a real SQL parser: a - `FROM (subquery)` can make it lock onto a table name from inside the - subquery instead of recognizing there's no real outer table yet. -- Only plain string/template literals with **no interpolation** - (`` db.query(`select ...`) ``) are recognized — which is the only form the - library ever expects you to write, since parameters are SQL placeholders - (`$1`/`?`/`@name`), never JS template interpolation. Interpolating - (`` db.query(`select ... ${x}`) ``) silently turns completions/hover off - for that call — there's no squiggle or warning telling you why. -- Completions after `ORDER BY`/`GROUP BY`/`HAVING`/etc. aren't offered yet — - only the `SELECT` column list, `WHERE` clause, and `FROM`/`JOIN` table - names. -- **Requires TypeScript < 7**, which its own `peerDependencies` range - enforces. TypeScript 7's native (Go-based) compiler ships no public - compiler API at all — the classic `ts.Node`/`ts.forEachChild`/ - `ts.createProgram` surface this plugin is built on is gone, and the - `tsserver` protocol that loads plugins has been replaced by LSP. That - affects every TypeScript language service plugin, not just this one; - TypeScript 7.1 is expected to introduce a new (and different) programmatic - API. The library itself is unaffected and is tested against TypeScript 7 in - CI — this is exactly why the plugin lives in a separate package with a - separate version, so `@owlsql/core` isn't held to the plugin's narrower - range. - -## API reference - -| Export | Kind | Description | -| ------ | ---- | ----------- | -| `createTypedDb(executor)` / `createTypedDb(executor, options?)` | function | Build a schema-bound client. When passing `options`, `DB` and `Options` must **both** be given explicitly — `createTypedDb(executor, options)` with a single type argument is a compile error, not a silent no-op. `options.strict` enables [strict mode](#8-strict-mode--turn-typos-into-type-errors); `options.placeholders` enables [placeholder-style checking](#10-typed-parameters). | -| `TypedDb` | interface | The client; has `query(sql, ...params)`. | -| `TypedDbOptions` | interface | `{ strict?: boolean; placeholders?: PlaceholderStyle }`. | -| `Executor` | type | `(sql: string, params: readonly unknown[]) => Promise`. | -| `ExecutorResult` | type | `unknown[]` or `{ rows: unknown[]; meta?: QueryMeta }`. | -| `QueryMeta` | interface | `{ rowCount?; lastInsertRowid? }`, surfaced on the Ok result. | -| `PlaceholderStyle` | type | `'dollar' \| 'question' \| 'at'`. | -| `DialectExecutor