diff --git a/CHANGELOG.md b/CHANGELOG.md index 684fed8..d5dcdb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes to this project, following [Keep a Changelog](https://keepachang ## [Unreleased] +### Fixed + +- A placeholder inside a multi-argument call in INSERT VALUES gets its tuple slot. `values (coalesce(?, 0), ?)` typed one slot for two placeholders, and with `@name` the consequence was silent: the caller could pass only one value, the adapter bound it to the first name and bound the second to null, and the INSERT succeeded having written into the wrong column ([#269](https://github.com/tiagolauer/OwlSQL/issues/269)). + ## [0.2.0] - 2026-07-29 ### Changed diff --git a/src/params.ts b/src/params.ts index 1fac6d6..d672e9e 100644 --- a/src/params.ts +++ b/src/params.ts @@ -349,6 +349,40 @@ type ColumnTypeAt = 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, @@ -381,7 +415,27 @@ type MatchInsertValues< NextSequentialNames > : never - : MatchInsertValues + : 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 }; diff --git a/tests/insert-values-call-params.test-d.ts b/tests/insert-values-call-params.test-d.ts new file mode 100644 index 0000000..045914f --- /dev/null +++ b/tests/insert-values-call-params.test-d.ts @@ -0,0 +1,71 @@ +import type { Params } from '../src/index.js'; + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + +type Expect = T; + +interface DB { + users: { id: number; name: string }; +} + +// The README promises "a placeholder inside a call is typed", and it is in a +// WHERE clause. In INSERT VALUES the entry was one token and the call carried +// an inner comma, so the placeholder registered no slot at all: the caller +// could pass only one value, the runtime scanner bound it to the first name +// and the second to null, and the INSERT wrote into the wrong column (#269). +type QuestionInsideMultiArgumentCall = Expect< + Equal< + Params, + [number, string] + > +>; + +type NamedInsideMultiArgumentCall = Expect< + Equal< + Params, + [number, string] + > +>; + +type NumberedInsideMultiArgumentCall = Expect< + Equal< + Params, + [number, string] + > +>; + +type NestedCallAroundThePlaceholder = Expect< + Equal< + Params, + [number, string] + > +>; + +// Controls: the shapes that already worked. +type BarePlaceholders = Expect< + Equal, [number, string]> +>; + +type SingleArgumentCall = Expect< + Equal, [number, string]> +>; + +type LiteralsTakeNoSlot = Expect< + Equal, [string]> +>; + +type PlaceholderInACallInWhere = Expect< + Equal, [number]> +>; + +export type InsertValuesCallParamsLock = [ + QuestionInsideMultiArgumentCall, + NamedInsideMultiArgumentCall, + NumberedInsideMultiArgumentCall, + NestedCallAroundThePlaceholder, + BarePlaceholders, + SingleArgumentCall, + LiteralsTakeNoSlot, + PlaceholderInACallInWhere, +];