@fragno-dev/db 0.0.1 → 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.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +137 -13
  2. package/.turbo/turbo-test.log +36 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/adapters/adapters.d.ts +18 -0
  5. package/dist/adapters/adapters.d.ts.map +1 -0
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
  8. package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
  10. package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
  11. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/drizzle-query.js +139 -0
  13. package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
  17. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
  19. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
  20. package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
  21. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
  22. package/dist/adapters/drizzle/generate.js +273 -0
  23. package/dist/adapters/drizzle/generate.js.map +1 -0
  24. package/dist/adapters/drizzle/join-column-utils.js +28 -0
  25. package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
  26. package/dist/adapters/drizzle/shared.js +11 -0
  27. package/dist/adapters/drizzle/shared.js.map +1 -0
  28. package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
  29. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
  30. package/dist/adapters/kysely/kysely-adapter.js +119 -0
  31. package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
  32. package/dist/adapters/kysely/kysely-query-builder.js +306 -0
  33. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
  34. package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
  35. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
  36. package/dist/adapters/kysely/kysely-query.js +158 -0
  37. package/dist/adapters/kysely/kysely-query.js.map +1 -0
  38. package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
  39. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
  40. package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
  41. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
  42. package/dist/adapters/kysely/migration/execute.js +176 -0
  43. package/dist/adapters/kysely/migration/execute.js.map +1 -0
  44. package/dist/fragment.d.ts +54 -0
  45. package/dist/fragment.d.ts.map +1 -0
  46. package/dist/fragment.js +92 -0
  47. package/dist/fragment.js.map +1 -0
  48. package/dist/id.d.ts +2 -0
  49. package/dist/migration-engine/auto-from-schema.js +116 -0
  50. package/dist/migration-engine/auto-from-schema.js.map +1 -0
  51. package/dist/migration-engine/create.d.ts +41 -0
  52. package/dist/migration-engine/create.d.ts.map +1 -0
  53. package/dist/migration-engine/create.js +58 -0
  54. package/dist/migration-engine/create.js.map +1 -0
  55. package/dist/migration-engine/shared.d.ts +90 -0
  56. package/dist/migration-engine/shared.d.ts.map +1 -0
  57. package/dist/migration-engine/shared.js +8 -0
  58. package/dist/migration-engine/shared.js.map +1 -0
  59. package/dist/mod.d.ts +55 -2
  60. package/dist/mod.d.ts.map +1 -1
  61. package/dist/mod.js +111 -2
  62. package/dist/mod.js.map +1 -1
  63. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
  64. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
  65. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
  66. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
  67. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
  68. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
  69. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
  70. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
  71. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
  72. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
  73. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
  74. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
  75. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
  76. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
  77. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
  78. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
  79. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
  80. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
  81. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
  82. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
  83. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
  84. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
  85. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
  86. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
  87. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
  88. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
  89. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
  90. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
  91. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
  92. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
  93. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
  94. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
  95. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
  96. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
  97. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
  98. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
  99. package/dist/query/condition-builder.d.ts +41 -0
  100. package/dist/query/condition-builder.d.ts.map +1 -0
  101. package/dist/query/condition-builder.js +93 -0
  102. package/dist/query/condition-builder.js.map +1 -0
  103. package/dist/query/cursor.d.ts +88 -0
  104. package/dist/query/cursor.d.ts.map +1 -0
  105. package/dist/query/cursor.js +103 -0
  106. package/dist/query/cursor.js.map +1 -0
  107. package/dist/query/orm/orm.d.ts +18 -0
  108. package/dist/query/orm/orm.d.ts.map +1 -0
  109. package/dist/query/orm/orm.js +48 -0
  110. package/dist/query/orm/orm.js.map +1 -0
  111. package/dist/query/query.d.ts +79 -0
  112. package/dist/query/query.d.ts.map +1 -0
  113. package/dist/query/query.js +1 -0
  114. package/dist/query/result-transform.js +155 -0
  115. package/dist/query/result-transform.js.map +1 -0
  116. package/dist/query/unit-of-work.d.ts +435 -0
  117. package/dist/query/unit-of-work.d.ts.map +1 -0
  118. package/dist/query/unit-of-work.js +549 -0
  119. package/dist/query/unit-of-work.js.map +1 -0
  120. package/dist/schema/create.d.ts +273 -116
  121. package/dist/schema/create.d.ts.map +1 -1
  122. package/dist/schema/create.js +410 -222
  123. package/dist/schema/create.js.map +1 -1
  124. package/dist/schema/serialize.js +101 -0
  125. package/dist/schema/serialize.js.map +1 -0
  126. package/dist/schema-generator/schema-generator.d.ts +15 -0
  127. package/dist/schema-generator/schema-generator.d.ts.map +1 -0
  128. package/dist/shared/providers.d.ts +6 -0
  129. package/dist/shared/providers.d.ts.map +1 -0
  130. package/dist/util/import-generator.js +26 -0
  131. package/dist/util/import-generator.js.map +1 -0
  132. package/dist/util/parse.js +15 -0
  133. package/dist/util/parse.js.map +1 -0
  134. package/dist/util/types.d.ts +8 -0
  135. package/dist/util/types.d.ts.map +1 -0
  136. package/package.json +63 -2
  137. package/src/adapters/adapters.ts +22 -0
  138. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
  139. package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
  140. package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
  141. package/src/adapters/drizzle/drizzle-query.ts +234 -0
  142. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
  143. package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
  144. package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
  145. package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
  146. package/src/adapters/drizzle/generate.test.ts +643 -0
  147. package/src/adapters/drizzle/generate.ts +481 -0
  148. package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
  149. package/src/adapters/drizzle/join-column-utils.ts +39 -0
  150. package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
  151. package/src/adapters/drizzle/shared.ts +22 -0
  152. package/src/adapters/drizzle/test-utils.ts +56 -0
  153. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
  154. package/src/adapters/kysely/kysely-adapter.ts +196 -0
  155. package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
  156. package/src/adapters/kysely/kysely-query-builder.ts +611 -0
  157. package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
  158. package/src/adapters/kysely/kysely-query.ts +254 -0
  159. package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
  160. package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
  161. package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
  162. package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
  163. package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
  164. package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
  165. package/src/adapters/kysely/migration/execute.ts +382 -0
  166. package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
  167. package/src/fragment.test.ts +287 -0
  168. package/src/fragment.ts +198 -0
  169. package/src/migration-engine/auto-from-schema.test.ts +118 -58
  170. package/src/migration-engine/auto-from-schema.ts +103 -32
  171. package/src/migration-engine/create.test.ts +34 -46
  172. package/src/migration-engine/create.ts +41 -26
  173. package/src/migration-engine/shared.ts +26 -6
  174. package/src/mod.ts +197 -1
  175. package/src/query/condition-builder.test.ts +379 -0
  176. package/src/query/condition-builder.ts +294 -0
  177. package/src/query/cursor.test.ts +296 -0
  178. package/src/query/cursor.ts +147 -0
  179. package/src/query/orm/orm.ts +92 -0
  180. package/src/query/query-type.test.ts +429 -0
  181. package/src/query/query.ts +200 -0
  182. package/src/query/result-transform.test.ts +795 -0
  183. package/src/query/result-transform.ts +247 -0
  184. package/src/query/unit-of-work-types.test.ts +192 -0
  185. package/src/query/unit-of-work.test.ts +947 -0
  186. package/src/query/unit-of-work.ts +1199 -0
  187. package/src/schema/create.test.ts +653 -110
  188. package/src/schema/create.ts +708 -337
  189. package/src/schema/serialize.test.ts +559 -0
  190. package/src/schema/serialize.ts +359 -0
  191. package/src/schema-generator/schema-generator.ts +12 -0
  192. package/src/shared/config.ts +0 -8
  193. package/src/util/import-generator.ts +28 -0
  194. package/src/util/parse.ts +16 -0
  195. package/src/util/types.ts +4 -0
  196. package/tsconfig.json +1 -1
  197. package/tsdown.config.ts +11 -1
  198. package/vitest.config.ts +3 -0
  199. /package/dist/{cuid.js → id.js} +0 -0
  200. /package/src/{cuid.ts → id.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { createId } from "../cuid";
1
+ import { createId } from "../id";
2
2
 
3
3
  export type AnySchema = Schema<Record<string, AnyTable>>;
4
4
 
@@ -8,17 +8,23 @@ export type AnyTable = Table;
8
8
 
9
9
  export type AnyColumn =
10
10
  | Column<keyof TypeMap, unknown, unknown>
11
- | IdColumn<IdColumnType, unknown, unknown>;
12
-
11
+ | IdColumn<IdColumnType, unknown, unknown>
12
+ | InternalIdColumn<unknown, unknown>
13
+ | VersionColumn<unknown, unknown>;
13
14
  /**
14
- * Operations that can be performed on a table during its definition.
15
+ * Sub-operations that can be performed within table operations.
16
+ * These are stored in order within add-table and alter-table operations.
15
17
  */
16
- export type TableOperation = {
17
- type: "add-index";
18
- name: string;
19
- columns: string[];
20
- unique: boolean;
21
- };
18
+ export type TableSubOperation =
19
+ | { type: "add-column"; columnName: string; column: AnyColumn }
20
+ | { type: "add-index"; name: string; columns: string[]; unique: boolean }
21
+ | {
22
+ type: "add-foreign-key";
23
+ name: string;
24
+ columns: string[];
25
+ referencedTable: string;
26
+ referencedColumns: string[];
27
+ };
22
28
 
23
29
  /**
24
30
  * Operations that can be performed on a schema during its definition.
@@ -28,24 +34,22 @@ export type SchemaOperation =
28
34
  | {
29
35
  type: "add-table";
30
36
  tableName: string;
31
- table: AnyTable;
37
+ operations: TableSubOperation[]; // Ordered list of sub-operations
32
38
  }
33
39
  | {
34
- type: "add-reference";
40
+ type: "alter-table";
35
41
  tableName: string;
42
+ operations: TableSubOperation[]; // Ordered list of sub-operations
43
+ }
44
+ | {
45
+ type: "add-reference";
46
+ tableName: string; // The table that has the foreign key
36
47
  referenceName: string;
37
48
  config: {
38
- columns: string[];
39
- targetTable: string;
40
- targetColumns: string[];
49
+ type: "one" | "many";
50
+ from: { table: string; column: string };
51
+ to: { table: string; column: string };
41
52
  };
42
- }
43
- | {
44
- type: "add-index";
45
- tableName: string;
46
- name: string;
47
- columns: string[];
48
- unique: boolean;
49
53
  };
50
54
 
51
55
  export interface ForeignKey {
@@ -74,61 +78,26 @@ class RelationInit<
74
78
  }
75
79
  }
76
80
 
77
- export interface Index {
81
+ export interface Index<
82
+ TColumns extends AnyColumn[] = AnyColumn[],
83
+ TColumnNames extends readonly string[] = readonly string[],
84
+ > {
78
85
  name: string;
79
- columns: AnyColumn[];
86
+ columns: TColumns;
87
+ columnNames: TColumnNames;
80
88
  unique: boolean;
81
89
  }
82
90
 
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
91
  export class ExplicitRelationInit<
100
92
  TRelationType extends RelationType,
101
93
  TTables extends Record<string, AnyTable>,
102
94
  TTableName extends keyof TTables,
103
95
  > 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
96
  init(ormName: string): Relation<TRelationType, TTables[TTableName]> {
127
97
  const id = `${this.referencer.ormName}_${this.referencedTable.ormName}`;
128
98
 
129
99
  return {
130
100
  id,
131
- foreignKey: this.initForeignKey(ormName),
132
101
  on: this.on,
133
102
  name: ormName,
134
103
  referencer: this.referencer,
@@ -136,14 +105,6 @@ export class ExplicitRelationInit<
136
105
  type: this.type,
137
106
  };
138
107
  }
139
-
140
- /**
141
- * Define custom foreign key name.
142
- */
143
- foreignKey(name: string) {
144
- this.foreignKeyName = name;
145
- return this;
146
- }
147
108
  }
