From b969d3e535c30e79e01f22e0305fa59e63bedf0d Mon Sep 17 00:00:00 2001 From: Ben Merckx Date: Fri, 26 Apr 2024 11:58:26 +0200 Subject: [PATCH] Interim --- src/core/Builder.ts | 55 +++++++++++++++++++++++++++++++++++-- src/core/Emitter.ts | 37 ++++++++++++++++++++++--- src/core/Field.ts | 4 +-- src/core/Internal.ts | 7 +++++ src/core/Query.ts | 3 ++ src/core/Selection.ts | 26 ++++++++++-------- src/core/Table.ts | 6 +++- src/core/query/Select.ts | 27 +++++++++--------- test/core/Selection.test.ts | 2 +- test/query/Select.test.ts | 17 ++++++++++++ 10 files changed, 148 insertions(+), 36 deletions(-) diff --git a/src/core/Builder.ts b/src/core/Builder.ts index 01944cf..cda67ad 100644 --- a/src/core/Builder.ts +++ b/src/core/Builder.ts @@ -1,17 +1,33 @@ -import {getData, internalData, type HasSql, type HasTable} from './Internal.ts' +import { + getData, + getQuery, + getSelection, + internalData, + internalQuery, + internalTarget, + type HasQuery, + type HasSql, + type HasTable, + type HasTarget +} from './Internal.ts' import type {IsPostgres, QueryMeta} from './MetaData.ts' import type {QueryData} from './Query.ts' import type {SelectionInput} from './Selection.ts' +import {sql} from './Sql.ts' import type {Table, TableDefinition} from './Table.ts' import {Create} from './query/Create.ts' import {DeleteFrom} from './query/Delete.ts' import {Drop} from './query/Drop.ts' import {InsertInto} from './query/Insert.ts' -import type {WithSelection, WithoutSelection} from './query/Select.ts' +import type { + SelectBase, + WithSelection, + WithoutSelection +} from './query/Select.ts' import {Select} from './query/Select.ts' import {UpdateTable} from './query/Update.ts' -export class Builder { +class BuilderBase { readonly [internalData]: QueryData constructor(data: QueryData) { @@ -93,3 +109,36 @@ export class Builder { return new DeleteFrom({...getData(this), from}) } } + +export type CTE = Input & HasTarget & HasQuery + +export class Builder extends BuilderBase { + $with(cteName: string) { + return { + as( + query: SelectBase + ): CTE { + const fields = getSelection(query).makeVirtual(cteName) + return Object.assign(fields, { + [internalTarget]: sql.identifier(cteName), + [internalQuery]: getQuery(query) + }) + } + } + } + + with(...cte: Array) { + return new BuilderBase({ + ...getData(this), + cte + }) + } + + create(table: Table) { + return new Create({...getData(this), table}) + } + + drop(table: HasTable) { + return new Drop({...getData(this), table}) + } +} diff --git a/src/core/Emitter.ts b/src/core/Emitter.ts index f63fe02..35740a9 100644 --- a/src/core/Emitter.ts +++ b/src/core/Emitter.ts @@ -1,6 +1,14 @@ import type {ColumnData} from './Column.ts' import type {FieldData} from './Field.ts' -import {getData, getQuery, getSelection, getTable} from './Internal.ts' +import { + getData, + getQuery, + getSelection, + getTable, + getTarget, + type HasQuery, + type HasTarget +} from './Internal.ts' import {ValueParam, type Param} from './Param.ts' import {sql} from './Sql.ts' import type {Create} from './query/Create.ts' @@ -81,7 +89,8 @@ export abstract class Emitter { } emitDelete(deleteOp: Delete): void { - const {from, where, returning} = getData(deleteOp) + const {cte, from, where, returning} = getData(deleteOp) + if (cte) this.emitWith(cte) const table = getTable(from) sql .query({ @@ -93,7 +102,8 @@ export abstract class Emitter { } emitInsert(insert: Insert): void { - const {into, values, onConflict, returning} = getData(insert) + const {cte, into, values, onConflict, returning} = getData(insert) + if (cte) this.emitWith(cte) const table = getTable(into) const tableName = sql.identifier(table.name) sql @@ -109,6 +119,7 @@ export abstract class Emitter { emitSelect(select: Select): void { const { + cte, from, distinct, distinctOn, @@ -119,6 +130,7 @@ export abstract class Emitter { limit, offset } = getData(select) + if (cte) this.emitWith(cte) const selected = getSelection(select) const prefix = distinctOn ? sql`distinct on (${sql.join(distinctOn, sql`, `)})` @@ -143,8 +155,9 @@ export abstract class Emitter { } emitUpdate(update: Update): void { - const {table, set, where, returning} = getData(update) + const {cte, table, set, where, returning} = getData(update) const tableApi = getTable(table) + if (cte) this.emitWith(cte) sql .query({ update: sql.identifier(tableApi.name), @@ -157,4 +170,20 @@ export abstract class Emitter { } abstract emitIdColumn(): void + + emitWith(cte: Array): void { + sql + .query({ + with: sql.join( + cte.map(cte => { + const query = getQuery(cte) + const target = getTarget(cte) + return sql`${target} as (${query})` + }), + sql`, ` + ) + }) + .add(sql` `) + .emit(this) + } } diff --git a/src/core/Field.ts b/src/core/Field.ts index 2e17a51..1384c5c 100644 --- a/src/core/Field.ts +++ b/src/core/Field.ts @@ -7,8 +7,8 @@ export interface FieldData { } export class Field implements HasSql { - private declare keep?: [Table]; - readonly [internalField]: FieldData; + private declare keep?: [Table] + readonly [internalField]: FieldData readonly [internalSql]: Sql constructor( targetName: string, diff --git a/src/core/Internal.ts b/src/core/Internal.ts index ed8d2be..5f6bd70 100644 --- a/src/core/Internal.ts +++ b/src/core/Internal.ts @@ -9,6 +9,7 @@ import type {TableApi, TableDefinition} from './Table.ts' export const internalData = Symbol() export const internalSql = Symbol() export const internalSelection = Symbol() +export const internalTarget = Symbol() export const internalQuery = Symbol() export const internalTable = Symbol() export const internalColumn = Symbol() @@ -24,6 +25,9 @@ export declare class HasSql { export declare class HasSelection { get [internalSelection](): Selection } +export declare class HasTarget { + get [internalTarget](): Sql +} export declare class HasQuery { get [internalQuery](): Sql } @@ -52,6 +56,9 @@ export const getSql = (obj: HasSql) => obj[internalSql] export const hasSelection = (obj: object): obj is HasSelection => internalSelection in obj export const getSelection = (obj: HasSelection) => obj[internalSelection] +export const hasTarget = (obj: object): obj is HasTarget => + internalTarget in obj +export const getTarget = (obj: HasTarget) => obj[internalTarget] export const hasQuery = (obj: object): obj is HasQuery => internalQuery in obj export const getQuery = (obj: HasQuery) => obj[internalQuery] export const hasTable = (obj: object): obj is HasTable => internalTable in obj diff --git a/src/core/Query.ts b/src/core/Query.ts index 9a03bc3..6705eb4 100644 --- a/src/core/Query.ts +++ b/src/core/Query.ts @@ -1,9 +1,11 @@ import { + type HasTarget, getData, getResolver, hasResolver, internalData, internalQuery, + type HasQuery, type HasResolver } from './Internal.ts' import type {Async, QueryMeta, Sync} from './MetaData.ts' @@ -12,6 +14,7 @@ import type {Sql} from './Sql.ts' export class QueryData { resolver?: Resolver + cte?: Array } export abstract class Query diff --git a/src/core/Selection.ts b/src/core/Selection.ts index 87c9de2..e850bf0 100644 --- a/src/core/Selection.ts +++ b/src/core/Selection.ts @@ -10,6 +10,7 @@ import { import {sql, type Sql} from './Sql.ts' import type {Table, TableRow} from './Table.ts' import type {Expand} from './Types.ts' +import {virtual} from './Virtual.ts' declare const nullable: unique symbol export interface SelectionRecord extends Record {} @@ -21,16 +22,15 @@ export type RowOfRecord = Expand<{ Input[Key] > }> -export type SelectionRow = - Input extends HasSql - ? Value - : Input extends IsNullable - ? RowOfRecord | null - : Input extends SelectionRecord - ? RowOfRecord - : Input extends Table - ? TableRow - : never +export type SelectionRow = Input extends HasSql + ? Value + : Input extends IsNullable + ? RowOfRecord | null + : Input extends SelectionRecord + ? RowOfRecord + : Input extends Table + ? TableRow + : never export class Selection implements HasSql { #input: SelectionInput @@ -41,6 +41,10 @@ export class Selection implements HasSql { this.#nullable = nullable } + makeVirtual(name: string) { + return virtual(name, this.#input) + } + mapRow = (values: Array) => { return this.#mapResult(this.#input, values) } @@ -81,7 +85,7 @@ export class Selection implements HasSql { ): Sql { const expr = this.#exprOf(input) if (expr) { - let exprName = expr.alias ?? name + let exprName = name ?? expr.alias if (exprName) { // The bun:sqlite driver cannot handle multiple columns by the same name while (names.has(exprName)) exprName = `${exprName}_` diff --git a/src/core/Table.ts b/src/core/Table.ts index 8dbefd7..0d81112 100644 --- a/src/core/Table.ts +++ b/src/core/Table.ts @@ -2,9 +2,11 @@ import type {Column, JsonColumn, RequiredColumn} from './Column.ts' import {jsonExpr, type Input, type JsonExpr} from './Expr.ts' import {Field} from './Field.ts' import { + type HasTarget, getColumn, getTable, internalTable, + internalTarget, type HasSql, type HasTable } from './Internal.ts' @@ -77,7 +79,7 @@ export class TableApi< export type Table< Definition extends TableDefinition = TableDefinition, Name extends string = string -> = TableFields & HasTable +> = TableFields & HasTable & HasTarget export type TableFields< Definition extends TableDefinition, @@ -127,6 +129,7 @@ export function table( const api = assign(new TableApi(), {name, columns}) return >{ [internalTable]: api, + [internalTarget]: api.from(), ...api.fields() } } @@ -138,6 +141,7 @@ export function alias( const api = assign(new TableApi(), {...getTable(table), alias}) return >{ [internalTable]: api, + [internalTarget]: api.from(), ...api.fields() } } diff --git a/src/core/query/Select.ts b/src/core/query/Select.ts index d4f71d8..824574e 100644 --- a/src/core/query/Select.ts +++ b/src/core/query/Select.ts @@ -5,15 +5,16 @@ import { getQuery, getSelection, getTable, - hasQuery, + getTarget, hasTable, internalData, internalQuery, internalSelection, - type HasQuery, + internalTarget, type HasSelection, type HasSql, - type HasTable + type HasTable, + type HasTarget } from '../Internal.ts' import type {IsMysql, IsPostgres, QueryMeta} from '../MetaData.ts' import {Query, QueryData} from '../Query.ts' @@ -28,7 +29,6 @@ import { import {sql} from '../Sql.ts' import type {Table, TableDefinition, TableFields} from '../Table.ts' import type {Expand} from '../Types.ts' -import {virtual} from '../Virtual.ts' import {Union} from './Union.ts' export type SelectionType = 'selection' | 'allFrom' | 'joinTables' @@ -63,19 +63,18 @@ export class Select this[internalData] = data } - as(name: string): SubQuery { - const {select} = getData(this) - if (!select.input) throw new Error('No selection defined') - return Object.assign(virtual(name, select.input) as any, { - [internalQuery]: sql`(${getQuery(this)}) as ${sql.identifier( - name + as(alias: string): SubQuery { + const fields = getSelection(this).makeVirtual(alias) + return Object.assign(fields, { + [internalTarget]: sql`(${getQuery(this)}) as ${sql.identifier( + alias )}`.inlineFields(true) }) } - from(target: HasQuery | Table): Select { + from(target: HasTarget): Select { const {select: current} = getData(this) - const from = hasQuery(target) ? getQuery(target) : getTable(target).from() + const from = getTarget(target) const isTable = hasTable(target) const selectionInput = current.input ?? (isTable ? target : sql`*`) return new Select({ @@ -252,9 +251,9 @@ export class Select } } -export type SubQuery = Input & HasQuery +export type SubQuery = Input & HasTarget -export interface SelectBase +export interface SelectBase extends Query, Meta>, HasSelection { where(where: HasSql): Select diff --git a/test/core/Selection.test.ts b/test/core/Selection.test.ts index 155fb34..927badd 100644 --- a/test/core/Selection.test.ts +++ b/test/core/Selection.test.ts @@ -5,7 +5,7 @@ import {emit} from '../TestUtils.ts' Test.describe('Selection', () => { Test.it('alias', () => { - const aliased = selection({id: sql.value(1).as('name')}) + const aliased = selection(sql.value(1).as('name')) Assert.isEqual(emit(aliased), '1 as "name"') }) }) diff --git a/test/query/Select.test.ts b/test/query/Select.test.ts index cafa73d..b44a193 100644 --- a/test/query/Select.test.ts +++ b/test/query/Select.test.ts @@ -74,4 +74,21 @@ Test.describe('Select', () => { 'select "sub"."id" from (select "Node"."id" from "Node") as "sub"' ) }) + + Test.it('with cte', () => { + const cte = builder.$with('myCte').as(builder.select().from(Node)) + const cte2 = builder.$with('cte2').as(builder.select(Node.id).from(Node)) + const query = builder + .with(cte, cte2) + .select({ + nodeId: cte.id, + cte2Id: cte2 + }) + .from(cte) + .limit(10) + Assert.isEqual( + emit(query), + 'with "myCte" as (select "Node"."id", "Node"."field1" from "Node"), "cte2" as (select "Node"."id" from "Node") select "myCte"."id" as "nodeId", "cte2"."id" as "cte2Id" from "myCte" limit 10' + ) + }) })