@fragno-dev/db 0.1.13 → 0.1.15

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 (178) hide show
  1. package/.turbo/turbo-build.log +179 -132
  2. package/CHANGELOG.md +30 -0
  3. package/dist/adapters/adapters.d.ts +27 -1
  4. package/dist/adapters/adapters.d.ts.map +1 -1
  5. package/dist/adapters/adapters.js.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +7 -5
  11. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  12. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
  19. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
  20. package/dist/adapters/drizzle/generate.d.ts +4 -1
  21. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  22. package/dist/adapters/drizzle/generate.js +11 -18
  23. package/dist/adapters/drizzle/generate.js.map +1 -1
  24. package/dist/adapters/drizzle/shared.d.ts +14 -1
  25. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  26. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
  27. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  28. package/dist/adapters/kysely/kysely-adapter.js +14 -3
  29. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  30. package/dist/adapters/kysely/kysely-query-builder.js +1 -1
  31. package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
  32. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
  33. package/dist/adapters/kysely/kysely-query.d.ts +1 -0
  34. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  35. package/dist/adapters/kysely/kysely-query.js +28 -19
  36. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  37. package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
  38. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  39. package/dist/adapters/kysely/kysely-shared.js +16 -1
  40. package/dist/adapters/kysely/kysely-shared.js.map +1 -1
  41. package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
  42. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  43. package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
  44. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  45. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  46. package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
  47. package/dist/db-fragment-definition-builder.d.ts +152 -0
  48. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  49. package/dist/db-fragment-definition-builder.js +137 -0
  50. package/dist/db-fragment-definition-builder.js.map +1 -0
  51. package/dist/fragments/internal-fragment.d.ts +19 -0
  52. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  53. package/dist/fragments/internal-fragment.js +39 -0
  54. package/dist/fragments/internal-fragment.js.map +1 -0
  55. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  56. package/dist/migration-engine/generation-engine.js +35 -15
  57. package/dist/migration-engine/generation-engine.js.map +1 -1
  58. package/dist/mod.d.ts +8 -18
  59. package/dist/mod.d.ts.map +1 -1
  60. package/dist/mod.js +7 -34
  61. package/dist/mod.js.map +1 -1
  62. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
  63. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
  64. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  65. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  66. package/dist/packages/fragno/dist/api/error.js +48 -0
  67. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  68. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  69. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  70. package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
  71. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  72. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  73. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  74. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  75. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  76. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  77. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  78. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  79. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  80. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  81. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  82. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  83. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  84. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  85. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  86. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  87. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  88. package/dist/packages/fragno/dist/api/route.js +17 -0
  89. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  90. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  91. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  92. package/dist/query/cursor.d.ts +10 -2
  93. package/dist/query/cursor.d.ts.map +1 -1
  94. package/dist/query/cursor.js +11 -4
  95. package/dist/query/cursor.js.map +1 -1
  96. package/dist/query/execute-unit-of-work.d.ts +123 -0
  97. package/dist/query/execute-unit-of-work.d.ts.map +1 -0
  98. package/dist/query/execute-unit-of-work.js +184 -0
  99. package/dist/query/execute-unit-of-work.js.map +1 -0
  100. package/dist/query/query.d.ts +3 -3
  101. package/dist/query/query.d.ts.map +1 -1
  102. package/dist/query/result-transform.js +4 -2
  103. package/dist/query/result-transform.js.map +1 -1
  104. package/dist/query/retry-policy.d.ts +88 -0
  105. package/dist/query/retry-policy.d.ts.map +1 -0
  106. package/dist/query/retry-policy.js +61 -0
  107. package/dist/query/retry-policy.js.map +1 -0
  108. package/dist/query/unit-of-work.d.ts +171 -32
  109. package/dist/query/unit-of-work.d.ts.map +1 -1
  110. package/dist/query/unit-of-work.js +530 -133
  111. package/dist/query/unit-of-work.js.map +1 -1
  112. package/dist/schema/serialize.js +12 -7
  113. package/dist/schema/serialize.js.map +1 -1
  114. package/dist/with-database.d.ts +28 -0
  115. package/dist/with-database.d.ts.map +1 -0
  116. package/dist/with-database.js +34 -0
  117. package/dist/with-database.js.map +1 -0
  118. package/package.json +10 -3
  119. package/src/adapters/adapters.ts +30 -0
  120. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
  121. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
  122. package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
  123. package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
  124. package/src/adapters/drizzle/drizzle-query.ts +25 -15
  125. package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
  126. package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
  127. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
  128. package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
  129. package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
  130. package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
  131. package/src/adapters/drizzle/generate.test.ts +102 -269
  132. package/src/adapters/drizzle/generate.ts +12 -30
  133. package/src/adapters/drizzle/test-utils.ts +36 -5
  134. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
  135. package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
  136. package/src/adapters/kysely/kysely-adapter.ts +25 -2
  137. package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
  138. package/src/adapters/kysely/kysely-query.ts +57 -37
  139. package/src/adapters/kysely/kysely-shared.ts +34 -0
  140. package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
  141. package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
  142. package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
  143. package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
  144. package/src/adapters/kysely/migration/execute-base.ts +1 -1
  145. package/src/db-fragment-definition-builder.test.ts +887 -0
  146. package/src/db-fragment-definition-builder.ts +506 -0
  147. package/src/db-fragment-instantiator.test.ts +467 -0
  148. package/src/db-fragment-integration.test.ts +408 -0
  149. package/src/fragments/internal-fragment.test.ts +160 -0
  150. package/src/fragments/internal-fragment.ts +85 -0
  151. package/src/migration-engine/generation-engine.test.ts +58 -15
  152. package/src/migration-engine/generation-engine.ts +78 -25
  153. package/src/mod.ts +35 -43
  154. package/src/query/cursor.test.ts +119 -0
  155. package/src/query/cursor.ts +17 -4
  156. package/src/query/execute-unit-of-work.test.ts +1310 -0
  157. package/src/query/execute-unit-of-work.ts +463 -0
  158. package/src/query/query.ts +4 -4
  159. package/src/query/result-transform.test.ts +129 -0
  160. package/src/query/result-transform.ts +4 -1
  161. package/src/query/retry-policy.test.ts +217 -0
  162. package/src/query/retry-policy.ts +141 -0
  163. package/src/query/unit-of-work-coordinator.test.ts +833 -0
  164. package/src/query/unit-of-work-types.test.ts +15 -2
  165. package/src/query/unit-of-work.test.ts +878 -200
  166. package/src/query/unit-of-work.ts +963 -321
  167. package/src/schema/serialize.ts +22 -11
  168. package/src/with-database.ts +140 -0
  169. package/tsdown.config.ts +1 -0
  170. package/dist/fragment.d.ts +0 -54
  171. package/dist/fragment.d.ts.map +0 -1
  172. package/dist/fragment.js +0 -92
  173. package/dist/fragment.js.map +0 -1
  174. package/dist/shared/settings-schema.js +0 -36
  175. package/dist/shared/settings-schema.js.map +0 -1
  176. package/src/fragment.test.ts +0 -341
  177. package/src/fragment.ts +0 -198
  178. package/src/shared/settings-schema.ts +0 -61