148
109
 
149
110
  export interface Relation<
@@ -158,28 +119,36 @@ export interface Relation<
158
119
  referencer: AnyTable;
159
120
 
160
121
  on: [string, string][];
161
- foreignKey: ForeignKey;
162
122
  }
163
123
 
164
124
  export interface Table<
165
125
  TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
166
126
  TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
127
+ TIndexes extends Record<string, Index> = Record<string, Index>,
167
128
  > {
168
129
  name: string;
169
130
  ormName: string;
170
131
 
171
132
  columns: TColumns;
172
133
  relations: TRelations;
173
- foreignKeys: ForeignKey[];
174
- indexes: Index[];
134
+ indexes: TIndexes;
175
135
 
176
136
  /**
177
137
  * Get column by name
178
138
  */
179
139
  getColumnByName: (name: string) => AnyColumn | undefined;
140
+ /**
141
+ * Get the external ID column (user-facing)
142
+ */
180
143
  getIdColumn: () => AnyColumn;
181
-
182
- clone: () => Table<TColumns, TRelations>;
144
+ /**
145
+ * Get the internal ID column (database-native, used for joins)
146
+ */
147
+ getInternalIdColumn: () => AnyColumn;
148
+ /**
149
+ * Get the version column (for optimistic concurrency control)
150
+ */
151
+ getVersionColumn: () => AnyColumn;
183
152
  }
