@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,433 @@
1
+ import { drizzle } from "drizzle-orm/pglite";
2
+ import { DrizzleAdapter } from "./drizzle-adapter";
3
+ import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
4
+ import { column, idColumn, referenceColumn, schema } from "../../schema/create";
5
+ import type { DBType } from "./shared";
6
+ import { createRequire } from "node:module";
7
+ import { encodeCursor } from "../../query/cursor";
8
+ import type { DrizzleCompiledQuery } from "./drizzle-uow-compiler";
9
+ import { writeAndLoadSchema } from "./test-utils";
10
+
11
+ // Import drizzle-kit for migrations
12
+ const require = createRequire(import.meta.url);
13
+ const { generateDrizzleJson, generateMigration } =
14
+ require("drizzle-kit/api") as typeof import("drizzle-kit/api");
15
+
16
+ describe("DrizzleAdapter PGLite", () => {
17
+ const testSchema = schema((s) => {
18
+ return s
19
+ .addTable("users", (t) => {
20
+ return t
21
+ .addColumn("id", idColumn())
22
+ .addColumn("name", column("string"))
23
+ .addColumn("age", column("integer").nullable())
24
+ .createIndex("name_idx", ["name"]);
25
+ })
26
+ .addTable("emails", (t) => {
27
+ return t
28
+ .addColumn("id", idColumn())
29
+ .addColumn("user_id", referenceColumn())
30
+ .addColumn("email", column("string"))
31
+ .addColumn("is_primary", column("bool").defaultTo(false))
32
+ .createIndex("unique_email", ["email"], { unique: true })
33
+ .createIndex("user_emails", ["user_id"]);
34
+ })
35
+ .addTable("posts", (t) => {
36
+ return t
37
+ .addColumn("id", idColumn())
38
+ .addColumn("user_id", referenceColumn())
39
+ .addColumn("title", column("string"))
40
+ .addColumn("content", column("string"))
41
+ .createIndex("posts_user_idx", ["user_id"]);
42
+ })
43
+ .addTable("comments", (t) => {
44
+ return t
45
+ .addColumn("id", idColumn())
46
+ .addColumn("post_id", referenceColumn())
47
+ .addColumn("user_id", referenceColumn())
48
+ .addColumn("text", column("string"))
49
+ .createIndex("comments_post_idx", ["post_id"])
50
+ .createIndex("comments_user_idx", ["user_id"]);
51
+ })
52
+ .addReference("user", {
53
+ type: "one",
54
+ from: { table: "emails", column: "user_id" },
55
+ to: { table: "users", column: "id" },
56
+ })
57
+ .addReference("author", {
58
+ type: "one",
59
+ from: { table: "posts", column: "user_id" },
60
+ to: { table: "users", column: "id" },
61
+ })
62
+ .addReference("post", {
63
+ type: "one",
64
+ from: { table: "comments", column: "post_id" },
65
+ to: { table: "posts", column: "id" },
66
+ })
67
+ .addReference("commenter", {
68
+ type: "one",
69
+ from: { table: "comments", column: "user_id" },
70
+ to: { table: "users", column: "id" },
71
+ });
72
+ });
73
+
74
+ let adapter: DrizzleAdapter;
75
+ let db: DBType;
76
+
77
+ beforeAll(async () => {
78
+ // Write schema to file and dynamically import it
79
+ const { schemaModule, cleanup } = await writeAndLoadSchema(
80
+ "drizzle-adapter-pglite",
81
+ testSchema,
82
+ "postgresql",
83
+ );
84
+
85
+ // Create Drizzle instance with PGLite (in-memory Postgres)
86
+ db = drizzle({
87
+ schema: schemaModule,
88
+ }) as unknown as DBType;
89
+
90
+ // Generate and run migrations
91
+ const migrationStatements = await generateMigration(
92
+ generateDrizzleJson({}), // Empty schema (starting state)
93
+ generateDrizzleJson(schemaModule), // Target schema
94
+ );
95
+
96
+ // Execute migration SQL
97
+ for (const statement of migrationStatements) {
98
+ await db.execute(statement);
99
+ }
100
+
101
+ adapter = new DrizzleAdapter({
102
+ db,
103
+ provider: "postgresql",
104
+ });
105
+
106
+ return async () => {
107
+ await cleanup();
108
+ };
109
+ }, 12000);
110
+
111
+ it("should execute Unit of Work with version checking", async () => {
112
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
113
+
114
+ // Create initial user using UOW
115
+ const createUow = queryEngine.createUnitOfWork("create-user").create("users", {
116
+ name: "Alice",
117
+ age: 25,
118
+ });
119
+
120
+ expectTypeOf(createUow.find).parameter(0).toEqualTypeOf<keyof typeof testSchema.tables>();
121
+
122
+ const { success: createSuccess } = await createUow.executeMutations();
123
+ expect(createSuccess).toBe(true);
124
+
125
+ // Fetch the created user to get its ID
126
+ const [[initialUser]] = await queryEngine
127
+ .createUnitOfWork("get-created-user")
128
+ .find("users")
129
+ .executeRetrieve();
130
+
131
+ expect(initialUser).toBeDefined();
132
+ expect(initialUser.name).toBe("Alice");
133
+ expect(initialUser.id.version).toBe(0);
134
+
135
+ const initialUserId = initialUser.id;
136
+
137
+ // Build a UOW to update the user with optimistic locking
138
+ const uow = queryEngine
139
+ .createUnitOfWork("update-user-age")
140
+ // Retrieval phase: find the user
141
+ .find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)));
142
+
143
+ // Execute retrieval and transition to mutation phase
144
+ const [users] = await uow.executeRetrieve();
145
+
146
+ // Mutation phase: update with version check
147
+ uow.update("users", initialUserId, (b) => b.set({ age: 26 }).check());
148
+
149
+ // Execute mutations
150
+ const { success } = await uow.executeMutations();
151
+
152
+ // Should succeed
153
+ expect(success).toBe(true);
154
+ expect(users).toHaveLength(1);
155
+
156
+ // Verify the user was updated
157
+ const [[updatedUser]] = await queryEngine
158
+ .createUnitOfWork("get-updated-user")
159
+ .find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)))
160
+ .executeRetrieve();
161
+
162
+ expect(updatedUser).toMatchObject({
163
+ id: expect.objectContaining({
164
+ externalId: initialUserId.externalId,
165
+ version: 1, // Version incremented
166
+ }),
167
+ name: "Alice",
168
+ age: 26,
169
+ });
170
+
171
+ // Try to update again with stale version (should fail)
172
+ const uow2 = queryEngine.createUnitOfWork("update-user-stale");
173
+
174
+ // Use the old version (0) which is now stale
175
+ uow2.update("users", initialUserId, (b) => b.set({ age: 27 }).check());
176
+
177
+ const { success: success2 } = await uow2.executeMutations();
178
+
179
+ // Should fail due to version conflict
180
+ expect(success2).toBe(false);
181
+
182
+ // Verify the user was NOT updated
183
+ const [[unchangedUser]] = await queryEngine
184
+ .createUnitOfWork("verify-unchanged")
185
+ .find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)))
186
+ .executeRetrieve();
187
+
188
+ expect(unchangedUser).toMatchObject({
189
+ id: expect.objectContaining({
190
+ version: 1, // Still version 1
191
+ }),
192
+ age: 26, // Still 26, not 27
193
+ });
194
+ });
195
+
196
+ it("should support count operations", async () => {
197
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
198
+
199
+ // Create some users
200
+ await queryEngine
201
+ .createUnitOfWork("create-users")
202
+ .create("users", { name: "User1", age: 20 })
203
+ .create("users", { name: "User2", age: 30 })
204
+ .create("users", { name: "User3", age: 40 })
205
+ .executeMutations();
206
+
207
+ // Count all users
208
+ const [totalCount] = await queryEngine
209
+ .createUnitOfWork("count-all")
210
+ .find("users", (b) => b.whereIndex("primary").selectCount())
211
+ .executeRetrieve();
212
+
213
+ // Tests are not isolated, so we can't use expect(totalCount).toBe(3)
214
+ expect(totalCount).toBeGreaterThanOrEqual(3);
215
+ });
216
+
217
+ it("should support cursor-based pagination", async () => {
218
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
219
+
220
+ const createUow = queryEngine
221
+ .createUnitOfWork("create-users")
222
+ .create("users", { name: "Page User A", age: 20 })
223
+ .create("users", { name: "Page User B", age: 30 })
224
+ .create("users", { name: "Page User C", age: 40 })
225
+ .create("users", { name: "Page User D", age: 50 })
226
+ .create("users", { name: "Page User E", age: 60 });
227
+
228
+ await createUow.executeMutations();
229
+
230
+ // Fetch first page ordered by name
231
+ const [firstPage] = await queryEngine
232
+ .createUnitOfWork("first-page")
233
+ .find("users", (b) => b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(2))
234
+ .executeRetrieve();
235
+
236
+ // Verify first page contains the first 2 users alphabetically
237
+ expect(firstPage.map((u) => u.name)).toEqual(["Alice", "Page User A"]);
238
+
239
+ // Create cursor from last item of first page
240
+ const lastItem = firstPage[firstPage.length - 1]!;
241
+ const cursor = encodeCursor({
242
+ indexValues: { name: lastItem.name },
243
+ direction: "forward",
244
+ });
245
+
246
+ // Fetch next page using cursor
247
+ const [secondPage] = await queryEngine
248
+ .createUnitOfWork("second-page")
249
+ .find("users", (b) =>
250
+ b.whereIndex("name_idx").orderByIndex("name_idx", "asc").after(cursor).pageSize(2),
251
+ )
252
+ .executeRetrieve();
253
+
254
+ // Verify page 2 continues alphabetically
255
+ expect(secondPage.map((u) => u.name)).toEqual(["Page User B", "Page User C"]);
256
+
257
+ // Ensure no overlap between pages
258
+ const firstPageNames = new Set(firstPage.map((u) => u.name));
259
+ for (const user of secondPage) {
260
+ expect(firstPageNames.has(user.name)).toBe(false);
261
+ }
262
+ });
263
+
264
+ it("should support joins", async () => {
265
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
266
+ const queries: DrizzleCompiledQuery[] = [];
267
+
268
+ const createUow = queryEngine
269
+ .createUnitOfWork("create-users")
270
+ .create("users", { name: "Email User", age: 20 });
271
+
272
+ await createUow.executeMutations();
273
+
274
+ // Get an existing user to create an email for
275
+ const [[existingUser]] = await queryEngine
276
+ .createUnitOfWork("get-existing-user")
277
+ .find("users", (b) => b.whereIndex("name_idx", (eb) => eb("name", "=", "Email User")))
278
+ .executeRetrieve();
279
+
280
+ // Create an email for testing joins
281
+ const createEmailUow = queryEngine.createUnitOfWork("create-test-email").create("emails", {
282
+ user_id: existingUser.id,
283
+ email: "test@example.com",
284
+ is_primary: true,
285
+ });
286
+ await createEmailUow.executeMutations();
287
+
288
+ // Test join query
289
+ const uow = queryEngine
290
+ .createUnitOfWork("test-joins", { onQuery: (query) => queries.push(query) })
291
+ .find("emails", (b) =>
292
+ b
293
+ .whereIndex("user_emails")
294
+ .join((jb) => jb.user((builder) => builder.select(["name", "id", "age"]))),
295
+ );
296
+
297
+ const [[email]] = await uow.executeRetrieve();
298
+
299
+ const [query] = queries;
300
+ expect(query.sql).toMatchInlineSnapshot(
301
+ `"select "emails"."id", "emails"."user_id", "emails"."email", "emails"."is_primary", "emails"."_internalId", "emails"."_version", "emails_user"."data" as "user" from "emails" "emails" left join lateral (select json_build_array("emails_user"."name", "emails_user"."id", "emails_user"."age", "emails_user"."_internalId", "emails_user"."_version") as "data" from (select * from "users" "emails_user" where "emails_user"."_internalId" = "emails"."user_id" limit $1) "emails_user") "emails_user" on true"`,
302
+ );
303
+
304
+ expect(email).toMatchObject({
305
+ id: expect.objectContaining({
306
+ externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
307
+ internalId: expect.any(Number),
308
+ }),
309
+ user_id: expect.objectContaining({
310
+ internalId: expect.any(Number),
311
+ }),
312
+ email: "test@example.com",
313
+ is_primary: true,
314
+ user: {
315
+ id: expect.objectContaining({
316
+ externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
317
+ internalId: expect.any(Number),
318
+ }),
319
+ name: existingUser.name,
320
+ age: existingUser.age,
321
+ },
322
+ });
323
+ });
324
+
325
+ it("should support complex nested joins (comments -> post -> author)", async () => {
326
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
327
+ const queries: DrizzleCompiledQuery[] = [];
328
+
329
+ // Create a user (author)
330
+ const createAuthorUow = queryEngine
331
+ .createUnitOfWork("create-author")
332
+ .create("users", { name: "Blog Author", age: 30 });
333
+ await createAuthorUow.executeMutations();
334
+
335
+ // Get the author
336
+ const [[author]] = await queryEngine
337
+ .createUnitOfWork("get-author")
338
+ .find("users", (b) => b.whereIndex("name_idx", (eb) => eb("name", "=", "Blog Author")))
339
+ .executeRetrieve();
340
+
341
+ // Create a post by the author
342
+ const createPostUow = queryEngine.createUnitOfWork("create-post").create("posts", {
343
+ user_id: author.id,
344
+ title: "My First Post",
345
+ content: "This is the content of my first post",
346
+ });
347
+ await createPostUow.executeMutations();
348
+
349
+ // Get the post
350
+ const [[post]] = await queryEngine.createUnitOfWork("get-post").find("posts").executeRetrieve();
351
+
352
+ // Create a commenter
353
+ const createCommenterUow = queryEngine
354
+ .createUnitOfWork("create-commenter")
355
+ .create("users", { name: "Commenter User", age: 25 });
356
+ await createCommenterUow.executeMutations();
357
+
358
+ // Get the commenter
359
+ const [[commenter]] = await queryEngine
360
+ .createUnitOfWork("get-commenter")
361
+ .find("users", (b) => b.whereIndex("name_idx", (eb) => eb("name", "=", "Commenter User")))
362
+ .executeRetrieve();
363
+
364
+ // Create a comment on the post
365
+ const createCommentUow = queryEngine.createUnitOfWork("create-comment").create("comments", {
366
+ post_id: post.id,
367
+ user_id: commenter.id,
368
+ text: "Great post!",
369
+ });
370
+ await createCommentUow.executeMutations();
371
+
372
+ // Now perform a complex nested join: comments -> post -> author, and comments -> commenter
373
+ const uow = queryEngine
374
+ .createUnitOfWork("test-complex-joins", { onQuery: (query) => queries.push(query) })
375
+ .find("comments", (b) =>
376
+ b.whereIndex("primary").join((jb) =>
377
+ jb
378
+ .post((postBuilder) =>
379
+ postBuilder
380
+ .select(["id", "title", "content"])
381
+ .orderByIndex("primary", "desc")
382
+ .pageSize(1)
383
+ .join((jb2) =>
384
+ // Nested join to the post's author
385
+ jb2.author((authorBuilder) =>
386
+ authorBuilder.select(["id", "name", "age"]).orderByIndex("name_idx", "asc"),
387
+ ),
388
+ ),
389
+ )
390
+ .commenter((commenterBuilder) => commenterBuilder.select(["id", "name"])),
391
+ ),
392
+ );
393
+
394
+ const [[comment]] = await uow.executeRetrieve();
395
+
396
+ // Verify the result structure with nested joins
397
+ expect(comment).toMatchObject({
398
+ id: expect.objectContaining({
399
+ externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
400
+ internalId: expect.any(Number),
401
+ }),
402
+ text: "Great post!",
403
+ // Post join (first level)
404
+ post: {
405
+ id: expect.objectContaining({
406
+ externalId: post.id.externalId,
407
+ }),
408
+ title: "My First Post",
409
+ content: "This is the content of my first post",
410
+ // Nested author join (second level) - now decoded!
411
+ author: {
412
+ id: expect.objectContaining({
413
+ externalId: author.id.externalId,
414
+ }),
415
+ name: "Blog Author",
416
+ age: 30,
417
+ },
418
+ },
419
+ // Commenter join (first level)
420
+ commenter: {
421
+ id: expect.objectContaining({
422
+ externalId: commenter.id.externalId,
423
+ }),
424
+ name: "Commenter User",
425
+ },
426
+ });
427
+
428
+ const [query] = queries;
429
+ expect(query.sql).toMatchInlineSnapshot(
430
+ `"select "comments"."id", "comments"."post_id", "comments"."user_id", "comments"."text", "comments"."_internalId", "comments"."_version", "comments_post"."data" as "post", "comments_commenter"."data" as "commenter" from "comments" "comments" left join lateral (select json_build_array("comments_post"."id", "comments_post"."title", "comments_post"."content", "comments_post"."_internalId", "comments_post"."_version", "comments_post_author"."data") as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."post_id" order by "comments_post"."id" desc limit $1) "comments_post" left join lateral (select json_build_array("comments_post_author"."id", "comments_post_author"."name", "comments_post_author"."age", "comments_post_author"."_internalId", "comments_post_author"."_version") as "data" from (select * from "users" "comments_post_author" where "comments_post_author"."_internalId" = "comments_post"."user_id" order by "comments_post_author"."name" asc limit $2) "comments_post_author") "comments_post_author" on true) "comments_post" on true left join lateral (select json_build_array("comments_commenter"."id", "comments_commenter"."name", "comments_commenter"."_internalId", "comments_commenter"."_version") as "data" from (select * from "users" "comments_commenter" where "comments_commenter"."_internalId" = "comments"."user_id" limit $3) "comments_commenter") "comments_commenter" on true"`,
431
+ );
432
+ });
433
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { column, idColumn, schema } from "../../schema/create";
3
+ import { DrizzleAdapter } from "./drizzle-adapter";
4
+
5
+ describe("DrizzleAdapter", () => {
6
+ const testSchema = schema((s) => {
7
+ return s.addTable("users", (t) => {
8
+ return t.addColumn("id", idColumn()).addColumn("name", column("string"));
9
+ });
10
+ });
11
+
12
+ it("should generate schema with settings table for postgresql", () => {
13
+ const adapter = new DrizzleAdapter({
14
+ db: {},
15
+ provider: "postgresql",
16
+ });
17
+
18
+ const generator = adapter.createSchemaGenerator(testSchema, "test");
19
+ const result = generator.generateSchema({ path: "schema.ts" });
20
+
21
+ expect(result.path).toBe("schema.ts");
22
+ expect(result.schema).toMatchInlineSnapshot(`
23
+ "import { pgTable, varchar, text, bigserial, integer, uniqueIndex } from "drizzle-orm/pg-core"
24
+ import { createId } from "@fragno-dev/db/id"
25
+
26
+ export const users = pgTable("users", {
27
+ id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
28
+ name: text("name").notNull(),
29
+ _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
30
+ _version: integer("_version").notNull().default(0)
31
+ })
32
+
33
+ export const fragno_db_settings = pgTable("fragno_db_settings", {
34
+ id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
35
+ key: text("key").notNull(),
36
+ value: text("value").notNull().default("1"),
37
+ _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
38
+ _version: integer("_version").notNull().default(0)
39
+ }, (table) => [
40
+ uniqueIndex("unique_key").on(table.key)
41
+ ])"
42
+ `);
43
+ });
44
+
45
+ it("should generate schema with settings table for sqlite", () => {
46
+ const adapter = new DrizzleAdapter({
47
+ db: {},
48
+ provider: "sqlite",
49
+ });
50
+
51
+ const generator = adapter.createSchemaGenerator(testSchema, "test");
52
+ const result = generator.generateSchema({ path: "schema.ts" });
53
+
54
+ expect(result.path).toBe("schema.ts");
55
+ expect(result.schema).toMatchInlineSnapshot(`
56
+ "import { sqliteTable, text, integer, uniqueIndex } from "drizzle-orm/sqlite-core"
57
+ import { createId } from "@fragno-dev/db/id"
58
+
59
+ export const users = sqliteTable("users", {
60
+ id: text("id").notNull().$defaultFn(() => createId()),
61
+ name: text("name").notNull(),
62
+ _internalId: integer("_internalId").primaryKey().autoincrement().notNull(),
63
+ _version: integer("_version").notNull().default(0)
64
+ })
65
+
66
+ export const fragno_db_settings = sqliteTable("fragno_db_settings", {
67
+ id: text("id").notNull().$defaultFn(() => createId()),
68
+ key: text("key").notNull(),
69
+ value: text("value").notNull().default("1"),
70
+ _internalId: integer("_internalId").primaryKey().autoincrement().notNull(),
71
+ _version: integer("_version").notNull().default(0)
72
+ }, (table) => [
73
+ uniqueIndex("unique_key").on(table.key)
74
+ ])"
75
+ `);
76
+ });
77
+
78
+ it("should use default path if not provided", () => {
79
+ const adapter = new DrizzleAdapter({
80
+ db: {},
81
+ provider: "postgresql",
82
+ });
83
+
84
+ const generator = adapter.createSchemaGenerator(testSchema, "myapp");
85
+ const result = generator.generateSchema();
86
+
87
+ expect(result.path).toBe("drizzle-schema-myapp.ts");
88
+ });
89
+
90
+ it("should preserve original schema tables", () => {
91
+ const adapter = new DrizzleAdapter({
92
+ db: {},
93
+ provider: "postgresql",
94
+ });
95
+
96
+ const generator = adapter.createSchemaGenerator(testSchema, "test");
97
+ const result = generator.generateSchema();
98
+
99
+ // Original table should still be there
100
+ expect(result.schema).toMatchInlineSnapshot(`
101
+ "import { pgTable, varchar, text, bigserial, integer, uniqueIndex } from "drizzle-orm/pg-core"
102
+ import { createId } from "@fragno-dev/db/id"
103
+
104
+ export const users = pgTable("users", {
105
+ id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
106
+ name: text("name").notNull(),
107
+ _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
108
+ _version: integer("_version").notNull().default(0)
109
+ })
110
+
111
+ export const fragno_db_settings = pgTable("fragno_db_settings", {
112
+ id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
113
+ key: text("key").notNull(),
114
+ value: text("value").notNull().default("1"),
115
+ _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
116
+ _version: integer("_version").notNull().default(0)
117
+ }, (table) => [
118
+ uniqueIndex("unique_key").on(table.key)
119
+ ])"
120
+ `);
121
+ });
122
+ });
@@ -0,0 +1,118 @@
1
+ import type { DatabaseAdapter } from "../adapters";
2
+ import {
3
+ column,
4
+ idColumn,
5
+ schema,
6
+ SchemaBuilder,
7
+ type AnySchema,
8
+ type FragnoId,
9
+ } from "../../schema/create";
10
+ import type { AbstractQuery } from "../../query/query";
11
+ import type { SchemaGenerator } from "../../schema-generator/schema-generator";
12
+ import { generateSchema } from "./generate";
13
+ import { fromDrizzle, type DrizzleUOWConfig } from "./drizzle-query";
14
+ import { createId } from "../../id";
15
+
16
+ const SETTINGS_TABLE_NAME = "fragno_db_settings" as const;
17
+
18
+ export interface DrizzleConfig {
19
+ db: unknown;
20
+ provider: "sqlite" | "mysql" | "postgresql";
21
+ }
22
+
23
+ export class DrizzleAdapter implements DatabaseAdapter<DrizzleUOWConfig> {
24
+ #drizzleConfig: DrizzleConfig;
25
+
26
+ constructor(config: DrizzleConfig) {
27
+ this.#drizzleConfig = config;
28
+ }
29
+
30
+ #createFullSchema<T extends AnySchema>(schema: T) {
31
+ return new SchemaBuilder()
32
+ .mergeWithExistingSchema(schema)
33
+ .mergeWithExistingSchema(createSettingsSchema(schema.version))
34
+ .build();
35
+ }
36
+
37
+ async getSchemaVersion(namespace: string): Promise<string | undefined> {
38
+ const queryEngine = this.createQueryEngine(createSettingsSchema(0), namespace);
39
+ const manager = createSettingsManager(queryEngine, namespace);
40
+ const randomId = createId();
41
+
42
+ const result = await manager.createKeyWithDefault(randomId);
43
+ if (result) {
44
+ await manager.delete(result.id);
45
+ }
46
+
47
+ return result?.value;
48
+ }
49
+
50
+ createQueryEngine<TSchema extends AnySchema>(
51
+ schema: TSchema,
52
+ _namespace: string,
53
+ ): AbstractQuery<TSchema, DrizzleUOWConfig> {
54
+ return fromDrizzle(schema, this.#drizzleConfig);
55
+ }
56
+
57
+ createSchemaGenerator(schema: AnySchema, namespace: string): SchemaGenerator {
58
+ return {
59
+ generateSchema: (options) => {
60
+ const path = options?.path ?? `drizzle-schema-${namespace}.ts`;
61
+
62
+ const schemaWithSettingsTable = this.#createFullSchema(schema);
63
+
64
+ return {
65
+ schema: generateSchema(schemaWithSettingsTable, this.#drizzleConfig.provider),
66
+ path,
67
+ };
68
+ },
69
+ };
70
+ }
71
+ }
72
+
73
+ function createSettingsSchema(version: number) {
74
+ return schema((s) => {
75
+ return s.addTable(SETTINGS_TABLE_NAME, (t) => {
76
+ return t
77
+ .addColumn("id", idColumn())
78
+ .addColumn("key", column("string"))
79
+ .addColumn("value", column("string").defaultTo(String(version)))
80
+ .createIndex("unique_key", ["key"], { unique: true });
81
+ });
82
+ });
83
+ }
84
+
85
+ function createSettingsManager(
86
+ queryEngine: AbstractQuery<ReturnType<typeof createSettingsSchema>, DrizzleUOWConfig>,
87
+ namespace: string,
88
+ ) {
89
+ return {
90
+ async createKeyWithDefault(key: string) {
91
+ const writeUow = queryEngine
92
+ .createUnitOfWork("createKeyWithDefault")
93
+ .create(SETTINGS_TABLE_NAME, {
94
+ key: `${namespace}.${key}`,
95
+ });
96
+ const { success } = await writeUow.executeMutations();
97
+ if (!success) {
98
+ throw new Error("Failed to create key with default");
99
+ }
100
+
101
+ return this.get(key);
102
+ },
103
+
104
+ async get(key: string): Promise<{ id: FragnoId; key: string; value: string } | undefined> {
105
+ const uow = queryEngine
106
+ .createUnitOfWork()
107
+ .find(SETTINGS_TABLE_NAME, (b) =>
108
+ b.whereIndex("unique_key", (eb) => eb("key", "=", `${namespace}.${key}`)),
109
+ );
110
+ const [[result]] = await uow.executeRetrieve();
111
+ return result; // FIXME: result should be maybe undefined
112
+ },
113
+
114
+ async delete(id: FragnoId) {
115
+ await queryEngine.delete(SETTINGS_TABLE_NAME, id);
116
+ },
117
+ };
118
+ }