@fragno-dev/db 0.0.1

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,809 @@
1
+ import { createId } from "../cuid";
2
+
3
+ export type AnySchema = Schema<Record<string, AnyTable>>;
4
+
5
+ export type AnyRelation = Relation;
6
+
7
+ export type AnyTable = Table;
8
+
9
+ export type AnyColumn =
10
+ | Column<keyof TypeMap, unknown, unknown>
11
+ | IdColumn<IdColumnType, unknown, unknown>;
12
+
13
+ /**
14
+ * Operations that can be performed on a table during its definition.
15
+ */
16
+ export type TableOperation = {
17
+ type: "add-index";
18
+ name: string;
19
+ columns: string[];
20
+ unique: boolean;
21
+ };
22
+
23
+ /**
24
+ * Operations that can be performed on a schema during its definition.
25
+ * These are tracked so we can generate migrations for specific version ranges.
26
+ */
27
+ export type SchemaOperation =
28
+ | {
29
+ type: "add-table";
30
+ tableName: string;
31
+ table: AnyTable;
32
+ }
33
+ | {
34
+ type: "add-reference";
35
+ tableName: string;
36
+ referenceName: string;
37
+ config: {
38
+ columns: string[];
39
+ targetTable: string;
40
+ targetColumns: string[];
41
+ };
42
+ }
43
+ | {
44
+ type: "add-index";
45
+ tableName: string;
46
+ name: string;
47
+ columns: string[];
48
+ unique: boolean;
49
+ };
50
+
51
+ export interface ForeignKey {
52
+ name: string;
53
+ table: AnyTable;
54
+ columns: AnyColumn[];
55
+
56
+ referencedTable: AnyTable;
57
+ referencedColumns: AnyColumn[];
58
+ }
59
+
60
+ class RelationInit<
61
+ TRelationType extends RelationType,
62
+ TTables extends Record<string, AnyTable>,
63
+ TTableName extends keyof TTables,
64
+ > {
65
+ type: TRelationType;
66
+ referencedTable: TTables[TTableName];
67
+ referencer: AnyTable;
68
+ on: [string, string][] = [];
69
+
70
+ constructor(type: TRelationType, referencedTable: TTables[TTableName], referencer: AnyTable) {
71
+ this.type = type;
72
+ this.referencedTable = referencedTable;
73
+ this.referencer = referencer;
74
+ }
75
+ }
76
+
77
+ export interface Index {
78
+ name: string;
79
+ columns: AnyColumn[];
80
+ unique: boolean;
81
+ }
82
+
83
+ /**
84
+ * Helper function to add an index to a table's index array
85
+ */
86
+ function addIndexToTable(
87
+ indexes: Index[],
88
+ name: string,
89
+ columns: AnyColumn[],
90
+ unique: boolean,
91
+ ): void {
92
+ indexes.push({
93
+ name,
94
+ columns,
95
+ unique,
96
+ });
97
+ }
98
+
99
+ export class ExplicitRelationInit<
100
+ TRelationType extends RelationType,
101
+ TTables extends Record<string, AnyTable>,
102
+ TTableName extends keyof TTables,
103
+ > extends RelationInit<TRelationType, TTables, TTableName> {
104
+ private foreignKeyName?: string;
105
+
106
+ private initForeignKey(ormName: string): ForeignKey {
107
+ const columns: AnyColumn[] = [];
108
+ const referencedColumns: AnyColumn[] = [];
109
+
110
+ for (const [left, right] of this.on) {
111
+ columns.push(this.referencer.columns[left]);
112
+ referencedColumns.push(this.referencedTable.columns[right]);
113
+ }
114
+
115
+ return {
116
+ columns,
117
+ referencedColumns,
118
+ referencedTable: this.referencedTable,
119
+ table: this.referencer,
120
+ name:
121
+ this.foreignKeyName ??
122
+ `${this.referencer.ormName}_${this.referencedTable.ormName}_${ormName}_fk`,
123
+ };
124
+ }
125
+
126
+ init(ormName: string): Relation<TRelationType, TTables[TTableName]> {
127
+ const id = `${this.referencer.ormName}_${this.referencedTable.ormName}`;
128
+
129
+ return {
130
+ id,
131
+ foreignKey: this.initForeignKey(ormName),
132
+ on: this.on,
133
+ name: ormName,
134
+ referencer: this.referencer,
135
+ table: this.referencedTable,
136
+ type: this.type,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Define custom foreign key name.
142
+ */
143
+ foreignKey(name: string) {
144
+ this.foreignKeyName = name;
145
+ return this;
146
+ }
147
+ }
148
+
149
+ export interface Relation<
150
+ TRelationType extends RelationType = RelationType,
151
+ TTable extends AnyTable = AnyTable,
152
+ > {
153
+ id: string;
154
+ name: string;
155
+ type: TRelationType;
156
+
157
+ table: TTable;
158
+ referencer: AnyTable;
159
+
160
+ on: [string, string][];
161
+ foreignKey: ForeignKey;
162
+ }
163
+
164
+ export interface Table<
165
+ TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
166
+ TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
167
+ > {
168
+ name: string;
169
+ ormName: string;
170
+
171
+ columns: TColumns;
172
+ relations: TRelations;
173
+ foreignKeys: ForeignKey[];
174
+ indexes: Index[];
175
+
176
+ /**
177
+ * Get column by name
178
+ */
179
+ getColumnByName: (name: string) => AnyColumn | undefined;
180
+ getIdColumn: () => AnyColumn;
181
+
182
+ clone: () => Table<TColumns, TRelations>;
183
+ }
184
+
185
+ type DefaultFunctionMap = {
186
+ date: "now";
187
+ timestamp: "now";
188
+ string: "auto";
189
+ } & Record<`varchar(${number})`, "auto">;
190
+
191
+ type DefaultFunction<TType extends keyof TypeMap> =
192
+ | (TType extends keyof DefaultFunctionMap ? DefaultFunctionMap[TType] : never)
193
+ | (() => TypeMap[TType]);
194
+
195
+ type IdColumnType = `varchar(${number})`;
196
+
197
+ export type TypeMap = {
198
+ string: string;
199
+ bigint: bigint;
200
+ integer: number;
201
+ decimal: number;
202
+ bool: boolean;
203
+ json: unknown;
204
+ /**
205
+ * this follows the same specs as Prisma `Bytes` for consistency.
206
+ */
207
+ binary: Uint8Array;
208
+ date: Date;
209
+ timestamp: Date;
210
+ } & Record<`varchar(${number})`, string>;
211
+
212
+ export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown> {
213
+ type: TType;
214
+ name: string = "";
215
+ ormName: string = "";
216
+ isNullable: boolean = false;
217
+ isUnique: boolean = false;
218
+ isReference: boolean = false;
219
+ default?:
220
+ | { value: TypeMap[TType] }
221
+ | {
222
+ runtime: DefaultFunction<TType>;
223
+ };
224
+
225
+ table: AnyTable = undefined as unknown as AnyTable;
226
+
227
+ constructor(type: TType) {
228
+ this.type = type;
229
+ }
230
+
231
+ nullable<TNullable extends boolean = true>(nullable?: TNullable) {
232
+ this.isNullable = nullable ?? true;
233
+
234
+ return this as Column<
235
+ TType,
236
+ TNullable extends true ? TIn | null : Exclude<TIn, null>,
237
+ TNullable extends true ? TOut | null : Exclude<TOut, null>
238
+ >;
239
+ }
240
+
241
+ /**
242
+ * Generate default value on runtime
243
+ */
244
+ defaultTo$(fn: DefaultFunction<TType>): Column<TType, TIn | null, TOut> {
245
+ this.default = { runtime: fn };
246
+ return this;
247
+ }
248
+
249
+ /**
250
+ * Set a database-level default value
251
+ *
252
+ * For schemaless database, it's still generated on runtime
253
+ */
254
+ defaultTo(value: TypeMap[TType]): Column<TType, TIn | null, TOut> {
255
+ this.default = { value };
256
+ return this;
257
+ }
258
+
259
+ clone() {
260
+ const clone = new Column(this.type);
261
+ clone.name = this.name;
262
+ clone.ormName = this.ormName;
263
+ clone.isNullable = this.isNullable;
264
+ clone.isUnique = this.isUnique;
265
+ clone.isReference = this.isReference;
266
+ clone.default = this.default;
267
+ clone.table = this.table;
268
+ return clone;
269
+ }
270
+
271
+ getUniqueConstraintName(): string {
272
+ return `unique_c_${this.table.ormName}_${this.ormName}`;
273
+ }
274
+
275
+ /**
276
+ * Generate default value for the column on runtime.
277
+ */
278
+ generateDefaultValue(): TypeMap[TType] | undefined {
279
+ if (!this.default) {
280
+ return;
281
+ }
282
+
283
+ if ("value" in this.default) {
284
+ return this.default.value;
285
+ }
286
+ if (this.default.runtime === "auto") {
287
+ return createId() as TypeMap[TType];
288
+ }
289
+ if (this.default.runtime === "now") {
290
+ return new Date(Date.now()) as TypeMap[TType];
291
+ }
292
+
293
+ return this.default.runtime();
294
+ }
295
+
296
+ get $in(): TIn {
297
+ throw new Error("Type inference only");
298
+ }
299
+ get $out(): TOut {
300
+ throw new Error("Type inference only");
301
+ }
302
+ }
303
+
304
+ export class IdColumn<
305
+ TType extends IdColumnType = IdColumnType,
306
+ TIn = unknown,
307
+ TOut = unknown,
308
+ > extends Column<TType, TIn, TOut> {
309
+ id = true;
310
+
311
+ clone() {
312
+ const clone = new IdColumn(this.type);
313
+ clone.name = this.name;
314
+ clone.ormName = this.ormName;
315
+ clone.isNullable = this.isNullable;
316
+ clone.isUnique = this.isUnique;
317
+ clone.isReference = this.isReference;
318
+ clone.default = this.default;
319
+ clone.table = this.table;
320
+ return clone;
321
+ }
322
+
323
+ override defaultTo$(fn: DefaultFunction<TType>) {
324
+ return super.defaultTo$(fn) as IdColumn<TType, TIn | null, TOut>;
325
+ }
326
+
327
+ override defaultTo(value: TypeMap[TType]) {
328
+ return super.defaultTo(value) as IdColumn<TType, TIn | null, TOut>;
329
+ }
330
+ }
331
+
332
+ export function column<TType extends keyof TypeMap>(
333
+ type: TType,
334
+ ): Column<TType, TypeMap[TType], TypeMap[TType]> {
335
+ return new Column(type);
336
+ }
337
+
338
+ /**
339
+ * Create a reference column that points to another table.
340
+ * This is used for foreign key relationships.
341
+ */
342
+ export function referenceColumn<TType extends keyof TypeMap = "varchar(30)">(
343
+ type?: TType,
344
+ ): Column<TType, TypeMap[TType], TypeMap[TType]> {
345
+ const actualType = (type ?? "varchar(30)") as TType;
346
+ const col = new Column<TType, TypeMap[TType], TypeMap[TType]>(actualType);
347
+ col.isReference = true;
348
+ return col as Column<TType, TypeMap[TType], TypeMap[TType]>;
349
+ }
350
+
351
+ export function idColumn(): IdColumn<"varchar(30)", string, string> {
352
+ const col = new IdColumn<"varchar(30)", string, string>("varchar(30)");
353
+ col.defaultTo$("auto");
354
+ return col as IdColumn<"varchar(30)", string, string>;
355
+ }
356
+
357
+ type RelationType = "one";
358
+
359
+ export class TableBuilder<
360
+ TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
361
+ TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
362
+ > {
363
+ #name: string;
364
+ #columns: TColumns;
365
+ #relations: TRelations;
366
+ #foreignKeys: ForeignKey[] = [];
367
+ #indexes: Index[] = [];
368
+ #version: number = 0;
369
+ #ormName: string = "";
370
+ #operations: TableOperation[] = [];
371
+
372
+ constructor(name: string) {
373
+ this.#name = name;
374
+ this.#columns = {} as TColumns;
375
+ this.#relations = {} as TRelations;
376
+ }
377
+
378
+ /**
379
+ * Add a column to the table. Increments the version counter.
380
+ */
381
+ addColumn<TColumnName extends string, TColumn extends AnyColumn>(
382
+ ormName: TColumnName,
383
+ col: TColumn,
384
+ ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations>;
385
+
386
+ /**
387
+ * Add a column to the table with simplified syntax. Increments the version counter.
388
+ */
389
+ addColumn<TColumnName extends string, TType extends keyof TypeMap>(
390
+ ormName: TColumnName,
391
+ type: TType,
392
+ ): TableBuilder<
393
+ TColumns & Record<TColumnName, Column<TType, TypeMap[TType], TypeMap[TType]>>,
394
+ TRelations
395
+ >;
396
+
397
+ addColumn<TColumnName extends string, TColumn extends AnyColumn, TType extends keyof TypeMap>(
398
+ ormName: TColumnName,
399
+ colOrType: TColumn | TType,
400
+ ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations> {
401
+ this.#version++;
402
+
403
+ // Create the column if a type string was provided
404
+ const col = typeof colOrType === "string" ? column(colOrType) : colOrType;
405
+
406
+ // Create a new instance to ensure immutability semantics
407
+ const builder = new TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations>(
408
+ this.#name,
409
+ );
410
+ builder.#columns = { ...this.#columns, [ormName]: col } as TColumns &
411
+ Record<TColumnName, TColumn>;
412
+ builder.#relations = this.#relations;
413
+ builder.#foreignKeys = this.#foreignKeys;
414
+ builder.#indexes = this.#indexes;
415
+ builder.#version = this.#version;
416
+ builder.#ormName = this.#ormName;
417
+ builder.#operations = this.#operations;
418
+
419
+ // Set column metadata
420
+ col.ormName = ormName;
421
+ col.name = ormName;
422
+
423
+ return builder;
424
+ }
425
+
426
+ /**
427
+ * Create an index on the specified columns. Increments the version counter.
428
+ */
429
+ createIndex<TColumnName extends string & keyof TColumns>(
430
+ name: string,
431
+ columns: TColumnName[],
432
+ options?: { unique?: boolean },
433
+ ): TableBuilder<TColumns, TRelations> {
434
+ this.#version++;
435
+
436
+ const cols = columns.map((name) => {
437
+ const column = this.#columns[name];
438
+ if (!column) {
439
+ throw new Error(`Unknown column name ${name}`);
440
+ }
441
+ return column;
442
+ });
443
+
444
+ const unique = options?.unique ?? false;
445
+ addIndexToTable(this.#indexes, name, cols, unique);
446
+
447
+ // Record the operation
448
+ this.#operations.push({
449
+ type: "add-index",
450
+ name,
451
+ columns: columns as string[],
452
+ unique,
453
+ });
454
+
455
+ return this;
456
+ }
457
+
458
+ /**
459
+ * Build the final table. This should be called after all columns are added.
460
+ */
461
+ build(): Table<TColumns, TRelations> {
462
+ let idCol: AnyColumn | undefined;
463
+
464
+ // Use name as ormName if ormName is not set
465
+ const ormName = this.#ormName || this.#name;
466
+
467
+ const table: Table<TColumns, TRelations> = {
468
+ name: this.#name,
469
+ ormName,
470
+ columns: this.#columns,
471
+ relations: this.#relations,
472
+ foreignKeys: this.#foreignKeys,
473
+ indexes: this.#indexes,
474
+ getColumnByName: (name) => {
475
+ return Object.values(this.#columns).find((c) => c.name === name);
476
+ },
477
+ getIdColumn: () => {
478
+ return idCol!;
479
+ },
480
+ clone: () => {
481
+ const cloneColumns: Record<string, AnyColumn> = {};
482
+
483
+ for (const [k, v] of Object.entries(this.#columns)) {
484
+ cloneColumns[k] = v.clone();
485
+ }
486
+
487
+ const builder = new TableBuilder<TColumns, TRelations>(this.#name);
488
+ builder.#columns = cloneColumns as TColumns;
489
+ builder.#relations = this.#relations;
490
+ builder.#foreignKeys = [...this.#foreignKeys];
491
+ builder.#indexes = [...this.#indexes];
492
+ builder.#version = this.#version;
493
+ builder.#ormName = this.#ormName;
494
+ builder.#operations = [...this.#operations];
495
+
496
+ const cloned = builder.build();
497
+
498
+ return cloned;
499
+ },
500
+ };
501
+
502
+ // Set table reference and find id column
503
+ for (const k in this.#columns) {
504
+ const column = this.#columns[k];
505
+ if (!column) {
506
+ continue;
507
+ }
508
+
509
+ column.table = table;
510
+ if (column instanceof IdColumn) {
511
+ idCol = column;
512
+ }
513
+ }
514
+
515
+ if (idCol === undefined) {
516
+ throw new Error(`there's no id column in your table ${this.#name}`);
517
+ }
518
+
519
+ return table;
520
+ }
521
+
522
+ /**
523
+ * Get the current version of the table builder.
524
+ */
525
+ getVersion(): number {
526
+ return this.#version;
527
+ }
528
+
529
+ /**
530
+ * Get the operations performed on this table.
531
+ */
532
+ getOperations(): TableOperation[] {
533
+ return this.#operations;
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Create a new table with callback pattern.
539
+ */
540
+ export function table<
541
+ TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
542
+ TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
543
+ >(
544
+ name: string,
545
+ callback: (
546
+ builder: TableBuilder<Record<string, AnyColumn>, Record<string, AnyRelation>>,
547
+ ) => TableBuilder<TColumns, TRelations>,
548
+ ): Table<TColumns, TRelations> {
549
+ const builder = new TableBuilder(name);
550
+ const result = callback(builder);
551
+ return result.build();
552
+ }
553
+
554
+ export interface Schema<TTables extends Record<string, AnyTable> = Record<string, AnyTable>> {
555
+ /**
556
+ * @description The version of the schema, automatically incremented on each change.
557
+ */
558
+ version: number;
559
+ tables: TTables;
560
+ /**
561
+ * @description Operations performed on this schema, in order.
562
+ * Used to generate migrations for specific version ranges.
563
+ */
564
+ operations: SchemaOperation[];
565
+
566
+ clone: () => Schema<TTables>;
567
+ }
568
+
569
+ export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<string, never>> {
570
+ #tables: TTables;
571
+ #version: number = 0;
572
+ #operations: SchemaOperation[] = [];
573
+
574
+ constructor() {
575
+ this.#tables = {} as TTables;
576
+ }
577
+
578
+ /**
579
+ * Add a table to the schema. Increments the version counter.
580
+ */
581
+ addTable<
582
+ TTableName extends string,
583
+ TColumns extends Record<string, AnyColumn>,
584
+ TRelations extends Record<string, AnyRelation>,
585
+ >(
586
+ ormName: TTableName,
587
+ callback: (
588
+ builder: TableBuilder<Record<string, AnyColumn>, Record<string, AnyRelation>>,
589
+ ) => TableBuilder<TColumns, TRelations>,
590
+ ): SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations>>> {
591
+ this.#version++;
592
+
593
+ const tableBuilder = new TableBuilder(ormName);
594
+ const result = callback(tableBuilder);
595
+ const builtTable = result.build();
596
+
597
+ // Set table metadata
598
+ builtTable.ormName = ormName;
599
+
600
+ const builder = new SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations>>>();
601
+ builder.#tables = { ...this.#tables, [ormName]: builtTable } as TTables &
602
+ Record<TTableName, Table<TColumns, TRelations>>;
603
+
604
+ // Start with existing operations plus the add-table operation
605
+ const newOperations: SchemaOperation[] = [
606
+ ...this.#operations,
607
+ {
608
+ type: "add-table",
609
+ tableName: ormName,
610
+ table: builtTable,
611
+ },
612
+ ];
613
+
614
+ // Promote table operations to schema operations and increment version for each
615
+ const tableOps = result.getOperations();
616
+ for (const tableOp of tableOps) {
617
+ if (tableOp.type === "add-index") {
618
+ this.#version++;
619
+ newOperations.push({
620
+ type: "add-index",
621
+ tableName: ormName,
622
+ name: tableOp.name,
623
+ columns: tableOp.columns,
624
+ unique: tableOp.unique,
625
+ });
626
+ }
627
+ }
628
+
629
+ builder.#version = this.#version;
630
+ builder.#operations = newOperations;
631
+
632
+ return builder;
633
+ }
634
+
635
+ /**
636
+ * Add a foreign key reference from this table to another table.
637
+ *
638
+ * @param tableName - The table that has the foreign key column
639
+ * @param referenceName - A name for this reference (e.g., "author", "category")
640
+ * @param config - Configuration specifying the foreign key mapping
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * // Basic foreign key: post -> user
645
+ * schema(s => s
646
+ * .addTable("users", t => t.addColumn("id", idColumn()))
647
+ * .addTable("posts", t => t
648
+ * .addColumn("id", idColumn())
649
+ * .addColumn("authorId", referenceColumn()))
650
+ * .addReference("posts", "author", {
651
+ * columns: ["authorId"],
652
+ * targetTable: "users",
653
+ * targetColumns: ["id"],
654
+ * })
655
+ * )
656
+ *
657
+ * // Self-referencing foreign key
658
+ * .addReference("users", "inviter", {
659
+ * columns: ["invitedBy"],
660
+ * targetTable: "users",
661
+ * targetColumns: ["id"],
662
+ * })
663
+ *
664
+ * // Multiple foreign keys - call addReference multiple times
665
+ * .addReference("posts", "author", {
666
+ * columns: ["authorId"],
667
+ * targetTable: "users",
668
+ * targetColumns: ["id"],
669
+ * })
670
+ * .addReference("posts", "category", {
671
+ * columns: ["categoryId"],
672
+ * targetTable: "categories",
673
+ * targetColumns: ["id"],
674
+ * })
675
+ * ```
676
+ */
677
+ addReference<
678
+ TTableName extends string & keyof TTables,
679
+ TReferencedTableName extends string & keyof TTables,
680
+ >(
681
+ tableName: TTableName,
682
+ referenceName: string,
683
+ config: {
684
+ columns: (keyof TTables[TTableName]["columns"])[];
685
+ targetTable: TReferencedTableName;
686
+ targetColumns: (keyof TTables[TReferencedTableName]["columns"])[];
687
+ },
688
+ ): SchemaBuilder<TTables> {
689
+ this.#version++;
690
+
691
+ const table = this.#tables[tableName];
692
+ const referencedTable = this.#tables[config.targetTable];
693
+
694
+ if (!table) {
695
+ throw new Error(`Table ${tableName} not found in schema`);
696
+ }
697
+ if (!referencedTable) {
698
+ throw new Error(`Referenced table ${config.targetTable} not found in schema`);
699
+ }
700
+
701
+ const { columns, targetColumns } = config;
702
+
703
+ if (columns.length !== targetColumns.length) {
704
+ throw new Error(
705
+ `Reference ${referenceName}: columns and targetColumns must have the same length`,
706
+ );
707
+ }
708
+
709
+ // For now, only support single column foreign keys
710
+ if (columns.length !== 1) {
711
+ throw new Error(
712
+ `Reference ${referenceName}: currently only single column foreign keys are supported`,
713
+ );
714
+ }
715
+
716
+ const columnName = columns[0] as string;
717
+ const targetColumnName = targetColumns[0] as string;
718
+
719
+ const column = table.columns[columnName];
720
+ const referencedColumn = referencedTable.columns[targetColumnName];
721
+
722
+ if (!column) {
723
+ throw new Error(`Column ${columnName} not found in table ${tableName}`);
724
+ }
725
+ if (!referencedColumn) {
726
+ throw new Error(`Column ${targetColumnName} not found in table ${config.targetTable}`);
727
+ }
728
+
729
+ // Create the relation
730
+ const init = new ExplicitRelationInit("one", referencedTable, table);
731
+ init.on.push([columnName, targetColumnName]);
732
+ const relation = init.init(referenceName);
733
+
734
+ // Add relation and foreign key to the table
735
+ table.relations[referenceName] = relation;
736
+ table.foreignKeys.push(relation.foreignKey);
737
+
738
+ // Record the operation
739
+ this.#operations.push({
740
+ type: "add-reference",
741
+ tableName: tableName as string,
742
+ referenceName,
743
+ config: {
744
+ columns: columns as string[],
745
+ targetTable: config.targetTable as string,
746
+ targetColumns: targetColumns as string[],
747
+ },
748
+ });
749
+
750
+ return this;
751
+ }
752
+
753
+ /**
754
+ * Build the final schema. This should be called after all tables are added.
755
+ */
756
+ build(): Schema<TTables> {
757
+ const operations = this.#operations;
758
+ const version = this.#version;
759
+ const tables = this.#tables;
760
+
761
+ const schema: Schema<TTables> = {
762
+ version,
763
+ tables,
764
+ operations,
765
+ clone: () => {
766
+ const cloneTables: Record<string, AnyTable> = {};
767
+
768
+ for (const [k, v] of Object.entries(tables)) {
769
+ cloneTables[k] = v.clone();
770
+ }
771
+
772
+ const builder = new SchemaBuilder<TTables>();
773
+ builder.#tables = cloneTables as TTables;
774
+ builder.#version = version;
775
+ builder.#operations = [...operations];
776
+
777
+ return builder.build();
778
+ },
779
+ };
780
+
781
+ return schema;
782
+ }
783
+
784
+ /**
785
+ * Get the current version of the schema builder.
786
+ */
787
+ getVersion(): number {
788
+ return this.#version;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Create a new schema with callback pattern.
794
+ */
795
+ export function schema<TTables extends Record<string, AnyTable> = Record<string, never>>(
796
+ callback: (builder: SchemaBuilder<Record<string, never>>) => SchemaBuilder<TTables>,
797
+ ): Schema<TTables> {
798
+ return callback(new SchemaBuilder()).build();
799
+ }
800
+
801
+ export function compileForeignKey(key: ForeignKey) {
802
+ return {
803
+ name: key.name,
804
+ table: key.table.name,
805
+ referencedTable: key.referencedTable.name,
806
+ referencedColumns: key.referencedColumns.map((col) => col.name),
807
+ columns: key.columns.map((col) => col.name),
808
+ };
809
+ }