@sigitex/outlaw 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,93 +1,82 @@
1
- // oxlint-disable typescript/no-explicit-any
2
- import type { BinaryOperator, Select, UnaryOperator } from "../api"
3
- import type { BuildTable, ColumnRef, TableData } from "../schemaBuilder"
4
- import { addWhereClause } from "./addWhereClause"
5
- import type { JoinTarget, JoinType, SelectQuery } from "./queryBuilders.types"
1
+ import type { BinaryOperator, UnaryOperator } from "../api/api.types"
2
+ import type { ColumnRef } from "../schemaBuilder/ColumnRef"
3
+ import { QuerySource } from "./QuerySource"
4
+ import type { SelectQuery } from "./queryBuilders.types"
5
+ import { isBinaryOperator, isUnaryOperator } from "./operators"
6
+ import { Projection } from "./Projection"
6
7
 
7
- /** Base query builder for constructing declarative `SELECT` statements without a connection. */
8
8
  export class SelectQueryBuilder {
9
- readonly table: TableData
10
9
  readonly query: SelectQuery
11
10
 
12
- constructor(table: TableData, columns: "*" | string[]) {
13
- this.table = table
14
- this.query = { table: table.name }
15
- if (Array.isArray(columns)) {
16
- this.query.selected = columns
17
- }
18
- }
19
-
20
- limit(n: number) {
21
- this.query.limit = n
22
- return this
11
+ constructor(query: SelectQuery) {
12
+ this.query = query
23
13
  }
24
14
 
25
- offset(n: number) {
26
- this.query.offset = n
27
- return this
15
+ as(name: string) {
16
+ return QuerySource.create({ ...QuerySource.target(this), alias: name })
28
17
  }
29
18
 
30
- where(column: string, value: any): this
31
- where(column: string, operator: UnaryOperator): this
32
- where(column: string, operator: BinaryOperator, value: any): this
33
- where(column: string, operator: any, value?: any) {
34
- addWhereClause(column, this.table, this.query, operator, value)
19
+ limit(count: number) {
20
+ this.query.limit = count
35
21
  return this
36
22
  }
37
23
 
38
- orderBy(sorts: [string, "asc" | "desc"][]): this {
39
- this.query.orderBy = sorts.map(([column, direction]) => ({
40
- column,
41
- direction,
42
- }))
24
+ offset(count: number) {
25
+ this.query.offset = count
43
26
  return this
44
27
  }
45
28
 
46
- private addJoin(type: JoinType, target: JoinTarget): this {
47
- if (!this.query.joins) {
48
- this.query.joins = []
29
+ where(column: string | ColumnRef, value: unknown): this
30
+ where(
31
+ column: string | ColumnRef,
32
+ operator: BinaryOperator | UnaryOperator,
33
+ value?: unknown,
34
+ ): this
35
+ where(column: string | ColumnRef, operator: unknown, value?: unknown) {
36
+ const scope = QuerySource.scope(this.query)
37
+ const identifier = QuerySource.identifier(scope, column)
38
+ this.query.conditions ??= []
39
+ if (
40
+ typeof operator === "string" &&
41
+ isUnaryOperator(operator) &&
42
+ arguments.length === 2
43
+ ) {
44
+ this.query.conditions.push({
45
+ column: identifier,
46
+ arity: 1,
47
+ operator: operator as UnaryOperator,
48
+ })
49
+ } else {
50
+ const binary =
51
+ arguments.length === 3 &&
52
+ typeof operator === "string" &&
53
+ isBinaryOperator(operator)
54
+ const operand = binary ? value : operator
55
+ const columns = QuerySource.resolve(scope, identifier)
56
+ const mapping = columns.length
57
+ ? Projection.merge(columns).mapping
58
+ : undefined
59
+ this.query.conditions.push({
60
+ column: identifier,
61
+ arity: 2,
62
+ operator: binary ? (operator as BinaryOperator) : "=",
63
+ value: mapping && operand !== null ? mapping.to(operand) : operand,
64
+ })
49
65
  }
50
- this.query.joins.push({ type, target, on: [] })
51
66
  return this
52
67
  }
53
68
 
54
- join(target: BuildTable<any> | Select<any, any>): this {
55
- return this.addJoin("join", resolveTarget(target))
56
- }
57
-
58
- leftJoin(target: BuildTable<any> | Select<any, any>): this {
59
- return this.addJoin("left join", resolveTarget(target))
60
- }
61
-
62
- rightJoin(target: BuildTable<any> | Select<any, any>): this {
63
- return this.addJoin("right join", resolveTarget(target))
64
- }
65
-
66
- crossJoin(target: BuildTable<any> | Select<any, any>): this {
67
- return this.addJoin("cross join", resolveTarget(target))
68
- }
69
-
70
- on(left: ColumnRef, operator: BinaryOperator, right: ColumnRef): this {
71
- const joins = this.query.joins
72
- if (!joins?.length) {
73
- throw new Error("on() called without a preceding join")
74
- }
75
- joins[joins.length - 1].on.push({ left, operator, right })
69
+ orderBy(sorts: readonly (readonly [string | ColumnRef, "asc" | "desc"])[]) {
70
+ const scope = QuerySource.scope(this.query)
71
+ const outputs = Projection.columns(this.query)
72
+ this.query.orderBy = sorts.map(([column, direction]) => ({
73
+ column:
74
+ typeof column === "string" &&
75
+ outputs.some((output) => output.name === column)
76
+ ? { column }
77
+ : QuerySource.identifier(scope, column),
78
+ direction,
79
+ }))
76
80
  return this
77
81
  }
78
82
  }
79
-
80
- export function resolveTarget(
81
- target: BuildTable<any> | Select<any, any>,
82
- ): JoinTarget {
83
- if ("$meta" in target) {
84
- return { kind: "table", name: target.$meta.name, tableData: target.$meta }
85
- }
86
- const builder = target as unknown as SelectQueryBuilder
87
- return {
88
- kind: "subquery",
89
- table: builder.query.table,
90
- query: { ...builder.query },
91
- tableData: builder.table,
92
- }
93
- }
@@ -1,4 +1,6 @@
1
1
  export * from "./queryBuilders.types"
2
+ export * from "./QueryScope"
3
+ export * from "./QuerySource"
2
4
  export * from "./operators"
3
5
  export * from "./SelectQueryBuilder"
4
6
  export * from "./SelectBuilder"
@@ -1,35 +1,39 @@
1
1
  // oxlint-disable typescript/consistent-type-definitions -- review
2
2
  import type { BinaryOperator, UnaryOperator } from "../api/api.types"
3
- import type { ColumnRef, TableData } from "../schemaBuilder"
3
+ import type { QuerySource } from "./QuerySource"
4
+ import type { Projection } from "./Projection"
5
+
6
+ export type ColumnIdentifier = { table?: string; column: string }
7
+ export type SelectCondition =
8
+ | (Omit<UnaryCondition, "column"> & { column: ColumnIdentifier })
9
+ | (Omit<BinaryCondition, "column"> & { column: ColumnIdentifier })
4
10
 
5
11
  /** Represents a select query. */
6
12
  export interface SelectQuery {
7
13
  /** Table this query is performed on. */
8
- table: string
14
+ source: QuerySource.Data
9
15
  /** Limit clause. */
10
16
  limit?: number
11
17
  /** Offset clause. */
12
18
  offset?: number
13
19
  /** Conditions of the SELECT. */
14
- conditions?: Condition[]
20
+ conditions?: SelectCondition[]
15
21
  /** Which columns are being selected in this query. */
16
- selected?: string[]
22
+ selected: Projection[]
17
23
  /** Order By clause */
18
- orderBy?: OrderBySort[]
24
+ orderBy?: { column: ColumnIdentifier; direction: "asc" | "desc" }[]
19
25
  /** Join clauses. */
20
26
  joins?: JoinClause[]
21
27
  }
22
28
 
23
- export type JoinType = "join" | "left join" | "right join" | "cross join"
29
+ export type JoinType =
30
+ | "join"
31
+ | "left join"
32
+ | "right join"
33
+ | "full join"
34
+ | "cross join"
24
35
 
25
- export type JoinTarget =
26
- | { kind: "table"; name: string; tableData: TableData }
27
- | {
28
- kind: "subquery"
29
- table: string
30
- query: SelectQuery
31
- tableData: TableData
32
- }
36
+ export type JoinTarget = QuerySource.Data
33
37
 
34
38
  export interface JoinClause {
35
39
  /** The type of join. */
@@ -38,15 +42,16 @@ export interface JoinClause {
38
42
  target: JoinTarget
39
43
  /** Join conditions (ON clause). */
40
44
  on: JoinOn[]
45
+ using?: string[]
41
46
  }
42
47
 
43
48
  export interface JoinOn {
44
49
  /** Left side of the ON condition. */
45
- left: ColumnRef
50
+ left: ColumnIdentifier
46
51
  /** The comparison operator. */
47
52
  operator: BinaryOperator
48
53
  /** Right side of the ON condition. */
49
- right: ColumnRef
54
+ right: ColumnIdentifier
50
55
  }
51
56
 
52
57
  /** Represents an update command. */
@@ -1,19 +1,24 @@
1
1
  import { indent, join, newline, type Node } from "@sigitex/print"
2
2
  import { Format } from "../framework"
3
- import type { Condition, JoinClause, JoinTarget } from "../queryBuilder"
4
- import type { ColumnRef } from "../schemaBuilder"
3
+ import type {
4
+ Condition,
5
+ JoinClause,
6
+ JoinTarget,
7
+ ColumnIdentifier,
8
+ SelectCondition,
9
+ } from "../queryBuilder"
5
10
  import { generateSelectNode } from "./generateSelect"
6
11
 
7
12
  export namespace Clause {
8
- export function where(conditions: Condition[], baseTable?: string): Node[] {
13
+ export function where(conditions: (Condition | SelectCondition)[]): Node[] {
9
14
  return [
10
15
  "where ",
11
16
  indent(
12
17
  conditions.map((condition, index) => [
13
18
  index > 0 && " and ",
14
- baseTable
15
- ? qualifyColumn(baseTable, condition.column)
16
- : Format.name(condition.column),
19
+ typeof condition.column === "string"
20
+ ? Format.name(condition.column)
21
+ : identifier(condition.column),
17
22
  " ",
18
23
  condition.operator,
19
24
  condition.arity === 2 && [" ", Format.value(condition.value)],
@@ -30,50 +35,46 @@ export namespace Clause {
30
35
  return ["returning ", join(", ", columns, Format.name), newline]
31
36
  }
32
37
 
33
- export function joins(
34
- clauses: JoinClause[],
35
- aliasMap: Map<JoinClause, string>,
36
- ): Node {
38
+ export function identifier(column: ColumnIdentifier): string {
39
+ return column.table === undefined
40
+ ? Format.identifier(column.column)
41
+ : `${Format.identifier(column.table)}.${Format.identifier(column.column)}`
42
+ }
43
+
44
+ export function source(target: JoinTarget): Node {
45
+ return [
46
+ target.kind === "table"
47
+ ? Format.identifier(target.name)
48
+ : ["(", newline, indent([generateSelectNode(target.query)]), ")"],
49
+ target.alias !== undefined && [" as ", Format.identifier(target.alias)],
50
+ ]
51
+ }
52
+
53
+ export function joins(clauses: JoinClause[]): Node {
37
54
  return clauses.map((clause) => {
38
55
  const { type, on } = clause
39
- const alias = aliasMap.get(clause)!
40
56
  return [
41
57
  type,
42
58
  " ",
43
- formatTarget(clause.target, alias),
59
+ source(clause.target),
44
60
  newline,
45
61
  on.length > 0 &&
46
62
  indent(
47
63
  on.map(({ left, operator, right }, index) => [
48
64
  index === 0 ? "on " : "and ",
49
- formatRef(left),
65
+ identifier(left),
50
66
  ` ${operator} `,
51
- formatRef(right),
67
+ identifier(right),
52
68
  newline,
53
69
  ]),
54
70
  ),
71
+ clause.using?.length && [
72
+ "using (",
73
+ join(", ", clause.using, Format.identifier),
74
+ ")",
75
+ newline,
76
+ ],
55
77
  ]
56
78
  })
57
79
  }
58
80
  }
59
-
60
- function formatTarget(target: JoinTarget, alias: string): Node {
61
- if (target.kind === "table") {
62
- return Format.name(target.name)
63
- }
64
- return [
65
- "(",
66
- newline,
67
- indent([generateSelectNode(target.query)]),
68
- ") as ",
69
- Format.name(alias),
70
- ]
71
- }
72
-
73
- function formatRef(ref: ColumnRef): string {
74
- return `${Format.name(ref.table)}.${Format.name(ref.column)}`
75
- }
76
-
77
- function qualifyColumn(baseTable: string, column: string): string {
78
- return `${Format.name(baseTable)}.${Format.name(column)}`
79
- }
@@ -1,89 +1,54 @@
1
- import { indent, join, newline, type Node, print } from "@sigitex/print"
1
+ import { join, newline, type Node, print } from "@sigitex/print"
2
2
  import { Format } from "../framework"
3
- import type { JoinClause, OrderBySort, SelectQuery } from "../queryBuilder"
3
+ import type { SelectQuery } from "../queryBuilder/queryBuilders.types"
4
4
  import { Clause } from "./Clause"
5
5
 
6
6
  export function generateSelect(query: SelectQuery) {
7
7
  return print([generateSelectNode(query)])
8
8
  }
9
9
 
10
- export function generateSelectNode({
11
- selected,
12
- table,
13
- conditions,
14
- limit,
15
- offset,
16
- orderBy,
17
- joins,
18
- }: SelectQuery): Node {
19
- const hasJoins = !!joins?.length
20
- const aliasMap = hasJoins ? buildAliasMap(table, joins!) : undefined
10
+ export function generateSelectNode(query: SelectQuery): Node {
21
11
  return [
22
12
  "select ",
23
- selected &&
24
- join(", ", selected, (col: string) =>
25
- hasJoins ? qualifyColumn(table, col) : Format.name(col),
26
- ),
27
- !selected &&
28
- (hasJoins ? allColumnsQualified(table, joins!, aliasMap!) : "*"),
13
+ join(", ", query.selected, (projection) => {
14
+ if (projection === "*") {
15
+ return "*"
16
+ }
17
+ if ("wildcard" in projection) {
18
+ return [Format.identifier(projection.table), ".*"]
19
+ }
20
+ return [
21
+ Clause.identifier(projection.column),
22
+ projection.alias !== undefined && [
23
+ " as ",
24
+ Format.identifier(projection.alias),
25
+ ],
26
+ ]
27
+ }),
29
28
  newline,
30
29
  "from ",
31
- Format.name(table),
30
+ Clause.source(query.source),
32
31
  newline,
33
- hasJoins && Clause.joins(joins!, aliasMap!),
34
- conditions?.length &&
35
- Clause.where(conditions, hasJoins ? table : undefined),
36
- limit && ["limit ", Format.number(limit), newline],
37
- offset && ["offset ", Format.number(offset), newline],
38
- orderBy && orderByClause(orderBy),
39
- ]
40
- }
41
-
42
- function buildAliasMap(
43
- baseTable: string,
44
- joins: JoinClause[],
45
- ): Map<JoinClause, string> {
46
- const map = new Map<JoinClause, string>()
47
- const used = new Set([baseTable])
48
-
49
- for (const clause of joins) {
50
- const sourceName =
51
- clause.target.kind === "table" ? clause.target.name : clause.target.table
52
- let alias = sourceName
53
- let i = 1
54
- while (used.has(alias)) {
55
- alias = `${sourceName}_${i++}`
56
- }
57
- used.add(alias)
58
- map.set(clause, alias)
59
- }
60
- return map
61
- }
62
-
63
- function qualifyColumn(baseTable: string, column: string): string {
64
- return `${Format.name(baseTable)}.${Format.name(column)}`
65
- }
66
-
67
- function allColumnsQualified(
68
- table: string,
69
- joins: JoinClause[],
70
- aliasMap: Map<JoinClause, string>,
71
- ): Node {
72
- const names = [table, ...joins.map((j) => aliasMap.get(j)!)]
73
- return names.map((t, i) => [i > 0 && ", ", Format.name(t), ".*"])
74
- }
75
-
76
- function orderByClause(orderBy: OrderBySort[]): Node {
77
- return [
78
- "order by ",
79
- indent(
80
- orderBy.map(({ column, direction }, index) => [
81
- index > 0 && ", ",
82
- Format.name(column),
32
+ query.joins?.length && Clause.joins(query.joins),
33
+ query.conditions?.length && Clause.where(query.conditions),
34
+ query.orderBy?.length && [
35
+ "order by ",
36
+ join(", ", query.orderBy, (sort) => [
37
+ Clause.identifier(sort.column),
83
38
  " ",
84
- direction,
85
- newline,
39
+ sort.direction,
86
40
  ]),
87
- ),
41
+ newline,
42
+ ],
43
+ query.limit !== undefined && [
44
+ "limit ",
45
+ Format.number(query.limit),
46
+ newline,
47
+ ],
48
+ query.offset !== undefined && [
49
+ "offset ",
50
+ Format.number(query.offset),
51
+ newline,
52
+ ],
88
53
  ]
89
54
  }
@@ -0,0 +1,36 @@
1
+ export type ColumnRef<
2
+ Table extends string = string,
3
+ Column extends string = string,
4
+ > = {
5
+ readonly table: Table
6
+ readonly column: Column
7
+ as<Alias extends string>(
8
+ alias: Alias,
9
+ ): ColumnRef.Aliased<Table, Column, Alias>
10
+ }
11
+
12
+ export namespace ColumnRef {
13
+ export type Aliased<
14
+ Table extends string,
15
+ Column extends string,
16
+ Alias extends string,
17
+ > = ColumnRef<Table, Column> & { readonly alias: Alias }
18
+
19
+ export type Wildcard<Table extends string = string> = {
20
+ readonly table: Table
21
+ readonly wildcard: true
22
+ }
23
+
24
+ export function create<Table extends string, Column extends string>(
25
+ table: Table,
26
+ column: Column,
27
+ ): ColumnRef<Table, Column> {
28
+ return {
29
+ table,
30
+ column,
31
+ as(alias) {
32
+ return { ...create(table, column), alias }
33
+ },
34
+ }
35
+ }
36
+ }
@@ -1,5 +1,10 @@
1
1
  // oxlint-disable typescript/no-explicit-any
2
- import type { CheckExpression, ColumnData, ColumnRef, MappingData } from "./metadata"
2
+ import type {
3
+ CheckExpression,
4
+ ColumnData,
5
+ ColumnRef,
6
+ MappingData,
7
+ } from "./metadata"
3
8
  import { Mapping } from "./Mapping"
4
9
  import type {
5
10
  BuildColumn,
@@ -49,7 +54,7 @@ function composeColumn(
49
54
  references(ref: ColumnRef) {
50
55
  return composeColumn({
51
56
  ...$meta,
52
- foreignKey: ref,
57
+ foreignKey: { table: ref.table, column: ref.column },
53
58
  })
54
59
  },
55
60
  }
@@ -1,15 +1,15 @@
1
- import type { ColumnData, ColumnRef, TableData } from "./metadata"
1
+ import type { ColumnData, TableData } from "./metadata"
2
2
  import type {
3
3
  BuildColumns,
4
4
  BuildColumnInner,
5
5
  BuildTable,
6
6
  } from "./schemaBuilder.types"
7
- import { SelectQueryBuilder } from "../queryBuilder/SelectQueryBuilder"
7
+ import { QuerySource } from "../queryBuilder/QuerySource"
8
8
 
9
- export function createTable<Columns extends BuildColumns>(
10
- name: string,
9
+ export function createTable<Columns extends BuildColumns, Name extends string>(
10
+ name: Name,
11
11
  defineColumns: Columns,
12
- ): BuildTable<Columns> {
12
+ ): BuildTable<Columns, Name> {
13
13
  const columns = Object.entries(defineColumns).map<ColumnData>(
14
14
  ([name, define]) => {
15
15
  const meta = (define as BuildColumnInner).$meta
@@ -31,27 +31,30 @@ export function createTable<Columns extends BuildColumns>(
31
31
  columns,
32
32
  constraints: [],
33
33
  }
34
- const by = new Proxy({}, {
35
- get(_, col: string) {
36
- return (value: unknown) => ({
37
- _tag: "RefBy" as const,
38
- table: defineTable,
39
- column: col,
40
- value,
41
- })
34
+ const by = new Proxy(
35
+ {},
36
+ {
37
+ get(_, col: string) {
38
+ return (value: unknown) => ({
39
+ _tag: "RefBy" as const,
40
+ table: defineTable,
41
+ column: col,
42
+ value,
43
+ })
44
+ },
42
45
  },
43
- })
44
- function select(...columns: ("*" | string)[]) {
45
- if (columns[0] === "*" || columns.length === 0) {
46
- return new SelectQueryBuilder($meta, "*")
47
- }
48
- return new SelectQueryBuilder($meta, columns)
49
- }
50
- const defineTable = { $kind: "table" as const, $meta, by, primaryKey, unique, check, select } as unknown as BuildTable<Columns>
51
- for (const col of Object.keys(defineColumns)) {
52
- // oxlint-disable-next-line typescript/no-explicit-any
53
- ;(defineTable as any)[col] = { table: name, column: col } satisfies ColumnRef
54
- }
46
+ )
47
+ const defineTable = Object.assign(
48
+ QuerySource.create({ kind: "table", name, tableData: $meta }),
49
+ {
50
+ $kind: "table" as const,
51
+ $meta,
52
+ by,
53
+ primaryKey,
54
+ unique,
55
+ check,
56
+ },
57
+ ) as unknown as BuildTable<Columns, Name>
55
58
  return defineTable
56
59
 
57
60
  function primaryKey(...columns: (keyof Columns)[]) {
@@ -70,7 +73,11 @@ export function createTable<Columns extends BuildColumns>(
70
73
  return defineTable
71
74
  }
72
75
 
73
- function check(expression: string | ((columns: Record<string, string>, table: TableData) => string)) {
76
+ function check(
77
+ expression:
78
+ | string
79
+ | ((columns: Record<string, string>, table: TableData) => string),
80
+ ) {
74
81
  $meta.constraints.push({
75
82
  type: "check",
76
83
  expression,
@@ -1,21 +1,33 @@
1
- // oxlint-disable typescript/no-explicit-any
2
1
  import { print } from "@sigitex/print"
3
2
  import { generateSelectNode } from "../queryGenerator/generateSelect"
4
3
  import type { SelectQueryBuilder } from "../queryBuilder/SelectQueryBuilder"
4
+ import { Projection } from "../queryBuilder/Projection"
5
+ import type { QueryScope } from "../queryBuilder/QueryScope"
6
+ import { QuerySource } from "../queryBuilder/QuerySource"
5
7
  import type { TableData, ViewData } from "./metadata"
6
- import type { BuildView, SchemaSelect } from "./schemaBuilder.types"
8
+ import type { BuildView } from "./schemaBuilder.types"
7
9
 
8
- export function createView<SelectColumns>(
9
- name: string,
10
- queryBuilder: SchemaSelect<SelectColumns, any>,
11
- ): BuildView<SelectColumns> {
10
+ export function createView<
11
+ Name extends string,
12
+ Columns extends QueryScope.Columns,
13
+ >(
14
+ name: Name,
15
+ queryBuilder: { readonly $type: QueryScope.State<"", Columns> },
16
+ ): BuildView<Columns, Name> {
12
17
  const builder = queryBuilder as unknown as SelectQueryBuilder
13
18
  const sql = print([generateSelectNode(builder.query)])
14
19
  const $meta: ViewData = { kind: "view", name, sql }
15
- const allColumns = [
16
- ...builder.table.columns,
17
- ...(builder.query.joins?.flatMap(j => j.target.tableData.columns) ?? []),
18
- ]
19
- const $tableData: TableData = { name, columns: allColumns, constraints: [] }
20
- return { $kind: "view", $meta, $tableData }
20
+ const $tableData: TableData = {
21
+ name,
22
+ columns: Projection.columns(builder.query),
23
+ constraints: [],
24
+ }
25
+ return Object.assign(
26
+ QuerySource.create({ kind: "table", name, tableData: $tableData }),
27
+ {
28
+ $kind: "view" as const,
29
+ $meta,
30
+ $tableData,
31
+ },
32
+ ) as unknown as BuildView<Columns, Name>
21
33
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./Mapping"
2
+ export { ColumnRef } from "./ColumnRef"
2
3
  export * from "./metadata"
3
4
  export * from "./schemaBuilder.types"
4
5
  export * from "./createSchema"