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
36 changes: 35 additions & 1 deletion src/builder/explain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ASTNode, ExplainNode } from "../ast/nodes.ts"
import type { SumakExecutor } from "../driver/execute.ts"
import { listenerFor, runQuery } from "../driver/execute.ts"
import type { Printer } from "../printer/types.ts"
import type { CompiledQuery } from "../types.ts"
import type { CompiledQueryFn } from "./compiled.ts"
Expand All @@ -17,11 +19,43 @@ export class ExplainBuilder {
readonly _printer?: Printer
/** @internal */
readonly _compile?: (node: ASTNode) => CompiledQuery
/** @internal */
readonly _executor?: SumakExecutor

constructor(node: ExplainNode, printer?: Printer, compile?: (node: ASTNode) => CompiledQuery) {
constructor(
node: ExplainNode,
printer?: Printer,
compile?: (node: ASTNode) => CompiledQuery,
executor?: SumakExecutor,
) {
this._node = node
this._printer = printer
this._compile = compile
this._executor = executor
}

/**
* Run the EXPLAIN and return the plan.
*
* The rows are whatever the engine prints, which differs per dialect and per
* option — `Record<string, unknown>` rather than a shape that would only be
* right for one of them. Result plugins are deliberately not applied: a query
* plan is not a row of your table.
*/
async many(options?: { signal?: AbortSignal }): Promise<Record<string, unknown>[]> {
if (!this._executor) {
throw new Error(
"explain().many() needs an instance to run against. " +
"Build it from db.selectFrom(...).explain(...) so the driver is wired up.",
)
}
return runQuery(
this._executor.driver(),
this.toSQL(),
(rows) => rows,
options,
listenerFor(this._executor),
)
}

build(): ExplainNode {
Expand Down
2 changes: 1 addition & 1 deletion src/builder/typed-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export class TypedDeleteBuilder<DB, TB extends keyof DB> {
analyze: options?.analyze,
format: options?.format,
}
return new ExplainBuilder(explainNode, this._printer, this._compile)
return new ExplainBuilder(explainNode, this._printer, this._compile, this._executor)
}

/** Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`. */
Expand Down
2 changes: 1 addition & 1 deletion src/builder/typed-insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export class TypedInsertBuilder<DB, TB extends keyof DB> {
analyze: options?.analyze,
format: options?.format,
}
return new ExplainBuilder(explainNode, this._printer, this._compile)
return new ExplainBuilder(explainNode, this._printer, this._compile, this._executor)
}

/** Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`. */
Expand Down
127 changes: 107 additions & 20 deletions src/builder/typed-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { param, star } from "../ast/expression.ts"
import type { ASTNode, ExpressionNode, MergeNode, SelectNode } from "../ast/nodes.ts"
import type { Expression } from "../ast/typed-expression.ts"
import { unwrap } from "../ast/typed-expression.ts"
import type { SumakExecutor } from "../driver/execute.ts"
import { listenerFor, resultTransformer, runExecute, runQuery } from "../driver/execute.ts"
import { deriveResultContext } from "../plugin/result-context.ts"
import type { Printer } from "../printer/types.ts"
import type { Insertable, SelectRow, Updateable } from "../schema/types.ts"
import type { CompiledQuery } from "../types.ts"
import type { CompiledQueryFn } from "./compiled.ts"
import { compileQuery } from "./compiled.ts"
import { Col } from "./eb.ts"
import { MergeBuilder } from "./merge.ts"
import { runnersFor } from "./runners.ts"

type MergeProxies<DB, Target extends keyof DB, Source extends keyof DB> = {
target: { [K in keyof DB[Target] & string]: Col<any> }
Expand Down Expand Up @@ -42,6 +46,8 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof
readonly _printer?: Printer
/** @internal */
readonly _compile?: (node: ASTNode) => CompiledQuery
/** @internal */
readonly _executor?: SumakExecutor
private readonly _targetTable: Target & string
private readonly _sourceAlias: string

Expand All @@ -62,11 +68,14 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof
compile?: (node: ASTNode) => CompiledQuery,
/** @internal */
existingBuilder?: MergeBuilder,
/** @internal */
executor?: SumakExecutor,
) {
this._targetTable = targetTable
this._sourceAlias = sourceAlias
this._printer = printer
this._compile = compile
this._executor = executor
this._builder =
existingBuilder ??
new MergeBuilder().into(targetTable).using(sourceTable, sourceAlias).on(unwrap(on))
Expand All @@ -82,6 +91,7 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof
this._printer,
this._compile,
builder,
this._executor,
)
}

