@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
@@ -0,0 +1,382 @@
1
+ import {
2
+ type ColumnBuilderCallback,
3
+ type Compilable,
4
+ type CreateTableBuilder,
5
+ type Kysely,
6
+ type RawBuilder,
7
+ sql,
8
+ } from "kysely";
9
+ import {
10
+ type CustomOperation,
11
+ isUpdated,
12
+ type ColumnOperation,
13
+ type MigrationOperation,
14
+ type ColumnInfo,
15
+ } from "../../../migration-engine/shared";
16
+ import type { SQLProvider } from "../../../shared/providers";
17
+ import { schemaToDBType } from "../../../schema/serialize";
18
+ import type { KyselyConfig } from "../kysely-adapter";
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ type KyselyAny = Kysely<any>;
22
+
23
+ export type ExecuteNode = Compilable & {
24
+ execute(): Promise<unknown>;
25
+ };
26
+
27
+ const errors = {
28
+ IdColumnUpdate:
29
+ "ID columns cannot be updated. Not every database supports updating primary keys and often requires workarounds.",
30
+ SQLiteUpdateColumn: "SQLite doesn't support updating columns. Recreate the table instead.",
31
+ SQLiteUpdateForeignKeys:
32
+ "SQLite doesn't support modifying foreign keys directly. Use `recreate-table` instead.",
33
+ } as const;
34
+
35
+ /**
36
+ * Returns the appropriate foreign key action based on the provider.
37
+ * MSSQL doesn't support RESTRICT, so we use NO ACTION (functionally equivalent).
38
+ */
39
+ function getForeignKeyAction(provider: SQLProvider): "restrict" | "no action" {
40
+ return provider === "mssql" ? "no action" : "restrict";
41
+ }
42
+
43
+ /**
44
+ * Generates MSSQL default constraint name following the DF_tableName_columnName pattern.
45
+ */
46
+ function getMssqlDefaultConstraintName(tableName: string, columnName: string): string {
47
+ const MSSQL_DEFAULT_CONSTRAINT_PREFIX = "DF" as const;
48
+ return `${MSSQL_DEFAULT_CONSTRAINT_PREFIX}_${tableName}_${columnName}`;
49
+ }
50
+
51
+ function createUniqueIndex(
52
+ db: KyselyAny,
53
+ name: string,
54
+ tableName: string,
55
+ cols: string[],
56
+ provider: SQLProvider,
57
+ ) {
58
+ const query = db.schema.createIndex(name).on(tableName).columns(cols).unique();
59
+
60
+ if (provider === "mssql") {
61
+ // MSSQL: ignore null values in unique indexes by default
62
+ return query.where((b) => {
63
+ return b.and(cols.map((col) => b(col, "is not", null)));
64
+ });
65
+ }
66
+
67
+ return query;
68
+ }
69
+
70
+ function dropUniqueIndex(db: KyselyAny, name: string, tableName: string, provider: SQLProvider) {
71
+ let query = db.schema.dropIndex(name).ifExists();
72
+
73
+ if (provider === "cockroachdb") {
74
+ query = query.cascade();
75
+ }
76
+
77
+ if (provider === "mssql") {
78
+ query = query.on(tableName);
79
+ }
80
+
81
+ return query;
82
+ }
83
+
84
+ function executeColumn(
85
+ tableName: string,
86
+ operation: ColumnOperation,
87
+ config: KyselyConfig,
88
+ ): ExecuteNode[] {
89
+ const { db, provider } = config;
90
+ const next = () => db.schema.alterTable(tableName);
91
+ const results: ExecuteNode[] = [];
92
+
93
+ switch (operation.type) {
94
+ case "rename-column":
95
+ results.push(next().renameColumn(operation.from, operation.to));
96
+ return results;
97
+
98
+ case "drop-column":
99
+ results.push(next().dropColumn(operation.name));
100
+
101
+ return results;
102
+ case "create-column": {
103
+ const col = operation.value;
104
+
105
+ results.push(
106
+ next().addColumn(
107
+ col.name,
108
+ sql.raw(schemaToDBType(col, provider)),
109
+ getColumnBuilderCallback(col, provider),
110
+ ),
111
+ );
112
+
113
+ return results;
114
+ }
115
+ case "update-column": {
116
+ const col = operation.value;
117
+
118
+ if (col.role === "external-id" || col.role === "internal-id") {
119
+ throw new Error(errors.IdColumnUpdate);
120
+ }
121
+ if (provider === "sqlite") {
122
+ throw new Error(errors.SQLiteUpdateColumn);
123
+ }
124
+
125
+ if (!isUpdated(operation)) {
126
+ return results;
127
+ }
128
+ if (provider === "mysql") {
129
+ results.push(
130
+ next().modifyColumn(
131
+ operation.name,
132
+ sql.raw(schemaToDBType(col, provider)),
133
+ getColumnBuilderCallback(col, provider),
134
+ ),
135
+ );
136
+ return results;
137
+ }
138
+
139
+ // MSSQL requires dropping and recreating default constraints when changing data type or default value
140
+ const mssqlRecreateDefaultConstraint = operation.updateDataType || operation.updateDefault;
141
+
142
+ if (provider === "mssql" && mssqlRecreateDefaultConstraint) {
143
+ results.push(rawToNode(db, mssqlDropDefaultConstraint(tableName, col.name)));
144
+ }
145
+
146
+ // TODO: We should maybe do some of these operations in a single query
147
+
148
+ if (operation.updateDataType) {
149
+ const dbType = sql.raw(schemaToDBType(col, provider));
150
+
151
+ if (provider === "postgresql" || provider === "cockroachdb") {
152
+ // PostgreSQL/CockroachDB: Use explicit USING clause for type conversion
153
+ results.push(
154
+ rawToNode(
155
+ db,
156
+ sql`ALTER TABLE ${sql.ref(tableName)} ALTER COLUMN ${sql.ref(operation.name)} TYPE ${dbType} USING (${sql.ref(operation.name)}::${dbType})`,
157
+ ),
158
+ );
159
+ } else {
160
+ results.push(next().alterColumn(operation.name, (b) => b.setDataType(dbType)));
161
+ }
162
+ }
163
+
164
+ if (operation.updateNullable) {
165
+ results.push(
166
+ next().alterColumn(operation.name, (build) =>
167
+ col.isNullable ? build.dropNotNull() : build.setNotNull(),
168
+ ),
169
+ );
170
+ }
171
+
172
+ if (provider === "mssql" && mssqlRecreateDefaultConstraint) {
173
+ const defaultValue = defaultValueToDB(col, provider);
174
+
175
+ if (defaultValue) {
176
+ const constraintName = getMssqlDefaultConstraintName(tableName, col.name);
177
+
178
+ results.push(
179
+ rawToNode(
180
+ db,
181
+ sql`ALTER TABLE ${sql.ref(tableName)} ADD CONSTRAINT ${sql.ref(constraintName)} DEFAULT ${defaultValue} FOR ${sql.ref(col.name)}`,
182
+ ),
183
+ );
184
+ }
185
+ } else if (operation.updateDefault) {
186
+ const defaultValue = defaultValueToDB(col, provider);
187
+
188
+ results.push(
189
+ next().alterColumn(operation.name, (build) => {
190
+ if (!defaultValue) {
191
+ return build.dropDefault();
192
+ }
193
+ return build.setDefault(defaultValue);
194
+ }),
195
+ );
196
+ }
197
+
198
+ return results;
199
+ }
200
+ }
201
+ }
202
+
203
+ export function execute(
204
+ operation: MigrationOperation,
205
+ config: KyselyConfig,
206
+ onCustomNode: (op: CustomOperation) => ExecuteNode | ExecuteNode[],
207
+ ): ExecuteNode | ExecuteNode[] {
208
+ const { db, provider } = config;
209
+
210
+ function createTable(tableName: string, columns: ColumnInfo[]) {
211
+ let builder = db.schema.createTable(tableName) as CreateTableBuilder<string, string>;
212
+
213
+ // Add columns from the column info array
214
+ for (const columnInfo of columns) {
215
+ builder = builder.addColumn(
216
+ columnInfo.name,
217
+ sql.raw(schemaToDBType(columnInfo, provider)),
218
+ getColumnBuilderCallback(columnInfo, provider),
219
+ );
220
+ }
221
+
222
+ return builder;
223
+ }
224
+
225
+ switch (operation.type) {
226
+ case "create-table":
227
+ return createTable(operation.name, operation.columns);
228
+ case "rename-table":
229
+ if (provider === "mssql") {
230
+ return rawToNode(
231
+ db,
232
+ sql`EXEC sp_rename ${sql.lit(operation.from)}, ${sql.lit(operation.to)}`,
233
+ );
234
+ }
235
+
236
+ return db.schema.alterTable(operation.from).renameTo(operation.to);
237
+ case "alter-table": {
238
+ const results: ExecuteNode[] = [];
239
+
240
+ for (const op of operation.value) {
241
+ results.push(...executeColumn(operation.name, op, config));
242
+ }
243
+
244
+ return results;
245
+ }
246
+ case "drop-table":
247
+ return db.schema.dropTable(operation.name);
248
+ case "custom":
249
+ return onCustomNode(operation);
250
+ case "add-foreign-key": {
251
+ if (provider === "sqlite") {
252
+ throw new Error(errors.SQLiteUpdateForeignKeys);
253
+ }
254
+
255
+ const { table, value } = operation;
256
+ const action = getForeignKeyAction(provider);
257
+
258
+ return db.schema
259
+ .alterTable(table)
260
+ .addForeignKeyConstraint(
261
+ value.name,
262
+ value.columns,
263
+ value.referencedTable,
264
+ value.referencedColumns,
265
+ (b) => b.onUpdate(action).onDelete(action),
266
+ );
267
+ }
268
+ case "drop-foreign-key": {
269
+ if (provider === "sqlite") {
270
+ throw new Error(errors.SQLiteUpdateForeignKeys);
271
+ }
272
+
273
+ const { table, name } = operation;
274
+ let query = db.schema.alterTable(table).dropConstraint(name);
275
+
276
+ // MySQL doesn't support IF EXISTS for dropping constraints
277
+ if (provider !== "mysql") {
278
+ query = query.ifExists();
279
+ }
280
+
281
+ return query;
282
+ }
283
+ case "add-index": {
284
+ if (operation.unique) {
285
+ return createUniqueIndex(db, operation.name, operation.table, operation.columns, provider);
286
+ }
287
+ return db.schema.createIndex(operation.name).on(operation.table).columns(operation.columns);
288
+ }
289
+ case "drop-index": {
290
+ return dropUniqueIndex(db, operation.name, operation.table, provider);
291
+ }
292
+ }
293
+ }
294
+
295
+ // ============================================================================
296
+ // Helper Functions
297
+ // ============================================================================
298
+
299
+ function getColumnBuilderCallback(col: ColumnInfo, provider: SQLProvider): ColumnBuilderCallback {
300
+ return (build) => {
301
+ if (!col.isNullable) {
302
+ build = build.notNull();
303
+ }
304
+
305
+ // Internal ID is the primary key with auto-increment
306
+ if (col.role === "internal-id") {
307
+ build = build.primaryKey();
308
+ // Auto-increment for internal ID
309
+ if (provider === "postgresql" || provider === "cockroachdb") {
310
+ // SERIAL/BIGSERIAL handles auto-increment
311
+ // Already handled in schemaToDBType
312
+ } else if (provider === "mysql") {
313
+ build = build.autoIncrement();
314
+ } else if (provider === "sqlite") {
315
+ build = build.autoIncrement();
316
+ } else if (provider === "mssql") {
317
+ build = build.identity();
318
+ }
319
+ }
320
+
321
+ // External ID must be unique
322
+ if (col.role === "external-id") {
323
+ build = build.unique();
324
+ }
325
+
326
+ const defaultValue = defaultValueToDB(col, provider);
327
+ if (defaultValue) {
328
+ build = build.defaultTo(defaultValue);
329
+ }
330
+ return build;
331
+ };
332
+ }
333
+
334
+ function rawToNode(db: KyselyAny, raw: RawBuilder<unknown>): ExecuteNode {
335
+ return {
336
+ compile() {
337
+ return raw.compile(db);
338
+ },
339
+ execute() {
340
+ return raw.execute(db);
341
+ },
342
+ };
343
+ }
344
+
345
+ function mssqlDropDefaultConstraint(tableName: string, columnName: string) {
346
+ return sql`
347
+ DECLARE @ConstraintName NVARCHAR(200);
348
+
349
+ SELECT @ConstraintName = dc.name
350
+ FROM sys.default_constraints dc
351
+ JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id
352
+ JOIN sys.tables t ON t.object_id = c.object_id
353
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
354
+ WHERE s.name = 'dbo' AND t.name = ${sql.lit(tableName)} AND c.name = ${sql.lit(columnName)};
355
+
356
+ IF @ConstraintName IS NOT NULL
357
+ BEGIN
358
+ EXEC('ALTER TABLE [dbo].[' + ${sql.lit(tableName)} + '] DROP CONSTRAINT [' + @ConstraintName + ']');
359
+ END`;
360
+ }
361
+
362
+ function defaultValueToDB(column: ColumnInfo, provider: SQLProvider) {
363
+ const value = column.default;
364
+ if (!value) {
365
+ return undefined;
366
+ }
367
+
368
+ // MySQL doesn't support default values for TEXT columns
369
+ if (provider === "mysql" && column.type === "string") {
370
+ return undefined;
371
+ }
372
+
373
+ if ("runtime" in value && value.runtime === "now") {
374
+ return sql`CURRENT_TIMESTAMP`;
375
+ }
376
+
377
+ if ("value" in value && value.value !== undefined) {
378
+ return sql.lit(value.value);
379
+ }
380
+
381
+ return undefined;
382
+ }
@@ -0,0 +1,197 @@
1
+ import { Kysely, PostgresDialect } from "kysely";
2
+ import { describe, expect, beforeAll, test } from "vitest";
3
+ import { KyselyAdapter } from "../kysely-adapter";
4
+ import { column, idColumn, schema } from "../../../schema/create";
5
+
6
+ describe("KyselyMigrator", () => {
7
+ const testSchema = schema((s) => {
8
+ return s
9
+ .addTable("users", (t) => {
10
+ return t.addColumn("id", idColumn()).addColumn("name", column("string"));
11
+ })
12
+ .alterTable("users", (t) => {
13
+ return t
14
+ .addColumn("age", column("integer").nullable())
15
+ .createIndex("name_idx", ["name"])
16
+ .createIndex("age_idx", ["age"]);
17
+ })
18
+ .addTable("posts", (t) => {
19
+ return t
20
+ .addColumn("id", idColumn())
21
+ .addColumn("title", column("string"))
22
+ .addColumn("user_id", column("string"));
23
+ })
24
+ .addReference("author", {
25
+ type: "one",
26
+ from: { table: "posts", column: "user_id" },
27
+ to: { table: "users", column: "id" },
28
+ });
29
+ });
30
+
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ let db: Kysely<any>;
33
+ let adapter: KyselyAdapter;
34
+
35
+ beforeAll(async () => {
36
+ // Create a Kysely instance with a PostgresDialect, but not actually connected to a database
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ db = new Kysely({ dialect: new PostgresDialect({} as any) });
39
+ adapter = new KyselyAdapter({ db, provider: "postgresql" });
40
+ });
41
+
42
+ test("generate SQL for migration 0 -> 1 (create users table)", async () => {
43
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
44
+ const preparedMigration = await migrator.prepareMigrationTo(1, {
45
+ updateSettings: true,
46
+ fromVersion: 0,
47
+ });
48
+
49
+ expect(preparedMigration.operations.length).toBeGreaterThan(0);
50
+ const sql = preparedMigration.getSQL?.();
51
+ expect(sql).toBeDefined();
52
+ expect(sql).toMatchInlineSnapshot(`
53
+ "create table "fragno_db_settings" ("key" varchar(255) primary key, "value" text not null);
54
+
55
+ insert into "fragno_db_settings" ("key", "value") values ('test_namespace.schema_version', '1');
56
+
57
+ create table "users" ("id" varchar(30) not null unique, "name" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);"
58
+ `);
59
+ });
60
+
61
+ test("generate SQL for migration 0 -> 2 (create users table and add age)", async () => {
62
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
63
+ const preparedMigration = await migrator.prepareMigrationTo(2, {
64
+ updateSettings: true,
65
+ fromVersion: 0,
66
+ });
67
+
68
+ expect(preparedMigration.operations.length).toBeGreaterThan(0);
69
+ const sql = preparedMigration.getSQL?.();
70
+ expect(sql).toBeDefined();
71
+ expect(sql).toMatchInlineSnapshot(`
72
+ "create table "fragno_db_settings" ("key" varchar(255) primary key, "value" text not null);
73
+
74
+ insert into "fragno_db_settings" ("key", "value") values ('test_namespace.schema_version', '2');
75
+
76
+ create table "users" ("id" varchar(30) not null unique, "name" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
77
+
78
+ alter table "users" add column "age" integer;
79
+
80
+ create index "name_idx" on "users" ("name");
81
+
82
+ create index "age_idx" on "users" ("age");"
83
+ `);
84
+ });
85
+
86
+ test("generate SQL for migration 1 -> 2 (add age column and indexes)", async () => {
87
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
88
+ const preparedMigration = await migrator.prepareMigrationTo(2, {
89
+ updateSettings: true,
90
+ fromVersion: 1,
91
+ });
92
+
93
+ expect(preparedMigration.operations.length).toBeGreaterThan(0);
94
+ const sql = preparedMigration.getSQL?.();
95
+ expect(sql).toBeDefined();
96
+ expect(sql).toMatchInlineSnapshot(`
97
+ "update "fragno_db_settings" set "value" = '2' where "key" = 'test_namespace.schema_version';
98
+
99
+ alter table "users" add column "age" integer;
100
+
101
+ create index "name_idx" on "users" ("name");
102
+
103
+ create index "age_idx" on "users" ("age");"
104
+ `);
105
+ });
106
+
107
+ test("generate SQL for migration 0 -> 3 (full schema)", async () => {
108
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
109
+ const preparedMigration = await migrator.prepareMigrationTo(3, {
110
+ updateSettings: true,
111
+ fromVersion: 0,
112
+ });
113
+
114
+ expect(preparedMigration.operations.length).toBeGreaterThan(0);
115
+ const sql = preparedMigration.getSQL?.();
116
+ expect(sql).toBeDefined();
117
+ expect(sql).toMatchInlineSnapshot(`
118
+ "create table "fragno_db_settings" ("key" varchar(255) primary key, "value" text not null);
119
+
120
+ insert into "fragno_db_settings" ("key", "value") values ('test_namespace.schema_version', '3');
121
+
122
+ create table "users" ("id" varchar(30) not null unique, "name" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
123
+
124
+ alter table "users" add column "age" integer;
125
+
126
+ create index "name_idx" on "users" ("name");
127
+
128
+ create index "age_idx" on "users" ("age");
129
+
130
+ create table "posts" ("id" varchar(30) not null unique, "title" text not null, "user_id" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);"
131
+ `);
132
+ });
133
+
134
+ test("generate SQL for migration 2 -> 3 (add posts table)", async () => {
135
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
136
+ const preparedMigration = await migrator.prepareMigrationTo(3, {
137
+ updateSettings: true,
138
+ fromVersion: 2,
139
+ });
140
+
141
+ expect(preparedMigration.operations.length).toBeGreaterThan(0);
142
+ const sql = preparedMigration.getSQL?.();
143
+ expect(sql).toBeDefined();
144
+ expect(sql).toMatchInlineSnapshot(`
145
+ "update "fragno_db_settings" set "value" = '3' where "key" = 'test_namespace.schema_version';
146
+
147
+ create table "posts" ("id" varchar(30) not null unique, "title" text not null, "user_id" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);"
148
+ `);
149
+ });
150
+
151
+ test("generate empty SQL for migration 2 -> 2 (no changes)", async () => {
152
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
153
+ const preparedMigration = await migrator.prepareMigrationTo(2, {
154
+ updateSettings: true,
155
+ fromVersion: 2,
156
+ });
157
+
158
+ expect(preparedMigration.operations.length).toBe(0);
159
+ const sql = preparedMigration.getSQL?.();
160
+ expect(sql).toBeDefined();
161
+ expect(sql).toBe("");
162
+ });
163
+
164
+ test("throw error for backward migration", async () => {
165
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
166
+
167
+ await expect(
168
+ migrator.prepareMigrationTo(1, {
169
+ updateSettings: true,
170
+ fromVersion: 2,
171
+ }),
172
+ ).rejects.toThrow("Cannot migrate backwards");
173
+ });
174
+
175
+ test("throw error for version beyond schema", async () => {
176
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
177
+
178
+ await expect(
179
+ migrator.prepareMigrationTo(10, {
180
+ updateSettings: true,
181
+ fromVersion: 0,
182
+ }),
183
+ ).rejects.toThrow("schema only has version 4");
184
+ });
185
+
186
+ test("getDefaultFileName returns correct format", () => {
187
+ const migrator = adapter.createMigrationEngine(testSchema, "test_namespace");
188
+
189
+ expect(migrator.getDefaultFileName).toBeDefined();
190
+
191
+ if (migrator.getDefaultFileName) {
192
+ const filename = migrator.getDefaultFileName("my-fragment", 0, 1);
193
+ // Should match format: YYYYMMDD_namespace_migration_0_to_1.sql
194
+ expect(filename).toMatch(/^\d{8}_my-fragment_migration_0_to_1\.sql$/);
195
+ }
196
+ });
197
+ });