@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.
- package/.turbo/turbo-build.log +179 -132
- package/CHANGELOG.md +30 -0
- package/dist/adapters/adapters.d.ts +27 -1
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +7 -5
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
- package/dist/adapters/drizzle/generate.d.ts +4 -1
- package/dist/adapters/drizzle/generate.d.ts.map +1 -1
- package/dist/adapters/drizzle/generate.js +11 -18
- package/dist/adapters/drizzle/generate.js.map +1 -1
- package/dist/adapters/drizzle/shared.d.ts +14 -1
- package/dist/adapters/drizzle/shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js +14 -3
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query-builder.js +1 -1
- package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +1 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-query.js +28 -19
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
- package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-shared.js +16 -1
- package/dist/adapters/kysely/kysely-shared.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
- package/dist/db-fragment-definition-builder.d.ts +152 -0
- package/dist/db-fragment-definition-builder.d.ts.map +1 -0
- package/dist/db-fragment-definition-builder.js +137 -0
- package/dist/db-fragment-definition-builder.js.map +1 -0
- package/dist/fragments/internal-fragment.d.ts +19 -0
- package/dist/fragments/internal-fragment.d.ts.map +1 -0
- package/dist/fragments/internal-fragment.js +39 -0
- package/dist/fragments/internal-fragment.js.map +1 -0
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js +35 -15
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +8 -18
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +7 -34
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
- package/dist/packages/fragno/dist/api/bind-services.js +20 -0
- package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
- package/dist/packages/fragno/dist/api/error.js +48 -0
- package/dist/packages/fragno/dist/api/error.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
- package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/route.js +10 -0
- package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
- package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
- package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
- package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/route.js +17 -0
- package/dist/packages/fragno/dist/api/route.js.map +1 -0
- package/dist/packages/fragno/dist/internal/symbols.js +10 -0
- package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
- package/dist/query/cursor.d.ts +10 -2
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +11 -4
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/execute-unit-of-work.d.ts +123 -0
- package/dist/query/execute-unit-of-work.d.ts.map +1 -0
- package/dist/query/execute-unit-of-work.js +184 -0
- package/dist/query/execute-unit-of-work.js.map +1 -0
- package/dist/query/query.d.ts +3 -3
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +4 -2
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/retry-policy.d.ts +88 -0
- package/dist/query/retry-policy.d.ts.map +1 -0
- package/dist/query/retry-policy.js +61 -0
- package/dist/query/retry-policy.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +171 -32
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +530 -133
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +12 -7
- package/dist/schema/serialize.js.map +1 -1
- package/dist/with-database.d.ts +28 -0
- package/dist/with-database.d.ts.map +1 -0
- package/dist/with-database.js +34 -0
- package/dist/with-database.js.map +1 -0
- package/package.json +10 -3
- package/src/adapters/adapters.ts +30 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
- package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
- package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
- package/src/adapters/drizzle/drizzle-query.ts +25 -15
- package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
- package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
- package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
- package/src/adapters/drizzle/generate.test.ts +102 -269
- package/src/adapters/drizzle/generate.ts +12 -30
- package/src/adapters/drizzle/test-utils.ts +36 -5
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
- package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
- package/src/adapters/kysely/kysely-adapter.ts +25 -2
- package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
- package/src/adapters/kysely/kysely-query.ts +57 -37
- package/src/adapters/kysely/kysely-shared.ts +34 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
- package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
- package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
- package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
- package/src/adapters/kysely/migration/execute-base.ts +1 -1
- package/src/db-fragment-definition-builder.test.ts +887 -0
- package/src/db-fragment-definition-builder.ts +506 -0
- package/src/db-fragment-instantiator.test.ts +467 -0
- package/src/db-fragment-integration.test.ts +408 -0
- package/src/fragments/internal-fragment.test.ts +160 -0
- package/src/fragments/internal-fragment.ts +85 -0
- package/src/migration-engine/generation-engine.test.ts +58 -15
- package/src/migration-engine/generation-engine.ts +78 -25
- package/src/mod.ts +35 -43
- package/src/query/cursor.test.ts +119 -0
- package/src/query/cursor.ts +17 -4
- package/src/query/execute-unit-of-work.test.ts +1310 -0
- package/src/query/execute-unit-of-work.ts +463 -0
- package/src/query/query.ts +4 -4
- package/src/query/result-transform.test.ts +129 -0
- package/src/query/result-transform.ts +4 -1
- package/src/query/retry-policy.test.ts +217 -0
- package/src/query/retry-policy.ts +141 -0
- package/src/query/unit-of-work-coordinator.test.ts +833 -0
- package/src/query/unit-of-work-types.test.ts +15 -2
- package/src/query/unit-of-work.test.ts +878 -200
- package/src/query/unit-of-work.ts +963 -321
- package/src/schema/serialize.ts +22 -11
- package/src/with-database.ts +140 -0
- package/tsdown.config.ts +1 -0
- package/dist/fragment.d.ts +0 -54
- package/dist/fragment.d.ts.map +0 -1
- package/dist/fragment.js +0 -92
- package/dist/fragment.js.map +0 -1
- package/dist/shared/settings-schema.js +0 -36
- package/dist/shared/settings-schema.js.map +0 -1
- package/src/fragment.test.ts +0 -341
- package/src/fragment.ts +0 -198
- package/src/shared/settings-schema.ts +0 -61
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
import { assert, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
column,
|
|
4
|
+
FragnoId,
|
|
5
|
+
idColumn,
|
|
6
|
+
referenceColumn,
|
|
7
|
+
schema,
|
|
8
|
+
type AnySchema,
|
|
9
|
+
} from "../../schema/create";
|
|
10
|
+
import { createDrizzleUOWCompiler } from "./drizzle-uow-compiler";
|
|
11
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
12
|
+
import { createClient } from "@libsql/client";
|
|
13
|
+
import type { DBType } from "./shared";
|
|
14
|
+
import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
|
|
15
|
+
import { writeAndLoadSchema } from "./test-utils";
|
|
16
|
+
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
17
|
+
import { createDrizzleConnectionPool } from "./drizzle-connection-pool";
|
|
18
|
+
import { Cursor } from "../../query/cursor";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Integration tests for Drizzle UOW compiler and executor.
|
|
22
|
+
* These tests generate a real Drizzle schema and verify compilation works correctly.
|
|
23
|
+
*/
|
|
24
|
+
describe("drizzle-uow-compiler", () => {
|
|
25
|
+
const testSchema = schema((s) => {
|
|
26
|
+
return s
|
|
27
|
+
.addTable("users", (t) => {
|
|
28
|
+
return t
|
|
29
|
+
.addColumn("id", idColumn())
|
|
30
|
+
.addColumn("name", column("string"))
|
|
31
|
+
.addColumn("email", column("string"))
|
|
32
|
+
.addColumn("age", column("integer").nullable())
|
|
33
|
+
.createIndex("idx_email", ["email"], { unique: true })
|
|
34
|
+
.createIndex("idx_name", ["name"]);
|
|
35
|
+
})
|
|
36
|
+
.addTable("posts", (t) => {
|
|
37
|
+
return t
|
|
38
|
+
.addColumn("id", idColumn())
|
|
39
|
+
.addColumn("title", column("string"))
|
|
40
|
+
.addColumn("content", column("string"))
|
|
41
|
+
.addColumn("userId", referenceColumn())
|
|
42
|
+
.addColumn("viewCount", column("integer").defaultTo(0))
|
|
43
|
+
.createIndex("idx_user", ["userId"])
|
|
44
|
+
.createIndex("idx_title", ["title"]);
|
|
45
|
+
})
|
|
46
|
+
.addReference("author", {
|
|
47
|
+
type: "one",
|
|
48
|
+
from: { table: "posts", column: "userId" },
|
|
49
|
+
to: { table: "users", column: "id" },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let db: DBType;
|
|
54
|
+
let pool: ConnectionPool<DBType>;
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
// Write schema to file and dynamically import it
|
|
58
|
+
const { schemaModule, cleanup } = await writeAndLoadSchema(
|
|
59
|
+
"drizzle-uow-compiler-sqlite",
|
|
60
|
+
testSchema,
|
|
61
|
+
"sqlite",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Create Drizzle instance with libsql (in-memory SQLite)
|
|
65
|
+
const client = createClient({
|
|
66
|
+
url: ":memory:",
|
|
67
|
+
});
|
|
68
|
+
db = drizzle({ client, schema: schemaModule }) as unknown as DBType;
|
|
69
|
+
|
|
70
|
+
// Wrap in connection pool
|
|
71
|
+
pool = createDrizzleConnectionPool(db);
|
|
72
|
+
|
|
73
|
+
return async () => {
|
|
74
|
+
await cleanup();
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function createTestUOWWithSchema<const T extends AnySchema>(schema: T) {
|
|
79
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
80
|
+
const mockExecutor = {
|
|
81
|
+
executeRetrievalPhase: async () => [],
|
|
82
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
83
|
+
};
|
|
84
|
+
const mockDecoder: UOWDecoder = (rawResults, operations) => {
|
|
85
|
+
if (rawResults.length !== operations.length) {
|
|
86
|
+
throw new Error("rawResults and ops must have the same length");
|
|
87
|
+
}
|
|
88
|
+
return rawResults;
|
|
89
|
+
};
|
|
90
|
+
return new UnitOfWork(compiler, mockExecutor, mockDecoder).forSchema(schema);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createTestUOW(name?: string) {
|
|
94
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
95
|
+
const mockExecutor = {
|
|
96
|
+
executeRetrievalPhase: async () => [],
|
|
97
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
98
|
+
};
|
|
99
|
+
const mockDecoder: UOWDecoder = (rawResults, operations) => {
|
|
100
|
+
if (rawResults.length !== operations.length) {
|
|
101
|
+
throw new Error("rawResults and ops must have the same length");
|
|
102
|
+
}
|
|
103
|
+
return rawResults;
|
|
104
|
+
};
|
|
105
|
+
const uow = new UnitOfWork(compiler, mockExecutor, mockDecoder, name).forSchema(testSchema);
|
|
106
|
+
|
|
107
|
+
return uow;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
it("should create a compiler with the correct structure", () => {
|
|
111
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
112
|
+
|
|
113
|
+
expect(compiler).toBeDefined();
|
|
114
|
+
expect(compiler.compileRetrievalOperation).toBeInstanceOf(Function);
|
|
115
|
+
expect(compiler.compileMutationOperation).toBeInstanceOf(Function);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("compileRetrievalOperation", () => {
|
|
119
|
+
it("should compile find operation with where clause", () => {
|
|
120
|
+
const uow = createTestUOW();
|
|
121
|
+
uow.find("users", (b) =>
|
|
122
|
+
b.whereIndex("idx_email", (eb) => eb("email", "=", "test@example.com")),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
126
|
+
const compiled = uow.compile(compiler);
|
|
127
|
+
|
|
128
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
129
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
130
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."email" = ?"`,
|
|
131
|
+
);
|
|
132
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["test@example.com"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should compile find operation with select clause", () => {
|
|
136
|
+
const uow = createTestUOW();
|
|
137
|
+
uow.find("users", (b) =>
|
|
138
|
+
b.whereIndex("idx_name", (eb) => eb("name", "=", "Alice")).select(["id", "name"]),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
142
|
+
const compiled = uow.compile(compiler);
|
|
143
|
+
|
|
144
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
145
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
146
|
+
`"select "id", "name", "_internalId", "_version" from "users" "users" where "users"."name" = ?"`,
|
|
147
|
+
);
|
|
148
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["Alice"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should compile find operation with pageSize", () => {
|
|
152
|
+
const uow = createTestUOW();
|
|
153
|
+
uow.find("users", (b) => b.whereIndex("primary").pageSize(10));
|
|
154
|
+
|
|
155
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
156
|
+
const compiled = uow.compile(compiler);
|
|
157
|
+
|
|
158
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
159
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
160
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" limit ?"`,
|
|
161
|
+
);
|
|
162
|
+
expect(compiled.retrievalBatch[0].params).toEqual([10]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should compile find operation with orderByIndex on primary index", () => {
|
|
166
|
+
const uow = createTestUOW();
|
|
167
|
+
uow.find("users", (b) => b.whereIndex("primary").orderByIndex("primary", "desc"));
|
|
168
|
+
|
|
169
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
170
|
+
const compiled = uow.compile(compiler);
|
|
171
|
+
|
|
172
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
173
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
174
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" order by "users"."id" desc"`,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should compile find operation with orderByIndex", () => {
|
|
179
|
+
const uow = createTestUOW();
|
|
180
|
+
uow.find("users", (b) => b.whereIndex("idx_name").orderByIndex("idx_name", "desc"));
|
|
181
|
+
|
|
182
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
183
|
+
const compiled = uow.compile(compiler);
|
|
184
|
+
|
|
185
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
186
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
187
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" order by "users"."name" desc"`,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should compile multiple find operations", () => {
|
|
192
|
+
const uow = createTestUOW();
|
|
193
|
+
uow.find("users", (b) =>
|
|
194
|
+
b.whereIndex("idx_email", (eb) => eb("email", "=", "user1@example.com")),
|
|
195
|
+
);
|
|
196
|
+
uow.find("posts", (b) => b.whereIndex("idx_title", (eb) => eb("title", "contains", "test")));
|
|
197
|
+
|
|
198
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
199
|
+
const compiled = uow.compile(compiler);
|
|
200
|
+
|
|
201
|
+
expect(compiled.retrievalBatch).toHaveLength(2);
|
|
202
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
203
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."email" = ?"`,
|
|
204
|
+
);
|
|
205
|
+
expect(compiled.retrievalBatch[1].sql).toMatchInlineSnapshot(
|
|
206
|
+
`"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version" from "posts" "posts" where "posts"."title" like ?"`,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should compile find operation with selectCount", () => {
|
|
211
|
+
const uow = createTestUOW();
|
|
212
|
+
uow.find("users", (b) => b.whereIndex("primary").selectCount());
|
|
213
|
+
|
|
214
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
215
|
+
const compiled = uow.compile(compiler);
|
|
216
|
+
|
|
217
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
218
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
219
|
+
`"select count(*) from "users""`,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should compile find operation with selectCount and where clause", () => {
|
|
224
|
+
const uow = createTestUOW();
|
|
225
|
+
uow.find("users", (b) =>
|
|
226
|
+
b.whereIndex("idx_name", (eb) => eb("name", "starts with", "John")).selectCount(),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
230
|
+
const compiled = uow.compile(compiler);
|
|
231
|
+
|
|
232
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
233
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
234
|
+
`"select count(*) from "users" where "users"."name" like ?"`,
|
|
235
|
+
);
|
|
236
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["John%"]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should compile find operation with cursor pagination using after", () => {
|
|
240
|
+
const uow = createTestUOW();
|
|
241
|
+
const cursor = new Cursor({
|
|
242
|
+
indexName: "idx_name",
|
|
243
|
+
orderDirection: "asc",
|
|
244
|
+
pageSize: 10,
|
|
245
|
+
indexValues: { name: "Alice" },
|
|
246
|
+
});
|
|
247
|
+
uow.find("users", (b) =>
|
|
248
|
+
b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
252
|
+
const compiled = uow.compile(compiler);
|
|
253
|
+
|
|
254
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
255
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
256
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."name" > ? order by "users"."name" asc limit ?"`,
|
|
257
|
+
);
|
|
258
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["Alice", 10]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should compile find operation with cursor pagination using before", () => {
|
|
262
|
+
const uow = createTestUOW();
|
|
263
|
+
const cursor = new Cursor({
|
|
264
|
+
indexName: "idx_name",
|
|
265
|
+
orderDirection: "desc",
|
|
266
|
+
pageSize: 10,
|
|
267
|
+
indexValues: { name: "Bob" },
|
|
268
|
+
});
|
|
269
|
+
uow.find("users", (b) =>
|
|
270
|
+
b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
274
|
+
const compiled = uow.compile(compiler);
|
|
275
|
+
|
|
276
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
277
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
278
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."name" > ? order by "users"."name" desc limit ?"`,
|
|
279
|
+
);
|
|
280
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["Bob", 10]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should compile find operation with cursor pagination and additional where conditions", () => {
|
|
284
|
+
const uow = createTestUOW();
|
|
285
|
+
const cursor = new Cursor({
|
|
286
|
+
indexName: "idx_name",
|
|
287
|
+
orderDirection: "asc",
|
|
288
|
+
pageSize: 5,
|
|
289
|
+
indexValues: { name: "Alice" },
|
|
290
|
+
});
|
|
291
|
+
uow.find("users", (b) =>
|
|
292
|
+
b
|
|
293
|
+
.whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
|
|
294
|
+
.orderByIndex("idx_name", "asc")
|
|
295
|
+
.after(cursor)
|
|
296
|
+
.pageSize(5),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
300
|
+
const compiled = uow.compile(compiler);
|
|
301
|
+
|
|
302
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
303
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
304
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where ("users"."name" like ? and "users"."name" > ?) order by "users"."name" asc limit ?"`,
|
|
305
|
+
);
|
|
306
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["John%", "Alice", 5]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should compile find operation with join", () => {
|
|
310
|
+
const uow = createTestUOW();
|
|
311
|
+
uow.find("posts", (b) =>
|
|
312
|
+
b
|
|
313
|
+
.whereIndex("idx_title", (eb) => eb("title", "contains", "test"))
|
|
314
|
+
.join((jb) => jb.author()),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
318
|
+
const compiled = uow.compile(compiler);
|
|
319
|
+
|
|
320
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
321
|
+
// This should generate SQL that joins posts with users and selects author name and email
|
|
322
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
323
|
+
`"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version", (select json_array("id", "name", "email", "age", "_internalId", "_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit ?) "posts_author") as "author" from "posts" "posts" where "posts"."title" like ?"`,
|
|
324
|
+
);
|
|
325
|
+
expect(compiled.retrievalBatch[0].params).toEqual([1, "%test%"]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should compile find operation with join filtering", () => {
|
|
329
|
+
const uow = createTestUOW();
|
|
330
|
+
uow.find("posts", (b) =>
|
|
331
|
+
b
|
|
332
|
+
.whereIndex("primary")
|
|
333
|
+
.join((jb) =>
|
|
334
|
+
jb.author((builder) =>
|
|
335
|
+
builder.whereIndex("idx_name", (eb) => eb("name", "=", "Alice")).select(["name"]),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
341
|
+
const compiled = uow.compile(compiler);
|
|
342
|
+
|
|
343
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
344
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
345
|
+
expect(sql).toMatchInlineSnapshot(
|
|
346
|
+
`"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version", (select json_array("name", "_internalId", "_version") as "data" from (select * from "users" "posts_author" where ("posts_author"."_internalId" = "posts"."userId" and "posts_author"."name" = ?) limit ?) "posts_author") as "author" from "posts" "posts""`,
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should compile find operation with join ordering", () => {
|
|
351
|
+
const uow = createTestUOW();
|
|
352
|
+
uow.find("posts", (b) =>
|
|
353
|
+
b
|
|
354
|
+
.whereIndex("primary")
|
|
355
|
+
.join((jb) => jb.author((builder) => builder.orderByIndex("idx_name", "desc"))),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
359
|
+
const compiled = uow.compile(compiler);
|
|
360
|
+
|
|
361
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
362
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
363
|
+
expect(sql).toMatchInlineSnapshot(
|
|
364
|
+
`"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version", (select json_array("id", "name", "email", "age", "_internalId", "_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" order by "posts_author"."name" desc limit ?) "posts_author") as "author" from "posts" "posts""`,
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("should compile find operation with join pageSize", () => {
|
|
369
|
+
const uow = createTestUOW();
|
|
370
|
+
uow.find("posts", (b) =>
|
|
371
|
+
b.whereIndex("primary").join((jb) => jb.author((builder) => builder.pageSize(5))),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
375
|
+
const compiled = uow.compile(compiler);
|
|
376
|
+
|
|
377
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
378
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
379
|
+
// Should have limit in the joined query
|
|
380
|
+
expect(sql).toMatchInlineSnapshot(
|
|
381
|
+
`"select "id", "title", "content", "userId", "viewCount", "_internalId", "_version", (select json_array("id", "name", "email", "age", "_internalId", "_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit ?) "posts_author") as "author" from "posts" "posts""`,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("compileMutationOperation", () => {
|
|
387
|
+
it("should compile create operation", () => {
|
|
388
|
+
const uow = createTestUOW();
|
|
389
|
+
uow.create("users", {
|
|
390
|
+
name: "John Doe",
|
|
391
|
+
email: "john@example.com",
|
|
392
|
+
age: 30,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
396
|
+
const compiled = uow.compile(compiler);
|
|
397
|
+
const [batch] = compiled.mutationBatch;
|
|
398
|
+
assert(batch);
|
|
399
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
400
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
401
|
+
`"insert into "users" ("id", "name", "email", "age", "_internalId", "_version") values (?, ?, ?, ?, null, ?)"`,
|
|
402
|
+
);
|
|
403
|
+
expect(batch.query.params).toMatchObject([
|
|
404
|
+
expect.any(String),
|
|
405
|
+
"John Doe",
|
|
406
|
+
"john@example.com",
|
|
407
|
+
30,
|
|
408
|
+
0, // _version default
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should compile create operation with external id string for reference column", () => {
|
|
413
|
+
const uow = createTestUOW();
|
|
414
|
+
// Create a post with userId as just an external id string
|
|
415
|
+
uow.create("posts", {
|
|
416
|
+
title: "Test Post",
|
|
417
|
+
content: "Post content",
|
|
418
|
+
userId: "user_external_id_123",
|
|
419
|
+
viewCount: 5,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
423
|
+
const compiled = uow.compile(compiler);
|
|
424
|
+
const [batch] = compiled.mutationBatch;
|
|
425
|
+
assert(batch);
|
|
426
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
427
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
428
|
+
`"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values (?, ?, ?, (select "_internalId" from "users" where "id" = ? limit 1), ?, null, ?)"`,
|
|
429
|
+
);
|
|
430
|
+
expect(batch.query.params).toMatchObject([
|
|
431
|
+
expect.any(String), // auto-generated post ID
|
|
432
|
+
"Test Post",
|
|
433
|
+
"Post content",
|
|
434
|
+
"user_external_id_123", // external id string
|
|
435
|
+
5, // viewCount
|
|
436
|
+
0, // _version default
|
|
437
|
+
]);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("should compile create operation with bigint for reference column (no subquery)", () => {
|
|
441
|
+
const uow = createTestUOW();
|
|
442
|
+
// Create a post with userId as a bigint directly (internal ID)
|
|
443
|
+
uow.create("posts", {
|
|
444
|
+
title: "Direct ID Post",
|
|
445
|
+
content: "Content with direct bigint",
|
|
446
|
+
userId: 12345n,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
450
|
+
const compiled = uow.compile(compiler);
|
|
451
|
+
const [batch] = compiled.mutationBatch;
|
|
452
|
+
assert(batch);
|
|
453
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
454
|
+
// Should NOT have a subquery when using bigint directly
|
|
455
|
+
expect(batch.query.sql).not.toMatch(/\(select.*from.*users/i);
|
|
456
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
457
|
+
`"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values (?, ?, ?, ?, ?, null, ?)"`,
|
|
458
|
+
);
|
|
459
|
+
expect(batch.query.params).toMatchObject([
|
|
460
|
+
expect.any(String), // auto-generated post ID
|
|
461
|
+
"Direct ID Post",
|
|
462
|
+
"Content with direct bigint",
|
|
463
|
+
12345n, // bigint stays as bigint for Drizzle (Drizzle handles conversion)
|
|
464
|
+
0, // viewCount default
|
|
465
|
+
0, // _version default
|
|
466
|
+
]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should compile create operation with FragnoId object for reference column", () => {
|
|
470
|
+
const uow = createTestUOW();
|
|
471
|
+
const userId = FragnoId.fromExternal("user_ext_789", 0);
|
|
472
|
+
// Create a post with userId as a FragnoId object
|
|
473
|
+
uow.create("posts", {
|
|
474
|
+
title: "Post with FragnoId",
|
|
475
|
+
content: "Content",
|
|
476
|
+
userId,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
480
|
+
const compiled = uow.compile(compiler);
|
|
481
|
+
const [batch] = compiled.mutationBatch;
|
|
482
|
+
assert(batch);
|
|
483
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
484
|
+
// FragnoId should generate a subquery to lookup the internal ID from external ID
|
|
485
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
486
|
+
`"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values (?, ?, ?, (select "_internalId" from "users" where "id" = ? limit 1), ?, null, ?)"`,
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should compile update operation with external id string for reference column", () => {
|
|
491
|
+
const uow = createTestUOW();
|
|
492
|
+
const postId = FragnoId.fromExternal("post123", 0);
|
|
493
|
+
uow.update("posts", postId, (b) =>
|
|
494
|
+
b.set({
|
|
495
|
+
userId: "new_user_external_id_456",
|
|
496
|
+
}),
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
500
|
+
const compiled = uow.compile(compiler);
|
|
501
|
+
const [batch] = compiled.mutationBatch;
|
|
502
|
+
assert(batch);
|
|
503
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
504
|
+
// Should generate a subquery for the string external ID in UPDATE
|
|
505
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
506
|
+
`"update "posts" set "userId" = (select "_internalId" from "users" where "id" = ? limit 1), "_version" = COALESCE(_version, 0) + 1 where "posts"."id" = ?"`,
|
|
507
|
+
);
|
|
508
|
+
expect(batch.query.params).toMatchObject([
|
|
509
|
+
"new_user_external_id_456", // external id string
|
|
510
|
+
"post123", // post external id
|
|
511
|
+
]);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("should compile update operation with bigint for reference column (no subquery)", () => {
|
|
515
|
+
const uow = createTestUOW();
|
|
516
|
+
const postId = FragnoId.fromExternal("post456", 0);
|
|
517
|
+
uow.update("posts", postId, (b) =>
|
|
518
|
+
b.set({
|
|
519
|
+
userId: 99999n,
|
|
520
|
+
}),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
524
|
+
const compiled = uow.compile(compiler);
|
|
525
|
+
const [batch] = compiled.mutationBatch;
|
|
526
|
+
assert(batch);
|
|
527
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
528
|
+
// Should NOT have a subquery when using bigint directly
|
|
529
|
+
expect(batch.query.sql).not.toMatch(/\(select.*from.*users/i);
|
|
530
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
531
|
+
`"update "posts" set "userId" = ?, "_version" = COALESCE(_version, 0) + 1 where "posts"."id" = ?"`,
|
|
532
|
+
);
|
|
533
|
+
expect(batch.query.params).toMatchObject([
|
|
534
|
+
99999n, // bigint stays as bigint for Drizzle (Drizzle handles conversion)
|
|
535
|
+
"post456", // post external id
|
|
536
|
+
]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("should compile update operation with ID", () => {
|
|
540
|
+
const uow = createTestUOW();
|
|
541
|
+
const userId = FragnoId.fromExternal("user123", 0);
|
|
542
|
+
uow.update("users", userId, (b) =>
|
|
543
|
+
b.set({
|
|
544
|
+
name: "Jane Doe",
|
|
545
|
+
age: 25,
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
550
|
+
const compiled = uow.compile(compiler);
|
|
551
|
+
const [batch] = compiled.mutationBatch;
|
|
552
|
+
assert(batch);
|
|
553
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
554
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
555
|
+
`"update "users" set "name" = ?, "age" = ?, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = ?"`,
|
|
556
|
+
);
|
|
557
|
+
expect(batch.query.params).toMatchObject(["Jane Doe", 25, "user123"]);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("should compile update operation with version check", () => {
|
|
561
|
+
const uow = createTestUOW();
|
|
562
|
+
const userId = FragnoId.fromExternal("user123", 5);
|
|
563
|
+
uow.update("users", userId, (b) => b.set({ age: 18 }).check());
|
|
564
|
+
|
|
565
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
566
|
+
const compiled = uow.compile(compiler);
|
|
567
|
+
const [batch] = compiled.mutationBatch;
|
|
568
|
+
assert(batch);
|
|
569
|
+
expect(batch.expectedAffectedRows).toBe(1);
|
|
570
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
571
|
+
`"update "users" set "age" = ?, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
572
|
+
);
|
|
573
|
+
expect(batch.query.params).toMatchObject([18, "user123", 5]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("should compile delete operation with ID", () => {
|
|
577
|
+
const uow = createTestUOW();
|
|
578
|
+
const userId = FragnoId.fromExternal("user123", 0);
|
|
579
|
+
uow.delete("users", userId);
|
|
580
|
+
|
|
581
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
582
|
+
const compiled = uow.compile(compiler);
|
|
583
|
+
const [batch] = compiled.mutationBatch;
|
|
584
|
+
assert(batch);
|
|
585
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
586
|
+
expect(batch.query.sql).toMatchInlineSnapshot(`"delete from "users" where "users"."id" = ?"`);
|
|
587
|
+
expect(batch.query.params).toMatchObject(["user123"]);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("should compile delete operation with version check", () => {
|
|
591
|
+
const uow = createTestUOW();
|
|
592
|
+
const userId = FragnoId.fromExternal("user123", 3);
|
|
593
|
+
uow.delete("users", userId, (b) => b.check());
|
|
594
|
+
|
|
595
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
596
|
+
const compiled = uow.compile(compiler);
|
|
597
|
+
const [batch] = compiled.mutationBatch;
|
|
598
|
+
assert(batch);
|
|
599
|
+
expect(batch.expectedAffectedRows).toBe(1);
|
|
600
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
601
|
+
`"delete from "users" where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
602
|
+
);
|
|
603
|
+
expect(batch.query.params).toMatchObject(["user123", 3]);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("should compile update operation with string ID", () => {
|
|
607
|
+
const uow = createTestUOW();
|
|
608
|
+
uow.update("users", "user123", (b) =>
|
|
609
|
+
b.set({
|
|
610
|
+
name: "Jane Doe",
|
|
611
|
+
age: 25,
|
|
612
|
+
}),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
616
|
+
const compiled = uow.compile(compiler);
|
|
617
|
+
const [batch] = compiled.mutationBatch;
|
|
618
|
+
assert(batch);
|
|
619
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
620
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
621
|
+
`"update "users" set "name" = ?, "age" = ?, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = ?"`,
|
|
622
|
+
);
|
|
623
|
+
expect(batch.query.params).toMatchObject(["Jane Doe", 25, "user123"]);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("should compile delete operation with string ID", () => {
|
|
627
|
+
const uow = createTestUOW();
|
|
628
|
+
uow.delete("users", "user123");
|
|
629
|
+
|
|
630
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
631
|
+
const compiled = uow.compile(compiler);
|
|
632
|
+
const [batch] = compiled.mutationBatch;
|
|
633
|
+
assert(batch);
|
|
634
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
635
|
+
expect(batch.query.sql).toMatchInlineSnapshot(`"delete from "users" where "users"."id" = ?"`);
|
|
636
|
+
expect(batch.query.params).toMatchObject(["user123"]);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("should throw when trying to check() with string ID on update", () => {
|
|
640
|
+
const uow = createTestUOW();
|
|
641
|
+
expect(() => {
|
|
642
|
+
uow.update("users", "user123", (b) => b.set({ name: "Jane" }).check());
|
|
643
|
+
}).toThrow(
|
|
644
|
+
'Cannot use check() with a string ID on table "users". Version checking requires a FragnoId with version information.',
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("should throw when trying to check() with string ID on delete", () => {
|
|
649
|
+
const uow = createTestUOW();
|
|
650
|
+
expect(() => {
|
|
651
|
+
uow.delete("users", "user123", (b) => b.check());
|
|
652
|
+
}).toThrow(
|
|
653
|
+
'Cannot use check() with a string ID on table "users". Version checking requires a FragnoId with version information.',
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("should compile multiple mutation operations", () => {
|
|
658
|
+
const uow = createTestUOW();
|
|
659
|
+
uow.create("users", {
|
|
660
|
+
name: "Alice",
|
|
661
|
+
email: "alice@example.com",
|
|
662
|
+
});
|
|
663
|
+
const postId = FragnoId.fromExternal("post123", 0);
|
|
664
|
+
uow.update("posts", postId, (b) => b.set({ viewCount: 10 }));
|
|
665
|
+
const userId = FragnoId.fromExternal("user456", 0);
|
|
666
|
+
uow.delete("posts", userId);
|
|
667
|
+
|
|
668
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
669
|
+
const compiled = uow.compile(compiler);
|
|
670
|
+
const [createBatch, updateBatch, deleteBatch] = compiled.mutationBatch;
|
|
671
|
+
|
|
672
|
+
expect(compiled.mutationBatch).toHaveLength(3);
|
|
673
|
+
|
|
674
|
+
assert(createBatch);
|
|
675
|
+
expect(createBatch.query.sql).toMatchInlineSnapshot(
|
|
676
|
+
`"insert into "users" ("id", "name", "email", "age", "_internalId", "_version") values (?, ?, ?, null, null, ?)"`,
|
|
677
|
+
);
|
|
678
|
+
expect(createBatch.query.params).toMatchObject([
|
|
679
|
+
expect.any(String), // auto-generated ID
|
|
680
|
+
"Alice",
|
|
681
|
+
"alice@example.com",
|
|
682
|
+
0, // _version default
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
assert(updateBatch);
|
|
686
|
+
expect(updateBatch.query.sql).toMatchInlineSnapshot(
|
|
687
|
+
`"update "posts" set "viewCount" = ?, "_version" = COALESCE(_version, 0) + 1 where "posts"."id" = ?"`,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
assert(deleteBatch);
|
|
691
|
+
expect(deleteBatch.query.sql).toMatchInlineSnapshot(
|
|
692
|
+
`"delete from "posts" where "posts"."id" = ?"`,
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("complete UOW workflow", () => {
|
|
698
|
+
it("should compile retrieval and mutation phases together", () => {
|
|
699
|
+
const uow = createTestUOW("update-user-balance");
|
|
700
|
+
|
|
701
|
+
// Retrieval phase
|
|
702
|
+
uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", "user123")));
|
|
703
|
+
|
|
704
|
+
// Mutation phase
|
|
705
|
+
const userId = FragnoId.fromExternal("user123", 3);
|
|
706
|
+
uow.update("users", userId, (b) => b.set({ age: 31 }).check());
|
|
707
|
+
|
|
708
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
709
|
+
const compiled = uow.compile(compiler);
|
|
710
|
+
|
|
711
|
+
expect(compiled.name).toBe("update-user-balance");
|
|
712
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
713
|
+
expect(compiled.mutationBatch).toHaveLength(1);
|
|
714
|
+
|
|
715
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
716
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where "users"."id" = ?"`,
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
// Update should include version check in WHERE clause
|
|
720
|
+
const [batch] = compiled.mutationBatch;
|
|
721
|
+
assert(batch);
|
|
722
|
+
expect(batch.expectedAffectedRows).toBe(1);
|
|
723
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
724
|
+
`"update "users" set "age" = ?, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("should handle complex where conditions", () => {
|
|
729
|
+
const uow = createTestUOW();
|
|
730
|
+
uow.find("users", (b) =>
|
|
731
|
+
b.whereIndex("idx_email", (eb) =>
|
|
732
|
+
eb.and(
|
|
733
|
+
eb("email", "contains", "@example.com"),
|
|
734
|
+
// @ts-expect-error - name is not indexed
|
|
735
|
+
eb.or(eb("name", "=", "Alice"), eb("name", "=", "Bob")),
|
|
736
|
+
),
|
|
737
|
+
),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
741
|
+
const compiled = uow.compile(compiler);
|
|
742
|
+
|
|
743
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
744
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
745
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users" where ("users"."email" like ? and ("users"."name" = ? or "users"."name" = ?))"`,
|
|
746
|
+
);
|
|
747
|
+
expect(compiled.retrievalBatch[0].params).toEqual(["%@example.com%", "Alice", "Bob"]);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("should return null for operations with always-false conditions", () => {
|
|
751
|
+
const uow = createTestUOW();
|
|
752
|
+
uow.find("users", (b) => b.whereIndex("primary", () => false));
|
|
753
|
+
|
|
754
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
755
|
+
const compiled = uow.compile(compiler);
|
|
756
|
+
|
|
757
|
+
// When condition is false, the operation should return null and not be added to batch
|
|
758
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("should handle always-true conditions", () => {
|
|
762
|
+
const uow = createTestUOW();
|
|
763
|
+
uow.find("users", (b) => b.whereIndex("primary", () => true));
|
|
764
|
+
|
|
765
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
766
|
+
const compiled = uow.compile(compiler);
|
|
767
|
+
|
|
768
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
769
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
770
|
+
`"select "id", "name", "email", "age", "_internalId", "_version" from "users" "users""`,
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
describe("version checking", () => {
|
|
776
|
+
it("should embed version check in update WHERE clause", () => {
|
|
777
|
+
const uow = createTestUOW();
|
|
778
|
+
|
|
779
|
+
const userId = FragnoId.fromExternal("user123", 5);
|
|
780
|
+
uow.update("users", userId, (b) => b.set({ age: 31 }).check());
|
|
781
|
+
|
|
782
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
783
|
+
const compiled = uow.compile(compiler);
|
|
784
|
+
const [batch] = compiled.mutationBatch;
|
|
785
|
+
assert(batch);
|
|
786
|
+
expect(batch.expectedAffectedRows).toBe(1);
|
|
787
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
788
|
+
`"update "users" set "age" = ?, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
789
|
+
);
|
|
790
|
+
expect(batch.query.params).toMatchObject([31, "user123", 5]);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("should embed version check in delete WHERE clause", () => {
|
|
794
|
+
const uow = createTestUOW();
|
|
795
|
+
|
|
796
|
+
const userId = FragnoId.fromExternal("user456", 3);
|
|
797
|
+
uow.delete("users", userId, (b) => b.check());
|
|
798
|
+
|
|
799
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
800
|
+
const compiled = uow.compile(compiler);
|
|
801
|
+
const [batch] = compiled.mutationBatch;
|
|
802
|
+
assert(batch);
|
|
803
|
+
expect(batch.expectedAffectedRows).toBe(1);
|
|
804
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
805
|
+
`"delete from "users" where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
806
|
+
);
|
|
807
|
+
expect(batch.query.params).toMatchObject(["user456", 3]);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("should handle version checks on different tables", () => {
|
|
811
|
+
const uow = createTestUOW();
|
|
812
|
+
|
|
813
|
+
const userId = FragnoId.fromExternal("user1", 2);
|
|
814
|
+
const postId = FragnoId.fromExternal("post1", 1);
|
|
815
|
+
|
|
816
|
+
uow.update("users", userId, (b) => b.set({ age: 30 }).check());
|
|
817
|
+
uow.update("posts", postId, (b) => b.set({ viewCount: 100 }).check());
|
|
818
|
+
|
|
819
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
820
|
+
const compiled = uow.compile(compiler);
|
|
821
|
+
const [userBatch, postBatch] = compiled.mutationBatch;
|
|
822
|
+
|
|
823
|
+
expect(compiled.mutationBatch).toHaveLength(2);
|
|
824
|
+
|
|
825
|
+
assert(userBatch);
|
|
826
|
+
expect(userBatch.expectedAffectedRows).toBe(1);
|
|
827
|
+
expect(userBatch.query.sql).toMatchInlineSnapshot(
|
|
828
|
+
`"update "users" set "age" = ?, "_version" = COALESCE(_version, 0) + 1 where ("users"."id" = ? and "users"."_version" = ?)"`,
|
|
829
|
+
);
|
|
830
|
+
expect(userBatch.query.params).toMatchObject([30, "user1", 2]);
|
|
831
|
+
|
|
832
|
+
assert(postBatch);
|
|
833
|
+
expect(postBatch.expectedAffectedRows).toBe(1);
|
|
834
|
+
expect(postBatch.query.sql).toMatchInlineSnapshot(
|
|
835
|
+
`"update "posts" set "viewCount" = ?, "_version" = COALESCE(_version, 0) + 1 where ("posts"."id" = ? and "posts"."_version" = ?)"`,
|
|
836
|
+
);
|
|
837
|
+
expect(postBatch.query.params).toMatchObject([100, "post1", 1]);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("should not affect updates without version checks", () => {
|
|
841
|
+
const uow = createTestUOW();
|
|
842
|
+
|
|
843
|
+
const userId = FragnoId.fromExternal("user1", 0);
|
|
844
|
+
uow.update("users", userId, (b) => b.set({ age: 25 }));
|
|
845
|
+
|
|
846
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
847
|
+
const compiled = uow.compile(compiler);
|
|
848
|
+
const [batch] = compiled.mutationBatch;
|
|
849
|
+
assert(batch);
|
|
850
|
+
expect(batch.expectedAffectedRows).toBeNull();
|
|
851
|
+
// Should be normal update without version check
|
|
852
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
853
|
+
`"update "users" set "age" = ?, "_version" = COALESCE(_version, 0) + 1 where "users"."id" = ?"`,
|
|
854
|
+
);
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
describe("edge cases", () => {
|
|
859
|
+
it("should handle UOW with no operations", () => {
|
|
860
|
+
const uow = createTestUOW();
|
|
861
|
+
|
|
862
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
863
|
+
const compiled = uow.compile(compiler);
|
|
864
|
+
|
|
865
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
866
|
+
expect(compiled.mutationBatch).toHaveLength(0);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("should handle UOW with only retrieval operations", () => {
|
|
870
|
+
const uow = createTestUOW();
|
|
871
|
+
uow.find("users");
|
|
872
|
+
|
|
873
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
874
|
+
const compiled = uow.compile(compiler);
|
|
875
|
+
|
|
876
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
877
|
+
expect(compiled.mutationBatch).toHaveLength(0);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("should handle UOW with only mutation operations", () => {
|
|
881
|
+
const uow = createTestUOW();
|
|
882
|
+
uow.create("users", {
|
|
883
|
+
name: "Test User",
|
|
884
|
+
email: "test@example.com",
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
888
|
+
const compiled = uow.compile(compiler);
|
|
889
|
+
|
|
890
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
891
|
+
expect(compiled.mutationBatch).toHaveLength(1);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
describe("default value generation", () => {
|
|
896
|
+
// Create a schema with columns that have different types of defaults
|
|
897
|
+
const defaultsSchema = schema((s) => {
|
|
898
|
+
return s.addTable("logs", (t) => {
|
|
899
|
+
return t
|
|
900
|
+
.addColumn("id", idColumn())
|
|
901
|
+
.addColumn("message", column("string"))
|
|
902
|
+
.addColumn(
|
|
903
|
+
"sessionId",
|
|
904
|
+
column("string").defaultTo$((b) => b.cuid()),
|
|
905
|
+
) // runtime cuid
|
|
906
|
+
.addColumn(
|
|
907
|
+
"timestamp",
|
|
908
|
+
column("timestamp").defaultTo$((b) => b.now()),
|
|
909
|
+
) // runtime now
|
|
910
|
+
.addColumn("counter", column("integer").defaultTo$(42)) // runtime function
|
|
911
|
+
.addColumn("status", column("string").defaultTo("pending")) // static default
|
|
912
|
+
.createIndex("idx_session", ["sessionId"]);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
let defaultsDb: DBType;
|
|
917
|
+
let defaultsPool: ConnectionPool<DBType>;
|
|
918
|
+
|
|
919
|
+
beforeAll(async () => {
|
|
920
|
+
// Write schema to file and dynamically import it
|
|
921
|
+
const { schemaModule, cleanup } = await writeAndLoadSchema(
|
|
922
|
+
"drizzle-uow-compiler-defaults",
|
|
923
|
+
defaultsSchema,
|
|
924
|
+
"sqlite",
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
// Create Drizzle instance with libsql (in-memory SQLite)
|
|
928
|
+
const defaultsClient = createClient({
|
|
929
|
+
url: ":memory:",
|
|
930
|
+
});
|
|
931
|
+
defaultsDb = drizzle({ client: defaultsClient, schema: schemaModule }) as unknown as DBType;
|
|
932
|
+
|
|
933
|
+
// Wrap in connection pool
|
|
934
|
+
defaultsPool = createDrizzleConnectionPool(defaultsDb);
|
|
935
|
+
|
|
936
|
+
return async () => {
|
|
937
|
+
await cleanup();
|
|
938
|
+
};
|
|
939
|
+
}, 12000);
|
|
940
|
+
|
|
941
|
+
it("should generate runtime defaults for missing columns", () => {
|
|
942
|
+
const uow = createTestUOWWithSchema(defaultsSchema);
|
|
943
|
+
// Only provide message, all other columns should get defaults
|
|
944
|
+
uow.create("logs", {
|
|
945
|
+
message: "Test log",
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
const compiler = createDrizzleUOWCompiler(defaultsPool, "sqlite");
|
|
949
|
+
const compiled = uow.compile(compiler);
|
|
950
|
+
const [batch] = compiled.mutationBatch;
|
|
951
|
+
assert(batch);
|
|
952
|
+
|
|
953
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
954
|
+
`"insert into "logs" ("id", "message", "sessionId", "timestamp", "counter", "status", "_internalId", "_version") values (?, ?, ?, ?, ?, ?, null, ?)"`,
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
expect(batch.query.params).toMatchObject([
|
|
958
|
+
expect.any(String), // auto-generated ID
|
|
959
|
+
"Test log",
|
|
960
|
+
expect.any(String), // auto-generated sessionId
|
|
961
|
+
expect.any(Number), // auto-generated timestamp (Drizzle serializes Date to Unix timestamp)
|
|
962
|
+
42, // function-generated counter
|
|
963
|
+
"pending", // status default
|
|
964
|
+
0, // _version default
|
|
965
|
+
]);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("should not override user-provided values with defaults", () => {
|
|
969
|
+
const uow = createTestUOWWithSchema(defaultsSchema);
|
|
970
|
+
const customTimestamp = new Date("2024-01-01");
|
|
971
|
+
// Provide all values explicitly
|
|
972
|
+
uow.create("logs", {
|
|
973
|
+
message: "Test log",
|
|
974
|
+
sessionId: "custom-session-id",
|
|
975
|
+
timestamp: customTimestamp,
|
|
976
|
+
counter: 100,
|
|
977
|
+
status: "active",
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
const compiler = createDrizzleUOWCompiler(defaultsPool, "sqlite");
|
|
981
|
+
const compiled = uow.compile(compiler);
|
|
982
|
+
const [batch] = compiled.mutationBatch;
|
|
983
|
+
assert(batch);
|
|
984
|
+
|
|
985
|
+
// All user-provided values should be used, not defaults
|
|
986
|
+
const params = batch.query.params;
|
|
987
|
+
expect(params[1]).toBe("Test log");
|
|
988
|
+
expect(params[2]).toBe("custom-session-id");
|
|
989
|
+
expect(params[3]).toBe(customTimestamp.getTime() / 1000); // Drizzle converts Date to Unix timestamp (seconds)
|
|
990
|
+
expect(params[4]).toBe(100);
|
|
991
|
+
expect(params[5]).toBe("active");
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("should handle mix of provided and default values", () => {
|
|
995
|
+
const uow = createTestUOWWithSchema(defaultsSchema);
|
|
996
|
+
// Provide some values, let others use defaults
|
|
997
|
+
uow.create("logs", {
|
|
998
|
+
message: "Partial log",
|
|
999
|
+
counter: 999, // override the function default
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const compiler = createDrizzleUOWCompiler(defaultsPool, "sqlite");
|
|
1003
|
+
const compiled = uow.compile(compiler);
|
|
1004
|
+
const [batch] = compiled.mutationBatch;
|
|
1005
|
+
assert(batch);
|
|
1006
|
+
|
|
1007
|
+
expect(batch.query.params).toMatchObject([
|
|
1008
|
+
expect.any(String), // auto-generated ID
|
|
1009
|
+
"Partial log",
|
|
1010
|
+
expect.any(String), // auto-generated sessionId
|
|
1011
|
+
expect.any(Number), // auto-generated timestamp (Drizzle converts Date to Unix timestamp)
|
|
1012
|
+
999, // user-provided counter
|
|
1013
|
+
expect.any(String), // status default
|
|
1014
|
+
0, // _version default
|
|
1015
|
+
]);
|
|
1016
|
+
// All defaults are included in the INSERT for SQLite
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("should generate unique values for auto defaults across multiple creates", () => {
|
|
1020
|
+
const uow = createTestUOWWithSchema(defaultsSchema);
|
|
1021
|
+
uow.create("logs", { message: "Log 1" });
|
|
1022
|
+
uow.create("logs", { message: "Log 2" });
|
|
1023
|
+
uow.create("logs", { message: "Log 3" });
|
|
1024
|
+
|
|
1025
|
+
const compiler = createDrizzleUOWCompiler(defaultsPool, "sqlite");
|
|
1026
|
+
const compiled = uow.compile(compiler);
|
|
1027
|
+
|
|
1028
|
+
expect(compiled.mutationBatch).toHaveLength(3);
|
|
1029
|
+
|
|
1030
|
+
// Extract sessionId from each create
|
|
1031
|
+
const sessionIds = compiled.mutationBatch.map((batch) => {
|
|
1032
|
+
assert(batch);
|
|
1033
|
+
return batch.query.params[2]; // sessionId is 3rd param (after id, message)
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// All sessionIds should be unique
|
|
1037
|
+
const uniqueSessionIds = new Set(sessionIds);
|
|
1038
|
+
expect(uniqueSessionIds.size).toBe(3);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
describe("nested joins", () => {
|
|
1043
|
+
// Create a schema that supports nested joins
|
|
1044
|
+
const nestedSchema = schema((s) => {
|
|
1045
|
+
return s
|
|
1046
|
+
.addTable("users", (t) => {
|
|
1047
|
+
return t
|
|
1048
|
+
.addColumn("id", idColumn())
|
|
1049
|
+
.addColumn("name", column("string"))
|
|
1050
|
+
.addColumn("email", column("string"))
|
|
1051
|
+
.createIndex("idx_name", ["name"]);
|
|
1052
|
+
})
|
|
1053
|
+
.addTable("posts", (t) => {
|
|
1054
|
+
return t
|
|
1055
|
+
.addColumn("id", idColumn())
|
|
1056
|
+
.addColumn("title", column("string"))
|
|
1057
|
+
.addColumn("userId", referenceColumn())
|
|
1058
|
+
.createIndex("idx_user", ["userId"]);
|
|
1059
|
+
})
|
|
1060
|
+
.addTable("comments", (t) => {
|
|
1061
|
+
return t
|
|
1062
|
+
.addColumn("id", idColumn())
|
|
1063
|
+
.addColumn("text", column("string"))
|
|
1064
|
+
.addColumn("postId", referenceColumn())
|
|
1065
|
+
.createIndex("idx_post", ["postId"]);
|
|
1066
|
+
})
|
|
1067
|
+
.addReference("author", {
|
|
1068
|
+
type: "one",
|
|
1069
|
+
from: { table: "posts", column: "userId" },
|
|
1070
|
+
to: { table: "users", column: "id" },
|
|
1071
|
+
})
|
|
1072
|
+
.addReference("post", {
|
|
1073
|
+
type: "one",
|
|
1074
|
+
from: { table: "comments", column: "postId" },
|
|
1075
|
+
to: { table: "posts", column: "id" },
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
let nestedDb: DBType;
|
|
1080
|
+
let nestedPool: ConnectionPool<DBType>;
|
|
1081
|
+
|
|
1082
|
+
beforeAll(async () => {
|
|
1083
|
+
// Write schema to file and dynamically import it
|
|
1084
|
+
const { schemaModule, cleanup } = await writeAndLoadSchema(
|
|
1085
|
+
"drizzle-uow-compiler-nested",
|
|
1086
|
+
nestedSchema,
|
|
1087
|
+
"sqlite",
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// Create Drizzle instance with libsql (in-memory SQLite)
|
|
1091
|
+
const nestedClient = createClient({
|
|
1092
|
+
url: ":memory:",
|
|
1093
|
+
});
|
|
1094
|
+
nestedDb = drizzle({ client: nestedClient, schema: schemaModule }) as unknown as DBType;
|
|
1095
|
+
|
|
1096
|
+
// Wrap in connection pool
|
|
1097
|
+
nestedPool = createDrizzleConnectionPool(nestedDb);
|
|
1098
|
+
|
|
1099
|
+
return async () => {
|
|
1100
|
+
await cleanup();
|
|
1101
|
+
};
|
|
1102
|
+
}, 20000);
|
|
1103
|
+
|
|
1104
|
+
function createNestedUOW(name?: string) {
|
|
1105
|
+
const compiler = createDrizzleUOWCompiler(nestedPool, "sqlite");
|
|
1106
|
+
const mockExecutor = {
|
|
1107
|
+
executeRetrievalPhase: async () => [],
|
|
1108
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1109
|
+
};
|
|
1110
|
+
const mockDecoder: UOWDecoder<typeof nestedSchema> = (rawResults, operations) => {
|
|
1111
|
+
if (rawResults.length !== operations.length) {
|
|
1112
|
+
throw new Error("rawResults and ops must have the same length");
|
|
1113
|
+
}
|
|
1114
|
+
return rawResults;
|
|
1115
|
+
};
|
|
1116
|
+
return new UnitOfWork(compiler, mockExecutor, mockDecoder, name).forSchema(nestedSchema);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
it("should compile nested joins (comments -> post -> author)", () => {
|
|
1120
|
+
const uow = createNestedUOW();
|
|
1121
|
+
uow.find("comments", (b) =>
|
|
1122
|
+
b
|
|
1123
|
+
.whereIndex("primary")
|
|
1124
|
+
.join((jb) =>
|
|
1125
|
+
jb.post((postBuilder) =>
|
|
1126
|
+
postBuilder
|
|
1127
|
+
.select(["title"])
|
|
1128
|
+
.join((jb2) => jb2.author((authorBuilder) => authorBuilder.select(["name"]))),
|
|
1129
|
+
),
|
|
1130
|
+
),
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
const compiler = createDrizzleUOWCompiler(nestedPool, "sqlite");
|
|
1134
|
+
const compiled = uow.compile(compiler);
|
|
1135
|
+
|
|
1136
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
1137
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
1138
|
+
|
|
1139
|
+
// Should contain nested lateral joins
|
|
1140
|
+
|
|
1141
|
+
// Should join comments -> post
|
|
1142
|
+
|
|
1143
|
+
// Should join post -> author within the first join
|
|
1144
|
+
|
|
1145
|
+
// Should have the nested structure with proper lateral joins
|
|
1146
|
+
expect(sql).toMatchInlineSnapshot(
|
|
1147
|
+
`"select "id", "text", "postId", "_internalId", "_version", (select json_array("title", "_internalId", "_version", (select json_array("name", "_internalId", "_version") as "data" from (select * from "users" "comments_post_author" where "comments_post_author"."_internalId" = "comments_post"."userId" limit ?) "comments_post_author")) as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit ?) "comments_post") as "post" from "comments" "comments""`,
|
|
1148
|
+
);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it("should compile nested joins with filtering at each level", () => {
|
|
1152
|
+
const uow = createNestedUOW();
|
|
1153
|
+
uow.find("comments", (b) =>
|
|
1154
|
+
b
|
|
1155
|
+
.whereIndex("primary")
|
|
1156
|
+
.join((jb) =>
|
|
1157
|
+
jb.post((postBuilder) =>
|
|
1158
|
+
postBuilder
|
|
1159
|
+
.select(["title"])
|
|
1160
|
+
.join((jb2) =>
|
|
1161
|
+
jb2.author((authorBuilder) =>
|
|
1162
|
+
authorBuilder
|
|
1163
|
+
.whereIndex("idx_name", (eb) => eb("name", "=", "Alice"))
|
|
1164
|
+
.select(["name"]),
|
|
1165
|
+
),
|
|
1166
|
+
),
|
|
1167
|
+
),
|
|
1168
|
+
),
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
const compiler = createDrizzleUOWCompiler(nestedPool, "sqlite");
|
|
1172
|
+
const compiled = uow.compile(compiler);
|
|
1173
|
+
|
|
1174
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
1175
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
1176
|
+
|
|
1177
|
+
// Should have WHERE clause in the nested author join
|
|
1178
|
+
|
|
1179
|
+
expect(sql).toMatchInlineSnapshot(
|
|
1180
|
+
`"select "id", "text", "postId", "_internalId", "_version", (select json_array("title", "_internalId", "_version", (select json_array("name", "_internalId", "_version") as "data" from (select * from "users" "comments_post_author" where ("comments_post_author"."_internalId" = "comments_post"."userId" and "comments_post_author"."name" = ?) limit ?) "comments_post_author")) as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit ?) "comments_post") as "post" from "comments" "comments""`,
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it("should compile nested joins with ordering and limits at each level", () => {
|
|
1185
|
+
const uow = createNestedUOW();
|
|
1186
|
+
uow.find("comments", (b) =>
|
|
1187
|
+
b
|
|
1188
|
+
.whereIndex("primary")
|
|
1189
|
+
.pageSize(10)
|
|
1190
|
+
.join((jb) =>
|
|
1191
|
+
jb.post((postBuilder) =>
|
|
1192
|
+
postBuilder
|
|
1193
|
+
.select(["title"])
|
|
1194
|
+
.pageSize(1)
|
|
1195
|
+
.join((jb2) =>
|
|
1196
|
+
jb2.author((authorBuilder) =>
|
|
1197
|
+
authorBuilder.orderByIndex("idx_name", "asc").pageSize(1),
|
|
1198
|
+
),
|
|
1199
|
+
),
|
|
1200
|
+
),
|
|
1201
|
+
),
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
const compiler = createDrizzleUOWCompiler(nestedPool, "sqlite");
|
|
1205
|
+
const compiled = uow.compile(compiler);
|
|
1206
|
+
|
|
1207
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
1208
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
1209
|
+
|
|
1210
|
+
// Should have limits at all levels and ordering in nested author join
|
|
1211
|
+
|
|
1212
|
+
expect(sql).toMatchInlineSnapshot(
|
|
1213
|
+
`"select "id", "text", "postId", "_internalId", "_version", (select json_array("title", "_internalId", "_version", (select json_array("id", "name", "email", "_internalId", "_version") as "data" from (select * from "users" "comments_post_author" where "comments_post_author"."_internalId" = "comments_post"."userId" order by "comments_post_author"."name" asc limit ?) "comments_post_author")) as "data" from (select * from "posts" "comments_post" where "comments_post"."_internalId" = "comments"."postId" limit ?) "comments_post") as "post" from "comments" "comments" limit ?"`,
|
|
1214
|
+
);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it("should compile multiple nested joins from same table", () => {
|
|
1218
|
+
const uow = createNestedUOW();
|
|
1219
|
+
uow.find("posts", (b) =>
|
|
1220
|
+
b.whereIndex("primary").join((jb) =>
|
|
1221
|
+
// Join to author with nested structure
|
|
1222
|
+
jb.author((authorBuilder) => authorBuilder.select(["name", "email"])),
|
|
1223
|
+
),
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
const compiler = createDrizzleUOWCompiler(nestedPool, "sqlite");
|
|
1227
|
+
const compiled = uow.compile(compiler);
|
|
1228
|
+
|
|
1229
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
1230
|
+
const sql = compiled.retrievalBatch[0].sql;
|
|
1231
|
+
expect(sql).toMatchInlineSnapshot(
|
|
1232
|
+
`"select "id", "title", "userId", "_internalId", "_version", (select json_array("name", "email", "_internalId", "_version") as "data" from (select * from "users" "posts_author" where "posts_author"."_internalId" = "posts"."userId" limit ?) "posts_author") as "author" from "posts" "posts""`,
|
|
1233
|
+
);
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
describe("auth schema with session joins", () => {
|
|
1238
|
+
const authSchema = schema((s) => {
|
|
1239
|
+
return s
|
|
1240
|
+
.addTable("user", (t) => {
|
|
1241
|
+
return t
|
|
1242
|
+
.addColumn("id", idColumn())
|
|
1243
|
+
.addColumn("email", column("string"))
|
|
1244
|
+
.addColumn("passwordHash", column("string"))
|
|
1245
|
+
.addColumn(
|
|
1246
|
+
"createdAt",
|
|
1247
|
+
column("timestamp").defaultTo$((b) => b.now()),
|
|
1248
|
+
)
|
|
1249
|
+
.createIndex("idx_user_email", ["email"]);
|
|
1250
|
+
})
|
|
1251
|
+
.addTable("session", (t) => {
|
|
1252
|
+
return t
|
|
1253
|
+
.addColumn("id", idColumn())
|
|
1254
|
+
.addColumn("userId", referenceColumn())
|
|
1255
|
+
.addColumn("expiresAt", column("timestamp"))
|
|
1256
|
+
.addColumn(
|
|
1257
|
+
"createdAt",
|
|
1258
|
+
column("timestamp").defaultTo$((b) => b.now()),
|
|
1259
|
+
)
|
|
1260
|
+
.createIndex("idx_session_user", ["userId"]);
|
|
1261
|
+
})
|
|
1262
|
+
.addReference("sessionOwner", {
|
|
1263
|
+
from: {
|
|
1264
|
+
table: "session",
|
|
1265
|
+
column: "userId",
|
|
1266
|
+
},
|
|
1267
|
+
to: {
|
|
1268
|
+
table: "user",
|
|
1269
|
+
column: "id",
|
|
1270
|
+
},
|
|
1271
|
+
type: "one",
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
let authDb: DBType;
|
|
1276
|
+
let authPool: ConnectionPool<DBType>;
|
|
1277
|
+
|
|
1278
|
+
beforeAll(async () => {
|
|
1279
|
+
// Write schema to file and dynamically import it
|
|
1280
|
+
const { schemaModule, cleanup } = await writeAndLoadSchema(
|
|
1281
|
+
"drizzle-uow-compiler-auth",
|
|
1282
|
+
authSchema,
|
|
1283
|
+
"sqlite",
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
// Create Drizzle instance with libsql (in-memory SQLite)
|
|
1287
|
+
const authClient = createClient({
|
|
1288
|
+
url: ":memory:",
|
|
1289
|
+
});
|
|
1290
|
+
authDb = drizzle({ client: authClient, schema: schemaModule }) as unknown as DBType;
|
|
1291
|
+
|
|
1292
|
+
// Wrap in connection pool
|
|
1293
|
+
authPool = createDrizzleConnectionPool(authDb);
|
|
1294
|
+
|
|
1295
|
+
return async () => {
|
|
1296
|
+
await cleanup();
|
|
1297
|
+
};
|
|
1298
|
+
}, 12000);
|
|
1299
|
+
|
|
1300
|
+
function createAuthUOW(name?: string) {
|
|
1301
|
+
const compiler = createDrizzleUOWCompiler(authPool, "sqlite");
|
|
1302
|
+
const mockExecutor = {
|
|
1303
|
+
executeRetrievalPhase: async () => [],
|
|
1304
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
1305
|
+
};
|
|
1306
|
+
const mockDecoder: UOWDecoder = (rawResults, operations) => {
|
|
1307
|
+
if (rawResults.length !== operations.length) {
|
|
1308
|
+
throw new Error("rawResults and ops must have the same length");
|
|
1309
|
+
}
|
|
1310
|
+
return rawResults;
|
|
1311
|
+
};
|
|
1312
|
+
return new UnitOfWork(compiler, mockExecutor, mockDecoder, name).forSchema(authSchema);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
it("should compile find session with user join", () => {
|
|
1316
|
+
const uow = createAuthUOW();
|
|
1317
|
+
const sessionId = "session123";
|
|
1318
|
+
uow.find("session", (b) =>
|
|
1319
|
+
b
|
|
1320
|
+
.whereIndex("primary", (eb) => eb("id", "=", sessionId))
|
|
1321
|
+
.join((j) => j.sessionOwner((b) => b.select(["id", "email"]))),
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
const compiler = createDrizzleUOWCompiler(authPool, "sqlite");
|
|
1325
|
+
const compiled = uow.compile(compiler);
|
|
1326
|
+
|
|
1327
|
+
expect(compiled.retrievalBatch).toHaveLength(1);
|
|
1328
|
+
expect(compiled.retrievalBatch[0].sql).toMatchInlineSnapshot(
|
|
1329
|
+
`"select "id", "userId", "expiresAt", "createdAt", "_internalId", "_version", (select json_array("id", "email", "_internalId", "_version") as "data" from (select * from "user" "session_sessionOwner" where "session_sessionOwner"."_internalId" = "session"."userId" limit ?) "session_sessionOwner") as "sessionOwner" from "session" "session" where "session"."id" = ?"`,
|
|
1330
|
+
);
|
|
1331
|
+
expect(compiled.retrievalBatch[0].params).toEqual([1, sessionId]);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it("should support creating and using ID in same UOW", () => {
|
|
1335
|
+
const uow = createAuthUOW("create-user-and-session");
|
|
1336
|
+
|
|
1337
|
+
// Create user and capture the returned ID
|
|
1338
|
+
const userId = uow.create("user", {
|
|
1339
|
+
email: "test@example.com",
|
|
1340
|
+
passwordHash: "hashed_password",
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// Use the returned FragnoId directly to create a session
|
|
1344
|
+
// The compiler should extract externalId and generate a subquery
|
|
1345
|
+
uow.create("session", {
|
|
1346
|
+
userId: userId,
|
|
1347
|
+
expiresAt: new Date("2025-12-31"),
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
const compiler = createDrizzleUOWCompiler(authPool, "sqlite");
|
|
1351
|
+
const compiled = uow.compile(compiler);
|
|
1352
|
+
|
|
1353
|
+
// Should have no retrieval operations
|
|
1354
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
1355
|
+
|
|
1356
|
+
// Should have 2 mutation operations (create user, create session)
|
|
1357
|
+
expect(compiled.mutationBatch).toHaveLength(2);
|
|
1358
|
+
|
|
1359
|
+
const [userCreate, sessionCreate] = compiled.mutationBatch;
|
|
1360
|
+
assert(userCreate);
|
|
1361
|
+
assert(sessionCreate);
|
|
1362
|
+
|
|
1363
|
+
// Verify user create SQL
|
|
1364
|
+
expect(userCreate.query.sql).toMatchInlineSnapshot(
|
|
1365
|
+
`"insert into "user" ("id", "email", "passwordHash", "createdAt", "_internalId", "_version") values (?, ?, ?, ?, null, ?)"`,
|
|
1366
|
+
);
|
|
1367
|
+
expect(userCreate.query.params).toMatchObject([
|
|
1368
|
+
userId.externalId, // The generated ID
|
|
1369
|
+
"test@example.com",
|
|
1370
|
+
"hashed_password",
|
|
1371
|
+
expect.any(Number), // timestamp (Drizzle converts Date to Unix timestamp)
|
|
1372
|
+
0, // _version default
|
|
1373
|
+
]);
|
|
1374
|
+
expect(userCreate.expectedAffectedRows).toBeNull();
|
|
1375
|
+
|
|
1376
|
+
// Verify session create SQL - FragnoId generates subquery to lookup internal ID
|
|
1377
|
+
expect(sessionCreate.query.sql).toMatchInlineSnapshot(
|
|
1378
|
+
`"insert into "session" ("id", "userId", "expiresAt", "createdAt", "_internalId", "_version") values (?, (select "_internalId" from "user" where "id" = ? limit 1), ?, ?, null, ?)"`,
|
|
1379
|
+
);
|
|
1380
|
+
expect(sessionCreate.query.params).toMatchObject([
|
|
1381
|
+
expect.any(String), // generated session ID
|
|
1382
|
+
userId.externalId, // FragnoId's externalId is used in the subquery
|
|
1383
|
+
expect.any(Number), // expiresAt timestamp (Drizzle converts Date to Unix timestamp)
|
|
1384
|
+
expect.any(Number), // createdAt timestamp (Drizzle converts Date to Unix timestamp)
|
|
1385
|
+
0, // _version default
|
|
1386
|
+
]);
|
|
1387
|
+
expect(sessionCreate.expectedAffectedRows).toBeNull();
|
|
1388
|
+
|
|
1389
|
+
// Verify the returned FragnoId has the expected structure
|
|
1390
|
+
expect(userId).toMatchObject({
|
|
1391
|
+
externalId: expect.any(String),
|
|
1392
|
+
version: 0,
|
|
1393
|
+
internalId: undefined,
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it("should compile check operation", () => {
|
|
1398
|
+
const uow = createTestUOW();
|
|
1399
|
+
const userId = FragnoId.fromExternal("user123", 5);
|
|
1400
|
+
uow.check("users", userId);
|
|
1401
|
+
|
|
1402
|
+
const compiler = createDrizzleUOWCompiler(pool, "sqlite");
|
|
1403
|
+
const compiled = uow.compile(compiler);
|
|
1404
|
+
const [batch] = compiled.mutationBatch;
|
|
1405
|
+
assert(batch);
|
|
1406
|
+
expect(batch.expectedAffectedRows).toBe(null);
|
|
1407
|
+
expect(batch.expectedReturnedRows).toBe(1);
|
|
1408
|
+
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
1409
|
+
`"select 1 as "exists" from "users" where ("users"."id" = ? and "users"."_version" = ?) limit ?"`,
|
|
1410
|
+
);
|
|
1411
|
+
expect(batch.query.params).toMatchObject(["user123", 5, 1]);
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
});
|