Expand Down Expand Up @@ -266,7 +276,7 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof
exprs = (args as string[]).map((c) => ({ type: "column_ref" as const, column: c }))
}
const builder = this._builder.returning(...exprs)
return new TypedMergeReturningBuilder(builder, this._printer, this._compile)
return new TypedMergeReturningBuilder(builder, this._printer, this._compile, this._executor)
}

/**
Expand All @@ -278,7 +288,7 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof
*/
returningAll(): TypedMergeReturningBuilder<DB, Target, SelectRow<DB, Target>> {
const builder = this._builder.returning(star())
return new TypedMergeReturningBuilder(builder, this._printer, this._compile)
return new TypedMergeReturningBuilder(builder, this._printer, this._compile, this._executor)
}

build(): MergeNode {
Expand All @@ -291,25 +301,51 @@ export class TypedMergeBuilder<DB, Target extends keyof DB, Source extends keyof

/** Compile to SQL using the dialect's printer. */
toSQL(): CompiledQuery {
if (this._compile) return this._compile(this.build())
return this._compileNode(this.build())
}

/** Compile an AST this builder already produced, without building it twice. */
private _compileNode(ast: MergeNode): CompiledQuery {
if (this._compile) return this._compile(ast)
if (!this._printer) {
throw new Error("toSQL() requires a printer. Use db.mergeInto() to construct the builder.")
}
return this._printer.print(this.build())
return this._printer.print(ast)
}
/**
* Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`.
*
* There is no executor behind this builder, so the compiled query carries the
* SQL and fills parameters but cannot run itself.
*/
private _requireExecutor(): SumakExecutor {
if (!this._executor) {
throw new Error(
"MERGE needs an instance to run against. Use db.mergeInto(...) so the driver is wired up.",
)
}
return this._executor
}

/** Run the MERGE and return the number of rows it touched. */
async run(options?: { signal?: AbortSignal }): Promise<number> {
const exec = this._requireExecutor()
const result = await runExecute(
exec.driver(),
this._compileNode(this.build()),
options,
listenerFor(exec),
)
return result.affected
}

/** Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`. */
toCompiled<P extends Record<string, unknown> = Record<string, unknown>>(): CompiledQueryFn<P> {
if (!this._printer) {
throw new Error(
"toCompiled() requires a printer. Use db.mergeInto() to construct the builder.",
)
}
return compileQuery<P>(this.build(), this._printer, this._compile)
const ast = this.build()
const executor = this._executor
if (executor === undefined) {
return compileQuery<P>(ast, this._printer, this._compile)
}
return compileQuery<P>(ast, this._printer, this._compile, runnersFor<P, unknown>(executor, ast))
}
}

Expand All @@ -328,15 +364,19 @@ export class TypedMergeReturningBuilder<DB, _Target extends keyof DB, _R> {
readonly _printer?: Printer
/** @internal */
readonly _compile?: (node: ASTNode) => CompiledQuery
/** @internal */
readonly _executor?: SumakExecutor

constructor(
builder: MergeBuilder,
printer?: Printer,
compile?: (node: ASTNode) => CompiledQuery,
executor?: SumakExecutor,
) {
this._builder = builder
this._printer = printer
this._compile = compile
this._executor = executor
}

/** Stack additional RETURNING expressions onto the projection. */
Expand All @@ -345,6 +385,7 @@ export class TypedMergeReturningBuilder<DB, _Target extends keyof DB, _R> {
this._builder.returning(...exprs),
this._printer,
this._compile,
this._executor,
)
}

