UNPKG

@kuindji/sql-type-parser

Version:
330 lines 26.1 kB
/** * Type-level schema matcher * Takes a parsed SQL AST and a database schema, returns the result row type */ import type { SQLQuery, SelectClause, SubquerySelectClause, ColumnRef, TableColumnRef, UnboundColumnRef, TableWildcard, ComplexExpr, SubqueryExpr, TableRef, TableSource, DerivedTableRef, CTEDefinition, JoinClause, AggregateExpr, SelectItem, UnionClause, UnionClauseAny, UnionOperatorType } from "./ast.js"; import type { Flatten } from "./utils.js"; /** * Error type for unresolvable columns/tables */ export type MatchError<Message extends string> = { readonly __error: true; readonly message: Message; }; /** * Expected structure of a database schema with schema support * Uses a generic object constraint to allow interfaces without index signatures * * Structure: * { * schemas: { * public: { users: {...}, posts: {...} }, * other: { ... } * }, * defaultSchema?: "public" // optional, defaults to first schema key * } */ export type DatabaseSchema = { schemas: { [schemaName: string]: { [tableName: string]: object; }; }; defaultSchema?: string; }; /** * Get the default schema name from a DatabaseSchema * Uses defaultSchema if specified, otherwise uses the first schema key */ type GetDefaultSchema<Schema extends DatabaseSchema> = Schema["defaultSchema"] extends string ? Schema["defaultSchema"] : keyof Schema["schemas"] extends infer Keys ? Keys extends string ? Keys : never : never; /** * Match a parsed SQL query against a schema to get the result type */ export type MatchQuery<Query, Schema extends DatabaseSchema> = Query extends SQLQuery<infer QueryContent> ? QueryContent extends UnionClauseAny ? MatchUnionClause<QueryContent, Schema> : QueryContent extends SelectClause ? MatchSelectClause<QueryContent, Schema> : MatchError<"Invalid query content type"> : MatchError<"Invalid query type">; /** * Match a union clause and return the combined result type * For UNION: result is the union of both sides (same shape, TypeScript union of values) * For INTERSECT: result is the intersection (same shape) * For EXCEPT: result is the left side's shape */ type MatchUnionClause<Union extends UnionClauseAny, Schema extends DatabaseSchema> = Union extends UnionClause<infer Left, infer Op, infer Right> ? MatchSelectClause<Left, Schema> extends infer LeftResult ? LeftResult extends MatchError<string> ? LeftResult : Right extends UnionClauseAny ? MatchUnionClause<Right, Schema> extends infer RightResult ? RightResult extends MatchError<string> ? RightResult : CombineUnionResults<LeftResult, RightResult, Op> : never : Right extends SelectClause ? MatchSelectClause<Right, Schema> extends infer RightResult ? RightResult extends MatchError<string> ? RightResult : CombineUnionResults<LeftResult, RightResult, Op> : never : MatchError<"Invalid right side of union"> : never : MatchError<"Invalid union clause">; /** * Combine results from two sides of a union operation * The result columns must have matching names - we return the left side's structure * with types that could come from either side */ type CombineUnionResults<Left, Right, Op extends UnionOperatorType> = Op extends "UNION" | "UNION ALL" ? UnionResultType<Left, Right> : Op extends "INTERSECT" | "INTERSECT ALL" ? IntersectResultType<Left, Right> : Op extends "EXCEPT" | "EXCEPT ALL" ? Left : Left; /** * For UNION: create a type that could be from either side * If both sides have the same column name, the result is the union of their types */ type UnionResultType<Left, Right> = { [K in keyof Left]: K extends keyof Right ? Left[K] | Right[K] : Left[K]; }; /** * For INTERSECT: create a type that exists in both sides * If both sides have the same column name, the result is their common type */ type IntersectResultType<Left, Right> = { [K in keyof Left]: K extends keyof Right ? Left[K] & Right[K] extends never ? Left[K] | Right[K] : Left[K] & Right[K] : Left[K]; }; /** * Match a SELECT clause against the schema */ type MatchSelectClause<Select, Schema extends DatabaseSchema> = Select extends SelectClause<infer Columns, infer From, infer Joins, infer _Where, infer _GroupBy, infer _Having, infer _OrderBy, infer _Limit, infer _Offset, infer _Distinct, infer CTEs> ? BuildTableContextWithCTEs<From, Joins, CTEs, Schema> extends infer Context ? Context extends MatchError<string> ? Context : MatchColumns<Columns, Context, Schema> : never : MatchError<"Invalid SELECT clause">; /** * Build a context with CTE support * First resolves CTEs to virtual tables, then builds the main context */ type BuildTableContextWithCTEs<From extends TableSource, Joins, CTEs, Schema extends DatabaseSchema> = BuildCTEContext<CTEs, Schema> extends infer CTEContext ? CTEContext extends MatchError<string> ? CTEContext : BuildTableContext<From, Joins, Schema, CTEContext> : never; /** * Build context from CTE definitions * Returns a context mapping CTE names to their column types */ type BuildCTEContext<CTEs, Schema extends DatabaseSchema, Acc = {}> = CTEs extends [infer First, ...infer Rest] ? First extends CTEDefinition<infer Name, infer Query> ? ResolveCTEQuery<Query, Schema, Acc> extends infer CTEColumns ? CTEColumns extends MatchError<string> ? CTEColumns : Rest extends CTEDefinition[] ? BuildCTEContext<Rest, Schema, Acc & { [K in Name]: CTEColumns; }> : Acc & { [K in Name]: CTEColumns; } : never : Acc : Acc; /** * Resolve a CTE query to its column types */ type ResolveCTEQuery<Query extends SubquerySelectClause, Schema extends DatabaseSchema, CTEContext> = Query extends { columns: infer Columns; from: infer From extends TableSource; joins: infer Joins; } ? BuildTableContext<From, Joins, Schema, CTEContext> extends infer InnerContext ? InnerContext extends MatchError<string> ? InnerContext : ExtractColumnsAsObject<Columns, InnerContext, Schema> : never : never; /** * Extract columns from a SELECT as an object type (for CTE/derived table) */ type ExtractColumnsAsObject<Columns, Context, Schema extends DatabaseSchema> = Columns extends "*" ? ExpandAllColumns<Context> : Columns extends SelectItem[] ? ExtractColumnListAsObject<Columns, Context, Schema> : {}; /** * Extract a list of columns as an object type */ type ExtractColumnListAsObject<Columns extends SelectItem[], Context, Schema extends DatabaseSchema> = Columns extends [infer First, ...infer Rest] ? ExtractSingleColumnAsObject<First, Context, Schema> extends infer FirstResult ? Rest extends SelectItem[] ? ExtractColumnListAsObject<Rest, Context, Schema> extends infer RestResult ? Flatten<FirstResult & RestResult> : FirstResult : FirstResult : {} : {}; /** * Extract a single column as an object entry { alias: type } */ type ExtractSingleColumnAsObject<Col, Context, Schema extends DatabaseSchema> = Col extends ColumnRef<infer Ref, infer Alias> ? { [K in Alias]: ResolveColumnRef<Ref, Context, Schema>; } : Col extends AggregateExpr<infer Func, infer Arg, infer Alias> ? { [K in Alias]: GetAggregateResultType<Func, Arg, Context, Schema>; } : Col extends TableWildcard<infer TableOrAlias, infer WildcardSchema> ? ResolveTableWildcard<TableOrAlias, WildcardSchema, Context, Schema> : {}; /** * Build a context mapping table aliases to their column types * This allows us to resolve both "table.column" and "alias.column" references * Note: We flatten only once at the end to reduce recursion depth */ type BuildTableContext<From extends TableSource, Joins, Schema extends DatabaseSchema, CTEContext = {}> = ResolveTableSource<From, Schema, CTEContext> extends infer FromContext ? FromContext extends MatchError<string> ? FromContext : Joins extends JoinClause[] ? FlattenContext<MergeJoinContexts<FromContext, Joins, Schema, CTEContext>> : FromContext : never; /** * Flatten the merged context - done once at the end */ type FlattenContext<T> = T extends MatchError<string> ? T : Flatten<T>; /** * Resolve a table source (can be TableRef or DerivedTableRef) */ type ResolveTableSource<Source extends TableSource, Schema extends DatabaseSchema, CTEContext = {}> = Source extends DerivedTableRef<infer Query, infer Alias> ? ResolveDerivedTable<Query, Alias, Schema, CTEContext> : Source extends TableRef<infer Table, infer Alias, infer TableSchema> ? ResolveTableRefOrCTE<Table, Alias, TableSchema, Schema, CTEContext> : MatchError<"Invalid table source">; /** * Resolve a table reference, checking CTEs first, then schema * TableSchema is the schema specified in the query (undefined if not specified) */ type ResolveTableRefOrCTE<Table extends string, Alias extends string, TableSchema extends string | undefined, Schema extends DatabaseSchema, CTEContext> = Table extends keyof CTEContext ? { [K in Alias]: CTEContext[Table]; } : ResolveTableInSchema<Table, Alias, TableSchema, Schema>; /** * Resolve a table within the database schema structure * If TableSchema is undefined, use the default schema * Note: We check for undefined first due to TypeScript 5.9+ behavior where * `undefined extends string` can be true in some contexts */ type ResolveTableInSchema<Table extends string, Alias extends string, TableSchema extends string | undefined, Schema extends DatabaseSchema> = TableSchema extends undefined ? GetDefaultSchema<Schema> extends infer DefaultSchema extends string ? DefaultSchema extends keyof Schema["schemas"] ? Table extends keyof Schema["schemas"][DefaultSchema] ? { [K in Alias]: Schema["schemas"][DefaultSchema][Table]; } : MatchError<`Table '${Table}' not found in default schema '${DefaultSchema}'`> : MatchError<`Default schema not found`> : MatchError<`Cannot determine default schema`> : TableSchema extends string ? TableSchema extends keyof Schema["schemas"] ? Table extends keyof Schema["schemas"][TableSchema] ? { [K in Alias]: Schema["schemas"][TableSchema][Table]; } : MatchError<`Table '${Table}' not found in schema '${TableSchema}'`> : MatchError<`Schema '${TableSchema}' not found`> : MatchError<`Invalid schema type`>; /** * Resolve a derived table (subquery in FROM) */ type ResolveDerivedTable<Query extends SubquerySelectClause, Alias extends string, Schema extends DatabaseSchema, CTEContext> = Query extends { columns: infer Columns; from: infer From extends TableSource; joins: infer Joins; } ? BuildTableContext<From, Joins, Schema, CTEContext> extends infer InnerContext ? InnerContext extends MatchError<string> ? InnerContext : ExtractColumnsAsObject<Columns, InnerContext, Schema> extends infer DerivedColumns ? { [K in Alias]: DerivedColumns; } : never : never : MatchError<"Invalid derived table query">; /** * Merge JOIN tables into the context * Note: We don't flatten during recursion to reduce type depth. * The intersection is only flattened once at the end. */ type MergeJoinContexts<Context, Joins extends JoinClause[], Schema extends DatabaseSchema, CTEContext = {}> = Joins extends [infer First, ...infer Rest] ? First extends JoinClause<infer _Type, infer JoinTable, infer _On> ? ResolveTableSource<JoinTable, Schema, CTEContext> extends infer JoinContext ? JoinContext extends MatchError<string> ? JoinContext : Rest extends JoinClause[] ? MergeJoinContexts<Context & JoinContext, Rest, Schema, CTEContext> : Context & JoinContext : never : Context : Context; /** * Match columns against the table context */ type MatchColumns<Columns, Context, Schema extends DatabaseSchema> = Columns extends "*" ? ExpandAllColumns<Context> : Columns extends SelectItem[] ? MatchColumnList<Columns, Context, Schema> : MatchError<"Invalid columns type">; /** * Expand * to all columns from all tables in context */ type ExpandAllColumns<Context> = UnionToIntersection<{ [Alias in keyof Context]: Context[Alias]; }[keyof Context]>; /** * Match a list of columns */ type MatchColumnList<Columns extends SelectItem[], Context, Schema extends DatabaseSchema> = Columns extends [infer First, ...infer Rest] ? MatchSingleColumn<First, Context, Schema> extends infer FirstResult ? FirstResult extends MatchError<string> ? FirstResult : Rest extends SelectItem[] ? MatchColumnList<Rest, Context, Schema> extends infer RestResult ? RestResult extends MatchError<string> ? RestResult : Flatten<FirstResult & RestResult> : never : FirstResult : never : {}; /** * Match a single column (ColumnRef, AggregateExpr, or TableWildcard) * Note: We use [ColType] extends [...] to prevent distribution over union types */ type MatchSingleColumn<Col, Context, Schema extends DatabaseSchema> = Col extends ColumnRef<infer Ref, infer Alias> ? ResolveColumnRef<Ref, Context, Schema> extends infer ColType ? [ColType] extends [MatchError<string>] ? { [K in Alias]: ColType; } : { [K in Alias]: ColType; } : never : Col extends TableWildcard<infer TableOrAlias, infer WildcardSchema> ? ResolveTableWildcard<TableOrAlias, WildcardSchema, Context, Schema> : Col extends AggregateExpr<infer Func, infer Arg, infer Alias> ? { [K in Alias]: GetAggregateResultType<Func, Arg, Context, Schema>; } : MatchError<"Unknown column type">; /** * Resolve a table.* or alias.* or schema.table.* wildcard to all columns from that table * Note: We check for undefined first due to TypeScript 5.9+ behavior */ type ResolveTableWildcard<TableOrAlias extends string, WildcardSchema extends string | undefined, Context, Schema extends DatabaseSchema = DatabaseSchema> = WildcardSchema extends undefined ? TableOrAlias extends keyof Context ? Context[TableOrAlias] : MatchError<`Table or alias '${TableOrAlias}' not found`> : WildcardSchema extends string ? ResolveSchemaTableWildcard<WildcardSchema, TableOrAlias, Schema> : MatchError<`Invalid schema type`>; /** * Resolve schema.table.* wildcard directly from schema */ type ResolveSchemaTableWildcard<SchemaName extends string, TableName extends string, Schema extends DatabaseSchema> = SchemaName extends keyof Schema["schemas"] ? TableName extends keyof Schema["schemas"][SchemaName] ? Schema["schemas"][SchemaName][TableName] : MatchError<`Table '${TableName}' not found in schema '${SchemaName}'`> : MatchError<`Schema '${SchemaName}' not found`>; /** * Resolve a column reference to its type */ type ResolveColumnRef<Ref, Context, Schema extends DatabaseSchema = DatabaseSchema> = Ref extends SubqueryExpr<infer Query, infer CastType> ? ResolveSubqueryExpr<Query, CastType, Context, Schema> : Ref extends ComplexExpr<infer ColumnRefs, infer CastType> ? ResolveComplexExpr<ColumnRefs, CastType, Context, Schema> : Ref extends TableColumnRef<infer Table, infer Column, infer ColSchema> ? ResolveTableColumn<Table, Column, ColSchema, Context, Schema> : Ref extends UnboundColumnRef<infer Column> ? ResolveUnboundColumn<Column, Context> : MatchError<"Invalid column reference">; /** * Resolve a complex expression * Validates all column references exist, then returns the cast type or unknown */ type ResolveComplexExpr<ColumnRefs, CastType, Context, Schema extends DatabaseSchema = DatabaseSchema> = ValidateAllColumnRefs<ColumnRefs, Context, Schema> extends infer ValidationResult ? ValidationResult extends MatchError<string> ? ValidationResult : CastType extends string ? MapSQLTypeToTS<CastType> : unknown : never; /** * Resolve a scalar subquery expression * Builds combined context (outer + inner), matches the inner query, * and returns the type of the first selected column */ type ResolveSubqueryExpr<Query extends SubquerySelectClause, CastType, OuterContext, Schema extends DatabaseSchema> = Query extends { columns: infer Columns; from: infer From extends TableSource; joins: infer Joins; } ? BuildSubqueryContext<From, Joins, Schema> extends infer InnerContext ? InnerContext extends MatchError<string> ? CastType extends string ? MapSQLTypeToTS<CastType> : InnerContext : MergeContexts<OuterContext, InnerContext> extends infer CombinedContext ? MatchSubqueryColumns<Columns, CombinedContext, Schema> extends infer ResultType ? ResultType extends MatchError<string> ? CastType extends string ? MapSQLTypeToTS<CastType> : ResultType : CastType extends string ? MapSQLTypeToTS<CastType> : ResultType : unknown : unknown : unknown : unknown; /** * Build context for a subquery, handling the looser types */ type BuildSubqueryContext<From extends TableSource, Joins, Schema extends DatabaseSchema> = ResolveTableSource<From, Schema, {}> extends infer FromContext ? FromContext extends MatchError<string> ? FromContext : Joins extends JoinClause[] ? FlattenContext<MergeJoinContexts<FromContext, Joins, Schema, {}>> : FromContext : never; /** * Merge outer and inner contexts for correlated subqueries * Inner context takes precedence (inner table aliases shadow outer) */ type MergeContexts<Outer, Inner> = Flatten<Outer & Inner>; /** * Match columns in a subquery and return the type of the first column * (scalar subqueries typically return a single value) */ type MatchSubqueryColumns<Columns, Context, Schema extends DatabaseSchema> = Columns extends "*" ? unknown : Columns extends readonly [infer First, ...infer _Rest] ? MatchSingleSubqueryColumn<First, Context, Schema> : Columns extends [infer First, ...infer _Rest] ? MatchSingleSubqueryColumn<First, Context, Schema> : Columns extends readonly (infer Item)[] ? MatchSingleSubqueryColumn<Item, Context, Schema> : Columns extends (infer Item)[] ? MatchSingleSubqueryColumn<Item, Context, Schema> : unknown; /** * Match a single column in a subquery context */ type MatchSingleSubqueryColumn<Col, Context, Schema extends DatabaseSchema> = Col extends ColumnRef<infer Ref, infer _Alias> ? ResolveColumnRef<Ref, Context, Schema> : Col extends AggregateExpr<infer Func, infer Arg, infer _Alias> ? GetAggregateResultType<Func, Arg, Context, Schema> : unknown; /** * Validate all column references in the array * Returns the first error found, or true if all valid */ type ValidateAllColumnRefs<ColumnRefs, Context, Schema extends DatabaseSchema = DatabaseSchema> = ColumnRefs extends [] ? true : ColumnRefs extends [infer First, ...infer Rest] ? ValidateSingleColumnRef<First, Context, Schema> extends infer FirstResult ? FirstResult extends MatchError<string> ? FirstResult : ValidateAllColumnRefs<Rest, Context, Schema> : never : true; /** * Validate a single column reference exists in context * Note: We check for undefined first due to TypeScript 5.9+ behavior */ type ValidateSingleColumnRef<Ref, Context, Schema extends DatabaseSchema = DatabaseSchema> = Ref extends TableColumnRef<infer Table, infer Column, infer ColSchema> ? ColSchema extends undefined ? Table extends keyof Context ? Context[Table] extends infer TableType ? Column extends keyof TableType ? true : MatchError<`Column '${Column}' not found in '${Table}'`> : never : MatchError<`Table or alias '${Table}' not found`> : ColSchema extends string ? ValidateSchemaTableColumn<ColSchema, Table, Column, Schema> : MatchError<`Invalid schema type`> : Ref extends UnboundColumnRef<infer Column> ? FindColumnExists<Column, Context, keyof Context> : true; /** * Validate a schema.table.column reference exists */ type ValidateSchemaTableColumn<SchemaName extends string, TableName extends string, ColumnName extends string, Schema extends DatabaseSchema> = SchemaName extends keyof Schema["schemas"] ? TableName extends keyof Schema["schemas"][SchemaName] ? ColumnName extends keyof Schema["schemas"][SchemaName][TableName] ? true : MatchError<`Column '${ColumnName}' not found in '${SchemaName}.${TableName}'`> : MatchError<`Table '${TableName}' not found in schema '${SchemaName}'`> : MatchError<`Schema '${SchemaName}' not found`>; /** * Check if an unbound column exists in any table */ type FindColumnExists<Column extends string, Context, Keys> = [Keys] extends [never] ? MatchError<`Column '${Column}' not found in any table`> : Keys extends keyof Context ? Context[Keys] extends infer Table ? Column extends keyof Table ? true : FindColumnExists<Column, Context, Exclude<keyof Context, Keys>> : FindColumnExists<Column, Context, Exclude<keyof Context, Keys>> : MatchError<`Column '${Column}' not found in any table`>; /** * Map SQL type names to TypeScript types */ type MapSQLTypeToTS<T extends string> = T extends "text" | "varchar" | "char" | "character varying" | "character" ? string : T extends "int" | "integer" | "int4" | "int8" | "bigint" | "smallint" | "serial" | "bigserial" ? number : T extends "float" | "float4" | "float8" | "real" | "double precision" | "numeric" | "decimal" ? number : T extends "bool" | "boolean" ? boolean : T extends "json" | "jsonb" ? object : T extends "date" | "timestamp" | "timestamptz" | "time" | "timetz" ? string : T extends "uuid" ? string : T extends "bytea" ? Uint8Array : unknown; /** * Resolve a table-qualified column (table.column, alias.column, or schema.table.column) * ColSchema is the schema from the query (undefined if not specified) * Note: We check for undefined first due to TypeScript 5.9+ behavior */ type ResolveTableColumn<TableOrAlias extends string, Column extends string, ColSchema extends string | undefined, Context, Schema extends DatabaseSchema = DatabaseSchema> = ColSchema extends undefined ? TableOrAlias extends keyof Context ? Context[TableOrAlias] extends infer Table ? Column extends keyof Table ? Table[Column] : MatchError<`Column '${Column}' not found in '${TableOrAlias}'`> : never : MatchError<`Table or alias '${TableOrAlias}' not found`> : ColSchema extends string ? ResolveSchemaTableColumn<ColSchema, TableOrAlias, Column, Schema> : MatchError<`Invalid schema type`>; /** * Resolve a fully qualified schema.table.column reference directly from schema */ type ResolveSchemaTableColumn<SchemaName extends string, TableName extends string, ColumnName extends string, Schema extends DatabaseSchema> = SchemaName extends keyof Schema["schemas"] ? TableName extends keyof Schema["schemas"][SchemaName] ? ColumnName extends keyof Schema["schemas"][SchemaName][TableName] ? Schema["schemas"][SchemaName][TableName][ColumnName] : MatchError<`Column '${ColumnName}' not found in '${SchemaName}.${TableName}'`> : MatchError<`Table '${TableName}' not found in schema '${SchemaName}'`> : MatchError<`Schema '${SchemaName}' not found`>; /** * Resolve an unbound column by searching all tables in context */ type ResolveUnboundColumn<Column extends string, Context> = FindColumnInContext<Column, Context, keyof Context>; /** * Search for a column across all tables in context * Note: We use [Keys] extends [never] to prevent distributive conditional behavior */ type FindColumnInContext<Column extends string, Context, Keys> = [Keys] extends [never] ? MatchError<`Column '${Column}' not found in any table`> : Keys extends keyof Context ? Context[Keys] extends infer Table ? Column extends keyof Table ? Table[Column] : FindColumnInContext<Column, Context, Exclude<keyof Context, Keys>> : FindColumnInContext<Column, Context, Exclude<keyof Context, Keys>> : MatchError<`Column '${Column}' not found in any table`>; /** * Get the result type of an aggregate function */ type GetAggregateResultType<Func extends string, Arg, Context, Schema extends DatabaseSchema = DatabaseSchema> = Func extends "COUNT" ? number : Func extends "SUM" | "AVG" ? Arg extends "*" ? number : Arg extends TableColumnRef<infer T, infer C, infer ColSchema> ? ResolveTableColumn<T, C, ColSchema, Context, Schema> extends number ? number : MatchError<`SUM/AVG requires numeric column`> : Arg extends UnboundColumnRef<infer C> ? ResolveUnboundColumn<C, Context> extends number ? number : MatchError<`SUM/AVG requires numeric column`> : number : Func extends "MIN" | "MAX" ? Arg extends "*" ? unknown : Arg extends TableColumnRef<infer T, infer C, infer ColSchema> ? ResolveTableColumn<T, C, ColSchema, Context, Schema> : Arg extends UnboundColumnRef<infer C> ? ResolveUnboundColumn<C, Context> : unknown : unknown; /** * Convert union to intersection * Used to merge all table columns when SELECT * */ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; /** * Parse SQL and match against schema in one step */ export type QueryResult<SQL extends string, Schema extends DatabaseSchema> = MatchQuery<import("./parser.js").ParseSQL<SQL>, Schema>; /** * Extract error message from a MatchError (if it is one) */ type ExtractError<T> = T extends { readonly __error: true; readonly message: infer M; } ? M : never; /** * Check if a type is a MatchError */ type IsMatchError<T> = T extends { readonly __error: true; } ? true : false; /** * Check if a type could potentially be or contain a MatchError * This prevents unnecessary recursion into valid column types like JSON objects or arrays */ type CouldContainError<T> = T extends { readonly __error: true; } ? true : false; /** * Find the first error in an object, checking direct properties only * MatchErrors only appear at the first level of the result object, * so we don't need to recursively descend into valid column types */ type FindFirstError<T> = IsMatchError<T> extends true ? ExtractError<T> : T extends object ? CollectErrors<T> extends infer Errors ? [Errors] extends [never] ? never : Errors : never : never; /** * Collect errors from direct properties of an object * Only checks if properties are MatchErrors, doesn't recursively descend * into valid column types (which would cause issues with complex objects like JSON fields) */ type CollectErrors<T> = { [K in keyof T]: IsMatchError<T[K]> extends true ? ExtractError<T[K]> : CouldContainError<T[K]> extends true ? FindFirstError<T[K]> : never; }[keyof T]; /** * Validate a query result - returns true if valid, or the error message if invalid */ export type ValidateQuery<Result> = FindFirstError<Result> extends never ? true : FindFirstError<Result>; /** * Validate a SQL query against a schema in one step * Returns true if valid, or error message if invalid */ export type ValidateSQL<SQL extends string, Schema extends DatabaseSchema> = ValidateQuery<QueryResult<SQL, Schema>>; export {}; //# sourceMappingURL=matcher.d.ts.map