@rawsql-ts/ztd-cli 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +33 -20
  2. package/dist/commands/init.d.ts +13 -0
  3. package/dist/commands/init.js +372 -127
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/lint.d.ts +4 -4
  6. package/dist/commands/lint.js +60 -40
  7. package/dist/commands/lint.js.map +1 -1
  8. package/dist/commands/ztdConfig.d.ts +2 -2
  9. package/dist/commands/ztdConfig.js +26 -12
  10. package/dist/commands/ztdConfig.js.map +1 -1
  11. package/dist/utils/optionalDependencies.d.ts +35 -0
  12. package/dist/utils/optionalDependencies.js +96 -0
  13. package/dist/utils/optionalDependencies.js.map +1 -0
  14. package/package.json +16 -9
  15. package/templates/AGENTS.md +36 -309
  16. package/templates/README.md +12 -215
  17. package/templates/dist/ztd-cli/templates/src/db/sql-client.ts +24 -0
  18. package/templates/src/AGENTS.md +26 -0
  19. package/templates/src/catalog/AGENTS.md +37 -0
  20. package/templates/src/catalog/runtime/AGENTS.md +75 -0
  21. package/templates/src/catalog/runtime/_coercions.ts +1 -0
  22. package/templates/src/catalog/runtime/_smoke.runtime.ts +21 -0
  23. package/templates/src/catalog/specs/AGENTS.md +48 -0
  24. package/templates/src/catalog/specs/_smoke.spec.arktype.ts +21 -0
  25. package/templates/src/catalog/specs/_smoke.spec.zod.ts +20 -0
  26. package/templates/src/db/sql-client.ts +5 -5
  27. package/templates/src/jobs/AGENTS.md +26 -0
  28. package/templates/src/jobs/README.md +3 -0
  29. package/templates/src/repositories/AGENTS.md +118 -0
  30. package/templates/src/repositories/tables/AGENTS.md +94 -0
  31. package/templates/src/repositories/tables/README.md +3 -0
  32. package/templates/src/repositories/views/AGENTS.md +25 -0
  33. package/templates/src/repositories/views/README.md +3 -0
  34. package/templates/src/sql/AGENTS.md +77 -0
  35. package/templates/src/sql/README.md +6 -0
  36. package/templates/tests/AGENTS.md +43 -150
  37. package/templates/tests/generated/AGENTS.md +16 -0
  38. package/templates/tests/smoke.test.ts +5 -0
  39. package/templates/tests/smoke.validation.test.ts +34 -0
  40. package/templates/tests/support/AGENTS.md +26 -0
  41. package/templates/tests/support/global-setup.ts +8 -23
  42. package/templates/tests/support/testkit-client.ts +13 -741
  43. package/templates/tsconfig.json +8 -1
  44. package/templates/ztd/AGENTS.md +11 -67
  45. package/templates/ztd/README.md +4 -13
  46. package/templates/ztd/ddl/AGENTS.md +34 -0
  47. package/templates/ztd/ddl/demo.sql +74 -0
  48. package/templates/src/repositories/user-accounts.ts +0 -179
  49. package/templates/tests/user-profiles.test.ts +0 -161
  50. package/templates/tests/writer-constraints.test.ts +0 -32