Expand All @@ -358,24 +399,70 @@ export class TypedMergeReturningBuilder<DB, _Target extends keyof DB, _R> {

/** Compile to SQL using the dialect's printer. */
toSQL(): CompiledQuery {
if (this._compile) return this._compile(this.build())
return this._compileNode(this.build())
}

/** Compile an AST this builder already produced, without building it twice. */
private _compileNode(ast: MergeNode): CompiledQuery {
if (this._compile) return this._compile(ast)
if (!this._printer) {
throw new Error("toSQL() requires a printer. Use db.mergeInto() to construct the builder.")
}
return this._printer.print(this.build())
return this._printer.print(ast)
}
/**
* Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`.
*
* There is no executor behind this builder, so the compiled query carries the
* SQL and fills parameters but cannot run itself.
*/
private _requireExecutor(): SumakExecutor {
if (!this._executor) {
throw new Error(
"MERGE needs an instance to run against. Use db.mergeInto(...) so the driver is wired up.",
)
}
return this._executor
}

/** Run the MERGE and return every row produced by `RETURNING`. */
async many(options?: { signal?: AbortSignal }): Promise<_R[]> {
const exec = this._requireExecutor()
const ast = this.build()
const rows = await runQuery(
exec.driver(),
this._compileNode(ast),
resultTransformer(exec, deriveResultContext(ast)),
options,
listenerFor(exec),
)
return rows as unknown as _R[]
}

/** Run the MERGE and return the first row produced by `RETURNING`, or null. */
async first(options?: { signal?: AbortSignal }): Promise<_R | null> {
const rows = await this.many(options)
return rows[0] ?? null
}

/** Run the MERGE and return the number of rows it touched. */
async run(options?: { signal?: AbortSignal }): Promise<number> {
const exec = this._requireExecutor()
const result = await runExecute(
exec.driver(),
this._compileNode(this.build()),
options,
listenerFor(exec),
)
return result.affected
}

/** Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`. */
toCompiled<P extends Record<string, unknown> = Record<string, unknown>>(): CompiledQueryFn<P> {
if (!this._printer) {
throw new Error(
"toCompiled() requires a printer. Use db.mergeInto() to construct the builder.",
)
}
return compileQuery<P>(this.build(), this._printer, this._compile)
const ast = this.build()
const executor = this._executor
if (executor === undefined) {
return compileQuery<P>(ast, this._printer, this._compile)
}
return compileQuery<P>(ast, this._printer, this._compile, runnersFor<P, unknown>(executor, ast))
}
}
2 changes: 1 addition & 1 deletion src/builder/typed-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ export class TypedSelectBuilder<DB, TB extends keyof DB, O> {
analyze: options?.analyze,
format: options?.format,
}
return new ExplainBuilder(explainNode, this._printer, this._compile)
return new ExplainBuilder(explainNode, this._printer, this._compile, this._executor)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/builder/typed-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export class TypedUpdateBuilder<DB, TB extends keyof DB> {
analyze: options?.analyze,
format: options?.format,
}
return new ExplainBuilder(explainNode, this._printer, this._compile)
return new ExplainBuilder(explainNode, this._printer, this._compile, this._executor)
}

/** Pre-compile the SQL with placeholders. See `TypedSelectBuilder.toCompiled()`. */
Expand Down
2 changes: 2 additions & 0 deletions src/sumak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,8 @@ export class Sumak<DB> {
onExpr,
this._dialect.createPrinter(),
(node: ASTNode) => this.compile(node),
undefined,
this,
)
}

Expand Down
20 changes: 19 additions & 1 deletion test/builder/every-builder-compiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface Builder {
file: string
name: string
compiles: boolean
runs: boolean
}

function builders(): Builder[] {
Expand All @@ -37,7 +38,12 @@ function builders(): Builder[] {
const end = classes[index + 1]?.index ?? source.length
const body = source.slice(start, end)
if (!/\n {2}toSQL\(/.test(body)) continue
found.push({ file, name: match[1] as string, compiles: /\n {2}toCompiled/.test(body) })
found.push({
file,
name: match[1] as string,
compiles: /\n {2}toCompiled/.test(body),
runs: /\n {2}async (many|one|first|run|exec)\(/.test(body),
})
}
}
return found
Expand All @@ -57,4 +63,16 @@ describe("every builder that emits SQL can be compiled", () => {

expect(missing).toEqual([])
})

it("leaves none of them unable to run", () => {
// `db.mergeInto(...)` was a public entry point producing a query nobody
// could run — no executor, so no `.run()` and a compiled MERGE rejected.
// A builder the API hands out and cannot execute is a feature that only
// half exists.
const missing = builders()
.filter((b) => !b.runs)
.map((b) => `${b.name} (${b.file})`)

expect(missing).toEqual([])
})
})
Loading