1import { describe, expect, it } from "alchemy-test";
2import * as Effect from "effect/Effect";
3import { makeDataApiDialect, type DataApiExecutor } from "@/AuroraDataApi.ts";
4
5interface Recorded {
6 sql: string;
7 parameters: unknown[] | undefined;
8 transactionId: string | undefined;
9}
10
11/** Recording mock of the Data API surface the dialect drives. */
12const makeMock = (
13 respond: (request: Recorded) => {
14 records?: unknown[][];
15 columnMetadata?: { label?: string; typeName?: string }[];
16 numberOfRecordsUpdated?: number;
17 } = () => ({}),
18) => {
19 const calls: {
20 executed: Recorded[];
21 begun: number;
22 committed: string[];
23 rolledBack: string[];
24 } = { executed: [], begun: 0, committed: [], rolledBack: [] };
25 const executor: DataApiExecutor = {
26 execute: async (request) => {
27 const recorded: Recorded = {
28 sql: request.sql,
29 parameters: request.parameters as unknown[] | undefined,
30 transactionId: request.transactionId,
31 };
32 calls.executed.push(recorded);
33 return respond(recorded) as never;
34 },
35 begin: async () => {
36 calls.begun += 1;
37 return { transactionId: `tx-${calls.begun}` };
38 },
39 commit: async (transactionId) => {
40 calls.committed.push(transactionId);
41 return {};
42 },
43 rollback: async (transactionId) => {
44 calls.rolledBack.push(transactionId);
45 return {};
46 },
47 };
48 return { executor, calls };
49};
50
51const makeDb = (executor: DataApiExecutor) =>
52 Effect.gen(function* () {
53 const dialect = yield* makeDataApiDialect(executor);
54 const { Kysely } = yield* Effect.promise(() => import("kysely"));
55 return new Kysely<Record<string, Record<string, unknown>>>({ dialect });
56 });
57
58describe(
59 "AuroraDataApi dialect",
60 { tags: ["unit", "provider:aws", "provider:aws:rdsdata", "local"] },
61 () => {
62 it.live("compiles postgres SQL with named :n parameters", () =>
63 Effect.gen(function* () {
64 const { executor, calls } = makeMock();
65 const db = yield* makeDb(executor);
66 const createdAt = new Date("2026-08-10T12:00:00.000Z");
67 yield* Effect.promise(() =>
68 db
69 .insertInto("user")
70 .values({
71 id: "u1",
72 email: "a@b.co",
73 emailVerified: false,
74 age: 42,
75 createdAt,
76 })
77 .execute(),
78 );
79 const call = calls.executed[0]!;
80 expect(call.sql).toContain('insert into "user"');
81 expect(call.sql).toContain(":1");
82 expect(call.sql).toContain(":5");
83 expect(call.parameters).toEqual([
84 { name: "1", value: { stringValue: "u1" } },
85 { name: "2", value: { stringValue: "a@b.co" } },
86 { name: "3", value: { booleanValue: false } },
87 { name: "4", value: { longValue: 42 } },
88 {
89 name: "5",
90 typeHint: "TIMESTAMP",
91 value: { stringValue: "2026-08-10 12:00:00.000" },
92 },
93 ]);
94 }),
95 );
96
97 it.live("maps records to rows and revives timestamps as Dates", () =>
98 Effect.gen(function* () {
99 const { executor } = makeMock(() => ({
100 columnMetadata: [
101 { label: "id", typeName: "varchar" },
102 { label: "createdAt", typeName: "timestamp" },
103 { label: "count", typeName: "int8" },
104 ],
105 records: [
106 [
107 { stringValue: "u1" },
108 { stringValue: "2026-08-10 12:00:00" },
109 { longValue: 7 },
110 ],
111 [{ stringValue: "u2" }, { isNull: true }, { isNull: true }],
112 ],
113 }));
114 const db = yield* makeDb(executor);
115 const rows = yield* Effect.promise(() =>
116 db.selectFrom("user").selectAll().execute(),
117 );
118 expect(rows).toHaveLength(2);
119 expect(rows[0]!.id).toBe("u1");
120 expect(rows[0]!.createdAt).toBeInstanceOf(Date);
121 expect((rows[0]!.createdAt as Date).toISOString()).toBe(
122 "2026-08-10T12:00:00.000Z",
123 );
124 expect(rows[0]!.count).toBe(7);
125 expect(rows[1]!.createdAt).toBeNull();
126 }),
127 );
128
129 it.live(
130 "preserves schema search paths and other array-valued results",
131 () =>
132 Effect.gen(function* () {
133 const { executor } = makeMock(() => ({
134 columnMetadata: [
135 { label: "schemas", typeName: "_text" },
136 { label: "flags" },
137 { label: "integers" },
138 { label: "decimals" },
139 { label: "nested" },
140 { label: "empty" },
141 ],
142 records: [
143 [
144 { arrayValue: { stringValues: ["pg_catalog", "public"] } },
145 { arrayValue: { booleanValues: [true, false] } },
146 { arrayValue: { longValues: [1, 2] } },
147 { arrayValue: { doubleValues: [1.5, 2.5] } },
148 {
149 arrayValue: {
150 arrayValues: [
151 { longValues: [1, 2] },
152 { longValues: [3, 4] },
153 ],
154 },
155 },
156 { arrayValue: { stringValues: [] } },
157 ],
158 ],
159 }));
160 const db = yield* makeDb(executor);
161 const rows = yield* Effect.promise(() =>
162 db.selectFrom("metadata").selectAll().execute(),
163 );
164 expect(rows).toEqual([
165 {
166 schemas: ["pg_catalog", "public"],
167 flags: [true, false],
168 integers: [1, 2],
169 decimals: [1.5, 2.5],
170 nested: [
171 [1, 2],
172 [3, 4],
173 ],
174 empty: [],
175 },
176 ]);
177 }),
178 );
179
180 it.live(
181 "casts catalog table kinds to text during schema introspection",
182 () =>
183 Effect.gen(function* () {
184 const { executor, calls } = makeMock(() => ({
185 columnMetadata: [
186 { label: "column" },
187 { label: "not_null" },
188 { label: "has_default" },
189 { label: "table" },
190 { label: "table_type" },
191 { label: "schema" },
192 { label: "type" },
193 { label: "type_schema" },
194 { label: "column_description" },
195 { label: "auto_incrementing" },
196 ],
197 records: [
198 [
199 { stringValue: "id" },
200 { booleanValue: true },
201 { booleanValue: false },
202 { stringValue: "user" },
203 { stringValue: "r" },
204 { stringValue: "public" },
205 { stringValue: "text" },
206 { stringValue: "pg_catalog" },
207 { isNull: true },
208 { isNull: true },
209 ],
210 ],
211 }));
212 const db = yield* makeDb(executor);
213 const tables = yield* Effect.promise(() =>
214 db.introspection.getTables(),
215 );
216 expect(calls.executed[0]!.sql).toContain(
217 'cast("c"."relkind" as text) as "table_type"',
218 );
219 expect(tables).toEqual([
220 {
221 name: "user",
222 schema: "public",
223 isView: false,
224 isForeign: false,
225 columns: [
226 {
227 name: "id",
228 dataType: "text",
229 dataTypeSchema: "pg_catalog",
230 isNullable: false,
231 hasDefaultValue: false,
232 isAutoIncrementing: false,
233 comment: undefined,
234 },
235 ],
236 },
237 ]);
238 }),
239 );
240
241 it.live("threads transactions through begin/commit", () =>
242 Effect.gen(function* () {
243 const { executor, calls } = makeMock();
244 const db = yield* makeDb(executor);
245 yield* Effect.promise(() =>
246 db.transaction().execute(async (trx) => {
247 await trx.deleteFrom("session").execute();
248 await trx.deleteFrom("account").execute();
249 }),
250 );
251 expect(calls.begun).toBe(1);
252 expect(calls.executed.map((call) => call.transactionId)).toEqual([
253 "tx-1",
254 "tx-1",
255 ]);
256 expect(calls.committed).toEqual(["tx-1"]);
257 expect(calls.rolledBack).toEqual([]);
258 }),
259 );
260
261 it.live("rolls back failed transactions", () =>
262 Effect.gen(function* () {
263 const { executor, calls } = makeMock();
264 const db = yield* makeDb(executor);
265 const result = yield* Effect.promise(() =>
266 db
267 .transaction()
268 .execute(async () => {
269 throw new Error("boom");
270 })
271 .then(
272 () => "ok",
273 () => "failed",
274 ),
275 );
276 expect(result).toBe("failed");
277 expect(calls.rolledBack).toEqual(["tx-1"]);
278 expect(calls.committed).toEqual([]);
279 }),
280 );
281 },
282);