@@ -1,6 +1,6 @@
1
1
  import { drizzle } from "drizzle-orm/pglite";
2
2
  import { DrizzleAdapter } from "./drizzle-adapter";
3
- import { assert, beforeAll, describe, expect, expectTypeOf, it } from "vitest";
3
+ import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
4
4
  import { column, idColumn, referenceColumn, schema } from "../../schema/create";
5
5
  import type { DBType } from "./shared";
6
6
  import { createRequire } from "node:module";
@@ -126,7 +126,12 @@ describe("DrizzleAdapter PGLite", () => {
126
126
  age: 25,
127
127
  });
128
128
 
129
- expectTypeOf(createUow.find).parameter(0).toEqualTypeOf<keyof typeof testSchema.tables>();
129
+ expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
130
+ Parameters<typeof createUow.find>[0]
131
+ >();
132
+ expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
133
+ "users" | "emails" | "posts" | "comments"
134
+ >();
130
135
 
131
136
  const { success: createSuccess } = await createUow.executeMutations();
132
137
  expect(createSuccess).toBe(true);
@@ -735,46 +740,61 @@ describe("DrizzleAdapter PGLite", () => {
735
740
  it("should support cursor-based pagination with findWithCursor()", async () => {
736
741
  const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
737
742
 
738
- // Create multiple users for pagination testing
739
- for (let i = 1; i <= 25; i++) {
743
+ // Create exactly 15 users for precise pagination testing
744
+ const prefix = "CursorPagTest";
745
+
746
+ for (let i = 1; i <= 15; i++) {
740
747
  await queryEngine.create("users", {
741
- name: `Cursor User ${i.toString().padStart(2, "0")}`,
748
+ name: `${prefix} ${i.toString().padStart(2, "0")}`,
742
749
  age: 20 + i,
743
750
  });
744
751
  }
745
752
 
746
- // Fetch first page with cursor
753
+ // Fetch first page with cursor (pageSize=10, total=15 items)
747
754
  const firstPage = await queryEngine.findWithCursor("users", (b) =>
748
- b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(10),
755
+ b
756
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
757
+ .orderByIndex("name_idx", "asc")
758
+ .pageSize(10),
749
759
  );
750
760
 
751
- // Check structure
761
+ // Check structure and hasNextPage
752
762
  expect(firstPage).toHaveProperty("items");
753
763
  expect(firstPage).toHaveProperty("cursor");
764
+ expect(firstPage).toHaveProperty("hasNextPage");
754
765
  expect(Array.isArray(firstPage.items)).toBe(true);
755
- expect(firstPage.items.length).toBeGreaterThan(0);
756
- expect(firstPage.items.length).toBeLessThanOrEqual(10);
757
-
758
- assert(firstPage.cursor instanceof Cursor);
759
766
  expect(firstPage.items).toHaveLength(10);
767
+ expect(firstPage.hasNextPage).toBe(true);
760
768
  expect(firstPage.cursor).toBeInstanceOf(Cursor);
761
769
 
762
- // Fetch second page using cursor
770
+ // Fetch second page using cursor (last page with 5 remaining items)
763
771
  const secondPage = await queryEngine.findWithCursor("users", (b) =>
764
772
  b
765
- .whereIndex("name_idx")
773
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
766
774
  .after(firstPage.cursor!)
767
775
  .orderByIndex("name_idx", "asc")
768
776
  .pageSize(10),
769
777
  );
770
778
 
771
- expect(secondPage.items.length).toBeGreaterThan(0);
772
- expect(secondPage.items.length).toBeLessThanOrEqual(10);
779
+ expect(secondPage.items).toHaveLength(5);
780
+ expect(secondPage.hasNextPage).toBe(false);
781
+ expect(secondPage.cursor).toBeUndefined();
773
782
 
774
783
  // Verify no overlap - first item of second page should come after last item of first page
775
784
  const firstPageLastName = firstPage.items[firstPage.items.length - 1].name;
776
785
  const secondPageFirstName = secondPage.items[0].name;
777
786
  expect(secondPageFirstName > firstPageLastName).toBe(true);
787
+
788
+ // Test empty results
789
+ const emptyPage = await queryEngine.findWithCursor("users", (b) =>
790
+ b
791
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", "NonExistentPrefix"))
792
+ .orderByIndex("name_idx", "asc")
793
+ .pageSize(10),
794
+ );
795
+ expect(emptyPage.items).toHaveLength(0);
796
+ expect(emptyPage.hasNextPage).toBe(false);
797
+ expect(emptyPage.cursor).toBeUndefined();
778
798
  });
779
799
 
780
800
  it("should support findWithCursor() in Unit of Work", async () => {
@@ -789,11 +809,60 @@ describe("DrizzleAdapter PGLite", () => {
789
809
 
790
810
  const [result] = await uow.executeRetrieve();
791
811
 
792
- // Verify result structure
812
+ // Verify result structure including hasNextPage
793
813
  expect(result).toHaveProperty("items");
794
814
  expect(result).toHaveProperty("cursor");
815
+ expect(result).toHaveProperty("hasNextPage");
795
816
  expect(Array.isArray(result.items)).toBe(true);
796
817
  expect(result.items).toHaveLength(5);
818
+ expect(typeof result.hasNextPage).toBe("boolean");
797
819
  expect(result.cursor).toBeInstanceOf(Cursor);
798
820
  });
821
+
822
+ it("should fail check() when version changes", async () => {
823
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
824
+
825
+ // Create a user
826
+ const createUserUow = queryEngine.createUnitOfWork("create-user-for-version-conflict");
827
+ createUserUow.create("users", {
828
+ name: "Version Conflict User",
829
+ age: 40,
830
+ });
831
+ await createUserUow.executeMutations();
832
+
833
+ // Get the user
834
+ const [[user]] = await queryEngine
835
+ .createUnitOfWork("get-user-for-version-conflict")
836
+ .find("users", (b) =>
837
+ b.whereIndex("name_idx", (eb) => eb("name", "=", "Version Conflict User")),
838
+ )
839
+ .executeRetrieve();
840
+
841
+ // Update the user to increment their version
842
+ const updateUow = queryEngine.createUnitOfWork("update-user-version");
843
+ updateUow.update("users", user.id, (b) => b.set({ age: 41 }));
844
+ await updateUow.executeMutations();
845
+
846
+ // Try to check with the old version (should fail)
847
+ const uow = queryEngine.createUnitOfWork("check-stale-version");
848
+ uow.check("users", user.id); // This has version 0, but the user now has version 1
849
+ uow.create("posts", {
850
+ user_id: user.id,
851
+ title: "Should Not Be Created",
852
+ content: "Content",
853
+ created_at: new Date(),
854
+ });
855
+
856
+ const { success } = await uow.executeMutations();
857
+ expect(success).toBe(false);
858
+
859
+ // Verify the post was NOT created
860
+ const [posts] = await queryEngine
861
+ .createUnitOfWork("get-posts-for-version-conflict")
862
+ .find("posts", (b) => b.whereIndex("posts_user_idx", (eb) => eb("user_id", "=", user.id)))
863
+ .executeRetrieve();
864
+
865
+ const conflictPosts = posts.filter((p) => p.title === "Should Not Be Created");
866
+ expect(conflictPosts).toHaveLength(0);
867
+ });
799
868
  });
@@ -1,12 +1,14 @@
1
1
  import { drizzle } from "drizzle-orm/libsql";
2
2
  import { createClient } from "@libsql/client";
3
3
  import { DrizzleAdapter } from "./drizzle-adapter";
4
- import { beforeAll, describe, expect, expectTypeOf, it } from "vitest";
5
- import { column, idColumn, referenceColumn, schema } from "../../schema/create";
4
+ import { beforeAll, describe, expect, expectTypeOf, it, assert } from "vitest";
5
+ import { column, idColumn, referenceColumn, schema, type FragnoId } from "../../schema/create";
6
6
  import type { DBType } from "./shared";
7
7
  import { createRequire } from "node:module";
8
8
  import { writeAndLoadSchema } from "./test-utils";
9
9
  import { Cursor } from "../../query/cursor";
10
+ import { executeUnitOfWork } from "../../query/execute-unit-of-work";
11
+ import { ExponentialBackoffRetryPolicy } from "../../query/retry-policy";
10
12
 
11
13
  // Import drizzle-kit for migrations
12
14
  const require = createRequire(import.meta.url);
@@ -71,6 +73,30 @@ describe("DrizzleAdapter SQLite", () => {
71
73
  });
72
74
  });
73
75
 
76
+ // Second schema for multi-schema testing
77
+ const schema2 = schema((s) => {
78
+ return s
79
+ .addTable("products", (t) => {
80
+ return t
81
+ .addColumn("id", idColumn())
82
+ .addColumn("name", column("string"))
83
+ .addColumn("price", column("integer"))
84
+ .createIndex("name_idx", ["name"]);
85
+ })
86
+ .addTable("orders", (t) => {
87
+ return t
88
+ .addColumn("id", idColumn())
89
+ .addColumn("product_id", referenceColumn())
90
+ .addColumn("quantity", column("integer"))
91
+ .createIndex("product_orders_idx", ["product_id"]);
92
+ })
93
+ .addReference("product", {
94
+ type: "one",
95
+ from: { table: "orders", column: "product_id" },
96
+ to: { table: "products", column: "id" },
97
+ });
98
+ });
99
+
74
100
  let adapter: DrizzleAdapter;
75
101
  let db: DBType;
76
102
  // let sqliteDb: Database.Database;
@@ -84,17 +110,28 @@ describe("DrizzleAdapter SQLite", () => {
84
110
  "namespace",
85
111
  );
86
112
 
113
+ // Write second schema to file and dynamically import it
114
+ const { schemaModule: schemaModule2, cleanup: cleanup2 } = await writeAndLoadSchema(
115
+ "drizzle-adapter-sqlite-schema2",
116
+ schema2,
117
+ "sqlite",
118
+ "namespace2",
119
+ );
120
+
87
121
  const client = createClient({
88
122
  url: "file::memory:?cache=shared",
89
123
  });
90
124
 
125
+ // Merge both schema modules for the db
126
+ const mergedSchema = { ...schemaModule, ...schemaModule2 };
127
+
91
128
  db = drizzle(client, {
92
- schema: schemaModule,
129
+ schema: mergedSchema,
93
130
  }) as unknown as DBType;
94
131
 
95
- // Generate and run migrations
132
+ // Generate and run migrations for both schemas
96
133
  const emptyJson = await generateSQLiteDrizzleJson({});
97
- const targetJson = await generateSQLiteDrizzleJson(schemaModule);
134
+ const targetJson = await generateSQLiteDrizzleJson(mergedSchema);
98
135
 
99
136
  const migrationStatements = await generateSQLiteMigration(emptyJson, targetJson);
100
137
 
@@ -110,6 +147,7 @@ describe("DrizzleAdapter SQLite", () => {
110
147
  return async () => {
111
148
  client.close();
112
149
  await cleanup();
150
+ await cleanup2();
113
151
  };
114
152
  }, 12000);
115
153
 
@@ -128,7 +166,12 @@ describe("DrizzleAdapter SQLite", () => {
128
166
  age: 30,
129
167
  });
130
168
 
131
- expectTypeOf(createUow.find).parameter(0).toEqualTypeOf<keyof typeof testSchema.tables>();
169
+ expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
170
+ Parameters<typeof createUow.find>[0]
171
+ >();
172
+ expectTypeOf<keyof typeof testSchema.tables>().toEqualTypeOf<
173
+ "users" | "emails" | "posts" | "comments"
174
+ >();
132
175
 
133
176
  const { success: createSuccess } = await createUow.executeMutations();
134
177
  expect(createSuccess).toBe(true);
@@ -230,7 +273,6 @@ describe("DrizzleAdapter SQLite", () => {
230
273
  it("should support count operations", async () => {
231
274
  const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
232
275
 
233
- // Create some users
234
276
  const createUow = queryEngine.createUnitOfWork("create-users");
235
277
  createUow.create("users", { name: "User1", age: 20 });
236
278
  createUow.create("users", { name: "User2", age: 30 });
@@ -636,4 +678,246 @@ describe("DrizzleAdapter SQLite", () => {
636
678
  expect(localDate).toBeInstanceOf(Date);
637
679
  expect(typeof localDate.getTimezoneOffset()).toBe("number");
638
680
  });
681
+
682
+ it("should support forSchema for multi-schema queries", async () => {
683
+ const queryEngine1 = adapter.createQueryEngine(testSchema, "namespace");
684
+ const queryEngine2 = adapter.createQueryEngine(schema2, "namespace2");
685
+
686
+ // Create test data in schema1 (users)
687
+ const createUsersUow = queryEngine1.createUnitOfWork("create-users-for-multi-schema");
688
+ createUsersUow.create("users", { name: "Multi Schema User 1", age: 25 });
689
+ createUsersUow.create("users", { name: "Multi Schema User 2", age: 30 });
690
+ const { success: success1 } = await createUsersUow.executeMutations();
691
+ expect(success1).toBe(true);
692
+
693
+ // Create test data in schema2 (products)
694
+ const createProductsUow = queryEngine2.createUnitOfWork("create-products-for-multi-schema");
695
+ createProductsUow.create("products", { name: "Product A", price: 100 });
696
+ createProductsUow.create("products", { name: "Product B", price: 200 });
697
+ const { success: success2 } = await createProductsUow.executeMutations();
698
+ expect(success2).toBe(true);
699
+
700
+ // Now use forSchema to query from both schemas
701
+ const uow = queryEngine1.createUnitOfWork("multi-schema-query");
702
+
703
+ const view1 = uow
704
+ .forSchema(testSchema)
705
+ .find("users", (b) =>
706
+ b
707
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", "Multi Schema User"))
708
+ .select(["id", "name"]),
709
+ )
710
+ .find("users", (b) =>
711
+ b
712
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", "Multi Schema User"))
713
+ .select(["name", "age"]),
714
+ );
715
+
716
+ const view2 = uow
717
+ .forSchema(schema2)
718
+ .find("products", (b) => b.whereIndex("primary").select(["name", "price"]));
719
+
720
+ // Execute the retrieval phase once
721
+ await uow.executeRetrieve();
722
+
723
+ // Get results from view1
724
+ const [users1, users2] = await view1.retrievalPhase;
725
+ const [user1] = users1;
726
+ expectTypeOf(user1).toMatchObjectType<{ id: FragnoId; name: string }>();
727
+
728
+ const [user2] = users2;
729
+ expectTypeOf(user2).toMatchObjectType<{ name: string; age: number | null }>();
730
+
731
+ // Get results from view2
732
+ const [products] = await view2.retrievalPhase;
733
+ const [product1] = products;
734
+ expectTypeOf(product1).toMatchObjectType<{ name: string; price: number }>();
735
+
736
+ // Verify users from schema1
737
+ expect(users1).toHaveLength(2);
738
+ expect(users1[0]).toMatchObject({
739
+ id: expect.any(Object),
740
+ name: "Multi Schema User 1",
741
+ });
742
+ expect(users1[1]).toMatchObject({
743
+ id: expect.any(Object),
744
+ name: "Multi Schema User 2",
745
+ });
746
+
747
+ expect(users2).toHaveLength(2);
748
+ expect(users2[0]).toMatchObject({
749
+ name: "Multi Schema User 1",
750
+ age: 25,
751
+ });
752
+ expect(users2[1]).toMatchObject({
753
+ name: "Multi Schema User 2",
754
+ age: 30,
755
+ });
756
+
757
+ // Verify products from schema2
758
+ expect(products).toHaveLength(2);
759
+ expect(products[0]).toMatchObject({
760
+ name: "Product A",
761
+ price: 100,
762
+ });
763
+ expect(products[1]).toMatchObject({
764
+ name: "Product B",
765
+ price: 200,
766
+ });
767
+ });
768
+
769
+ it("should verify hasNextPage in cursor pagination", async () => {
770
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
771
+
772
+ // Create exactly 15 users for precise pagination testing
773
+ const prefix = "HasNextPageTest";
774
+
775
+ for (let i = 1; i <= 15; i++) {
776
+ await queryEngine.create("users", {
777
+ name: `${prefix} ${i.toString().padStart(2, "0")}`,
778
+ age: 20 + i,
779
+ });
780
+ }
781
+
782
+ // Test 1: First page with more results available (pageSize=10, total=15)
783
+ const firstPage = await queryEngine.findWithCursor("users", (b) =>
784
+ b
785
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
786
+ .orderByIndex("name_idx", "asc")
787
+ .pageSize(10),
788
+ );
789
+
790
+ expect(firstPage.items).toHaveLength(10);
791
+ expect(firstPage.hasNextPage).toBe(true);
792
+ expect(firstPage.cursor).toBeInstanceOf(Cursor);
793
+
794
+ // Test 2: Second page (last page, partial results: 5 items remaining)
795
+ const secondPage = await queryEngine.findWithCursor("users", (b) =>
796
+ b
797
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", prefix))
798
+ .after(firstPage.cursor!)
799
+ .orderByIndex("name_idx", "asc")
800
+ .pageSize(10),
801
+ );
802
+
803
+ expect(secondPage.items).toHaveLength(5);
804
+ expect(secondPage.hasNextPage).toBe(false);
805
+ expect(secondPage.cursor).toBeUndefined();
806
+
807
+ // Test 3: Empty results
808
+ const emptyPage = await queryEngine.findWithCursor("users", (b) =>
809
+ b
810
+ .whereIndex("name_idx", (eb) => eb("name", "starts with", "NonExistentPrefix"))
811
+ .orderByIndex("name_idx", "asc")
812
+ .pageSize(10),
813
+ );
814
+
815
+ expect(emptyPage.items).toHaveLength(0);
816
+ expect(emptyPage.hasNextPage).toBe(false);
817
+ expect(emptyPage.cursor).toBeUndefined();
818
+ });
819
+
820
+ it("should support executeUnitOfWork with retry logic", async () => {
821
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
822
+
823
+ // Create a test user
824
+ const createUow = queryEngine.createUnitOfWork("create-user-for-execute-uow");
825
+ createUow.create("users", { name: "Execute UOW User", age: 42 });
826
+ await createUow.executeMutations();
827
+
828
+ // Fetch the user to get their ID
829
+ const [[user]] = await queryEngine
830
+ .createUnitOfWork("get-user-for-execute-uow")
831
+ .find("users", (b) => b.whereIndex("name_idx", (eb) => eb("name", "=", "Execute UOW User")))
832
+ .executeRetrieve();
833
+
834
+ // Use executeUnitOfWork to increment age with optimistic locking
835
+ const result = await executeUnitOfWork(
836
+ {
837
+ retrieve: (uow) =>
838
+ uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", user.id))),
839
+ mutate: (uow, [users]) => {
840
+ const foundUser = users[0];
841
+ const newAge = foundUser.age! + 1;
842
+ uow.update("users", foundUser.id, (b) => b.set({ age: newAge }).check());
843
+ return { previousAge: foundUser.age, newAge };
844
+ },
845
+ onSuccess: ({ mutationResult }) => {
846
+ // Verify the age was incremented correctly
847
+ expect(mutationResult.newAge).toBe(mutationResult.previousAge! + 1);
848
+ },
849
+ },
850
+ {
851
+ createUnitOfWork: () => queryEngine.createUnitOfWork("execute-uow-update"),
852
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
853
+ },
854
+ );
855
+
856
+ // Verify the operation succeeded
857
+ assert(result.success);
858
+ expect(result.mutationResult).toEqual({
859
+ previousAge: 42,
860
+ newAge: 43,
861
+ });
862
+
863
+ // Verify the user was actually updated in the database
864
+ const updatedUser = await queryEngine.findFirst("users", (b) =>
865
+ b.whereIndex("primary", (eb) => eb("id", "=", user.id)),
866
+ );
867
+
868
+ expect(updatedUser).toMatchObject({
869
+ id: expect.objectContaining({
870
+ externalId: user.id.externalId,
871
+ version: 1, // Version incremented due to check()
872
+ }),
873
+ name: "Execute UOW User",
874
+ age: 43,
875
+ });
876
+ });
877
+
878
+ it("should fail check() when version changes", async () => {
879
+ const queryEngine = adapter.createQueryEngine(testSchema, "namespace");
880
+
881
+ // Create a user
882
+ const createUserUow = queryEngine.createUnitOfWork("create-user-for-version-conflict");
883
+ createUserUow.create("users", {
884
+ name: "Version Conflict User SQLite",
885
+ age: 40,
886
+ });
887
+ await createUserUow.executeMutations();
888
+
889
+ // Get the user
890
+ const [[user]] = await queryEngine
891
+ .createUnitOfWork("get-user-for-version-conflict")
892
+ .find("users", (b) =>
893
+ b.whereIndex("name_idx", (eb) => eb("name", "=", "Version Conflict User SQLite")),
894
+ )
895
+ .executeRetrieve();
896
+
897
+ // Update the user to increment their version
898
+ const updateUow = queryEngine.createUnitOfWork("update-user-version");
899
+ updateUow.update("users", user.id, (b) => b.set({ age: 41 }));
900
+ await updateUow.executeMutations();
901
+
902
+ // Try to check with the old version (should fail)
903
+ const uow = queryEngine.createUnitOfWork("check-stale-version");
904
+ uow.check("users", user.id); // This has version 0, but the user now has version 1
905
+ uow.create("posts", {
906
+ user_id: user.id,
907
+ title: "Should Not Be Created SQLite",
908
+ content: "Content",
909
+ });
910
+
911
+ const { success } = await uow.executeMutations();
912
+ expect(success).toBe(false);
913
+
914
+ // Verify the post was NOT created
915
+ const [posts] = await queryEngine
916
+ .createUnitOfWork("get-posts-for-version-conflict")
917
+ .find("posts", (b) => b.whereIndex("posts_user_idx", (eb) => eb("user_id", "=", user.id)))
918
+ .executeRetrieve();
919
+
920
+ const conflictPosts = posts.filter((p) => p.title === "Should Not Be Created SQLite");
921
+ expect(conflictPosts).toHaveLength(0);
922
+ });
639
923
  });
@@ -20,25 +20,9 @@ describe("DrizzleAdapter", () => {
20
20
 
21
21
  expect(result.path).toBe("schema.ts");
22
22
  expect(result.schema).toMatchInlineSnapshot(`
23
- "import { pgTable, varchar, text, bigserial, integer, uniqueIndex } from "drizzle-orm/pg-core"
23
+ "import { pgTable, varchar, text, bigserial, integer } from "drizzle-orm/pg-core"
24
24
  import { createId } from "@fragno-dev/db/id"
25
25
 
26
- // ============================================================================
27
- // Settings Table (shared across all fragments)
28
- // ============================================================================
29
-
30
- export const fragno_db_settings = pgTable("fragno_db_settings", {
31
- id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
32
- key: text("key").notNull(),
33
- value: text("value").notNull(),
34
- _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
35
- _version: integer("_version").notNull().default(0)
36
- }, (table) => [
37
- uniqueIndex("unique_key").on(table.key)
38
- ])
39
-
40
- export const fragnoDbSettingSchemaVersion = 1;
41
-
42
26
  // ============================================================================
43
27
  // Fragment: test
44
28
  // ============================================================================
@@ -69,25 +53,9 @@ describe("DrizzleAdapter", () => {
69
53
 
70
54
  expect(result.path).toBe("schema.ts");
71
55
  expect(result.schema).toMatchInlineSnapshot(`
72
- "import { sqliteTable, text, integer, uniqueIndex } from "drizzle-orm/sqlite-core"
56
+ "import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
73
57
  import { createId } from "@fragno-dev/db/id"
74
58
 
75
- // ============================================================================
76
- // Settings Table (shared across all fragments)
77
- // ============================================================================
78
-
79
- export const fragno_db_settings = sqliteTable("fragno_db_settings", {
80
- id: text("id").notNull().$defaultFn(() => createId()),
81
- key: text("key").notNull(),
82
- value: text("value").notNull(),
83
- _internalId: integer("_internalId").primaryKey({ autoIncrement: true }).notNull(),
84
- _version: integer("_version").notNull().default(0)
85
- }, (table) => [
86
- uniqueIndex("unique_key").on(table.key)
87
- ])
88
-
89
- export const fragnoDbSettingSchemaVersion = 1;
90
-
91
59
  // ============================================================================
92
60
  // Fragment: test
93
61
  // ============================================================================
@@ -130,25 +98,9 @@ describe("DrizzleAdapter", () => {
130
98
 
131
99
  // Original table should still be there
132
100
  expect(result.schema).toMatchInlineSnapshot(`
133
- "import { pgTable, varchar, text, bigserial, integer, uniqueIndex } from "drizzle-orm/pg-core"
101
+ "import { pgTable, varchar, text, bigserial, integer } from "drizzle-orm/pg-core"
134
102
  import { createId } from "@fragno-dev/db/id"
135
103
 
136
- // ============================================================================
137
- // Settings Table (shared across all fragments)
138
- // ============================================================================
139
-
140
- export const fragno_db_settings = pgTable("fragno_db_settings", {
141
- id: varchar("id", { length: 30 }).notNull().$defaultFn(() => createId()),
142
- key: text("key").notNull(),
143
- value: text("value").notNull(),
144
- _internalId: bigserial("_internalId", { mode: "number" }).primaryKey().notNull(),
145
- _version: integer("_version").notNull().default(0)
146
- }, (table) => [
147
- uniqueIndex("unique_key").on(table.key)
148
- ])
149
-
150
- export const fragnoDbSettingSchemaVersion = 1;
151
-
152
104
  // ============================================================================
153
105
  // Fragment: test
154
106
  // ============================================================================