Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,22 @@ type IsMergeActionPseudoColumn<Expression extends string> = Lowercase<Expression
? 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<DB, Sources, Operand, Strict> extends QueryTypeError<infer Message>
? QueryTypeError<Message>
: unknown;

export type ResolveColumnType<
DB extends SchemaLike,
Sources extends Source[],
Expand All @@ -762,7 +778,9 @@ export type ResolveColumnType<
: [LiteralType<Expression>] extends [never]
? IsOperatorExpression<Expression> extends true
? unknown
: Qualifier<Expression> extends ''
: Expression extends `${infer CastOperand}::${string}`
? CastExpressionType<DB, Sources, CastOperand, Strict>
: Qualifier<Expression> extends ''
? BareColumnType<DB, Sources, Unquote<StripQualifier<Expression>>, Strict>
: QualifiedColumnType<
DB,
Expand Down
68 changes: 68 additions & 0 deletions tests/cast-columns.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Query, QueryTypeError, StrictRow } from '../src/index.js';

type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;

type Expect<T extends true> = 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<StrictRow<DB, 'select id::text as id_text from users'>, { id_text: unknown }>
>;

type BareCastInStrictMode = Expect<
Equal<StrictRow<DB, 'select id::text from users'>, { 'id::text': unknown }>
>;

type QualifiedCastInStrictMode = Expect<
Equal<StrictRow<DB, 'select u.id::text as id_text from users u'>, { id_text: unknown }>
>;

type CastBesideOrdinaryColumns = Expect<
Equal<
StrictRow<DB, 'select name, id::numeric as amount from users'>,
{ name: string; amount: unknown }
>
>;

// The operand is still resolved, so a typo inside the cast is still caught.
type CastOperandIsStillChecked = Expect<
Equal<StrictRow<DB, 'select naem::text as x from users'>, QueryTypeError<'unknown column: naem'>>
>;

type CastOperandAliasIsStillChecked = Expect<
Equal<StrictRow<DB, 'select p.id::text as x from users u'>, QueryTypeError<'unknown alias: p'>>
>;

// Loose mode was already harmless and stays as it was.
type LooseCastIsUnchanged = Expect<
Equal<Query<DB, 'select id::text from users'>, { 'id::text': unknown }[]>
>;

// Controls: a plain column and a cast written on a placeholder elsewhere.
type PlainColumnIsUnaffected = Expect<
Equal<StrictRow<DB, 'select id from users'>, { id: number }>
>;

type CastOnAFunctionCall = Expect<
Equal<StrictRow<DB, 'select count(*)::int as total from users'>, { total: unknown }>
>;

export type CastColumnsLock = [
AliasedCastInStrictMode,
BareCastInStrictMode,
QualifiedCastInStrictMode,
CastBesideOrdinaryColumns,
CastOperandIsStillChecked,
CastOperandAliasIsStillChecked,
LooseCastIsUnchanged,
PlainColumnIsUnaffected,
CastOnAFunctionCall,
];