kysely-ddl 0.1.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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Column builders as chains, like in drizzle.
3
+ *
4
+ * The set of types is what application schemas actually need:
5
+ * uuid / varchar / integer / bigint / boolean / numeric / timestamp / jsonb,
6
+ * plus `enum`, a varchar with an automatic check constraint.
7
+ * Adding a type is one line at the bottom of the file.
8
+ *
9
+ * ── Why there is no `mode` ───────────────────────────────────────────────────
10
+ *
11
+ * In drizzle, `bigint({ mode: 'number' })` and `numeric({ mode: 'bigint' })` change
12
+ * the column's TS type. But the mode describes the behaviour of drizzle's RUNTIME:
13
+ * it converts the values itself. This library has no runtime, the driver does the
14
+ * reading, so the column type is set once and matches what `pg` returns:
15
+ *
16
+ * int8 -> string (no precision loss beyond 2^53)
17
+ * numeric -> string (no float error)
18
+ *
19
+ * For another type use `$type<T>()` plus an explicit conversion on your side.
20
+ */
21
+ import type { Sql } from './sql.ts';
22
+ /** Column config at the type level. */
23
+ export interface ColumnCfg {
24
+ /** The explicitly set column name; `undefined` means it is taken from the property name. */
25
+ readonly name: string | undefined;
26
+ readonly data: unknown;
27
+ readonly notNull: boolean;
28
+ readonly hasDefault: boolean;
29
+ readonly array: boolean;
30
+ readonly identity: 'always' | 'byDefault' | undefined;
31
+ /** `enum()` values: `defineTable` builds a check from them. */
32
+ readonly enumValues: readonly string[] | undefined;
33
+ /** A jsonb column: values for writes go through `jsonb()`, see the kysely layer. */
34
+ readonly json: boolean;
35
+ }
36
+ /**
37
+ * A targeted config update: what U has overrides T, the rest is carried over.
38
+ * `Omit<T, keyof U> & U` does not work here: TypeScript cannot prove that the
39
+ * result is still a `ColumnCfg` when U is declared as `Partial`.
40
+ */
41
+ type Update<T extends ColumnCfg, U extends Partial<ColumnCfg>> = {
42
+ readonly name: U extends {
43
+ name: infer V;
44
+ } ? V : T['name'];
45
+ readonly data: U extends {
46
+ data: infer V;
47
+ } ? V : T['data'];
48
+ readonly notNull: U extends {
49
+ notNull: infer V extends boolean;
50
+ } ? V : T['notNull'];
51
+ readonly hasDefault: U extends {
52
+ hasDefault: infer V extends boolean;
53
+ } ? V : T['hasDefault'];
54
+ readonly array: U extends {
55
+ array: infer V extends boolean;
56
+ } ? V : T['array'];
57
+ readonly identity: U extends {
58
+ identity: infer V;
59
+ } ? V : T['identity'];
60
+ readonly enumValues: U extends {
61
+ enumValues: infer V;
62
+ } ? V : T['enumValues'];
63
+ readonly json: U extends {
64
+ json: infer V extends boolean;
65
+ } ? V : T['json'];
66
+ };
67
+ /** A default: a literal or an SQL expression. */
68
+ export type DefaultValue = Sql | string | number | boolean | null;
69
+ /** Everything needed to generate DDL. */
70
+ export interface ColumnSpec {
71
+ /** The base postgres type without `[]`: 'uuid', 'varchar(2)', 'numeric(10, 2)'. */
72
+ readonly sqlType: string;
73
+ readonly name: string | undefined;
74
+ readonly notNull: boolean;
75
+ readonly default: DefaultValue | undefined;
76
+ readonly identity: 'always' | 'byDefault' | undefined;
77
+ readonly array: boolean;
78
+ readonly enumValues: readonly string[] | undefined;
79
+ }
80
+ export declare class ColumnBuilder<T extends ColumnCfg = ColumnCfg> {
81
+ readonly spec: ColumnSpec;
82
+ readonly _: T;
83
+ constructor(spec: ColumnSpec);
84
+ private next;
85
+ notNull(): ColumnBuilder<Update<T, {
86
+ notNull: true;
87
+ }>>;
88
+ default(value: DefaultValue): ColumnBuilder<Update<T, {
89
+ hasDefault: true;
90
+ }>>;
91
+ /** Sugar for `.default(sql\`now()\`)`. */
92
+ defaultNow(): ColumnBuilder<Update<T, {
93
+ hasDefault: true;
94
+ }>>;
95
+ /** `GENERATED ALWAYS AS IDENTITY`: postgres owns the value, it cannot be inserted. */
96
+ generatedAlwaysAsIdentity(): ColumnBuilder<Update<T, {
97
+ notNull: true;
98
+ hasDefault: true;
99
+ identity: 'always';
100
+ }>>;
101
+ array(): ColumnBuilder<Update<T, {
102
+ data: T['data'][];
103
+ array: true;
104
+ }>>;
105
+ /** Narrows the value type without touching the column type in the database. */
106
+ $type<U>(): ColumnBuilder<Update<T, {
107
+ data: U;
108
+ }>>;
109
+ }
110
+ export type AnyColumn = ColumnBuilder<ColumnCfg>;
111
+ /** A fresh column: the name is either explicit or taken from the object key. */
112
+ type Fresh<N extends string | undefined, TData, TEnum extends readonly string[] | undefined = undefined, TJson extends boolean = false> = ColumnBuilder<{
113
+ name: N;
114
+ data: TData;
115
+ notNull: false;
116
+ hasDefault: false;
117
+ array: false;
118
+ identity: undefined;
119
+ enumValues: TEnum;
120
+ json: TJson;
121
+ }>;
122
+ declare function uuid<N extends string>(name: N): Fresh<N, string>;
123
+ declare function uuid(): Fresh<undefined, string>;
124
+ interface VarcharConfig {
125
+ length?: number;
126
+ }
127
+ declare function varchar<N extends string>(name: N, config?: VarcharConfig): Fresh<N, string>;
128
+ declare function varchar(config?: VarcharConfig): Fresh<undefined, string>;
129
+ /**
130
+ * A set of allowed values: the column stays `varchar`, and the restriction goes
131
+ * into a check constraint that `defineTable` builds itself:
132
+ *
133
+ * ```sql
134
+ * CONSTRAINT "ticket_status_check" CHECK ("status" in ('new', 'closed'))
135
+ * ```
136
+ *
137
+ * A native `create type ... as enum` is deliberately not used: adding a value
138
+ * requires `ALTER TYPE`, and the new value cannot be used in the same transaction
139
+ * that added it. `varchar` + check changes with a regular `ALTER TABLE`.
140
+ *
141
+ * In the types this is a union of string literals, not `string`.
142
+ *
143
+ * The function is declared as `enumColumn` because `enum` is a reserved word and
144
+ * cannot name a declaration. It can be an object key though, and the builders are
145
+ * handed out only as an object, so from the outside it is exactly `t.enum(...)`.
146
+ */
147
+ declare function enumColumn<N extends string, const T extends readonly [string, ...string[]]>(name: N, values: T): Fresh<N, T[number], T>;
148
+ declare function enumColumn<const T extends readonly [string, ...string[]]>(values: T): Fresh<undefined, T[number], T>;
149
+ declare function integer<N extends string>(name: N): Fresh<N, number>;
150
+ declare function integer(): Fresh<undefined, number>;
151
+ /** int8. A string in JS: that is what `pg` returns, and no precision is lost. */
152
+ declare function bigint<N extends string>(name: N): Fresh<N, string>;
153
+ declare function bigint(): Fresh<undefined, string>;
154
+ declare function boolean<N extends string>(name: N): Fresh<N, boolean>;
155
+ declare function boolean(): Fresh<undefined, boolean>;
156
+ interface NumericConfig {
157
+ precision?: number;
158
+ scale?: number;
159
+ }
160
+ /** numeric. A string in JS: no float error. */
161
+ declare function numeric<N extends string>(name: N, config?: NumericConfig): Fresh<N, string>;
162
+ declare function numeric(config?: NumericConfig): Fresh<undefined, string>;
163
+ interface TimestampConfig {
164
+ withTimezone?: boolean;
165
+ precision?: number;
166
+ }
167
+ declare function timestamp<N extends string>(name: N, config?: TimestampConfig): Fresh<N, Date>;
168
+ declare function timestamp(config?: TimestampConfig): Fresh<undefined, Date>;
169
+ /**
170
+ * `unknown` in TS, narrow it with `$type<T>()`. Values for writes go only
171
+ * through `jsonb()` from `kysely-ddl/kysely`: drivers accept a raw jsonb
172
+ * parameter differently, and the helper evens that out.
173
+ */
174
+ declare function jsonb<N extends string>(name: N): Fresh<N, unknown, undefined, true>;
175
+ declare function jsonb(): Fresh<undefined, unknown, undefined, true>;
176
+ /**
177
+ * What arrives in `columns: t => ({ ... })`.
178
+ *
179
+ * Builders are not exported one by one: the only way to declare a column is the
180
+ * callback. So the schema file has no import list to maintain with every new
181
+ * column, and there is exactly one declaration form.
182
+ */
183
+ declare const columnBuilders: {
184
+ readonly bigint: typeof bigint;
185
+ readonly boolean: typeof boolean;
186
+ readonly integer: typeof integer;
187
+ readonly jsonb: typeof jsonb;
188
+ readonly numeric: typeof numeric;
189
+ readonly timestamp: typeof timestamp;
190
+ readonly enum: typeof enumColumn;
191
+ readonly uuid: typeof uuid;
192
+ readonly varchar: typeof varchar;
193
+ };
194
+ export { columnBuilders };
195
+ export type ColumnBuilders = typeof columnBuilders;
@@ -0,0 +1,114 @@
1
+ export class ColumnBuilder {
2
+ spec;
3
+ constructor(spec) {
4
+ this.spec = spec;
5
+ }
6
+ next(patch) {
7
+ return new ColumnBuilder({ ...this.spec, ...patch });
8
+ }
9
+ notNull() {
10
+ return this.next({ notNull: true });
11
+ }
12
+ default(value) {
13
+ return this.next({ default: value });
14
+ }
15
+ /** Sugar for `.default(sql\`now()\`)`. */
16
+ defaultNow() {
17
+ return this.next({
18
+ default: { kind: 'sql', chunks: ['now()'] },
19
+ });
20
+ }
21
+ /** `GENERATED ALWAYS AS IDENTITY`: postgres owns the value, it cannot be inserted. */
22
+ generatedAlwaysAsIdentity() {
23
+ return this.next({
24
+ notNull: true,
25
+ identity: 'always',
26
+ });
27
+ }
28
+ array() {
29
+ return this.next({ array: true });
30
+ }
31
+ /** Narrows the value type without touching the column type in the database. */
32
+ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the type parameter is the whole point of the method
33
+ $type() {
34
+ return this;
35
+ }
36
+ }
37
+ function fresh(sqlType, name, enumValues) {
38
+ return new ColumnBuilder({
39
+ sqlType,
40
+ name,
41
+ notNull: false,
42
+ default: undefined,
43
+ identity: undefined,
44
+ array: false,
45
+ enumValues,
46
+ });
47
+ }
48
+ /** Parses `(name?, config?)`, both forms of calling a builder. */
49
+ function args(a, b) {
50
+ return typeof a === 'string' ? { name: a, config: b } : { name: undefined, config: a };
51
+ }
52
+ function uuid(name) {
53
+ return fresh('uuid', name);
54
+ }
55
+ function varchar(a, b) {
56
+ const { name, config } = args(a, b);
57
+ const sqlType = config?.length === undefined ? 'varchar' : `varchar(${config.length})`;
58
+ return fresh(sqlType, name);
59
+ }
60
+ function enumColumn(a, b) {
61
+ const name = typeof a === 'string' ? a : undefined;
62
+ const values = typeof a === 'string' ? b : a;
63
+ if (values === undefined || values.length === 0) {
64
+ throw new Error('enum(): a non-empty list of values is required');
65
+ }
66
+ return fresh('varchar', name, values);
67
+ }
68
+ function integer(name) {
69
+ return fresh('integer', name);
70
+ }
71
+ function bigint(name) {
72
+ return fresh('bigint', name);
73
+ }
74
+ function boolean(name) {
75
+ return fresh('boolean', name);
76
+ }
77
+ function numeric(a, b) {
78
+ const { name, config } = args(a, b);
79
+ const sqlType = config?.precision === undefined
80
+ ? 'numeric'
81
+ : config.scale === undefined
82
+ ? `numeric(${config.precision})`
83
+ : `numeric(${config.precision}, ${config.scale})`;
84
+ return fresh(sqlType, name);
85
+ }
86
+ function timestamp(a, b) {
87
+ const { name, config } = args(a, b);
88
+ const precision = config?.precision === undefined ? '' : `(${config.precision})`;
89
+ const tz = config?.withTimezone === true ? ' with time zone' : '';
90
+ return fresh(`timestamp${precision}${tz}`, name);
91
+ }
92
+ function jsonb(name) {
93
+ return fresh('jsonb', name);
94
+ }
95
+ // ── the builder set for the callback form ────────────────────────────────────
96
+ /**
97
+ * What arrives in `columns: t => ({ ... })`.
98
+ *
99
+ * Builders are not exported one by one: the only way to declare a column is the
100
+ * callback. So the schema file has no import list to maintain with every new
101
+ * column, and there is exactly one declaration form.
102
+ */
103
+ const columnBuilders = {
104
+ bigint,
105
+ boolean,
106
+ integer,
107
+ jsonb,
108
+ numeric,
109
+ timestamp,
110
+ enum: enumColumn,
111
+ uuid,
112
+ varchar,
113
+ };
114
+ export { columnBuilders };
@@ -0,0 +1,165 @@
1
+ /**
2
+ * `defineTable`, a hybrid: columns as chains, everything else as a declarative block.
3
+ *
4
+ * The key difference from drizzle rc5: **the column name stays a literal in the type**.
5
+ * When a name is not set explicitly, it is derived from the property name with the
6
+ * same snake_case that is applied at runtime, at the type level too (see `SnakeCase`).
7
+ * This is exactly what rc5 lacks, where `Column['_']['name']` collapses to `string`.
8
+ *
9
+ * Index and constraint names are optional as well, see `AUTO_NAMES`.
10
+ */
11
+ import { type SnakeCase } from './casing.ts';
12
+ import { type AnyColumn, type ColumnBuilders, type ColumnCfg, type ColumnSpec } from './columns.ts';
13
+ import { type ColumnRef, type Sql } from './sql.ts';
14
+ /**
15
+ * Auto-name suffixes. The convention:
16
+ *
17
+ * user_pk
18
+ * ticket_number_uq
19
+ * session_user_id_fk
20
+ * ticket_status_check
21
+ * user_resource_transaction_user_id_resource_idx
22
+ *
23
+ * The primary key is the only one named without columns: there is one per table.
24
+ */
25
+ export declare const AUTO_NAMES: {
26
+ readonly primaryKey: 'pk';
27
+ readonly unique: 'uq';
28
+ readonly foreignKey: 'fk';
29
+ readonly check: 'check';
30
+ readonly index: 'idx';
31
+ };
32
+ /** Column config after its name has been resolved. */
33
+ export interface ResolvedColumnCfg extends Omit<ColumnCfg, 'name'> {
34
+ readonly name: string;
35
+ }
36
+ type ResolveName<K extends string, C extends AnyColumn> = C['_']['name'] extends string ? C['_']['name'] : SnakeCase<K>;
37
+ export type ResolveColumns<TCols extends Record<string, AnyColumn>> = {
38
+ readonly [K in keyof TCols & string]: {
39
+ readonly name: ResolveName<K, TCols[K]>;
40
+ readonly data: TCols[K]['_']['data'];
41
+ readonly notNull: TCols[K]['_']['notNull'];
42
+ readonly hasDefault: TCols[K]['_']['hasDefault'];
43
+ readonly array: TCols[K]['_']['array'];
44
+ readonly identity: TCols[K]['_']['identity'];
45
+ readonly enumValues: TCols[K]['_']['enumValues'];
46
+ readonly json: TCols[K]['_']['json'];
47
+ };
48
+ };
49
+ export type ReferentialAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
50
+ /** A column with its name already resolved; this is what goes into the snapshot. */
51
+ export interface ResolvedColumn extends Omit<ColumnSpec, 'name'> {
52
+ readonly name: string;
53
+ /** The property name in the schema; needed for clear error messages. */
54
+ readonly key: string;
55
+ }
56
+ export interface TableSpec {
57
+ readonly name: string;
58
+ readonly columns: readonly ResolvedColumn[];
59
+ readonly primaryKey: {
60
+ readonly name: string;
61
+ readonly columns: readonly string[];
62
+ } | undefined;
63
+ readonly uniques: readonly {
64
+ readonly name: string;
65
+ readonly columns: readonly string[];
66
+ }[];
67
+ readonly indexes: readonly {
68
+ readonly name: string;
69
+ readonly unique: boolean;
70
+ readonly columns: readonly string[];
71
+ readonly where: Sql | undefined;
72
+ }[];
73
+ readonly foreignKeys: readonly {
74
+ readonly name: string;
75
+ readonly columns: readonly string[];
76
+ readonly refTable: string;
77
+ readonly refColumns: readonly string[];
78
+ readonly onDelete: ReferentialAction | undefined;
79
+ readonly onUpdate: ReferentialAction | undefined;
80
+ }[];
81
+ readonly checks: readonly {
82
+ readonly name: string;
83
+ readonly expression: Sql;
84
+ }[];
85
+ }
86
+ export interface Table<TName extends string = string, TColumns extends Record<string, ResolvedColumnCfg> = Record<string, ResolvedColumnCfg>> {
87
+ readonly _: {
88
+ readonly name: TName;
89
+ readonly columns: TColumns;
90
+ };
91
+ readonly spec: TableSpec;
92
+ }
93
+ export type AnyTable = Table<string, Record<string, ResolvedColumnCfg>>;
94
+ /** Column references for the callbacks in checks and partial indexes. */
95
+ type Refs<TCols> = {
96
+ readonly [K in keyof TCols]: ColumnRef;
97
+ };
98
+ export interface Reference {
99
+ readonly table: AnyTable;
100
+ readonly columns: readonly string[];
101
+ }
102
+ export interface ForeignKeyDef<TCols> {
103
+ /** When not set, built as `{table}_{columns}_fk`. */
104
+ readonly name?: string;
105
+ readonly columns: readonly (keyof TCols & string)[];
106
+ readonly references: Reference;
107
+ readonly onDelete?: ReferentialAction;
108
+ readonly onUpdate?: ReferentialAction;
109
+ }
110
+ /**
111
+ * A reference to another table's columns.
112
+ *
113
+ * The target table comes first, so TypeScript infers it and checks the column
114
+ * names. A field inside `foreignKeys` cannot do that: the array element type is
115
+ * fixed, and there is nothing to infer from a neighbouring field.
116
+ *
117
+ * Checking element by element is still possible, via `const` inference of the
118
+ * whole array intersected with a mapped type, but measurements put it at six
119
+ * times the cost (175k instantiations vs 30k for 64 tables) for the same errors.
120
+ */
121
+ export declare function ref<T extends AnyTable>(table: T, columns: readonly (keyof T['_']['columns'] & string)[]): Reference;
122
+ export interface TableOptions<TName extends string, TCols extends Record<string, AnyColumn>> {
123
+ /** The table name in the database. Inferred as a literal; the Kysely interface keys come from it. */
124
+ readonly tableName: TName;
125
+ /**
126
+ * Columns are declared with a callback that receives the builder set:
127
+ *
128
+ * ```ts
129
+ * columns: t => ({ id: t.uuid().notNull(), status: t.enum(['new']) })
130
+ * ```
131
+ *
132
+ * Builders are not exported one by one: there is exactly one form, and the
133
+ * import list in the schema file does not need editing for every new column.
134
+ *
135
+ * Type inference is unaffected: TypeScript infers `TCols` from the callback's
136
+ * return type BEFORE it starts checking the rest of the literal, so `indexes`,
137
+ * `uniques`, `foreignKeys` and `checks` are typed as usual.
138
+ */
139
+ readonly columns: (t: ColumnBuilders) => TCols;
140
+ readonly primaryKey?: {
141
+ /** When not set, built as `{table}_pk`. */
142
+ readonly name?: string;
143
+ readonly columns: readonly (keyof TCols & string)[];
144
+ };
145
+ readonly uniques?: readonly {
146
+ /** When not set, built as `{table}_{columns}_uq`. */
147
+ readonly name?: string;
148
+ readonly columns: readonly (keyof TCols & string)[];
149
+ }[];
150
+ readonly indexes?: readonly {
151
+ /** When not set, built as `{table}_{columns}_idx`. */
152
+ readonly name?: string;
153
+ readonly unique?: boolean;
154
+ readonly columns: readonly (keyof TCols & string)[];
155
+ readonly where?: (c: Refs<TCols>) => Sql;
156
+ }[];
157
+ readonly foreignKeys?: readonly ForeignKeyDef<TCols>[];
158
+ readonly checks?: readonly {
159
+ /** When not set, built as `{table}_{expression columns}_check`. */
160
+ readonly name?: string;
161
+ readonly expression: (c: Refs<TCols>) => Sql;
162
+ }[];
163
+ }
164
+ export declare function defineTable<TName extends string, TCols extends Record<string, AnyColumn>>(options: TableOptions<TName, TCols>): Table<TName, ResolveColumns<TCols>>;
165
+ export {};
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `defineTable`, a hybrid: columns as chains, everything else as a declarative block.
3
+ *
4
+ * The key difference from drizzle rc5: **the column name stays a literal in the type**.
5
+ * When a name is not set explicitly, it is derived from the property name with the
6
+ * same snake_case that is applied at runtime, at the type level too (see `SnakeCase`).
7
+ * This is exactly what rc5 lacks, where `Column['_']['name']` collapses to `string`.
8
+ *
9
+ * Index and constraint names are optional as well, see `AUTO_NAMES`.
10
+ */
11
+ import { toSnakeCase } from './casing.js';
12
+ import { columnBuilders } from './columns.js';
13
+ import { assertIdentifier, autoName } from './identifier.js';
14
+ import { collectColumns, inArray } from './sql.js';
15
+ /**
16
+ * Auto-name suffixes. The convention:
17
+ *
18
+ * user_pk
19
+ * ticket_number_uq
20
+ * session_user_id_fk
21
+ * ticket_status_check
22
+ * user_resource_transaction_user_id_resource_idx
23
+ *
24
+ * The primary key is the only one named without columns: there is one per table.
25
+ */
26
+ export const AUTO_NAMES = {
27
+ primaryKey: 'pk',
28
+ unique: 'uq',
29
+ foreignKey: 'fk',
30
+ check: 'check',
31
+ index: 'idx',
32
+ };
33
+ /**
34
+ * A reference to another table's columns.
35
+ *
36
+ * The target table comes first, so TypeScript infers it and checks the column
37
+ * names. A field inside `foreignKeys` cannot do that: the array element type is
38
+ * fixed, and there is nothing to infer from a neighbouring field.
39
+ *
40
+ * Checking element by element is still possible, via `const` inference of the
41
+ * whole array intersected with a mapped type, but measurements put it at six
42
+ * times the cost (175k instantiations vs 30k for 64 tables) for the same errors.
43
+ */
44
+ export function ref(table, columns) {
45
+ return { table, columns };
46
+ }
47
+ // ── defineTable ──────────────────────────────────────────────────────────────
48
+ export function defineTable(options) {
49
+ const name = options.tableName;
50
+ assertIdentifier(name, 'table');
51
+ const columns = [];
52
+ const dbName = {};
53
+ const refs = {};
54
+ for (const [key, builder] of Object.entries(options.columns(columnBuilders))) {
55
+ const resolved = builder.spec.name ?? toSnakeCase(key);
56
+ assertIdentifier(resolved, `${name}.${key}`);
57
+ if (builder.spec.enumValues !== undefined && builder.spec.array) {
58
+ throw new Error(`${name}.${key}: enum().array() is not supported yet: a check for an array ` +
59
+ 'is written with `<@ ARRAY[...]`, add it by hand.');
60
+ }
61
+ columns.push({ ...builder.spec, name: resolved, key });
62
+ dbName[key] = resolved;
63
+ refs[key] = { kind: 'column', name: resolved };
64
+ }
65
+ const seenColumns = new Set();
66
+ for (const column of columns) {
67
+ if (seenColumns.has(column.name)) {
68
+ throw new Error(`${name}: column name "${column.name}" is used twice`);
69
+ }
70
+ seenColumns.add(column.name);
71
+ }
72
+ const toDb = (keys) => keys.map(key => {
73
+ const resolved = dbName[key];
74
+ if (resolved === undefined) {
75
+ throw new Error(`${name}: no column ${key}`);
76
+ }
77
+ return resolved;
78
+ });
79
+ /** An explicit name is checked against the limit; a missing one is built and shortened. */
80
+ const resolveName = (explicit, parts, suffix, what) => {
81
+ if (explicit !== undefined) {
82
+ assertIdentifier(explicit, `${name}: ${what}`);
83
+ return explicit;
84
+ }
85
+ return autoName([name, ...parts], suffix);
86
+ };
87
+ const typedRefs = refs;
88
+ // ── indexes ────────────────────────────────────────────────────────────────
89
+ const indexes = (options.indexes ?? []).map(index => {
90
+ const indexColumns = toDb(index.columns);
91
+ return {
92
+ name: resolveName(index.name, indexColumns, AUTO_NAMES.index, 'index'),
93
+ unique: index.unique ?? false,
94
+ columns: indexColumns,
95
+ where: index.where !== undefined ? index.where(typedRefs) : undefined,
96
+ };
97
+ });
98
+ // ── primary key ────────────────────────────────────────────────────────────
99
+ const primaryKey = options.primaryKey !== undefined
100
+ ? {
101
+ // the only one named without columns: there is one per table
102
+ name: resolveName(options.primaryKey.name, [], AUTO_NAMES.primaryKey, 'primary key'),
103
+ columns: toDb(options.primaryKey.columns),
104
+ }
105
+ : undefined;
106
+ // ── unique constraints ─────────────────────────────────────────────────────
107
+ const uniques = (options.uniques ?? []).map(unique => {
108
+ const uniqueColumns = toDb(unique.columns);
109
+ return {
110
+ name: resolveName(unique.name, uniqueColumns, AUTO_NAMES.unique, 'unique'),
111
+ columns: uniqueColumns,
112
+ };
113
+ });
114
+ // ── foreign keys ───────────────────────────────────────────────────────────
115
+ const foreignKeys = (options.foreignKeys ?? []).map(foreignKey => {
116
+ const fkColumns = toDb(foreignKey.columns);
117
+ return {
118
+ name: resolveName(foreignKey.name, fkColumns, AUTO_NAMES.foreignKey, 'foreign key'),
119
+ columns: fkColumns,
120
+ refTable: foreignKey.references.table.spec.name,
121
+ refColumns: foreignKey.references.columns.map(key => {
122
+ const target = foreignKey.references.table.spec.columns.find(c => c.key === key);
123
+ if (target === undefined) {
124
+ throw new Error(`${name}: table ${foreignKey.references.table.spec.name} has no column ${key}`);
125
+ }
126
+ return target.name;
127
+ }),
128
+ onDelete: foreignKey.onDelete,
129
+ onUpdate: foreignKey.onUpdate,
130
+ };
131
+ });
132
+ // ── checks: automatic ones from enum() first, then the declared ones ───────
133
+ const checks = [];
134
+ for (const column of columns) {
135
+ if (column.enumValues === undefined) {
136
+ continue;
137
+ }
138
+ checks.push({
139
+ name: autoName([name, column.name], AUTO_NAMES.check),
140
+ expression: inArray({ kind: 'column', name: column.name }, column.enumValues),
141
+ });
142
+ }
143
+ for (const check of options.checks ?? []) {
144
+ const expression = check.expression(typedRefs);
145
+ const referenced = collectColumns(expression);
146
+ if (check.name === undefined && referenced.length === 0) {
147
+ throw new Error(`${name}: a check without a name must reference at least one column, ` +
148
+ 'otherwise there is nothing to build the name from. Set the name explicitly.');
149
+ }
150
+ checks.push({
151
+ name: resolveName(check.name, referenced, AUTO_NAMES.check, 'check'),
152
+ expression,
153
+ });
154
+ }
155
+ // ── name uniqueness ────────────────────────────────────────────────────────
156
+ // Constraint names are unique within a table, index names within the schema.
157
+ // The overlap is checked only where it really conflicts: PRIMARY KEY and
158
+ // UNIQUE create an index under their own name.
159
+ const constraintNames = new Map();
160
+ const claim = (kind, constraintName) => {
161
+ const owner = constraintNames.get(constraintName);
162
+ if (owner !== undefined) {
163
+ const who = owner === kind ? `twice as ${kind}` : `as ${owner} and as ${kind}`;
164
+ throw new Error(`${name}: name "${constraintName}" is taken twice, ${who}. ` +
165
+ 'Auto-names are built from the table and columns, so two objects on the same ' +
166
+ 'columns collide: set an explicit name for at least one of them.');
167
+ }
168
+ constraintNames.set(constraintName, kind);
169
+ };
170
+ if (primaryKey !== undefined) {
171
+ claim('primary key', primaryKey.name);
172
+ }
173
+ for (const unique of uniques) {
174
+ claim('unique', unique.name);
175
+ }
176
+ for (const foreignKey of foreignKeys) {
177
+ claim('foreign key', foreignKey.name);
178
+ }
179
+ for (const check of checks) {
180
+ claim('check', check.name);
181
+ }
182
+ const indexNames = new Set();
183
+ for (const index of indexes) {
184
+ if (indexNames.has(index.name)) {
185
+ throw new Error(`${name}: index name "${index.name}" is used twice. ` +
186
+ 'Two indexes on the same columns get the same auto-name: set an explicit name.');
187
+ }
188
+ // PRIMARY KEY and UNIQUE create an index under their own name, so this would conflict
189
+ const owner = constraintNames.get(index.name);
190
+ if (owner === 'primary key' || owner === 'unique') {
191
+ throw new Error(`${name}: index "${index.name}" has the same name as the ${owner}, ` +
192
+ 'which already creates an index with that name.');
193
+ }
194
+ indexNames.add(index.name);
195
+ }
196
+ const spec = { name, columns, primaryKey, uniques, indexes, foreignKeys, checks };
197
+ return { _: { name, columns: {} }, spec };
198
+ }