@@ -1,742 +1,14 @@
1
- // ZTD testkit helper - AUTO GENERATED
2
- // ztd-cli emits this file during project bootstrapping to wire @rawsql-ts/adapter-node-pg adapters.
3
- // Regenerate via npx ztd init (choose overwrite when prompted); avoid manual edits.
4
-
5
- import { existsSync, promises as fsPromises } from 'node:fs';
6
- import path from 'node:path';
7
- import { Client, types } from 'pg';
8
- import type { ClientConfig, QueryResultRow } from 'pg';
9
- import { createPgTestkitClient } from '@rawsql-ts/adapter-node-pg';
10
- import type { PgQueryInput, PgQueryable } from '@rawsql-ts/adapter-node-pg';
11
- import type { TableFixture } from '@rawsql-ts/testkit-core';
12
-
13
- const ddlDirectories = [path.resolve(__dirname, '../../ztd/ddl')];
14
-
15
- let sharedPgClient: Client | undefined;
16
- let sharedQueryable: PgQueryable | undefined;
17
-
18
- type ZtdSqlLogPhase = 'original' | 'rewritten';
19
-
20
- type ZtdSqlLogEvent = {
21
- kind: 'ztd-sql';
22
- phase: ZtdSqlLogPhase;
23
- queryId: number;
24
- sql: string;
25
- params?: unknown[];
26
- fixturesApplied?: string[];
27
- timestamp: string;
28
- };
29
-
30
- export type ZtdExecutionMode = 'ztd' | 'traditional';
31
-
32
- export type TraditionalIsolationMode = 'schema' | 'none';
33
- export type TraditionalCleanupStrategy = 'drop_schema' | 'custom_sql' | 'none';
34
-
35
- export interface TraditionalExecutionConfig {
36
- isolation?: TraditionalIsolationMode;
37
- setupSql?: string[];
38
- cleanup?: TraditionalCleanupStrategy;
39
- cleanupSql?: string[];
40
- schemaName?: string;
41
- }
42
-
43
- export type ZtdSqlLogOptions = {
44
- enabled?: boolean;
45
- includeParams?: boolean;
46
- logger?: (event: ZtdSqlLogEvent) => void;
47
- profile?: ZtdProfileOptions;
48
- mode?: ZtdExecutionMode;
49
- traditional?: TraditionalExecutionConfig;
50
- };
51
-
52
- type ZtdProfilePhase = 'connection' | 'setup' | 'query' | 'teardown';
53
-
54
- type ZtdProfileEvent = {
55
- kind: 'ztd-profile';
56
- phase: ZtdProfilePhase;
57
- testName?: string;
58
- workerId?: string;
59
- processId: number;
60
- executionMode?: 'serial' | 'parallel' | 'unknown';
61
- connectionReused?: boolean;
62
- queryId?: number;
63
- queryCount?: number;
64
- durationMs?: number;
65
- totalQueryMs?: number;
66
- sql?: string;
67
- params?: unknown[];
68
- fixturesApplied?: string[];
69
- sampleSql?: string[];
70
- timestamp: string;
71
- };
72
-
73
- export type ZtdProfileOptions = {
74
- enabled?: boolean;
75
- perQuery?: boolean;
76
- includeParams?: boolean;
77
- includeSql?: boolean;
78
- sampleLimit?: number;
79
- testName?: string;
80
- executionMode?: 'serial' | 'parallel' | 'unknown';
81
- logger?: (event: ZtdProfileEvent) => void;
82
- };
83
-
84
- const { INT2, INT4, INT8, NUMERIC, DATE } = types.builtins;
85
- const parseInteger = (value: string | null) => (value === null ? null : Number(value));
86
- const parseNumeric = (value: string | null) => (value === null ? null : Number(value));
87
-
88
- // Align pg parsers with the primitive shapes the fixtures assert in tests.
89
- types.setTypeParser(INT2, parseInteger);
90
- types.setTypeParser(INT4, parseInteger);
91
- types.setTypeParser(INT8, parseInteger);
92
- types.setTypeParser(NUMERIC, parseNumeric);
93
- types.setTypeParser(DATE, (value) => value);
94
-
95
- function isTruthyEnv(value: string | undefined): boolean {
96
- if (!value) {
97
- return false;
98
- }
99
-
100
- return value === '1' || value.toLowerCase() === 'true' || value.toLowerCase() === 'yes';
101
- }
102
-
103
- function resolveNumberEnv(value: string | undefined): number | undefined {
104
- if (!value) {
105
- return undefined;
106
- }
107
-
108
- const parsed = Number(value);
109
- return Number.isFinite(parsed) ? parsed : undefined;
110
- }
111
-
112
- function safeJsonStringify(value: unknown): string {
113
- const seen = new WeakSet<object>();
114
- return JSON.stringify(value, (_key, item) => {
115
- // Avoid JSON.stringify throwing on BigInt params when logging is enabled.
116
- if (typeof item === 'bigint') {
117
- return item.toString();
118
- }
119
-
120
- // Avoid JSON.stringify throwing on circular references when logging is enabled.
121
- if (typeof item === 'object' && item !== null) {
122
- if (seen.has(item)) {
123
- return '[Circular]';
124
- }
125
- seen.add(item);
126
- }
127
- return item;
128
- });
129
- }
130
-
131
- async function resolveDatabaseUrl(): Promise<string> {
132
- const configuredUrl = process.env.DATABASE_URL;
133
- if (configuredUrl) {
134
- return configuredUrl;
135
- }
136
-
137
- throw new Error('DATABASE_URL is required. It should be provided by Vitest globalSetup or your environment.');
138
- }
139
-
140
- async function getPgClient(): Promise<Client> {
141
- if (sharedPgClient) {
142
- return sharedPgClient;
143
- }
144
-
145
- const databaseUrl = await resolveDatabaseUrl();
146
-
147
- const clientConfig: ClientConfig = { connectionString: databaseUrl };
148
- sharedPgClient = new Client(clientConfig);
149
-
150
- // Keep the shared Client connected for the duration of the test run.
151
- await sharedPgClient.connect();
152
- sharedPgClient.once('end', () => {
153
- sharedPgClient = undefined;
154
- sharedQueryable = undefined;
155
- });
156
- process.once('exit', () => {
157
- if (!sharedPgClient) {
158
- return;
159
- }
160
-
161
- // Ensure node exits cleanly by closing the connection if tests end early.
162
- void sharedPgClient.end();
163
- });
164
-
165
- return sharedPgClient;
166
- }
167
-
168
- async function getPgQueryable(): Promise<PgQueryable> {
169
- if (sharedQueryable) {
170
- return sharedQueryable;
171
- }
172
-
173
- const client = await getPgClient();
174
-
175
- // Wrap the pg.Client to expose only the subset needed by @rawsql-ts/adapter-node-pg.
176
- const wrappedQueryable: PgQueryable = {
177
- query: <T extends QueryResultRow>(textOrConfig: PgQueryInput, values?: unknown[]) =>
178
- client.query<T>(textOrConfig as never, values),
179
- release: () => {
180
- // Release is intentionally a no-op because the shared client should stay open.
181
- return;
182
- }
183
- };
184
-
185
- sharedQueryable = wrappedQueryable;
186
- return wrappedQueryable;
187
- }
188
-
189
- export type ZtdPlaygroundQueryResult<T extends QueryResultRow = QueryResultRow> = Promise<T[]>;
190
-
191
- export type ZtdPlaygroundClient = {
192
- query<T extends QueryResultRow = QueryResultRow>(text: string, values?: unknown[]): ZtdPlaygroundQueryResult<T>;
193
- close(): Promise<void>;
194
- };
195
-
196
- export async function createTestkitClient(
197
- fixtures: TableFixture[],
198
- options: ZtdSqlLogOptions = {}
199
- ): Promise<ZtdPlaygroundClient> {
200
- const mode = resolveExecutionMode(options.mode);
201
- if (mode === 'traditional') {
202
- return createTraditionalPlaygroundClient(fixtures, options);
203
- }
204
-
205
- return createZtdPlaygroundClient(fixtures, options);
206
- }
207
-
208
- async function createZtdPlaygroundClient(
209
- fixtures: TableFixture[],
210
- options: ZtdSqlLogOptions
211
- ): Promise<ZtdPlaygroundClient> {
212
- const {
213
- logEnabled,
214
- logParams,
215
- logSink,
216
- profileEnabled,
217
- profilePerQuery,
218
- profileParams,
219
- profileIncludeSql,
220
- profileSampleLimit,
221
- profileTestName,
222
- profileExecutionMode,
223
- profileWorkerId,
224
- profileSink,
225
- } = buildLoggingState(options);
226
-
227
- let nextQueryId = 1;
228
- const queryIdStack: number[] = [];
229
- // Keep fixture info aligned with the active query so profiling includes applied fixtures.
230
- const queryFixtureMap = new Map<number, string[] | undefined>();
231
- // Track aggregate timings so teardown logs can summarize the run.
232
- const profileStats = {
233
- queryCount: 0,
234
- totalQueryMs: 0,
235
- sampleSql: [] as string[],
236
- };
237
-
238
- // Capture a consistent timestamp baseline for the setup phase.
239
- const setupStartedAt = profileEnabled ? Date.now() : 0;
240
-
241
- const hadSharedConnection = Boolean(sharedPgClient);
242
- const connectionStartedAt = profileEnabled ? Date.now() : 0;
243
- const queryable = await getPgQueryable();
244
- if (profileEnabled) {
245
- profileSink({
246
- kind: 'ztd-profile',
247
- phase: 'connection',
248
- testName: profileTestName,
249
- workerId: profileWorkerId,
250
- processId: process.pid,
251
- executionMode: profileExecutionMode,
252
- connectionReused: hadSharedConnection,
253
- durationMs: Date.now() - connectionStartedAt,
254
- timestamp: new Date().toISOString(),
255
- });
256
- }
257
-
258
- // TableNameResolver keeps DDL and fixtures aligned on canonical schema-qualified identifiers like 'public.table_name'.
259
- const driver = createPgTestkitClient({
260
- connectionFactory: () => queryable,
261
- tableRows: fixtures,
262
- ddl: { directories: ddlDirectories },
263
- onExecute: (rewrittenSql: string, params: unknown[] | undefined, fixturesApplied: string[]) => {
264
- if (profileEnabled) {
265
- // Capture fixture metadata while the original query is still on the stack.
266
- const activeQueryId = queryIdStack.at(-1);
267
- if (typeof activeQueryId === 'number') {
268
- queryFixtureMap.set(activeQueryId, fixturesApplied);
269
- }
270
- }
271
-
272
- if (!logEnabled) {
273
- return;
274
- }
275
-
276
- // Use a stack so concurrent async queries can still correlate "original" and "rewritten" logs.
277
- const queryId = queryIdStack.at(-1) ?? -1;
278
- logSink({
279
- kind: 'ztd-sql',
280
- phase: 'rewritten',
281
- queryId,
282
- sql: rewrittenSql,
283
- params: logParams ? (params as unknown[] | undefined) : undefined,
284
- fixturesApplied,
285
- timestamp: new Date().toISOString(),
286
- });
287
- },
288
- });
289
-
290
- if (profileEnabled) {
291
- profileSink({
292
- kind: 'ztd-profile',
293
- phase: 'setup',
294
- testName: profileTestName,
295
- workerId: profileWorkerId,
296
- processId: process.pid,
297
- executionMode: profileExecutionMode,
298
- durationMs: Date.now() - setupStartedAt,
299
- timestamp: new Date().toISOString(),
300
- });
301
- }
302
-
303
- // Expose a simplified query API so tests can assert on plain row arrays.
304
- return {
305
- async query<T extends QueryResultRow>(text: string, values?: unknown[]) {
306
- const queryId = nextQueryId++;
307
-
308
- if (logEnabled) {
309
- logSink({
310
- kind: 'ztd-sql',
311
- phase: 'original',
312
- queryId,
313
- sql: text,
314
- params: logParams ? values : undefined,
315
- timestamp: new Date().toISOString(),
316
- });
317
- }
318
-
319
- const queryStartedAt = profileEnabled ? Date.now() : 0;
320
- queryIdStack.push(queryId);
321
- try {
322
- const result = await driver.query<T>(text, values);
323
- return result.rows;
324
- } finally {
325
- queryIdStack.pop();
326
-
327
- if (profileEnabled) {
328
- // Record per-query timing and keep a small SQL sample for teardown summaries.
329
- const durationMs = Date.now() - queryStartedAt;
330
- profileStats.queryCount += 1;
331
- profileStats.totalQueryMs += durationMs;
332
-
333
- if (profileIncludeSql && profileSampleLimit > 0 && profileStats.sampleSql.length < profileSampleLimit) {
334
- profileStats.sampleSql.push(text);
335
- }
336
-
337
- const fixturesApplied = queryFixtureMap.get(queryId);
338
- queryFixtureMap.delete(queryId);
339
-
340
- if (profilePerQuery) {
341
- profileSink({
342
- kind: 'ztd-profile',
343
- phase: 'query',
344
- testName: profileTestName,
345
- workerId: profileWorkerId,
346
- processId: process.pid,
347
- executionMode: profileExecutionMode,
348
- queryId,
349
- durationMs,
350
- sql: profileIncludeSql ? text : undefined,
351
- params: profileParams ? values : undefined,
352
- fixturesApplied,
353
- timestamp: new Date().toISOString(),
354
- });
355
- }
356
- }
357
- }
358
- },
359
- async close() {
360
- const teardownStartedAt = profileEnabled ? Date.now() : 0;
361
- try {
362
- await driver.close();
363
- } finally {
364
- if (profileEnabled) {
365
- // Always emit the teardown summary, even if close fails mid-flight.
366
- profileSink({
367
- kind: 'ztd-profile',
368
- phase: 'teardown',
369
- testName: profileTestName,
370
- workerId: profileWorkerId,
371
- processId: process.pid,
372
- executionMode: profileExecutionMode,
373
- durationMs: Date.now() - teardownStartedAt,
374
- queryCount: profileStats.queryCount,
375
- totalQueryMs: profileStats.totalQueryMs,
376
- sampleSql: profileIncludeSql ? profileStats.sampleSql : undefined,
377
- timestamp: new Date().toISOString(),
378
- });
379
- }
380
- }
381
- }
382
- };
383
- }
384
-
385
-
386
- async function createTraditionalPlaygroundClient(
387
- fixtures: TableFixture[],
388
- options: ZtdSqlLogOptions
389
- ): Promise<ZtdPlaygroundClient> {
390
- const {
391
- logEnabled,
392
- logParams,
393
- logSink,
394
- profileEnabled,
395
- profilePerQuery,
396
- profileParams,
397
- profileIncludeSql,
398
- profileSampleLimit,
399
- profileTestName,
400
- profileExecutionMode,
401
- profileWorkerId,
402
- profileSink,
403
- } = buildLoggingState(options);
404
-
405
- const databaseUrl = await resolveDatabaseUrl();
406
- const clientConfig: ClientConfig = { connectionString: databaseUrl };
407
- const client = new Client(clientConfig);
408
- await client.connect();
409
-
410
- const traditional = options.traditional;
411
- const isolation = traditional?.isolation ?? 'schema';
412
- const schemaName = isolation === 'schema' ? traditional?.schemaName ?? generateSchemaName() : undefined;
413
- const setupSql = traditional?.setupSql ?? [];
414
- const cleanupSql = traditional?.cleanupSql ?? [];
415
- const cleanupStrategy: TraditionalCleanupStrategy =
416
- traditional?.cleanup ?? (schemaName ? 'drop_schema' : 'none');
417
-
418
- let initializationPromise: Promise<void> | null = null;
419
- const setupStartedAt = profileEnabled ? Date.now() : 0;
420
- let setupLogged = false;
421
-
422
- const ensureInitialized = (): Promise<void> => {
423
- if (!initializationPromise) {
424
- initializationPromise = (async () => {
425
- if (schemaName) {
426
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(schemaName)}`);
427
- await client.query(`SET search_path TO ${quoteIdentifier(schemaName)}, public`);
428
- }
429
-
430
- // Build the schema objects before seeding or running custom setup SQL.
431
- await applySqlFiles(client, ddlDirectories);
432
-
433
- // Allow callers to run additional setup steps before the fixtures load.
434
- for (const sql of setupSql) {
435
- if (!sql.trim()) {
436
- continue;
437
- }
438
- await client.query(sql);
439
- }
440
-
441
- // Materialize the fixture rows into the isolated schema.
442
- await seedFixtureRows(client, fixtures, schemaName);
443
- })().then(() => {
444
- if (profileEnabled && !setupLogged) {
445
- profileSink({
446
- kind: 'ztd-profile',
447
- phase: 'setup',
448
- testName: profileTestName,
449
- workerId: profileWorkerId,
450
- processId: process.pid,
451
- executionMode: profileExecutionMode,
452
- durationMs: Date.now() - setupStartedAt,
453
- timestamp: new Date().toISOString(),
454
- });
455
- setupLogged = true;
456
- }
457
- });
458
- }
459
- return initializationPromise;
460
- };
461
-
462
- let cleanupRun = false;
463
- const runCleanup = async () => {
464
- if (cleanupRun) {
465
- return;
466
- }
467
- cleanupRun = true;
468
-
469
- // Execute any caller-supplied cleanup statements before further teardown.
470
- if (cleanupStrategy === 'custom_sql') {
471
- for (const sql of cleanupSql) {
472
- if (!sql.trim()) {
473
- continue;
474
- }
475
- await client.query(sql);
476
- }
477
- }
478
-
479
- // Tear down the isolated schema when requested.
480
- if (cleanupStrategy === 'drop_schema' && schemaName) {
481
- await client.query(`DROP SCHEMA IF EXISTS ${quoteIdentifier(schemaName)} CASCADE`);
482
- }
483
- };
484
-
485
- const connectionStartedAt = profileEnabled ? Date.now() : 0;
486
- if (profileEnabled) {
487
- profileSink({
488
- kind: 'ztd-profile',
489
- phase: 'connection',
490
- testName: profileTestName,
491
- workerId: profileWorkerId,
492
- processId: process.pid,
493
- executionMode: profileExecutionMode,
494
- connectionReused: false,
495
- durationMs: Date.now() - connectionStartedAt,
496
- timestamp: new Date().toISOString(),
497
- });
498
- }
499
-
500
- let nextQueryId = 1;
501
- const queryIdStack: number[] = [];
502
- const profileStats = {
503
- queryCount: 0,
504
- totalQueryMs: 0,
505
- sampleSql: [] as string[],
506
- };
507
-
508
- return {
509
- async query<T extends QueryResultRow>(text: string, values?: unknown[]) {
510
- const queryId = nextQueryId++;
511
-
512
- // Emit the original SQL so the tracing/logging pipeline stays consistent.
513
- if (logEnabled) {
514
- logSink({
515
- kind: 'ztd-sql',
516
- phase: 'original',
517
- queryId,
518
- sql: text,
519
- params: logParams ? values : undefined,
520
- timestamp: new Date().toISOString(),
521
- });
522
- }
523
-
524
- const queryStartedAt = profileEnabled ? Date.now() : 0;
525
- queryIdStack.push(queryId);
526
- try {
527
- await ensureInitialized();
528
- const result = await client.query<T>(text, values);
529
- return result.rows;
530
- } finally {
531
- queryIdStack.pop();
532
-
533
- if (profileEnabled) {
534
- const durationMs = Date.now() - queryStartedAt;
535
- profileStats.queryCount += 1;
536
- profileStats.totalQueryMs += durationMs;
537
-
538
- if (profileIncludeSql && profileSampleLimit > 0 && profileStats.sampleSql.length < profileSampleLimit) {
539
- profileStats.sampleSql.push(text);
540
- }
541
-
542
- if (profilePerQuery) {
543
- profileSink({
544
- kind: 'ztd-profile',
545
- phase: 'query',
546
- testName: profileTestName,
547
- workerId: profileWorkerId,
548
- processId: process.pid,
549
- executionMode: profileExecutionMode,
550
- queryId,
551
- durationMs,
552
- sql: profileIncludeSql ? text : undefined,
553
- params: profileParams ? values : undefined,
554
- timestamp: new Date().toISOString(),
555
- });
556
- }
557
- }
558
- }
559
- },
560
- async close() {
561
- const teardownStartedAt = profileEnabled ? Date.now() : 0;
562
- // Preserve the first teardown error so we can rethrow it after ensuring the client closes.
563
- let teardownError: unknown;
564
-
565
- try {
566
- if (initializationPromise) {
567
- await initializationPromise.catch(() => undefined);
568
- }
569
- await runCleanup();
570
- } catch (error) {
571
- teardownError = error;
572
- } finally {
573
- if (profileEnabled) {
574
- profileSink({
575
- kind: 'ztd-profile',
576
- phase: 'teardown',
577
- testName: profileTestName,
578
- workerId: profileWorkerId,
579
- processId: process.pid,
580
- executionMode: profileExecutionMode,
581
- durationMs: Date.now() - teardownStartedAt,
582
- queryCount: profileStats.queryCount,
583
- totalQueryMs: profileStats.totalQueryMs,
584
- sampleSql: profileIncludeSql ? profileStats.sampleSql : undefined,
585
- timestamp: new Date().toISOString(),
586
- });
587
- }
588
-
589
- // Always attempt to close the client and record any failure without overriding earlier errors.
590
- try {
591
- await client.end();
592
- } catch (clientError) {
593
- if (!teardownError) {
594
- teardownError = clientError;
595
- }
596
- }
597
- }
598
-
599
- if (teardownError) {
600
- throw teardownError;
601
- }
602
- },
603
- };
604
- }
605
-
606
- function buildLoggingState(options: ZtdSqlLogOptions) {
607
- const logEnabled = options.enabled ?? isTruthyEnv(process.env.ZTD_SQL_LOG);
608
- const logParams = options.includeParams ?? isTruthyEnv(process.env.ZTD_SQL_LOG_PARAMS);
609
- const logSink =
610
- options.logger ??
611
- ((event: ZtdSqlLogEvent) => {
612
- console.log(safeJsonStringify(event));
613
- });
614
-
615
- const profileOptions = options.profile ?? {};
616
- const profileEnabled = profileOptions.enabled ?? isTruthyEnv(process.env.ZTD_PROFILE);
617
- const profilePerQuery = profileOptions.perQuery ?? isTruthyEnv(process.env.ZTD_PROFILE_PER_QUERY);
618
- const profileParams = profileOptions.includeParams ?? isTruthyEnv(process.env.ZTD_PROFILE_PARAMS);
619
- const profileIncludeSql = profileOptions.includeSql ?? isTruthyEnv(process.env.ZTD_PROFILE_SQL);
620
- const profileSampleLimit =
621
- profileOptions.sampleLimit ?? resolveNumberEnv(process.env.ZTD_PROFILE_SAMPLE_LIMIT) ?? 5;
622
- const profileTestName = profileOptions.testName ?? process.env.ZTD_PROFILE_TEST_NAME;
623
- const profileExecutionMode =
624
- profileOptions.executionMode ??
625
- (process.env.ZTD_PROFILE_EXECUTION as 'serial' | 'parallel' | 'unknown' | undefined) ??
626
- (process.env.VITEST_WORKER_ID ? 'parallel' : 'serial');
627
- const profileWorkerId = process.env.VITEST_WORKER_ID ?? process.env.JEST_WORKER_ID;
628
- const profileSink =
629
- profileOptions.logger ??
630
- ((event: ZtdProfileEvent) => {
631
- console.log(safeJsonStringify(event));
632
- });
633
-
634
- return {
635
- logEnabled,
636
- logParams,
637
- logSink,
638
- profileEnabled,
639
- profilePerQuery,
640
- profileParams,
641
- profileIncludeSql,
642
- profileSampleLimit,
643
- profileTestName,
644
- profileExecutionMode,
645
- profileWorkerId,
646
- profileSink,
647
- };
648
- }
649
-
650
- function resolveExecutionMode(mode?: ZtdExecutionMode): ZtdExecutionMode {
651
- if (mode === 'traditional') {
652
- return 'traditional';
653
- }
654
-
655
- const envMode = process.env.ZTD_EXECUTION_MODE as ZtdExecutionMode | undefined;
656
- return envMode === 'traditional' ? 'traditional' : 'ztd';
657
- }
658
-
659
- function generateSchemaName(): string {
660
- const timestamp = Date.now().toString(36);
661
- const random = Math.random().toString(36).slice(2, 7);
662
- return `ztd_traditional_${timestamp}_${random}`;
663
- }
664
-
665
- async function applySqlFiles(client: Client, directories: string[]): Promise<void> {
666
- // Execute each .sql file so the physical schema matches the ZTD definitions.
667
- for (const directory of directories) {
668
- if (!existsSync(directory)) {
669
- continue;
670
- }
671
-
672
- const entries = await fsPromises.readdir(directory, { withFileTypes: true });
673
- for (const entry of entries) {
674
- if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.sql')) {
675
- continue;
676
- }
677
-
678
- const filePath = path.join(directory, entry.name);
679
- const sql = await fsPromises.readFile(filePath, 'utf8');
680
- if (!sql.trim()) {
681
- continue;
682
- }
683
-
684
- await client.query(sql);
685
- }
686
- }
687
- }
688
-
689
- async function seedFixtureRows(client: Client, fixtures: TableFixture[], isolationSchema?: string): Promise<void> {
690
- for (const fixture of fixtures) {
691
- if (fixture.rows.length === 0) {
692
- continue;
693
- }
694
-
695
- const columnNames = getColumnNamesFromFixture(fixture);
696
- if (columnNames.length === 0) {
697
- continue;
698
- }
699
-
700
- const tableIdentifier = buildTableIdentifier(fixture.tableName, isolationSchema);
701
- const columnsSql = columnNames.map(quoteIdentifier).join(', ');
702
-
703
- for (const row of fixture.rows) {
704
- const values = columnNames.map((column) =>
705
- Object.prototype.hasOwnProperty.call(row, column) ? row[column] : null,
706
- );
707
- const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
708
- await client.query(`INSERT INTO ${tableIdentifier} (${columnsSql}) VALUES (${placeholders})`, values);
709
- }
710
- }
711
- }
712
-
713
- function getColumnNamesFromFixture(fixture: TableFixture): string[] {
714
- if (fixture.schema && Array.isArray((fixture.schema as { columns?: unknown }).columns)) {
715
- return (fixture.schema as { columns: { name: string }[] }).columns.map((column) => column.name);
716
- }
717
-
718
- if (fixture.schema && 'columns' in fixture.schema && typeof fixture.schema.columns === 'object') {
719
- return Object.keys(fixture.schema.columns);
720
- }
721
-
722
- if (fixture.rows.length > 0) {
723
- return Object.keys(fixture.rows[0]);
724
- }
725
-
726
- return [];
727
- }
728
-
729
- function buildTableIdentifier(tableName: string, isolationSchema?: string): string {
730
- const segments = tableName.split('.');
731
- const baseTable = segments.length > 1 ? segments[segments.length - 1] : tableName;
732
- const schema = isolationSchema ?? (segments.length > 1 ? segments.slice(0, -1).join('.') : undefined);
733
- if (schema) {
734
- return `${quoteIdentifier(schema)}.${quoteIdentifier(baseTable)}`;
735
- }
736
- return quoteIdentifier(baseTable);
737
- }
738
-
739
- function quoteIdentifier(value: string): string {
740
- const escaped = value.replace(/"/g, '""');
741
- return `"${escaped}"`;
1
+ import type { SqlClient } from '../../src/db/sql-client';
2
+
3
+ /**
4
+ * Placeholder for wiring an SQL client that tests can reuse.
5
+ *
6
+ * Replace this implementation with your chosen adapter (pg, mysql2, etc.) or a
7
+ * fixture-based helper that fulfills the `SqlClient` contract. Avoid committing
8
+ * production credentials inside this file.
9
+ */
10
+ export async function createTestkitClient(): Promise<SqlClient> {
11
+ throw new Error(
12
+ 'Provide a SqlClient implementation here (for example by importing @rawsql-ts/adapter-node-pg or another driver).',
13
+ );
742
14
  }