@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.
package/README.md CHANGED
@@ -238,17 +238,182 @@ db.users.select("*")
238
238
 
239
239
  ### Joins
240
240
 
241
+ Compose sources **before** selecting results. Examples in this section use a
242
+ `Connection` named `connection` and these schema definitions:
243
+
241
244
  ```ts
242
- db.users.select("*")
243
- .join(posts).on(users.id, "=", posts.userId)
245
+ const users = createTable("users", {
246
+ id: integer.primaryKey,
247
+ name: text.notNull,
248
+ managerId: integer,
249
+ })
250
+ const posts = createTable("posts", {
251
+ id: integer.primaryKey,
252
+ userId: integer.notNull,
253
+ tenantId: integer.notNull,
254
+ title: text.notNull,
255
+ published: integer.map.boolean.notNull,
256
+ createdAt: integer.map.timestamp.notNull,
257
+ })
258
+ const profiles = createTable("profiles", {
259
+ userId: integer.notNull,
260
+ tenantId: integer.notNull,
261
+ bio: text,
262
+ })
263
+ const db = createDatabase(connection, createSchema({ users, posts, profiles }))
264
+ ```
265
+
266
+ Strings work throughout joined queries. Bare names retain SQLite name resolution;
267
+ qualified strings identify a specific source. Only selected columns appear in the
268
+ result:
269
+
270
+ ```ts
271
+ await db.users
272
+ .join(posts)
273
+ .on("users.id", "=", "posts.userId")
274
+ .select("name", "title")
275
+ .where("title", "Hello")
276
+ .orderBy([["name", "asc"]])
277
+ .limit(20)
278
+ .offset(40)
244
279
  .fetch()
280
+ ```
245
281
 