184
153
 
185
154
  type DefaultFunctionMap = {
@@ -214,15 +183,16 @@ export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown>
214
183
  name: string = "";
215
184
  ormName: string = "";
216
185
  isNullable: boolean = false;
217
- isUnique: boolean = false;
218
- isReference: boolean = false;
186
+ role: "external-id" | "internal-id" | "version" | "reference" | "regular" = "regular";
187
+ isHidden: boolean = false;
188
+
219
189
  default?:
220
190
  | { value: TypeMap[TType] }
221
191
  | {
222
192
  runtime: DefaultFunction<TType>;
223
193
  };
224
194
 
225
- table: AnyTable = undefined as unknown as AnyTable;
195
+ tableName: string = "";
226
196
 
227
197
  constructor(type: TType) {
228
198
  this.type = type;
@@ -238,6 +208,11 @@ export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown>
238
208
  >;
239
209
  }
240
210
 
211
+ hidden<THidden extends boolean = true>(hidden?: THidden) {
212
+ this.isHidden = hidden ?? true;
213
+ return this as Column<TType, null, null>;
214
+ }
215
+
241
216
  /**
242
217
  * Generate default value on runtime
243
218
  */
@@ -256,22 +231,6 @@ export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown>
256
231
  return this;
257
232
  }
258
233
 
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
234
  /**
276
235
  * Generate default value for the column on runtime.
277
236
  */
@@ -293,11 +252,20 @@ export class Column<TType extends keyof TypeMap, TIn = unknown, TOut = unknown>
293
252
  return this.default.runtime();
294
253
  }
295
254
 
255
+ /**
256
+ * @description This is used for type inference only. Runtime value will be undefined.
257
+ * @internal
258
+ */
296
259
  get $in(): TIn {
297
- throw new Error("Type inference only");
260
+ return undefined as unknown as TIn;
298
261
  }
262
+
263
+ /**
264
+ * @description This is used for type inference only. Runtime value will be undefined.
265
+ * @internal
266
+ */
299
267
  get $out(): TOut {
300
- throw new Error("Type inference only");
268
+ return undefined as unknown as TOut;
301
269
  }
302
270
  }
303
271
 
@@ -308,18 +276,6 @@ export class IdColumn<
308
276
  > extends Column<TType, TIn, TOut> {
309
277
  id = true;
310
278
 
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
279
  override defaultTo$(fn: DefaultFunction<TType>) {
324
280
  return super.defaultTo$(fn) as IdColumn<TType, TIn | null, TOut>;
325
281
  }
@@ -329,6 +285,32 @@ export class IdColumn<
329
285
  }
330
286
  }
331
287
 
