@sigitex/outlaw 1.0.1 → 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,15 +1,14 @@
1
- // oxlint-disable typescript/consistent-type-definitions
2
1
  // oxlint-disable typescript/no-explicit-any
2
+ // oxlint-disable typescript/method-signature-style
3
+ // oxlint-disable typescript/consistent-type-definitions
3
4
  import type { BINARY_OPERATORS, UNARY_OPERATORS } from "../queryBuilder"
4
5
  import type {
5
- BuildColumn,
6
- BuildTable,
7
- BuildView,
8
- ColumnRef,
6
+ InferColumn,
9
7
  SchemaMembers,
10
8
  TablesOf,
11
9
  ViewsOf,
12
10
  } from "../schemaBuilder"
11
+ import type { QueryScope } from "../queryBuilder/QueryScope"
13
12
 
14
13
  // Utilities
15
14
 
@@ -26,59 +25,82 @@ export type InsertRecord<R> = Partial<R> &
26
25
 
27
26
  // Deconstruction
28
27
 
29
- export type ColumnsOf<BT> =
30
- BT extends BuildTable<infer Columns> ? Columns : never
31
-
32
- export type ColumnTypeOf<BC> =
33
- BC extends BuildColumn<infer Type, infer Constraints>
34
- ? Constraints extends "notNull" | "primaryKey"
35
- ? Type
36
- : Type | null
37
- : never
28
+ export type ColumnsOf<BT> = BT extends { readonly $columns: infer Columns }
29
+ ? Columns
30
+ : never
38
31
 
39
32
  export type ColumnTypesOf<BCS> = {
40
- readonly [BCK in keyof BCS]: ColumnTypeOf<BCS[BCK]>
33
+ readonly [BCK in keyof BCS]: InferColumn<BCS[BCK]>
41
34
  }
42
35
 
43
36
  // API
44
37
 
45
38
  export type DefaultRow = Record<string, unknown>
46
39
 