246
- db.users.select("*")
247
- .leftJoin(posts).on(users.id, "=", posts.userId)
282
+ Refs and strings can be mixed. Projection aliases are optional; use them when
283
+ distinct output names are useful. `users.all` selects `users.*`, not every joined
284
+ column:
285
+
286
+ ```ts
287
+ await db.users
288
+ .leftJoin(posts)
289
+ .on(users.id, "=", posts.userId)
290
+ .select(users.id.as("userId"), posts.id.as("postId"), posts.title)
248
291
  .fetch()
292
+
293
+ await db.users
294
+ .join(posts)
295
+ .on("users.id", "=", posts.userId)
296
+ .select(users.all, posts.title)
297
+ .where(posts.published, true)
298
+ .orderBy([[posts.createdAt, "desc"]])
299
+ .fetch()
300
+ ```
301
+
302
+ #### Join forms and constraints
303
+
304
+ `join`, `leftJoin`, `rightJoin`, `fullJoin`, and `crossJoin` each accept tables,
305
+ views, or SELECT subqueries. Constraints are optional for every form. Repeated
306
+ `on()` calls combine with `AND` on the latest join. `using()` accepts one or more
307
+ common column names and cannot be combined with `on()` on that same join.
308
+
309
+ ```ts
310
+ await db.users.rightJoin(posts).on(users.id, "=", posts.userId)
311
+ .select("name", "title").fetch()
312
+ await db.users.fullJoin(posts).on(users.id, "=", posts.userId)
313
+ .select("name", "title").fetch()
314
+ await db.users.crossJoin(posts).select("name", "title").fetch()
315
+ await db.users.join(posts).select("name", "title").fetch()
316
+
317
+ await db.users.crossJoin(posts)
318
+ .on(users.id, "=", posts.userId)
319
+ .on(users.name, "!=", posts.title)
320
+ .select("name", "title").fetch()
321
+
322
+ await db.posts.join(profiles).using("userId", "tenantId").select("*").fetch()
323
+ await db.posts.crossJoin(profiles).using("userId").select("userId").fetch()
324
+ await db.posts.fullJoin(profiles).using("userId").select("userId").fetch()
325
+ ```
326
+
327
+ Both operands of `on(left, operator, right)` are **column identifiers**, including
328
+ strings. By contrast, the right-hand value of `where(column, value)` or
329
+ `where(column, operator, value)` is always a **literal value**, even if it happens
330
+ to equal a column name. Literal-value ON expressions are not supported.
331
+
332
+ `select("*")` emits an actual `*`, preserving SQLite's common-column suppression
333
+ with `USING`. Qualified refs and wildcards retain each source's own columns.
334
+ Unqualified common keys from right/full joins include surviving right-side values.
335
+ For explicitly projected common columns coalesced by `FULL JOIN ... USING`, Outlaw
336
+ emits a same-name `AS` solely to prevent SQLite from retaining identifier quotes in
337
+ the output key. No source names or distinct output names are invented.
338
+
339
+ `RIGHT JOIN` and `FULL JOIN` require **SQLite 3.39.0 or newer**. Outlaw does not probe
340
+ capabilities, emulate unsupported joins, validate rows, or preflight SQL structure.
341
+ SQLite errors, including ambiguous bare identifiers, propagate from the connection.
342
+ `NATURAL` joins and comma-separated sources are deferred. Join verification uses
343
+ local Bun SQLite; no live D1 verification is claimed.
344
+
345
+ #### Self-joins, subqueries, and views
346
+
347
+ Source aliases are explicit and non-mutating:
348
+
349
+ ```ts
350
+ const managers = users.as("managers")
351
+ await db.users.leftJoin(managers)
352
+ .on(users.managerId, "=", managers.id)
353
+ .select(users.name.as("employeeName"), managers.name.as("managerName"))
354
+ .orderBy([["employeeName", "asc"]])
355
+ .fetch()
356
+ ```
357
+
358
+ Subqueries expose only their projected outputs. Aliases remain optional; both
359
+ forms execute inline as part of one SELECT. Aliasing or embedding snapshots the
360
+ query, so later changes to the original builder do not alter an embedded query:
361
+
362
+ ```ts
363
+ const publishedPosts = db.posts
364
+ .select("id", "userId", "title", "createdAt")
365
+ .where("published", true)
366
+ .as("publishedPosts")
367
+
368
+ await db.users.join(publishedPosts)
369
+ .on(users.id, "=", publishedPosts.userId)
370
+ .select("name", publishedPosts.title, publishedPosts.createdAt)
371
+ .fetch()
372
+
373
+ await db.users.join(db.posts.select("userId", "title"))
374
+ .on("users.id", "=", "userId")
375
+ .select("name", "title")
376
+ .fetch()
377
+
378
+ const userPosts = createView("user_posts",
379
+ users.leftJoin(posts).on(users.id, "=", posts.userId)
380
+ .select(users.name.as("userName"), posts.title),
381
+ )
382
+ ```
383
+
384
+ Schema-level queries need no connection. Add `userPosts` to `createSchema` to query
385
+ the view through the database API; it exposes only `userName` and nullable `title`.
386
+
387
+ #### Result types and duplicate names
388
+
389
+ Results stay flat. Left joins add `null` to incoming columns; right joins add
390
+ `null` to all accumulated left sources; full joins add it to both sides. Selected
391
+ properties remain required, never optional. For example, selecting `users.name`
392
+ and `posts.title` after a left join yields `{ name: string; title: string | null }`.
393
+
394
+ Mappings follow the selected source and output alias, including through subqueries
395
+ and views. Outer-join SQL nulls stay null; mapped values decode once at execution.
396
+ Filters use the referenced source's storage mapping.
397
+
398
+ Duplicate output names are allowed without aliases, nesting, or automatic
399
+ renaming. The connection's object-row behavior determines which duplicate value
400
+ survives; ordinary collisions have conservative union types. Incompatible mapping
401
+ collisions stay undecoded and are typed as `unknown`. Explicit distinct aliases
402
+ preserve precise field types when both values are needed.
403
+
404
+ #### Migrating the old join chain
405
+
406
+ `db.users.select(...).join(posts).on(...)` is removed, including schema-level
407
+ join-after-select calls. Move `select(...)` after source composition:
408
+
409
+ ```ts
410
+ await db.users.join(posts).on(users.id, "=", posts.userId)
411
+ .select(users.name, posts.title).fetch()
249
412
  ```
250
413
 
