diff --git a/CHANGELOG.md b/CHANGELOG.md index 684fed8..4918fa6 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 Postgres `::` cast in the select list is no longer a strict-mode error. `select id::text as id_text from users` reported `unknown column: id::text`, because `:` is not an operator character and nothing stripped the suffix, so the whole token was looked up as a column name - and one cast poisoned the entire row. The operand is still resolved, so a typo in it is still caught; the column itself types `unknown`, since the cast decides the type ([#277](https://github.com/tiagolauer/OwlSQL/issues/277)). + ## [0.2.0] - 2026-07-29 ### Changed diff --git a/src/parse.ts b/src/parse.ts index 8f3ee9f..f40d8e1 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -743,6 +743,22 @@ type IsMergeActionPseudoColumn = Lowercase = ResolveColumnType extends QueryTypeError + ? QueryTypeError + : unknown; + export type ResolveColumnType< DB extends SchemaLike, Sources extends Source[], @@ -762,7 +778,9 @@ export type ResolveColumnType< : [LiteralType] extends [never] ? IsOperatorExpression extends true ? unknown - : Qualifier extends '' + : Expression extends `${infer CastOperand}::${string}` + ? CastExpressionType + : Qualifier extends '' ? BareColumnType>, Strict> : QualifiedColumnType< DB, diff --git a/tests/cast-columns.test-d.ts b/tests/cast-columns.test-d.ts new file mode 100644 index 0000000..f35112e --- /dev/null +++ b/tests/cast-columns.test-d.ts @@ -0,0 +1,68 @@ +import type { Query, QueryTypeError, StrictRow } 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 }; + posts: { id: number; user_id: number; title: string }; +} + +// One cast used to poison the whole strict row: `:` is not an operator +// character and nothing stripped the suffix, so `id::text` was looked up as a +// column name (issue #277). +type AliasedCastInStrictMode = Expect< + Equal, { id_text: unknown }> +>; + +type BareCastInStrictMode = Expect< + Equal, { 'id::text': unknown }> +>; + +type QualifiedCastInStrictMode = Expect< + Equal, { id_text: unknown }> +>; + +type CastBesideOrdinaryColumns = Expect< + Equal< + StrictRow, + { name: string; amount: unknown } + > +>; + +// The operand is still resolved, so a typo inside the cast is still caught. +type CastOperandIsStillChecked = Expect< + Equal, QueryTypeError<'unknown column: naem'>> +>; + +type CastOperandAliasIsStillChecked = Expect< + Equal, QueryTypeError<'unknown alias: p'>> +>; + +// Loose mode was already harmless and stays as it was. +type LooseCastIsUnchanged = Expect< + Equal, { 'id::text': unknown }[]> +>; + +// Controls: a plain column and a cast written on a placeholder elsewhere. +type PlainColumnIsUnaffected = Expect< + Equal, { id: number }> +>; + +type CastOnAFunctionCall = Expect< + Equal, { total: unknown }> +>; + +export type CastColumnsLock = [ + AliasedCastInStrictMode, + BareCastInStrictMode, + QualifiedCastInStrictMode, + CastBesideOrdinaryColumns, + CastOperandIsStillChecked, + CastOperandAliasIsStillChecked, + LooseCastIsUnchanged, + PlainColumnIsUnaffected, + CastOnAFunctionCall, +];