288
+ /**
289
+ * Internal ID column - used for database-native joins and foreign keys.
290
+ * Hidden from user API by default.
291
+ */
292
+ export class InternalIdColumn<TIn = unknown, TOut = unknown> extends Column<"bigint", TIn, TOut> {
293
+ override role = "internal-id" as const;
294
+
295
+ constructor() {
296
+ super("bigint");
297
+ this.hidden();
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Version column - used for optimistic concurrency control.
303
+ * Automatically incremented on each update.
304
+ */
305
+ export class VersionColumn<TIn = unknown, TOut = unknown> extends Column<"integer", TIn, TOut> {
306
+ override role = "version" as const;
307
+
308
+ constructor() {
309
+ super("integer");
310
+ this.defaultTo(0).hidden();
311
+ }
312
+ }
313
+
332
314
  export function column<TType extends keyof TypeMap>(
333
315
  type: TType,
334
316
  ): Column<TType, TypeMap[TType], TypeMap[TType]> {
@@ -336,140 +318,316 @@ export function column<TType extends keyof TypeMap>(
336
318
  }
337
319
 
338
320
  /**
339
- * Create a reference column that points to another table.
321
+ * Create a reference column that points to another table's internal ID.
340
322
  * This is used for foreign key relationships.
323
+ * Always uses bigint to match the internal ID type.
341
324
  */
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]>;
325
+ export function referenceColumn(): Column<
326
+ "bigint",
327
+ string | bigint | FragnoId | FragnoReference,
328
+ FragnoReference
329
+ > {
330
+ const col = new Column<"bigint", string | bigint | FragnoId | FragnoReference, FragnoReference>(
331
+ "bigint",
332
+ );
333
+ col.role = "reference";
334
+ return col;
349
335
  }
350
336
 
351
- export function idColumn(): IdColumn<"varchar(30)", string, string> {
352
- const col = new IdColumn<"varchar(30)", string, string>("varchar(30)");
337
+ /**
338
+ * Create an external ID column (user-facing).
339
+ * This is a CUID string that can be auto-generated or user-provided.
340
+ * Input accepts string | FragnoId | null, output returns FragnoId.
341
+ */
342
+ export function idColumn(): IdColumn<"varchar(30)", string | FragnoId | null, FragnoId> {
343
+ const col = new IdColumn<"varchar(30)", string | FragnoId | null, FragnoId>("varchar(30)");
344
+ col.role = "external-id";
353
345
  col.defaultTo$("auto");
354
- return col as IdColumn<"varchar(30)", string, string>;
346
+ return col;
347
+ }
348
+
349
+ /**
350
+ * Create an internal ID column (database-native, hidden from user API).
351
+ * Used for joins and foreign keys.
352
+ * @internal
353
+ */
354
+ export function internalIdColumn(): InternalIdColumn<null, bigint> {
355
+ const col = new InternalIdColumn<null, bigint>();
356
+ col.role = "internal-id";
357
+ col.hidden();
358
+ return col;
359
+ }
360
+
361
+ /**
362
+ * Create a version column for optimistic concurrency control.
363
+ * @internal
364
+ */
365
+ export function versionColumn(): VersionColumn<null, number> {
366
+ const col = new VersionColumn<null, number>();
367
+ col.role = "version";
368
+ col.hidden();
369
+ return col;
355
370
  }
356
371
 
357
- type RelationType = "one";
372
+ /**
373
+ * FragnoId represents a unified ID object that can contain external ID, internal ID, or both.
374
+ * @internal
375
+ *
376
+ * For query inputs: externalId is sufficient (internalId is optional)
377
+ * For query results: both externalId and internalId are provided
378
+ */
379
+ export class FragnoId {
380
+ readonly #externalId: string;
381
+ readonly #internalId?: bigint;
382
+ readonly #version: number;
383
+
384
+ constructor({
385
+ externalId,
386
+ internalId,
387
+ version,
388
+ }: {
389
+ externalId: string;
390
+ internalId?: bigint;
391
+ version: number;
392
+ }) {
393
+ this.#externalId = externalId;
394
+ this.#internalId = internalId;
395
+ this.#version = version;
396
+ }
397
+
398
+ /**
399
+ * Create a FragnoId from just an external ID (for inputs)
400
+ */
401
+ static fromExternal(externalId: string, version: number): FragnoId {
402
+ return new FragnoId({ externalId, version });
403
+ }
404
+
405
+ get version(): number {
406
+ return this.#version;
407
+ }
408
+
409
+ get externalId(): string {
410
+ return this.#externalId;
411
+ }
412
+
413
+ get internalId(): bigint | undefined {
414
+ return this.#internalId;
415
+ }
416
+
417
+ /**
418
+ * Get the appropriate ID for database operations
419
+ * Prefers internal ID if available, falls back to external ID
420
+ */
421
+ get databaseId(): string | bigint {
422
+ return this.#internalId ?? this.#externalId;
423
+ }
424
+
425
+ /**
426
+ * Convert to a plain object for serialization
427
+ */
428
+ toJSON(): { externalId: string; internalId?: string } {
429
+ return {
430
+ externalId: this.#externalId,
431
+ internalId: this.#internalId?.toString(),
432
+ };
433
+ }
434
+
435
+ toString(): string {
436
+ return this.#externalId;
437
+ }
438
+
439
+ valueOf(): string {
440
+ return this.#externalId;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * FragnoReference represents a foreign key reference to another table's internal ID.
446
+ * Unlike FragnoId, it only contains the internal ID (bigint) of the referenced record.
447
+ * This is used for reference columns in query results.
448
+ * @internal
449
+ */
450
+ export class FragnoReference {
451
+ readonly #internalId: bigint;
452
+
453
+ constructor(internalId: bigint) {
454
+ this.#internalId = internalId;
455
+ }
456
+
457
+ /**
458
+ * Create a FragnoReference from an internal ID
459
+ */
460
+ static fromInternal(internalId: bigint): FragnoReference {
461
+ return new FragnoReference(internalId);
462
+ }
463
+
464
+ /**
465
+ * Get the internal ID for database operations
466
+ */
467
+ get internalId(): bigint {
468
+ return this.#internalId;
469
+ }
470
+ }
471
+
472
+ type RelationType = "one" | "many";
358
473
 
359
474
  export class TableBuilder<
360
475
  TColumns extends Record<string, AnyColumn> = Record<string, AnyColumn>,
361
476
  TRelations extends Record<string, AnyRelation> = Record<string, AnyRelation>,
477
+ TIndexes extends Record<string, Index> = Record<string, Index>,
362
478
  > {
363
479
  #name: string;
364
480
  #columns: TColumns;
365
481
  #relations: TRelations;
366
- #foreignKeys: ForeignKey[] = [];
367
- #indexes: Index[] = [];
368
- #version: number = 0;
482
+ #indexes: TIndexes;
369
483
  #ormName: string = "";
370
- #operations: TableOperation[] = [];
484
+ #columnOrder: string[] = [];
371
485
 
372
486
  constructor(name: string) {
373
487
  this.#name = name;
374
488
  this.#columns = {} as TColumns;
375
489
  this.#relations = {} as TRelations;
490
+ this.#indexes = {} as TIndexes;
491
+ }
492
+
493
+ // For alterTable to set existing state
494
+ setColumns(columns: TColumns): void {
495
+ this.#columns = { ...columns };
496
+ }
497
+
498
+ setRelations(relations: TRelations): void {
499
+ this.#relations = { ...relations };
500
+ }
501
+
502
+ setIndexes(indexes: TIndexes): void {
503
+ this.#indexes = { ...indexes };
504
+ }
505
+
506
+ // For SchemaBuilder to read collected indexes
507
+ getIndexes(): Index[] {
508
+ return Object.values(this.#indexes) as Index[];
509
+ }
510
+
511
+ getColumnOrder(): string[] {
512
+ return this.#columnOrder;
376
513
  }
377
514
 
378
515
  /**
379
- * Add a column to the table. Increments the version counter.
516
+ * Add a column to the table.
380
517
  */
381
518
  addColumn<TColumnName extends string, TColumn extends AnyColumn>(
382
519
  ormName: TColumnName,
383
520
  col: TColumn,
384
- ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations>;
521
+ ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations, TIndexes>;
385
522
 
386
523
  /**
387
- * Add a column to the table with simplified syntax. Increments the version counter.
524
+ * Add a column to the table with simplified syntax.
388
525
  */
389
526
  addColumn<TColumnName extends string, TType extends keyof TypeMap>(
390
527
  ormName: TColumnName,
391
528
  type: TType,
392
529
  ): TableBuilder<
393
530
  TColumns & Record<TColumnName, Column<TType, TypeMap[TType], TypeMap[TType]>>,
394
- TRelations
531
+ TRelations,
532
+ TIndexes
395
533
  >;
396
534
 
397
535
  addColumn<TColumnName extends string, TColumn extends AnyColumn, TType extends keyof TypeMap>(
398
536
  ormName: TColumnName,
399
537
  colOrType: TColumn | TType,
400
- ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations> {
401
- this.#version++;
402
-
538
+ ): TableBuilder<TColumns & Record<TColumnName, TColumn>, TRelations, TIndexes> {
403
539
  // Create the column if a type string was provided
404
540
  const col = typeof colOrType === "string" ? column(colOrType) : colOrType;
405
541
 
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
542
  // Set column metadata
420
543
  col.ormName = ormName;
421
544
  col.name = ormName;
422
545
 
423
- return builder;
546
+ // Add column directly to this builder
547
+ this.#columns[ormName] = col as unknown as TColumns[TColumnName];
548
+ this.#columnOrder.push(ormName);
549
+
550
+ return this as unknown as TableBuilder<
551
+ TColumns & Record<TColumnName, TColumn>,
552
+ TRelations,
553
+ TIndexes
554
+ >;
424
555
  }
425
556
 
426
557
  /**
427
- * Create an index on the specified columns. Increments the version counter.
558
+ * Create an index on the specified columns.
428
559
  */
429
- createIndex<TColumnName extends string & keyof TColumns>(
430
- name: string,
431
- columns: TColumnName[],
560
+ createIndex<
561
+ TIndexName extends string,
562
+ const TColumnNames extends readonly (string & keyof TColumns)[],
563
+ >(
564
+ name: TIndexName,
565
+ columns: TColumnNames,
432
566
  options?: { unique?: boolean },
433
- ): TableBuilder<TColumns, TRelations> {
434
- this.#version++;
435
-
436
- const cols = columns.map((name) => {
437
- const column = this.#columns[name];
567
+ ): TableBuilder<
568
+ TColumns,
569
+ TRelations,
570
+ TIndexes & Record<TIndexName, Index<ColumnsToTuple<TColumns, TColumnNames>, TColumnNames>>
571
+ > {
572
+ const cols = columns.map((colName) => {
573
+ const column = this.#columns[colName];
438
574
  if (!column) {
439
- throw new Error(`Unknown column name ${name}`);
575
+ throw new Error(`Unknown column name ${colName}`);
440
576
  }
441
577
  return column;
442
578
  });
443
579
 
444
580
  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",
581
+ // Safe: we're adding the index to the internal indexes object
582
+ this.#indexes[name] = {
450
583
  name,
451
- columns: columns as string[],
584
+ columns: cols,
585
+ columnNames: columns,
452
586
  unique,
453
- });
587
+ } as unknown as TIndexes[TIndexName];
454
588
 
455
- return this;
589
+ return this as unknown as TableBuilder<
590
+ TColumns,
591
+ TRelations,
592
+ TIndexes & Record<TIndexName, Index<ColumnsToTuple<TColumns, TColumnNames>, TColumnNames>>
593
+ >;
456
594
  }
457
595
 
458
596
  /**
459
597
  * Build the final table. This should be called after all columns are added.
460
598
  */
461
- build(): Table<TColumns, TRelations> {
599
+ build(): Table<TColumns, TRelations, TIndexes> {
462
600
  let idCol: AnyColumn | undefined;
601
+ let internalIdCol: AnyColumn | undefined;
602
+ let versionCol: AnyColumn | undefined;
603
+
604
+ // TODO: Throw if user manually added version/internalId columns
605
+
606
+ // Auto-add _internalId and _version columns if not already present
607
+ if (!this.#columns["_internalId"]) {
608
+ const col = internalIdColumn();
609
+ col.ormName = "_internalId";
610
+ col.name = "_internalId";
611
+ // Safe: we're adding system columns to the internal columns object
612
+ (this.#columns as Record<string, AnyColumn>)["_internalId"] = col;
613
+ }
614
+
615
+ if (!this.#columns["_version"]) {
616
+ const col = versionColumn();
617
+ col.ormName = "_version";
618
+ col.name = "_version";
619
+ // Safe: we're adding system columns to the internal columns object
620
+ (this.#columns as Record<string, AnyColumn>)["_version"] = col;
621
+ }
463
622
 
464
623
  // Use name as ormName if ormName is not set
465
624
  const ormName = this.#ormName || this.#name;
466
625
 
467
- const table: Table<TColumns, TRelations> = {
626
+ const table: Table<TColumns, TRelations, TIndexes> = {
468
627
  name: this.#name,
469
628
  ormName,
470
629
  columns: this.#columns,
471
630
  relations: this.#relations,
472
- foreignKeys: this.#foreignKeys,
473
631
  indexes: this.#indexes,
474
632
  getColumnByName: (name) => {
475
633
  return Object.values(this.#columns).find((c) => c.name === name);
@@ -477,78 +635,45 @@ export class TableBuilder<
477
635
  getIdColumn: () => {
478
636
  return idCol!;
479
637
  },
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;
638
+ getInternalIdColumn: () => {
639
+ return internalIdCol!;
640
+ },
641
+ getVersionColumn: () => {
642
+ return versionCol!;
499
643
  },
500
644
  };
501
645
 
502
- // Set table reference and find id column
646
+ // Set table reference and find special columns
503
647
  for (const k in this.#columns) {
504
648
  const column = this.#columns[k];
505
649
  if (!column) {
506
650
  continue;
507
651
  }
508
652
 
509
- column.table = table;
510
- if (column instanceof IdColumn) {
653
+ column.tableName = table.name;
654
+ if (column instanceof IdColumn || column.role === "external-id") {
511
655
  idCol = column;
512
656
  }
657
+ if (column instanceof InternalIdColumn || column.role === "internal-id") {
658
+ internalIdCol = column;
659
+ }
660
+ if (column instanceof VersionColumn || column.role === "version") {
661
+ versionCol = column;
662
+ }
513
663
  }
514
664
 
515
665
  if (idCol === undefined) {
516
666
  throw new Error(`there's no id column in your table ${this.#name}`);
517
667
  }
668
+ if (internalIdCol === undefined) {
669
+ throw new Error(`there's no internal id column in your table ${this.#name}`);
670
+ }
671
+ if (versionCol === undefined) {
672
+ throw new Error(`there's no version column in your table ${this.#name}`);
673
+ }
518
674
 
519
675
  return table;
520
676
  }
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
677
  }
553
678
 
554
679
  export interface Schema<TTables extends Record<string, AnyTable> = Record<string, AnyTable>> {
@@ -566,13 +691,90 @@ export interface Schema<TTables extends Record<string, AnyTable> = Record<string
566
691
  clone: () => Schema<TTables>;
567
692
  }
568
693
 
569
- export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<string, never>> {
694
+ /**
695
+ * Utility type for updating a single table's relations in a schema.
696
+ * Used to properly type the return value of addReference.
697
+ */
698
+ type UpdateTableRelations<
699
+ TTables extends Record<string, AnyTable>,
700
+ TTableName extends keyof TTables,
701
+ TReferenceName extends string,
702
+ TReferencedTableName extends keyof TTables,
703
+ TRelationType extends RelationType = RelationType,
704
+ > = {
705
+ [K in keyof TTables]: K extends TTableName
706
+ ? Table<
707
+ TTables[TTableName]["columns"],
708
+ TTables[TTableName]["relations"] &
709
+ Record<TReferenceName, Relation<TRelationType, TTables[TReferencedTableName]>>,
710
+ TTables[TTableName]["indexes"]
711
+ >
712
+ : TTables[K];
713
+ };
714
+
715
+ /**
716
+ * Utility type for updating a single table in a schema.
717
+ * Used to properly type the return value of alterTable.
718
+ */
719
+ type UpdateTable<
720
+ TTables extends Record<string, AnyTable>,
721
+ TTableName extends keyof TTables,
722
+ TNewColumns extends Record<string, AnyColumn>,
723
+ TNewRelations extends Record<string, AnyRelation>,
724
+ TNewIndexes extends Record<string, Index>,
725
+ > = {
726
+ [K in keyof TTables]: K extends TTableName
727
+ ? Table<TNewColumns, TNewRelations, TNewIndexes>
728
+ : TTables[K];
729
+ };
730
+
731
+ /**
732
+ * Map an array of column names to a tuple of their actual column types
733
+ */
734
+ type ColumnsToTuple<
735
+ TColumns extends Record<string, AnyColumn>,
736
+ TColumnNames extends readonly (keyof TColumns)[],
737
+ > = {
738
+ [K in keyof TColumnNames]: TColumnNames[K] extends keyof TColumns
739
+ ? TColumns[TColumnNames[K]]
740
+ : never;
741
+ } & AnyColumn[];
742
+
743
+ export class SchemaBuilder<TTables extends Record<string, AnyTable> = {}> {
570
744
  #tables: TTables;
571
745
  #version: number = 0;
572
746
  #operations: SchemaOperation[] = [];
573
747
 
574
- constructor() {
575
- this.#tables = {} as TTables;
748
+ constructor(existingSchema?: Schema<TTables>) {
749
+ if (existingSchema) {
750
+ this.#tables = existingSchema.tables;
751
+ this.#version = existingSchema.version;
752
+ this.#operations = [...existingSchema.operations];
753
+ } else {
754
+ this.#tables = {} as TTables;
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Add an existing schema to this builder.
760
+ * Merges tables and operations from the provided schema.
761
+ *
762
+ * @example
763
+ * ```ts
764
+ * const builder = new SchemaBuilder()
765
+ * .add(userSchema)
766
+ * .add(postSchema)
767
+ * .addTable("comments", ...);
768
+ * ```
769
+ */
770
+ mergeWithExistingSchema<TNewTables extends Record<string, AnyTable>>(
771
+ schema: Schema<TNewTables>,
772
+ ): SchemaBuilder<TTables & TNewTables> {
773
+ this.#tables = { ...this.#tables, ...schema.tables } as TTables & TNewTables;
774
+ this.#operations = [...this.#operations, ...schema.operations];
775
+ this.#version += schema.version;
776
+
777
+ return this as unknown as SchemaBuilder<TTables & TNewTables>;
576
778
  }
577
779
 
578
780
  /**
@@ -582,172 +784,309 @@ export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<str
582
784
  TTableName extends string,
583
785
  TColumns extends Record<string, AnyColumn>,
584
786
  TRelations extends Record<string, AnyRelation>,
787
+ TIndexes extends Record<string, Index> = Record<string, Index>,
585
788
  >(
586
789
  ormName: TTableName,
587
790
  callback: (
588
- builder: TableBuilder<Record<string, AnyColumn>, Record<string, AnyRelation>>,
589
- ) => TableBuilder<TColumns, TRelations>,
590
- ): SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations>>> {
791
+ builder: TableBuilder<
792
+ Record<string, AnyColumn>,
793
+ Record<string, AnyRelation>,
794
+ Record<string, Index>
795
+ >,
796
+ ) => TableBuilder<TColumns, TRelations, TIndexes>,
797
+ ): SchemaBuilder<TTables & Record<TTableName, Table<TColumns, TRelations, TIndexes>>> {
591
798
  this.#version++;
592
799
 
593
800
  const tableBuilder = new TableBuilder(ormName);
594
801
  const result = callback(tableBuilder);
595
802
  const builtTable = result.build();
596
-
597
- // Set table metadata
598
803
  builtTable.ormName = ormName;
599
804
 
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
- }
805
+ // Collect sub-operations in order
806
+ const subOperations: TableSubOperation[] = [];
807
+
808
+ // Add user-defined columns first
809
+ const columnOrder = result.getColumnOrder();
810
+ for (const colName of columnOrder) {
811
+ const col = builtTable.columns[colName];
812
+ subOperations.push({
813
+ type: "add-column",
814
+ columnName: colName,
815
+ column: col,
816
+ });
627
817
  }
628
818
 
629
- builder.#version = this.#version;
630
- builder.#operations = newOperations;
819
+ // Add system columns (_internalId and _version) that were auto-added
820
+ if (builtTable.columns["_internalId"]) {
821
+ subOperations.push({
822
+ type: "add-column",
823
+ columnName: "_internalId",
824
+ column: builtTable.columns["_internalId"],
825
+ });
826
+ }
827
+ if (builtTable.columns["_version"]) {
828
+ subOperations.push({
829
+ type: "add-column",
830
+ columnName: "_version",
831
+ column: builtTable.columns["_version"],
832
+ });
833
+ }
631
834
 
632
- return builder;
835
+ // Add indexes from builder
836
+ for (const idx of result.getIndexes()) {
837
+ subOperations.push({
838
+ type: "add-index",
839
+ name: idx.name,
840
+ columns: idx.columns.map((c) => c.ormName),
841
+ unique: idx.unique,
842
+ });
843
+ }
844
+
845
+ // Add the add-table operation
846
+ this.#operations.push({
847
+ type: "add-table",
848
+ tableName: ormName,
849
+ operations: subOperations,
850
+ });
851
+
852
+ // Update tables map
853
+ this.#tables = { ...this.#tables, [ormName]: builtTable } as TTables &
854
+ Record<TTableName, Table<TColumns, TRelations, TIndexes>>;
855
+
856
+ return this as unknown as SchemaBuilder<
857
+ TTables & Record<TTableName, Table<TColumns, TRelations, TIndexes>>
858
+ >;
633
859
  }
634
860
 
635
861
  /**
636
- * Add a foreign key reference from this table to another table.
862
+ * Add a relation between two tables.
637
863
  *
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
864
+ * @param referenceName - A name for this relation (e.g., "author", "posts")
865
+ * @param config - Configuration specifying the relation type and foreign key mapping
641
866
  *
642
867
  * @example
643
868
  * ```ts
644
- * // Basic foreign key: post -> user
869
+ * // One-to-one or many-to-one: post -> user
645
870
  * schema(s => s
646
871
  * .addTable("users", t => t.addColumn("id", idColumn()))
647
872
  * .addTable("posts", t => t
648
873
  * .addColumn("id", idColumn())
649
- * .addColumn("authorId", referenceColumn()))
650
- * .addReference("posts", "author", {
651
- * columns: ["authorId"],
652
- * targetTable: "users",
653
- * targetColumns: ["id"],
874
+ * .addColumn("userId", referenceColumn()))
875
+ * .addReference("author", {
876
+ * type: "one",
877
+ * from: { table: "posts", column: "userId" },
878
+ * to: { table: "users", column: "id" },
654
879
  * })
655
880
  * )
656
881
  *
657
- * // Self-referencing foreign key
658
- * .addReference("users", "inviter", {
659
- * columns: ["invitedBy"],
660
- * targetTable: "users",
661
- * targetColumns: ["id"],
882
+ * // One-to-many (inverse relation): user -> posts
883
+ * .addReference("posts", {
884
+ * type: "many",
885
+ * from: { table: "users", column: "id" },
886
+ * to: { table: "posts", column: "userId" },
662
887
  * })
663
888
  *
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"],
889
+ * // Self-referencing foreign key
890
+ * .addReference("inviter", {
891
+ * type: "one",
892
+ * from: { table: "users", column: "invitedBy" },
893
+ * to: { table: "users", column: "id" },
674
894
  * })
675
895
  * ```
676
896
  */
677
897
  addReference<
678
- TTableName extends string & keyof TTables,
679
- TReferencedTableName extends string & keyof TTables,
898
+ TFromTableName extends string & keyof TTables,
899
+ TToTableName extends string & keyof TTables,
900
+ TReferenceName extends string,
901
+ TRelationType extends RelationType,
680
902
  >(
681
- tableName: TTableName,
682
- referenceName: string,
903
+ referenceName: TReferenceName,
683
904
  config: {
684
- columns: (keyof TTables[TTableName]["columns"])[];
685
- targetTable: TReferencedTableName;
686
- targetColumns: (keyof TTables[TReferencedTableName]["columns"])[];
905
+ type: TRelationType;
906
+ from: {
907
+ table: TFromTableName;
908
+ column: keyof TTables[TFromTableName]["columns"];
909
+ };
910
+ to: {
911
+ table: TToTableName;
912
+ column: keyof TTables[TToTableName]["columns"];
913
+ };
687
914
  },
688
- ): SchemaBuilder<TTables> {
915
+ ): SchemaBuilder<
916
+ UpdateTableRelations<TTables, TFromTableName, TReferenceName, TToTableName, TRelationType>
917
+ > {
689
918
  this.#version++;
690
919
 
691
- const table = this.#tables[tableName];
692
- const referencedTable = this.#tables[config.targetTable];
920
+ const table = this.#tables[config.from.table];
921
+ const referencedTable = this.#tables[config.to.table];
693
922
 
694
923
  if (!table) {
695
- throw new Error(`Table ${tableName} not found in schema`);
924
+ throw new Error(`Table ${config.from.table} not found in schema`);
696
925
  }
697
926
  if (!referencedTable) {
698
- throw new Error(`Referenced table ${config.targetTable} not found in schema`);
927
+ throw new Error(`Referenced table ${config.to.table} not found in schema`);
699
928
  }
700
929
 
701
- const { columns, targetColumns } = config;
930
+ const columnName = config.from.column as string;
931
+ const targetColumnName = config.to.column as string;
702
932
 
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;
933
+ // Foreign keys always reference internal IDs, not external IDs
934
+ // If user specifies "id", translate to "_internalId" for the actual FK
935
+ const actualTargetColumnName = targetColumnName === "id" ? "_internalId" : targetColumnName;
718
936
 
719
937
  const column = table.columns[columnName];
720
- const referencedColumn = referencedTable.columns[targetColumnName];
938
+ const referencedColumn = referencedTable.columns[actualTargetColumnName];
721
939
 
722
940
  if (!column) {
723
- throw new Error(`Column ${columnName} not found in table ${tableName}`);
941
+ throw new Error(`Column ${columnName} not found in table ${config.from.table}`);
724
942
  }
725
943
  if (!referencedColumn) {
726
- throw new Error(`Column ${targetColumnName} not found in table ${config.targetTable}`);
944
+ throw new Error(`Column ${actualTargetColumnName} not found in table ${config.to.table}`);
945
+ }
946
+
947
+ // Verify that reference columns are bigint (matching internal ID type)
948
+ if (column.role === "reference" && column.type !== "bigint") {
949
+ throw new Error(
950
+ `Reference column ${columnName} must be of type bigint to match internal ID type`,
951
+ );
727
952
  }
728
953
 
729
- // Create the relation
730
- const init = new ExplicitRelationInit("one", referencedTable, table);
954
+ // Create the relation (use the user-facing column name for the relation)
955
+ const init = new ExplicitRelationInit(config.type, referencedTable, table);
731
956
  init.on.push([columnName, targetColumnName]);
732
957
  const relation = init.init(referenceName);
733
958
 
734
- // Add relation and foreign key to the table
959
+ // Add relation to the table
735
960
  table.relations[referenceName] = relation;
736
- table.foreignKeys.push(relation.foreignKey);
737
961
 
738
962
  // Record the operation
739
963
  this.#operations.push({
740
964
  type: "add-reference",
741
- tableName: tableName as string,
965
+ tableName: config.from.table,
742
966
  referenceName,
743
967
  config: {
744
- columns: columns as string[],
745
- targetTable: config.targetTable as string,
746
- targetColumns: targetColumns as string[],
968
+ type: config.type,
969
+ from: { table: config.from.table, column: columnName },
970
+ to: { table: config.to.table, column: actualTargetColumnName },
747
971
  },
748
972
  });
749
973
 
750
- return this;
974
+ // Return this with updated type
975
+ // Safe: The relation was added to the table in place and now has the updated relations
976
+ return this as unknown as SchemaBuilder<
977
+ UpdateTableRelations<TTables, TFromTableName, TReferenceName, TToTableName, TRelationType>
978
+ >;
979
+ }
980
+
981
+ /**
982
+ * Alter an existing table by adding columns or indexes.
983
+ * This is used for append-only schema modifications.
984
+ *
985
+ * @param tableName - The name of the table to modify
986
+ * @param callback - A callback that receives a table builder for adding columns/indexes
987
+ *
988
+ * @example
989
+ * ```ts
990
+ * // Add a new column to an existing table
991
+ * schema(s => s
992
+ * .addTable("users", t => t
993
+ * .addColumn("id", idColumn())
994
+ * .addColumn("name", column("string")))
995
+ * .alterTable("users", t => t
996
+ * .addColumn("email", column("string"))
997
+ * .addColumn("age", column("integer").nullable())
998
+ * .createIndex("idx_email", ["email"]))
999
+ * )
1000
+ * ```
1001
+ */
1002
+ alterTable<
1003
+ TTableName extends string & keyof TTables,
1004
+ TNewColumns extends Record<string, AnyColumn>,
1005
+ TNewRelations extends Record<string, AnyRelation>,
1006
+ TNewIndexes extends Record<string, Index> = Record<string, Index>,
1007
+ >(
1008
+ tableName: TTableName,
1009
+ callback: (
1010
+ builder: TableBuilder<
1011
+ TTables[TTableName]["columns"],
1012
+ TTables[TTableName]["relations"],
1013
+ Record<string, Index>
1014
+ >,
1015
+ ) => TableBuilder<TNewColumns, TNewRelations, TNewIndexes>,
1016
+ ): SchemaBuilder<UpdateTable<TTables, TTableName, TNewColumns, TNewRelations, TNewIndexes>> {
1017
+ const table = this.#tables[tableName];
1018
+
1019
+ if (!table) {
1020
+ throw new Error(`Table ${tableName} not found in schema`);
1021
+ }
1022
+
1023
+ // Create builder with existing table state
1024
+ const tableBuilder = new TableBuilder(tableName);
1025
+ tableBuilder.setColumns(table.columns);
1026
+ tableBuilder.setRelations(table.relations);
1027
+ tableBuilder.setIndexes(table.indexes);
1028
+
1029
+ // Track existing columns and indexes
1030
+ const existingColumns = new Set(Object.keys(table.columns));
1031
+ const existingIndexes = new Set(Object.keys(table.indexes));
1032
+
1033
+ // Apply modifications
1034
+ const resultBuilder = callback(
1035
+ tableBuilder as TableBuilder<
1036
+ TTables[TTableName]["columns"],
1037
+ TTables[TTableName]["relations"],
1038
+ Record<string, Index>
1039
+ >,
1040
+ );
1041
+ const newTable = resultBuilder.build();
1042
+
1043
+ // Collect sub-operations
1044
+ const subOperations: TableSubOperation[] = [];
1045
+
1046
+ // Find new columns (preserve order from builder)
1047
+ const columnOrder = resultBuilder.getColumnOrder();
1048
+ for (const colName of columnOrder) {
1049
+ if (!existingColumns.has(colName)) {
1050
+ subOperations.push({
1051
+ type: "add-column",
1052
+ columnName: colName,
1053
+ column: newTable.columns[colName],
1054
+ });
1055
+ }
1056
+ }
1057
+
1058
+ // Add only new indexes
1059
+ for (const idx of resultBuilder.getIndexes()) {
1060
+ if (!existingIndexes.has(idx.name)) {
1061
+ subOperations.push({
1062
+ type: "add-index",
1063
+ name: idx.name,
1064
+ columns: idx.columns.map((c) => c.ormName),
1065
+ unique: idx.unique,
1066
+ });
1067
+ }
1068
+ }
1069
+
1070
+ if (subOperations.length > 0) {
1071
+ this.#version++;
1072
+ this.#operations.push({
1073
+ type: "alter-table",
1074
+ tableName,
1075
+ operations: subOperations,
1076
+ });
1077
+ }
1078
+
1079
+ // Update table reference in schema
1080
+ this.#tables[tableName] = newTable as unknown as TTables[TTableName];
1081
+
1082
+ // Set table name for all columns
1083
+ for (const col of Object.values(newTable.columns)) {
1084
+ col.tableName = newTable.name;
1085
+ }
1086
+
1087
+ return this as unknown as SchemaBuilder<
1088
+ UpdateTable<TTables, TTableName, TNewColumns, TNewRelations, TNewIndexes>
1089
+ >;
751
1090
  }
752
1091
 
753
1092
  /**
@@ -766,15 +1105,45 @@ export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<str
766
1105
  const cloneTables: Record<string, AnyTable> = {};
767
1106
 
768
1107
  for (const [k, v] of Object.entries(tables)) {
769
- cloneTables[k] = v.clone();
1108
+ // Create a new table with cloned columns
1109
+ const clonedColumns: Record<string, AnyColumn> = {};
1110
+ for (const [colName, col] of Object.entries(v.columns)) {
1111
+ // Create a new column with the same properties, preserving the column type
1112
+ let clonedCol: AnyColumn;
1113
+ if (col instanceof InternalIdColumn) {
1114
+ clonedCol = new InternalIdColumn();
1115
+ } else if (col instanceof VersionColumn) {
1116
+ clonedCol = new VersionColumn();
1117
+ } else if (col instanceof IdColumn) {
1118
+ clonedCol = new IdColumn(col.type);
1119
+ } else {
1120
+ clonedCol = new Column(col.type);
1121
+ }
1122
+
1123
+ clonedCol.name = col.name;
1124
+ clonedCol.ormName = col.ormName;
1125
+ clonedCol.isNullable = col.isNullable;
1126
+ clonedCol.role = col.role;
1127
+ clonedCol.isHidden = col.isHidden;
1128
+ clonedCol.default = col.default;
1129
+ clonedCol.tableName = col.tableName;
1130
+ clonedColumns[colName] = clonedCol;
1131
+ }
1132
+
1133
+ cloneTables[k] = {
1134
+ ...v,
1135
+ columns: clonedColumns,
1136
+ };
770
1137
  }
771
1138
 
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();
1139
+ return new SchemaBuilder<TTables>({
1140
+ version,
1141
+ tables: cloneTables as TTables,
1142
+ operations: [...operations],
1143
+ clone: () => {
1144
+ throw new Error("Cannot clone during clone");
1145
+ },
1146
+ }).build();
778
1147
  },
779
1148
  };
780
1149
 
@@ -792,18 +1161,20 @@ export class SchemaBuilder<TTables extends Record<string, AnyTable> = Record<str
792
1161
  /**
793
1162
  * Create a new schema with callback pattern.
794
1163
  */
795
- export function schema<TTables extends Record<string, AnyTable> = Record<string, never>>(
796
- callback: (builder: SchemaBuilder<Record<string, never>>) => SchemaBuilder<TTables>,
1164
+ export function schema<const TTables extends Record<string, AnyTable> = {}>(
1165
+ callback: (builder: SchemaBuilder<{}>) => SchemaBuilder<TTables>,
797
1166
  ): Schema<TTables> {
798
1167
  return callback(new SchemaBuilder()).build();
799
1168
  }
800
1169
 
801
- export function compileForeignKey(key: ForeignKey) {
1170
+ export function compileForeignKey(key: ForeignKey, nameType: "sql" | "orm" = "orm") {
802
1171
  return {
803
1172
  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),
1173
+ table: nameType === "sql" ? key.table.name : key.table.ormName,
1174
+ referencedTable: nameType === "sql" ? key.referencedTable.name : key.referencedTable.ormName,
1175
+ referencedColumns: key.referencedColumns.map((col) =>
1176
+ nameType === "sql" ? col.name : col.ormName,
1177
+ ),
1178
+ columns: key.columns.map((col) => (nameType === "sql" ? col.name : col.ormName)),
808
1179
  };
809
1180
  }