47
- export type Connection = {
40
+ export type ConnectionOperations = {
48
41
  readonly query: <Row = DefaultRow>(sql: string) => Promise<Row[]>
49
42
  readonly script: (statements: string[]) => Promise<void>
50
- readonly reset?: () => Promise<void>
51
43
  }
52
44
 
53
- export type DatabaseApi<M extends SchemaMembers> = {
54
- readonly [K in keyof TablesOf<M>]: TableApi<ColumnsOf<TablesOf<M>[K]>>
55
- } & {
56
- readonly [K in keyof ViewsOf<M>]: ViewApi<ColumnsOfView<ViewsOf<M>[K]>>
57
- } & {
58
- readonly connection: Connection
45
+ export type TransactionWork<Result> = (
46
+ connection: TransactionalConnection,
47
+ ) => Promise<Result>
48
+
49
+ export type Transaction = <Result>(
50
+ work: TransactionWork<Result>,
51
+ ) => Promise<Result>
52
+
53
+ export type TransactionalConnection = ConnectionOperations & {
54
+ readonly transaction: Transaction
59
55
  }
60
56
 
61
- export type ColumnsOfView<BV> = BV extends BuildView<infer Columns> ? Columns : never
57
+ export type Connection = ConnectionOperations & {
58
+ readonly reset?: () => Promise<void>
59
+ readonly transaction?: Transaction
60
+ }
62
61
 
63
- export type ViewApi<Columns> = {
64
- /** Issue a `SELECT *` query to the API. */
65
- select(all: "*"): Select<Columns, Columns>
66
- /** Issue a `SELECT` query to the API with the chosen columns. */
67
- select<Column extends keyof Columns>(
68
- ...columns: Column[]
69
- ): Select<Pick<Columns, Column>, Columns>
62
+ export type DatabaseApi<
63
+ M extends SchemaMembers,
64
+ C extends Connection = Connection,
65
+ > = {
66
+ readonly [K in keyof TablesOf<M>]: TableApi<
67
+ ColumnsOf<TablesOf<M>[K]>,
68
+ QueryScope.Of<TablesOf<M>[K]>["name"]
69
+ >
70
+ } & {
71
+ readonly [K in keyof ViewsOf<M>]: ViewApi<
72
+ QueryScope.Of<ViewsOf<M>[K]>["columns"],
73
+ QueryScope.Of<ViewsOf<M>[K]>["name"]
74
+ >
75
+ } & {
76
+ readonly connection: C
77
+ readonly transaction: <Result>(
78
+ work: (
79
+ database: DatabaseApi<M, TransactionalConnection>,
80
+ ) => Promise<Result>,
81
+ ) => Promise<Result>
70
82
  }
71
83
 
72
- export type TableApi<Columns> = {
73
- /** Issue a `SELECT *` query to the API. */
74
- select(all: "*"): Select<Columns, Columns>
75
- /** Issue a `SELECT` query to the API with the chosen columns. */
76
- select<Column extends keyof Columns>(
77
- ...columns: Column[]
78
- ): Select<Pick<Columns, Column>, Columns>
84
+ export type ColumnsOfView<BV> = QueryScope.Of<BV>["columns"]
85
+
86
+ export type ViewApi<
87
+ Columns extends QueryScope.Columns,
88
+ Name extends string = string,
89
+ > = QueryScope.Composition<
90
+ QueryScope.Initial<QueryScope.State<Name, Columns>>,
91
+ true
92
+ >
93
+
94
+ export type TableApi<
95
+ Columns,
96
+ Name extends string = string,
97
+ > = QueryScope.Composition<
98
+ QueryScope.Initial<QueryScope.State<Name, QueryScope.SchemaColumns<Columns>>>,
99
+ true
100
+ > & {
79
101
  /** Issue an `INSERT` statement. */
80
102
  insert<InsertColumns extends Partial<ColumnTypesOf<Columns>>>(
81
- ...rows: InsertColumns[]
103
+ ...rows: [InsertColumns, ...InsertColumns[]]
82
104
  ): Insert<InsertColumns, Columns, number>
83
105
  /** Issue an `UPDATE` statement. */
84
106
  update(row: Partial<ColumnTypesOf<Columns>>): Update<Columns, number>
@@ -91,7 +113,7 @@ export interface HasWhereClause<Columns> {
91
113
  /** Add a condition where the given column equals the given value. */
92
114
  where<Column extends keyof Columns>(
93
115
  column: Column,
94
- value: Columns[Column],
116
+ value: InferColumn<Columns[Column]>,
95
117
  ): this
96
118
  /** Add a unary `WHERE` condition. */
97
119
  where<Column extends keyof Columns>(
@@ -102,63 +124,14 @@ export interface HasWhereClause<Columns> {
102
124
  where<Column extends keyof Columns>(
103
125
  column: Column,
104
126
  operator: BinaryOperator,
105
- value: ColumnTypeOf<Columns[Column]>,
127
+ value: InferColumn<Columns[Column]>,
106
128
  ): this
107
129
  }
108
130
 
109
- /** Select query API. */
110
- export interface Select<SelectColumns, Columns>
111
- extends HasWhereClause<Columns> {
112
- /** Issue the query, expecting an array of results. */
113
- fetch(): Promise<ColumnTypesOf<SelectColumns>[]>
114
- /** Issue the query, returning a single result, or throwing.. */
115
- first(): Promise<ColumnTypesOf<SelectColumns>>
116
- /** Add a `LIMIT` clause. */
117
- limit(n: number): this
118
- /** Add an `OFFSET` clause. */
119
- offset(n: number): this
120
- /** Add an `ORDER BY` sort expression. */
121
- orderBy(sorts: [keyof Columns, "asc" | "desc"][]): this
122
-
123
- /** Inner join on a table. */
124
- join<JC>(
125
- table: BuildTable<JC>,
126
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
127
- /** Inner join on a subquery. */
128
- join<SC>(
129
- query: Select<SC, any>,
130
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
131
-
132
- /** Left join on a table — joined columns become nullable. */
133
- leftJoin<JC>(
134
- table: BuildTable<JC>,
135
- ): Select<SelectColumns & Partial<ColumnTypesOf<JC>>, Columns & JC>
136
- /** Left join on a subquery — joined columns become nullable. */
137
- leftJoin<SC>(
138
- query: Select<SC, any>,
139
- ): Select<SelectColumns & Partial<ColumnTypesOf<SC>>, Columns & SC>
140
-
141
- /** Right join on a table. */
142
- rightJoin<JC>(
143
- table: BuildTable<JC>,
144
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
145
- /** Right join on a subquery. */
146
- rightJoin<SC>(
147
- query: Select<SC, any>,
148
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
149
-
150
- /** Cross join on a table. */
151
- crossJoin<JC>(
152
- table: BuildTable<JC>,
153
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
154
- /** Cross join on a subquery. */
155
- crossJoin<SC>(
156
- query: Select<SC, any>,
157
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
158
-
159
- /** Add an ON condition to the most recent join. */
160
- on(left: ColumnRef, operator: BinaryOperator, right: ColumnRef): this
161
- }
131
+ export type Select<
132
+ SelectColumns extends QueryScope.Columns,
133
+ Scope extends QueryScope,
134
+ > = QueryScope.Selection<SelectColumns, Scope, true>
162
135
 
163
136
  /** Insert statement API. */
164
137
  export interface Insert<InsertColumns, Columns, Returning> {
@@ -177,11 +150,11 @@ export interface Update<Columns, Returning> extends HasWhereClause<Columns> {
177
150
  /** Execute the statement. */
178
151
  execute(): Promise<Returning>
179
152
  /** Specify a `RETURNING *` clause. */
180
- returning(all: "*"): Update<Columns, Columns[]>
153
+ returning(all: "*"): Update<Columns, ColumnTypesOf<Columns>[]>
181
154
  /** Specify a `RETURNING` clause with the chosen columns. */
182
155
  returning<Column extends keyof Columns>(
183
- columns: Column[],
184
- ): Update<Columns, Pick<Columns, Column>[]>
156
+ ...columns: Column[]
157
+ ): Update<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
185
158
  }
186
159
 
187
160
  /** Delete statement API. */
@@ -189,11 +162,11 @@ export interface Delete<Columns, Returning> extends HasWhereClause<Columns> {
189
162
  /** Execute the statement. */
190
163
  execute(): Promise<Returning>
191
164
  /** Specify a `RETURNING *` clause. */
192
- returning(all: "*"): Delete<Columns, Columns[]>
165
+ returning(all: "*"): Delete<Columns, ColumnTypesOf<Columns>[]>
193
166
  /** Specify a `RETURNING` clause with the chosen columns. */
194
167
  returning<Column extends keyof Columns>(
195
- columns: Column[],
196
- ): Delete<Columns, Pick<Columns, Column>[]>
168
+ ...columns: Column[]
169
+ ): Delete<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
197
170
  }
198
171
 
199
172
  export type UnaryOperator = ElementOf<typeof UNARY_OPERATORS>
@@ -1,13 +1,38 @@
1
- import type { BuildTable, BuildView, SchemaMembers, Schema } from "../schemaBuilder"
2
- import type { Connection, DatabaseApi } from "./api.types"
1
+ import type {
2
+ BuildTable,
3
+ BuildView,
4
+ SchemaMembers,
5
+ Schema,
6
+ } from "../schemaBuilder"
7
+ import type {
8
+ Connection,
9
+ DatabaseApi,
10
+ TransactionalConnection,
11
+ } from "./api.types"
3
12
  import { DatabaseTable } from "./DatabaseTable"
4
13
  import { DatabaseView } from "./DatabaseView"
14
+ import { UnsupportedTransactionError } from "./UnsupportedTransactionError"
5
15
 
6
- export function createDatabase<M extends SchemaMembers>(
7
- connection: Connection,
16
+ export function createDatabase<M extends SchemaMembers, C extends Connection>(
17
+ connection: C,
8
18
  schema: Schema<M>,
9
19
  ) {
10
- const api = { connection }
20
+ const api = {
21
+ connection,
22
+ transaction: async <Result>(
23
+ work: (
24
+ database: DatabaseApi<M, TransactionalConnection>,
25
+ ) => Promise<Result>,
26
+ ) => {
27
+ const transaction = connection.transaction
28
+ if (!transaction) {
29
+ throw new UnsupportedTransactionError()
30
+ }
31
+ return transaction((scopedConnection) =>
32
+ work(createDatabase(scopedConnection, schema)),
33
+ )
34
+ },
35
+ }
11
36
  for (const [property, member] of Object.entries(schema.tables)) {
12
37
  const table = member as BuildTable<unknown>
13
38
  Object.defineProperty(api, property, {
@@ -20,5 +45,5 @@ export function createDatabase<M extends SchemaMembers>(
20
45
  value: new DatabaseView(connection, view.$tableData),
21
46
  })
22
47
  }
23
- return api as DatabaseApi<M>
48
+ return api as DatabaseApi<M, C>
24
49
  }
package/src/api/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./api.types"
2
2
  export * from "./DatabaseTable"
3
3
  export * from "./DatabaseView"
4
4
  export * from "./createDatabase"
5
+ export * from "./UnsupportedTransactionError"
package/src/bun/bun.ts CHANGED
@@ -1,27 +1,140 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import type { DefaultRow, Connection } from "../api/api.types"
2
+ import type {
3
+ Connection,
4
+ DefaultRow,
5
+ TransactionalConnection,
6
+ TransactionWork,
7
+ } from "../api/api.types"
8
+
9
+ type TransactionState = {
10
+ active: boolean
11
+ nextSavepoint: number
12
+ }
3
13
 
4
14
  export class BunConnection implements Connection {
5
15
  private readonly db: Database
16
+ private pending = Promise.resolve()
6
17
 
7
18
  constructor(db: Database) {
8
19
  this.db = db
9
20
  }
10
21
 
11
22
  async query<Row = DefaultRow>(sql: string) {
12
- return this.db.query(sql).all() as Row[]
23
+ return this.exclusive(() => query<Row>(this.db, sql))
13
24
  }
14
25
 
15
26
  async script(statements: string[]) {
16
- this.db.run("BEGIN")
27
+ await this.exclusive(() => script(this.db, statements, "BEGIN"))
28
+ }
29
+
30
+ async transaction<Result>(work: TransactionWork<Result>) {
31
+ return this.exclusive(async () => {
32
+ await run(this.db, "BEGIN IMMEDIATE")
33
+ const state: TransactionState = { active: true, nextSavepoint: 0 }
34
+ const connection = new BunTransactionConnection(this.db, state)
35
+
36
+ try {
37
+ const result = await work(connection)
38
+ connection.close()
39
+ await run(this.db, "COMMIT")
40
+ return result
41
+ } catch (error) {
42
+ connection.close()
43
+ await run(this.db, "ROLLBACK")
44
+ throw error
45
+ } finally {
46
+ state.active = false
47
+ }
48
+ })
49
+ }
50
+
51
+ private async exclusive<Result>(work: () => Promise<Result>) {
52
+ const previous = this.pending
53
+ let release!: () => void
54
+ this.pending = new Promise<void>((resolve) => {
55
+ release = resolve
56
+ })
57
+
58
+ await previous
17
59
  try {
18
- for (const s of statements) {
19
- this.db.run(s)
60
+ return await work()
61
+ } finally {
62
+ release()
63
+ }
64
+ }
65
+ }
66
+
67
+ class BunTransactionConnection implements TransactionalConnection {
68
+ private readonly db: Database
69
+ private readonly state: TransactionState
70
+ private active = true
71
+
72
+ constructor(db: Database, state: TransactionState) {
73
+ this.db = db
74
+ this.state = state
75
+ }
76
+
77
+ async query<Row = DefaultRow>(sql: string) {
78
+ this.assertActive()
79
+ return query<Row>(this.db, sql)
80
+ }
81
+
82
+ async script(statements: string[]) {
83
+ this.assertActive()
84
+ await this.transaction(async (connection) => {
85
+ for (const statement of statements) {
86
+ await connection.query(statement)
20
87
  }
21
- this.db.run("COMMIT")
88
+ })
89
+ }
90
+
91
+ async transaction<Result>(work: TransactionWork<Result>) {
92
+ this.assertActive()
93
+ const name = `outlaw_transaction_${++this.state.nextSavepoint}`
94
+ await run(this.db, `SAVEPOINT ${name}`)
95
+ const connection = new BunTransactionConnection(this.db, this.state)
96
+
97
+ try {
98
+ const result = await work(connection)
99
+ connection.close()
100
+ await run(this.db, `RELEASE SAVEPOINT ${name}`)
101
+ return result
22
102
  } catch (error) {
23
- this.db.run("ROLLBACK")
103
+ connection.close()
104
+ await run(this.db, `ROLLBACK TO SAVEPOINT ${name}`)
105
+ await run(this.db, `RELEASE SAVEPOINT ${name}`)
24
106
  throw error
25
107
  }
26
108
  }
109
+
110
+ close() {
111
+ this.active = false
112
+ }
113
+
114
+ private assertActive() {
115
+ if (!this.active || !this.state.active) {
116
+ throw new Error("Transaction connection is no longer active.")
117
+ }
118
+ }
119
+ }
120
+
121
+ async function query<Row>(db: Database, sql: string) {
122
+ return db.query(sql).all() as Row[]
123
+ }
124
+
125
+ async function run(db: Database, sql: string) {
126
+ db.run(sql)
127
+ }
128
+
129
+ async function script(db: Database, statements: string[], begin: "BEGIN") {
130
+ await run(db, begin)
131
+ try {
132
+ for (const statement of statements) {
133
+ await run(db, statement)
134
+ }
135
+ await run(db, "COMMIT")
136
+ } catch (error) {
137
+ await run(db, "ROLLBACK")
138
+ throw error
139
+ }
27
140
  }
@@ -1,5 +1,5 @@
1
1
  import type { Fixtures, Schema, SchemaMembers, Seeds } from "../schemaBuilder"
2
- import type { Connection, DefaultRow } from "../api"
2
+ import type { Connection, DefaultRow, Transaction } from "../api"
3
3
  import type { SchemaHack } from "./cowboyMigration.types"
4
4
  import { CowboyMigrator } from "./CowboyMigrator"
5
5
  import { CowboySeeder } from "./CowboySeeder"
@@ -15,36 +15,68 @@ export class CowboyConnection implements Connection {
15
15
  private readonly raw: Connection
16
16
  private readonly schema: Schema<SchemaMembers>
17
17
  private readonly options: CowboyOptions
18
- private shotFirst = false
18
+ private readiness?: Promise<void>
19
+ readonly transaction?: Transaction
19
20
 
20
- constructor(raw: Connection, schema: Schema<SchemaMembers>, options?: CowboyOptions) {
21
+ constructor(
22
+ raw: Connection,
23
+ schema: Schema<SchemaMembers>,
24
+ options?: CowboyOptions,
25
+ ) {
21
26
  this.raw = raw
22
27
  this.schema = schema
23
28
  this.options = options ?? {}
29
+ const transaction = raw.transaction?.bind(raw)
30
+ if (transaction) {
31
+ this.transaction = async (work) => {
32
+ await this.ready()
33
+ return transaction(work)
34
+ }
35
+ }
24
36
  }
25
37
 
26
38
  async query<Row = DefaultRow>(sql: string) {
27
- if (!this.shotFirst) {
28
- await this.shotgun()
29
- }
39
+ await this.ready()
30
40
  return this.raw.query(sql) as Promise<Row[]>
31
41
  }
32
42
 
33
43
  async script(statements: string[]) {
34
- if (!this.shotFirst) {
35
- await this.shotgun()
36
- }
44
+ await this.ready()
37
45
  await this.raw.script(statements)
38
46
  }
39
47
 
40
48
  async reset() {
41
- this.shotFirst = false
49
+ try {
50
+ await this.readiness
51
+ } catch {
52
+ // A failed initialization is already reset by ready().
53
+ }
54
+ this.readiness = undefined
55
+ }
56
+
57
+ private ready() {
58
+ if (this.readiness) {
59
+ return this.readiness
60
+ }
61
+
62
+ const readiness = this.shotgun().catch((error: unknown) => {
63
+ if (this.readiness === readiness) {
64
+ this.readiness = undefined
65
+ }
66
+ throw error
67
+ })
68
+ this.readiness = readiness
69
+ return readiness
42
70
  }
43
71
 
44
72
  private async shotgun() {
45
- this.shotFirst = true
46
- await new CowboyMigrator(this.raw, this.schema, this.options.hacks ?? []).migrate()
47
- const hasSeeds = this.options.seeds || (this.options.runFixtures && this.options.fixtures)
73
+ await new CowboyMigrator(
74
+ this.raw,
75
+ this.schema,
76
+ this.options.hacks ?? [],
77
+ ).migrate()
78
+ const hasSeeds =
79
+ this.options.seeds || (this.options.runFixtures && this.options.fixtures)
48
80
  if (hasSeeds) {
49
81
  await new CowboySeeder(
50
82
  this.raw,
@@ -11,6 +11,10 @@ export namespace Format {
11
11
  return SqlString.escapeId(name)
12
12
  }
13
13
 
14
+ export function identifier(name: string) {
15
+ return SqlString.escapeId(name, true)
16
+ }
17
+
14
18
  export function number(number: number) {
15
19
  if (typeof number !== "number" && typeof number !== "bigint") {
16
20
  throw new Error("Not a number.")
@@ -1,4 +1,7 @@
1
1
  declare module "sqlstring-sqlite" {
2
2
  export function escape(text: string | null | undefined): string
3
- export function escapeId(text: string | null | undefined): string
3
+ export function escapeId(
4
+ text: string | null | undefined,
5
+ forbidQualified?: boolean,
6
+ ): string
4
7
  }
@@ -20,13 +20,15 @@ export class DeleteBuilder implements Delete<any, any> {
20
20
 
21
21
  async execute(): Promise<any> {
22
22
  const result = await this.connection.query(generateDelete(this.command))
23
- return this.command.returning ? Mappings.results(this.table, result) : result
23
+ return this.command.returning
24
+ ? Mappings.results(this.table, result)
25
+ : result
24
26
  }
25
27
 
26
28
  returning(all: "*"): Delete<any, any[]>
27
- returning(columns: string[]): Delete<any, Pick<any, any>[]>
28
- returning(columns: any): Delete<any, any[]> | Delete<any, Pick<any, any>[]> {
29
- this.command.returning = columns === "*" ? ["*"] : columns
29
+ returning(...columns: string[]): Delete<any, Pick<any, any>[]>
30
+ returning(...columns: ("*" | string)[]): Delete<any, any[]> {
31
+ this.command.returning = columns[0] === "*" ? ["*"] : columns
30
32
  return this
31
33
  }
32
34
 
@@ -41,7 +41,7 @@ export namespace Mappings {
41
41
  return rows.map((row) => {
42
42
  const mapped: Record<string, unknown> = { ...row }
43
43
  for (const [name, mapping] of mappings) {
44
- if (name in mapped) {
44
+ if (name in mapped && mapped[name] !== null) {
45
45
  mapped[name] = mapping.from(mapped[name])
46
46
  }
47
47
  }
@@ -0,0 +1,83 @@
1
+ import type { ColumnRef } from "../schemaBuilder/ColumnRef"
2
+ import type { ColumnData } from "../schemaBuilder/metadata"
3
+ import type { ColumnIdentifier, SelectQuery } from "./queryBuilders.types"
4
+ import { QuerySource } from "./QuerySource"
5
+
6
+ export type Projection =
7
+ | "*"
8
+ | { column: ColumnIdentifier; alias?: string }
9
+ | ColumnRef.Wildcard
10
+
11
+ export namespace Projection {
12
+ export type Input =
13
+ | "*"
14
+ | string
15
+ | ColumnRef
16
+ | ColumnRef.Aliased<string, string, string>
17
+ | ColumnRef.Wildcard
18
+
19
+ export function create(scope: QuerySource.Scope, input: Input): Projection {
20
+ if (input === "*") {
21
+ return input
22
+ }
23
+ if (typeof input === "object" && "wildcard" in input) {
24
+ return { ...input }
25
+ }
26
+ const column = QuerySource.identifier(scope, input)
27
+ const alias =
28
+ typeof input === "object" && "alias" in input
29
+ ? input.alias
30
+ : column.table === undefined && scope.coalesced.has(column.column)
31
+ ? column.column
32
+ : undefined
33
+ return {
34
+ column,
35
+ ...(alias !== undefined ? { alias } : {}),
36
+ }
37
+ }
38
+
39
+ export function columns(query: SelectQuery): ColumnData[] {
40
+ const scope = QuerySource.scope(query)
41
+ const selected = query.selected.flatMap((projection) => {
42
+ if (projection === "*") {
43
+ return scope.output
44
+ }
45
+ if ("wildcard" in projection) {
46
+ return scope.sources
47
+ .filter(
48
+ (source) => QuerySource.qualifier(source) === projection.table,
49
+ )
50
+ .flatMap((source) => source.tableData.columns)
51
+ }
52
+ return QuerySource.resolve(scope, projection.column).map((column) =>
53
+ renameColumn(column, projection.alias ?? column.name),
54
+ )
55
+ })
56
+ const outputs = new Map<string, ColumnData[]>()
57
+ for (const column of selected) {
58
+ const existing = outputs.get(column.name)
59
+ if (existing) {
60
+ existing.push(column)
61
+ } else {
62
+ outputs.set(column.name, [column])
63
+ }
64
+ }
65
+ return Array.from(outputs.values(), merge)
66
+ }
67
+
68
+ export function merge(columns: ColumnData[]): ColumnData {
69
+ const first = columns[0]
70
+ return {
71
+ ...first,
72
+ notNull: columns.every((column) => column.notNull || !!column.primaryKey),
73
+ primaryKey: undefined,
74
+ mapping: columns.every((column) => column.mapping === first.mapping)
75
+ ? first.mapping
76
+ : undefined,
77
+ }
78
+ }
79
+
80
+ function renameColumn(column: ColumnData, name: string): ColumnData {
81
+ return { ...column, name }
82
+ }
83
+ }