251
- Join types: `join`, `leftJoin`, `rightJoin`, `crossJoin`. Each accepts a table or a subquery.
414
+ Joined columns are no longer implicitly appended to the projection. Select the
415
+ outputs needed explicitly, or use `*`/qualified wildcards. Existing single-table
416
+ SELECT and unrelated INSERT/UPDATE/DELETE chains retain their interfaces.
252
417
 
253
418
  ### Insert
254
419
 
package/package.json CHANGED
@@ -49,5 +49,5 @@
49
49
  "files": [
50
50
  "src"
51
51
  ],
52
- "version": "1.2.0"
52
+ "version": "2.0.0"
53
53
  }
@@ -1,36 +1,50 @@
1
1
  // oxlint-disable typescript/no-explicit-any
2
- import type {
3
- Delete,
4
- Connection,
5
- Insert,
6
- Select,
7
- TableApi,
8
- Update,
9
- } from "./api.types"
2
+ import type { Delete, Connection, Insert, Update } from "./api.types"
10
3
  import type { TableData } from "../schemaBuilder"
11
- import {
12
- DeleteBuilder,
13
- InsertBuilder,
14
- SelectBuilder,
15
- UpdateBuilder,
16
- } from "../queryBuilder"
17
-
18
- export class DatabaseTable implements TableApi<any> {
19
- private connection: Connection
4
+ import { DeleteBuilder, InsertBuilder, UpdateBuilder } from "../queryBuilder"
5
+ import { QuerySourceBuilder } from "../queryBuilder/QuerySourceBuilder"
6
+ import type { QuerySource } from "../queryBuilder/QuerySource"
7
+ import type { Projection } from "../queryBuilder/Projection"
8
+
9
+ export class DatabaseTable {
10
+ private readonly connection: Connection
20
11
  private table: TableData
12
+ private readonly source: QuerySource.Data
21
13
 
22
14
  constructor(connection: Connection, table: TableData) {
23
15
  this.connection = connection
24
16
  this.table = table
17
+ this.source = { kind: "table", name: table.name, tableData: table }
18
+ }
19
+
20
+ select(...columns: Projection.Input[]) {
21
+ return new QuerySourceBuilder(this.source, this.connection).select(
22
+ ...columns,
23
+ )
24
+ }
25
+
26
+ join(target: QuerySource.Input) {
27
+ return new QuerySourceBuilder(this.source, this.connection).join(target)
28
+ }
29
+
30
+ leftJoin(target: QuerySource.Input) {
31
+ return new QuerySourceBuilder(this.source, this.connection).leftJoin(target)
32
+ }
33
+
34
+ rightJoin(target: QuerySource.Input) {
35
+ return new QuerySourceBuilder(this.source, this.connection).rightJoin(
36
+ target,
37
+ )
38
+ }
39
+
40
+ fullJoin(target: QuerySource.Input) {
41
+ return new QuerySourceBuilder(this.source, this.connection).fullJoin(target)
25
42
  }
26
43
 
27
- select(all: "*"): Select<any, any>
28
- select<Column extends string>(
29
- ...columns: Column[]
30
- ): Select<Pick<any, any>, any>
31
- select(...columns: ("*" | string)[]): Select<any, any> {
32
- const selected = columns[0] === "*" || columns.length === 0 ? "*" : columns
33
- return new SelectBuilder(this.connection, this.table, selected)
44
+ crossJoin(target: QuerySource.Input) {
45
+ return new QuerySourceBuilder(this.source, this.connection).crossJoin(
46
+ target,
47
+ )
34
48
  }
35
49
 
36
50
  insert(...rows: [Partial<any>, ...Partial<any>[]]): Insert<any, any, number>
@@ -1,23 +1,45 @@
1
- // oxlint-disable typescript/no-explicit-any
2
- import type { Connection, Select, ViewApi } from "./api.types"
1
+ import type { Connection } from "./api.types"
3
2
  import type { TableData } from "../schemaBuilder"
4
- import { SelectBuilder } from "../queryBuilder"
3
+ import { QuerySourceBuilder } from "../queryBuilder/QuerySourceBuilder"
4
+ import type { QuerySource } from "../queryBuilder/QuerySource"
5
+ import type { Projection } from "../queryBuilder/Projection"
5
6
 
6
- export class DatabaseView implements ViewApi<any> {
7
- private connection: Connection
8
- private tableData: TableData
7
+ export class DatabaseView {
8
+ private readonly connection: Connection
9
+ private readonly source: QuerySource.Data
9
10
 
10
11
  constructor(connection: Connection, tableData: TableData) {
11
12
  this.connection = connection
12
- this.tableData = tableData
13
+ this.source = { kind: "table", name: tableData.name, tableData }
13
14
  }
14
15
 
15
- select(all: "*"): Select<any, any>
16
- select<Column extends string>(
17
- ...columns: Column[]
18
- ): Select<Pick<any, any>, any>
19
- select(...columns: ("*" | string)[]): Select<any, any> {
20
- const selected = columns[0] === "*" || columns.length === 0 ? "*" : columns
21
- return new SelectBuilder(this.connection, this.tableData, selected)
16
+ select(...columns: Projection.Input[]) {
17
+ return new QuerySourceBuilder(this.source, this.connection).select(
18
+ ...columns,
19
+ )
20
+ }
21
+
22
+ join(target: QuerySource.Input) {
23
+ return new QuerySourceBuilder(this.source, this.connection).join(target)
24
+ }
25
+
26
+ leftJoin(target: QuerySource.Input) {
27
+ return new QuerySourceBuilder(this.source, this.connection).leftJoin(target)
28
+ }
29
+
30
+ rightJoin(target: QuerySource.Input) {
31
+ return new QuerySourceBuilder(this.source, this.connection).rightJoin(
32
+ target,
33
+ )
34
+ }
35
+
36
+ fullJoin(target: QuerySource.Input) {
37
+ return new QuerySourceBuilder(this.source, this.connection).fullJoin(target)
38
+ }
39
+
40
+ crossJoin(target: QuerySource.Input) {
41
+ return new QuerySourceBuilder(this.source, this.connection).crossJoin(
42
+ target,
43
+ )
22
44
  }
23
45
  }
@@ -3,14 +3,12 @@
3
3
  // oxlint-disable typescript/consistent-type-definitions
4
4
  import type { BINARY_OPERATORS, UNARY_OPERATORS } from "../queryBuilder"
5
5
  import type {
6
- BuildTable,
7
- BuildView,
8
- ColumnRef,
9
6
  InferColumn,
10
7
  SchemaMembers,
11
8
  TablesOf,
12
9
  ViewsOf,
13
10
  } from "../schemaBuilder"
11
+ import type { QueryScope } from "../queryBuilder/QueryScope"
14
12
 
15
13
  // Utilities
16
14
 
@@ -27,8 +25,9 @@ export type InsertRecord<R> = Partial<R> &
27
25
 
28
26
  // Deconstruction
29
27
 
30
- export type ColumnsOf<BT> =
31
- BT extends BuildTable<infer Columns> ? Columns : never
28
+ export type ColumnsOf<BT> = BT extends { readonly $columns: infer Columns }
29
+ ? Columns
30
+ : never
32
31
 
33
32
  export type ColumnTypesOf<BCS> = {
34
33
  readonly [BCK in keyof BCS]: InferColumn<BCS[BCK]>
@@ -64,9 +63,15 @@ export type DatabaseApi<
64
63
  M extends SchemaMembers,
65
64
  C extends Connection = Connection,
66
65
  > = {
67
- readonly [K in keyof TablesOf<M>]: TableApi<ColumnsOf<TablesOf<M>[K]>>
66
+ readonly [K in keyof TablesOf<M>]: TableApi<
67
+ ColumnsOf<TablesOf<M>[K]>,
68
+ QueryScope.Of<TablesOf<M>[K]>["name"]
69
+ >
68
70
  } & {
69
- readonly [K in keyof ViewsOf<M>]: ViewApi<ColumnsOfView<ViewsOf<M>[K]>>
71
+ readonly [K in keyof ViewsOf<M>]: ViewApi<
72
+ QueryScope.Of<ViewsOf<M>[K]>["columns"],
73
+ QueryScope.Of<ViewsOf<M>[K]>["name"]
74
+ >
70
75
  } & {
71
76
  readonly connection: C
72
77
  readonly transaction: <Result>(
@@ -76,25 +81,23 @@ export type DatabaseApi<
76
81
  ) => Promise<Result>
77
82
  }
78
83
 
79
- export type ColumnsOfView<BV> =
80
- BV extends BuildView<infer Columns> ? Columns : never
84
+ export type ColumnsOfView<BV> = QueryScope.Of<BV>["columns"]
81
85
 
82
- export type ViewApi<Columns> = {
83
- /** Issue a `SELECT *` query to the API. */
84
- select(all: "*"): Select<Columns, Columns>
85
- /** Issue a `SELECT` query to the API with the chosen columns. */
86
- select<Column extends keyof Columns>(
87
- ...columns: Column[]
88
- ): Select<Pick<Columns, Column>, Columns>
89
- }
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
+ >
90
93
 
91
- export type TableApi<Columns> = {
92
- /** Issue a `SELECT *` query to the API. */
93
- select(all: "*"): Select<Columns, Columns>
94
- /** Issue a `SELECT` query to the API with the chosen columns. */
95
- select<Column extends keyof Columns>(
96
- ...columns: Column[]
97
- ): Select<Pick<Columns, Column>, Columns>
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
+ > & {
98
101
  /** Issue an `INSERT` statement. */
99
102
  insert<InsertColumns extends Partial<ColumnTypesOf<Columns>>>(
100
103
  ...rows: [InsertColumns, ...InsertColumns[]]
@@ -125,61 +128,10 @@ export interface HasWhereClause<Columns> {
125
128
  ): this
126
129
  }
127
130
 
128
- /** Select query API. */
129
- export interface Select<
130
- SelectColumns,
131
- Columns,
132
- > extends HasWhereClause<Columns> {
133
- /** Issue the query, expecting an array of results. */
134
- fetch(): Promise<ColumnTypesOf<SelectColumns>[]>
135
- /** Issue the query, returning a single result, or throwing.. */
136
- first(): Promise<ColumnTypesOf<SelectColumns>>
137
- /** Add a `LIMIT` clause. */
138
- limit(n: number): this
139
- /** Add an `OFFSET` clause. */
140
- offset(n: number): this
141
- /** Add an `ORDER BY` sort expression. */
142
- orderBy(sorts: [keyof Columns, "asc" | "desc"][]): this
143
-
144
- /** Inner join on a table. */
145
- join<JC>(
146
- table: BuildTable<JC>,
147
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
148
- /** Inner join on a subquery. */
149
- join<SC>(
150
- query: Select<SC, any>,
151
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
152
-
153
- /** Left join on a table — joined columns become nullable. */
154
- leftJoin<JC>(
155
- table: BuildTable<JC>,
156
- ): Select<SelectColumns & Partial<ColumnTypesOf<JC>>, Columns & JC>
157
- /** Left join on a subquery — joined columns become nullable. */
158
- leftJoin<SC>(
159
- query: Select<SC, any>,
160
- ): Select<SelectColumns & Partial<ColumnTypesOf<SC>>, Columns & SC>
161
-
162
- /** Right join on a table. */
163
- rightJoin<JC>(
164
- table: BuildTable<JC>,
165
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
166
- /** Right join on a subquery. */
167
- rightJoin<SC>(
168
- query: Select<SC, any>,
169
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
170
-
171
- /** Cross join on a table. */
172
- crossJoin<JC>(
173
- table: BuildTable<JC>,
174
- ): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
175
- /** Cross join on a subquery. */
176
- crossJoin<SC>(
177
- query: Select<SC, any>,
178
- ): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
179
-
180
- /** Add an ON condition to the most recent join. */
181
- on(left: ColumnRef, operator: BinaryOperator, right: ColumnRef): this
182
- }
131
+ export type Select<
132
+ SelectColumns extends QueryScope.Columns,
133
+ Scope extends QueryScope,
134
+ > = QueryScope.Selection<SelectColumns, Scope, true>
183
135
 
184
136
  /** Insert statement API. */
185
137
  export interface Insert<InsertColumns, Columns, Returning> {
@@ -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
  }
@@ -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
+ }