sqlstack 1.0.25 → 3.0.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 (154) hide show
  1. package/dist/cjs/adapters/mysql.d.ts.map +1 -1
  2. package/dist/cjs/adapters/mysql.js +3 -3
  3. package/dist/cjs/adapters/mysql.js.map +1 -1
  4. package/dist/cjs/adapters/pg.d.ts.map +1 -1
  5. package/dist/cjs/adapters/pg.js +3 -3
  6. package/dist/cjs/adapters/pg.js.map +1 -1
  7. package/dist/cjs/codegen/cli.d.ts +3 -0
  8. package/dist/cjs/codegen/cli.d.ts.map +1 -0
  9. package/dist/cjs/codegen/cli.js +50 -0
  10. package/dist/cjs/codegen/cli.js.map +1 -0
  11. package/dist/cjs/codegen/generateManifest.d.ts +72 -0
  12. package/dist/cjs/codegen/generateManifest.d.ts.map +1 -0
  13. package/dist/cjs/codegen/generateManifest.js +191 -0
  14. package/dist/cjs/codegen/generateManifest.js.map +1 -0
  15. package/dist/cjs/core/loadNodeModule.d.ts +17 -0
  16. package/dist/cjs/core/loadNodeModule.d.ts.map +1 -0
  17. package/dist/cjs/core/loadNodeModule.js +36 -0
  18. package/dist/cjs/core/loadNodeModule.js.map +1 -0
  19. package/dist/cjs/core/resolver.d.ts +94 -0
  20. package/dist/cjs/core/resolver.d.ts.map +1 -0
  21. package/dist/cjs/core/resolver.js +145 -0
  22. package/dist/cjs/core/resolver.js.map +1 -0
  23. package/dist/cjs/core/sourceScanResolver.d.ts +25 -0
  24. package/dist/cjs/core/sourceScanResolver.d.ts.map +1 -0
  25. package/dist/cjs/core/sourceScanResolver.js +56 -0
  26. package/dist/cjs/core/sourceScanResolver.js.map +1 -0
  27. package/dist/cjs/decorators/query.d.ts.map +1 -1
  28. package/dist/cjs/decorators/query.js +42 -14
  29. package/dist/cjs/decorators/query.js.map +1 -1
  30. package/dist/cjs/families/sqliteFamily.d.ts +26 -0
  31. package/dist/cjs/families/sqliteFamily.d.ts.map +1 -0
  32. package/dist/cjs/families/sqliteFamily.js +187 -0
  33. package/dist/cjs/families/sqliteFamily.js.map +1 -0
  34. package/dist/cjs/family.d.ts +69 -0
  35. package/dist/cjs/family.d.ts.map +1 -0
  36. package/dist/cjs/family.js +206 -0
  37. package/dist/cjs/family.js.map +1 -0
  38. package/dist/cjs/familyBinding.d.ts +22 -0
  39. package/dist/cjs/familyBinding.d.ts.map +1 -0
  40. package/dist/cjs/familyBinding.js +22 -0
  41. package/dist/cjs/familyBinding.js.map +1 -0
  42. package/dist/cjs/index.d.ts +10 -0
  43. package/dist/cjs/index.d.ts.map +1 -1
  44. package/dist/cjs/index.js +18 -1
  45. package/dist/cjs/index.js.map +1 -1
  46. package/dist/cjs/ioc.d.ts +22 -0
  47. package/dist/cjs/ioc.d.ts.map +1 -0
  48. package/dist/cjs/ioc.js +35 -0
  49. package/dist/cjs/ioc.js.map +1 -0
  50. package/dist/cjs/runtime.d.ts +25 -0
  51. package/dist/cjs/runtime.d.ts.map +1 -0
  52. package/dist/cjs/runtime.js +133 -0
  53. package/dist/cjs/runtime.js.map +1 -0
  54. package/dist/cjs/stack.d.ts +101 -0
  55. package/dist/cjs/stack.d.ts.map +1 -0
  56. package/dist/cjs/stack.js +288 -0
  57. package/dist/cjs/stack.js.map +1 -0
  58. package/dist/cjs/testing/builder.d.ts +29 -0
  59. package/dist/cjs/testing/builder.d.ts.map +1 -0
  60. package/dist/cjs/testing/builder.js +131 -0
  61. package/dist/cjs/testing/builder.js.map +1 -0
  62. package/dist/cjs/testing/index.d.ts +10 -0
  63. package/dist/cjs/testing/index.d.ts.map +1 -0
  64. package/dist/cjs/testing/index.js +18 -0
  65. package/dist/cjs/testing/index.js.map +1 -0
  66. package/dist/cjs/testing/postgres/allocator.d.ts +17 -0
  67. package/dist/cjs/testing/postgres/allocator.d.ts.map +1 -0
  68. package/dist/cjs/testing/postgres/allocator.js +54 -0
  69. package/dist/cjs/testing/postgres/allocator.js.map +1 -0
  70. package/dist/cjs/testing/postgres/database.d.ts +21 -0
  71. package/dist/cjs/testing/postgres/database.d.ts.map +1 -0
  72. package/dist/cjs/testing/postgres/database.js +39 -0
  73. package/dist/cjs/testing/postgres/database.js.map +1 -0
  74. package/dist/cjs/testing/postgres/loadData.d.ts +31 -0
  75. package/dist/cjs/testing/postgres/loadData.d.ts.map +1 -0
  76. package/dist/cjs/testing/postgres/loadData.js +184 -0
  77. package/dist/cjs/testing/postgres/loadData.js.map +1 -0
  78. package/dist/cjs/testing/postgres/server.d.ts +19 -0
  79. package/dist/cjs/testing/postgres/server.d.ts.map +1 -0
  80. package/dist/cjs/testing/postgres/server.js +78 -0
  81. package/dist/cjs/testing/postgres/server.js.map +1 -0
  82. package/dist/cjs/testing/schema/ddl.d.ts +14 -0
  83. package/dist/cjs/testing/schema/ddl.d.ts.map +1 -0
  84. package/dist/cjs/testing/schema/ddl.js +54 -0
  85. package/dist/cjs/testing/schema/ddl.js.map +1 -0
  86. package/dist/cjs/testing/schema/normalize.d.ts +7 -0
  87. package/dist/cjs/testing/schema/normalize.d.ts.map +1 -0
  88. package/dist/cjs/testing/schema/normalize.js +202 -0
  89. package/dist/cjs/testing/schema/normalize.js.map +1 -0
  90. package/dist/cjs/testing/schema/parser.d.ts +9 -0
  91. package/dist/cjs/testing/schema/parser.d.ts.map +1 -0
  92. package/dist/cjs/testing/schema/parser.js +80 -0
  93. package/dist/cjs/testing/schema/parser.js.map +1 -0
  94. package/dist/cjs/testing/schema/types.d.ts +82 -0
  95. package/dist/cjs/testing/schema/types.d.ts.map +1 -0
  96. package/dist/cjs/testing/schema/types.js +10 -0
  97. package/dist/cjs/testing/schema/types.js.map +1 -0
  98. package/dist/cjs/transactions.d.ts +14 -5
  99. package/dist/cjs/transactions.d.ts.map +1 -1
  100. package/dist/cjs/transactions.js +40 -24
  101. package/dist/cjs/transactions.js.map +1 -1
  102. package/dist/esm/adapters/mysql.js +3 -3
  103. package/dist/esm/adapters/mysql.js.map +1 -1
  104. package/dist/esm/adapters/pg.js +3 -3
  105. package/dist/esm/adapters/pg.js.map +1 -1
  106. package/dist/esm/codegen/cli.js +48 -0
  107. package/dist/esm/codegen/cli.js.map +1 -0
  108. package/dist/esm/codegen/generateManifest.js +183 -0
  109. package/dist/esm/codegen/generateManifest.js.map +1 -0
  110. package/dist/esm/core/loadNodeModule.js +33 -0
  111. package/dist/esm/core/loadNodeModule.js.map +1 -0
  112. package/dist/esm/core/resolver.js +132 -0
  113. package/dist/esm/core/resolver.js.map +1 -0
  114. package/dist/esm/core/sourceScanResolver.js +51 -0
  115. package/dist/esm/core/sourceScanResolver.js.map +1 -0
  116. package/dist/esm/decorators/query.js +43 -15
  117. package/dist/esm/decorators/query.js.map +1 -1
  118. package/dist/esm/families/sqliteFamily.js +151 -0
  119. package/dist/esm/families/sqliteFamily.js.map +1 -0
  120. package/dist/esm/family.js +202 -0
  121. package/dist/esm/family.js.map +1 -0
  122. package/dist/esm/familyBinding.js +18 -0
  123. package/dist/esm/familyBinding.js.map +1 -0
  124. package/dist/esm/index.js +8 -0
  125. package/dist/esm/index.js.map +1 -1
  126. package/dist/esm/ioc.js +32 -0
  127. package/dist/esm/ioc.js.map +1 -0
  128. package/dist/esm/runtime.js +94 -0
  129. package/dist/esm/runtime.js.map +1 -0
  130. package/dist/esm/stack.js +251 -0
  131. package/dist/esm/stack.js.map +1 -0
  132. package/dist/esm/testing/builder.js +95 -0
  133. package/dist/esm/testing/builder.js.map +1 -0
  134. package/dist/esm/testing/index.js +7 -0
  135. package/dist/esm/testing/index.js.map +1 -0
  136. package/dist/esm/testing/postgres/allocator.js +49 -0
  137. package/dist/esm/testing/postgres/allocator.js.map +1 -0
  138. package/dist/esm/testing/postgres/database.js +36 -0
  139. package/dist/esm/testing/postgres/database.js.map +1 -0
  140. package/dist/esm/testing/postgres/loadData.js +176 -0
  141. package/dist/esm/testing/postgres/loadData.js.map +1 -0
  142. package/dist/esm/testing/postgres/server.js +69 -0
  143. package/dist/esm/testing/postgres/server.js.map +1 -0
  144. package/dist/esm/testing/schema/ddl.js +49 -0
  145. package/dist/esm/testing/schema/ddl.js.map +1 -0
  146. package/dist/esm/testing/schema/normalize.js +164 -0
  147. package/dist/esm/testing/schema/normalize.js.map +1 -0
  148. package/dist/esm/testing/schema/parser.js +43 -0
  149. package/dist/esm/testing/schema/parser.js.map +1 -0
  150. package/dist/esm/testing/schema/types.js +9 -0
  151. package/dist/esm/testing/schema/types.js.map +1 -0
  152. package/dist/esm/transactions.js +39 -24
  153. package/dist/esm/transactions.js.map +1 -1
  154. package/package.json +29 -4
@@ -0,0 +1,176 @@
1
+ import { SqlStackError } from '../../core/errors.js';
2
+ import { quoteIdent } from '../schema/ddl.js';
3
+ export class TestDataError extends SqlStackError {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'TestDataError';
7
+ }
8
+ }
9
+ /** Validate one `.data()` payload against the structured schema (fail-fast). */
10
+ export function toOperations(schema, data) {
11
+ const ops = [];
12
+ const tables = new Map(schema.tables.map((t) => [t.name, t]));
13
+ for (const [tableName, rows] of Object.entries(data)) {
14
+ const table = tables.get(tableName);
15
+ if (!table) {
16
+ const known = schema.tables.map((t) => t.name).join(', ') || '(none)';
17
+ throw new TestDataError(`.data(): unknown table "${tableName}". Structured tables declared in ${schema.source}: ${known}. ` +
18
+ `Tables created only through raw SQL cannot receive .data() rows; insert them via raw SQL instead.`);
19
+ }
20
+ if (!Array.isArray(rows)) {
21
+ throw new TestDataError(`.data(): rows for table "${tableName}" must be an array`);
22
+ }
23
+ const columnNames = new Set(table.columns.map((c) => c.name));
24
+ rows.forEach((row, i) => {
25
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
26
+ throw new TestDataError(`.data(): row #${i} for table "${tableName}" must be an object`);
27
+ }
28
+ for (const key of Object.keys(row)) {
29
+ if (!columnNames.has(key)) {
30
+ throw new TestDataError(`.data(): unknown column "${key}" in row #${i} for table "${tableName}" ` +
31
+ `(declared columns: ${[...columnNames].join(', ')})`);
32
+ }
33
+ }
34
+ for (const pk of table.primaryKey) {
35
+ if (row[pk] === undefined) {
36
+ throw new TestDataError(`.data(): row #${i} for table "${tableName}" is missing primary-key column "${pk}" ` +
37
+ `(primary key: ${table.primaryKey.join(', ')})`);
38
+ }
39
+ }
40
+ ops.push({ table: tableName, row });
41
+ });
42
+ }
43
+ return ops;
44
+ }
45
+ /**
46
+ * Compose row operations into effective rows per table.
47
+ * PK tables: identity = ordered PK values; later writes replace, first
48
+ * occurrence keeps its position. No-PK tables: append in authored order.
49
+ */
50
+ export function composeRows(schema, ops) {
51
+ const tables = new Map(schema.tables.map((t) => [t.name, t]));
52
+ const result = new Map();
53
+ const identityIndex = new Map();
54
+ for (const op of ops) {
55
+ const table = tables.get(op.table);
56
+ let rows = result.get(op.table);
57
+ if (!rows) {
58
+ rows = [];
59
+ result.set(op.table, rows);
60
+ }
61
+ if (table.primaryKey.length === 0) {
62
+ rows.push(op.row);
63
+ continue;
64
+ }
65
+ const identity = JSON.stringify(table.primaryKey.map((pk) => op.row[pk]));
66
+ let index = identityIndex.get(op.table);
67
+ if (!index) {
68
+ index = new Map();
69
+ identityIndex.set(op.table, index);
70
+ }
71
+ const existing = index.get(identity);
72
+ if (existing === undefined) {
73
+ index.set(identity, rows.length);
74
+ rows.push(op.row);
75
+ }
76
+ else {
77
+ rows[existing] = op.row;
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ /**
83
+ * Deterministic insertion order for the tables that actually have rows,
84
+ * topologically sorted by declared references between those tables.
85
+ * Cycles fail with actionable guidance.
86
+ */
87
+ export function planInsertOrder(schema, tablesWithRows) {
88
+ const inPlan = new Set(tablesWithRows);
89
+ const tables = schema.tables.filter((t) => inPlan.has(t.name));
90
+ // deps[t] = set of tables (with rows) that must be inserted before t
91
+ const deps = new Map();
92
+ for (const t of tables) {
93
+ const set = new Set();
94
+ for (const c of t.columns) {
95
+ if (c.references && c.references.table !== t.name && inPlan.has(c.references.table)) {
96
+ set.add(c.references.table);
97
+ }
98
+ }
99
+ deps.set(t.name, set);
100
+ }
101
+ const order = [];
102
+ const done = new Set();
103
+ let progressed = true;
104
+ while (progressed && order.length < tables.length) {
105
+ progressed = false;
106
+ for (const t of tables) {
107
+ if (done.has(t.name))
108
+ continue;
109
+ const pending = [...deps.get(t.name)].filter((d) => !done.has(d));
110
+ if (pending.length === 0) {
111
+ order.push(t.name);
112
+ done.add(t.name);
113
+ progressed = true;
114
+ }
115
+ }
116
+ }
117
+ if (order.length < tables.length) {
118
+ const cycle = tables.filter((t) => !done.has(t.name)).map((t) => t.name);
119
+ throw new TestDataError(`Cannot derive an insertion order: declared references between tables [${cycle.join(', ')}] form a cycle. ` +
120
+ `Insert this data via a raw "sql" entry (e.g. with deferred constraints), or restructure the references.`);
121
+ }
122
+ return order;
123
+ }
124
+ /**
125
+ * Insert effective rows. Parameterized and batched per table by column
126
+ * signature; missing columns are omitted from the INSERT so PostgreSQL
127
+ * defaults apply, while explicit null is sent as NULL.
128
+ */
129
+ export async function insertRows(query, schema, effective) {
130
+ const order = planInsertOrder(schema, [...effective.keys()]);
131
+ const declared = new Map(schema.tables.map((t) => [t.name, t.columns.map((c) => c.name)]));
132
+ for (const tableName of order) {
133
+ const rows = effective.get(tableName);
134
+ const columnOrder = declared.get(tableName);
135
+ // Group consecutive-compatible rows into batches sharing a column signature.
136
+ const batches = new Map();
137
+ for (const row of rows) {
138
+ const columns = columnOrder.filter((c) => row[c] !== undefined);
139
+ const sig = columns.join(',');
140
+ let batch = batches.get(sig);
141
+ if (!batch) {
142
+ batch = { columns, rows: [] };
143
+ batches.set(sig, batch);
144
+ }
145
+ batch.rows.push(row);
146
+ }
147
+ for (const { columns, rows: batchRows } of batches.values()) {
148
+ if (columns.length === 0) {
149
+ for (let i = 0; i < batchRows.length; i++) {
150
+ await run(query, tableName, `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES`, []);
151
+ }
152
+ continue;
153
+ }
154
+ const params = [];
155
+ const tuples = batchRows.map((row) => {
156
+ const placeholders = columns.map((c) => {
157
+ params.push(row[c]);
158
+ return `$${params.length}`;
159
+ });
160
+ return `(${placeholders.join(', ')})`;
161
+ });
162
+ const sql = `INSERT INTO ${quoteIdent(tableName)} (${columns.map(quoteIdent).join(', ')}) ` +
163
+ `VALUES ${tuples.join(', ')}`;
164
+ await run(query, tableName, sql, params);
165
+ }
166
+ }
167
+ }
168
+ async function run(query, tableName, sql, params) {
169
+ try {
170
+ await query(sql, params);
171
+ }
172
+ catch (err) {
173
+ throw new TestDataError(`Failed to insert scenario rows into table "${tableName}": ${err.message}`);
174
+ }
175
+ }
176
+ //# sourceMappingURL=loadData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadData.js","sourceRoot":"","sources":["../../../../src/testing/postgres/loadData.ts"],"names":[],"mappings":"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","sourcesContent":["import { SqlStackError } from '../../core/errors';\nimport { quoteIdent } from '../schema/ddl';\nimport type { NormalizedTestSchema } from '../schema/types';\n\nexport class TestDataError extends SqlStackError {\n constructor(message: string) {\n super(message);\n this.name = 'TestDataError';\n }\n}\n\nexport type Row = Record<string, unknown>;\n\nexport interface DataOperation {\n readonly table: string;\n readonly row: Readonly<Row>;\n}\n\n/** Validate one `.data()` payload against the structured schema (fail-fast). */\nexport function toOperations(schema: NormalizedTestSchema, data: Record<string, Row[]>): DataOperation[] {\n const ops: DataOperation[] = [];\n const tables = new Map(schema.tables.map((t) => [t.name, t]));\n for (const [tableName, rows] of Object.entries(data)) {\n const table = tables.get(tableName);\n if (!table) {\n const known = schema.tables.map((t) => t.name).join(', ') || '(none)';\n throw new TestDataError(\n `.data(): unknown table \"${tableName}\". Structured tables declared in ${schema.source}: ${known}. ` +\n `Tables created only through raw SQL cannot receive .data() rows; insert them via raw SQL instead.`\n );\n }\n if (!Array.isArray(rows)) {\n throw new TestDataError(`.data(): rows for table \"${tableName}\" must be an array`);\n }\n const columnNames = new Set(table.columns.map((c) => c.name));\n rows.forEach((row, i) => {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new TestDataError(`.data(): row #${i} for table \"${tableName}\" must be an object`);\n }\n for (const key of Object.keys(row)) {\n if (!columnNames.has(key)) {\n throw new TestDataError(\n `.data(): unknown column \"${key}\" in row #${i} for table \"${tableName}\" ` +\n `(declared columns: ${[...columnNames].join(', ')})`\n );\n }\n }\n for (const pk of table.primaryKey) {\n if (row[pk] === undefined) {\n throw new TestDataError(\n `.data(): row #${i} for table \"${tableName}\" is missing primary-key column \"${pk}\" ` +\n `(primary key: ${table.primaryKey.join(', ')})`\n );\n }\n }\n ops.push({ table: tableName, row });\n });\n }\n return ops;\n}\n\n/**\n * Compose row operations into effective rows per table.\n * PK tables: identity = ordered PK values; later writes replace, first\n * occurrence keeps its position. No-PK tables: append in authored order.\n */\nexport function composeRows(\n schema: NormalizedTestSchema,\n ops: readonly DataOperation[]\n): Map<string, Row[]> {\n const tables = new Map(schema.tables.map((t) => [t.name, t]));\n const result = new Map<string, Row[]>();\n const identityIndex = new Map<string, Map<string, number>>();\n for (const op of ops) {\n const table = tables.get(op.table)!;\n let rows = result.get(op.table);\n if (!rows) {\n rows = [];\n result.set(op.table, rows);\n }\n if (table.primaryKey.length === 0) {\n rows.push(op.row);\n continue;\n }\n const identity = JSON.stringify(table.primaryKey.map((pk) => op.row[pk]));\n let index = identityIndex.get(op.table);\n if (!index) {\n index = new Map();\n identityIndex.set(op.table, index);\n }\n const existing = index.get(identity);\n if (existing === undefined) {\n index.set(identity, rows.length);\n rows.push(op.row);\n } else {\n rows[existing] = op.row;\n }\n }\n return result;\n}\n\n/**\n * Deterministic insertion order for the tables that actually have rows,\n * topologically sorted by declared references between those tables.\n * Cycles fail with actionable guidance.\n */\nexport function planInsertOrder(schema: NormalizedTestSchema, tablesWithRows: readonly string[]): string[] {\n const inPlan = new Set(tablesWithRows);\n const tables = schema.tables.filter((t) => inPlan.has(t.name));\n // deps[t] = set of tables (with rows) that must be inserted before t\n const deps = new Map<string, Set<string>>();\n for (const t of tables) {\n const set = new Set<string>();\n for (const c of t.columns) {\n if (c.references && c.references.table !== t.name && inPlan.has(c.references.table)) {\n set.add(c.references.table);\n }\n }\n deps.set(t.name, set);\n }\n const order: string[] = [];\n const done = new Set<string>();\n let progressed = true;\n while (progressed && order.length < tables.length) {\n progressed = false;\n for (const t of tables) {\n if (done.has(t.name)) continue;\n const pending = [...deps.get(t.name)!].filter((d) => !done.has(d));\n if (pending.length === 0) {\n order.push(t.name);\n done.add(t.name);\n progressed = true;\n }\n }\n }\n if (order.length < tables.length) {\n const cycle = tables.filter((t) => !done.has(t.name)).map((t) => t.name);\n throw new TestDataError(\n `Cannot derive an insertion order: declared references between tables [${cycle.join(', ')}] form a cycle. ` +\n `Insert this data via a raw \"sql\" entry (e.g. with deferred constraints), or restructure the references.`\n );\n }\n return order;\n}\n\n/**\n * Insert effective rows. Parameterized and batched per table by column\n * signature; missing columns are omitted from the INSERT so PostgreSQL\n * defaults apply, while explicit null is sent as NULL.\n */\nexport async function insertRows(\n query: (sql: string, params: unknown[]) => Promise<unknown>,\n schema: NormalizedTestSchema,\n effective: Map<string, Row[]>\n): Promise<void> {\n const order = planInsertOrder(schema, [...effective.keys()]);\n const declared = new Map(schema.tables.map((t) => [t.name, t.columns.map((c) => c.name)]));\n for (const tableName of order) {\n const rows = effective.get(tableName)!;\n const columnOrder = declared.get(tableName)!;\n // Group consecutive-compatible rows into batches sharing a column signature.\n const batches = new Map<string, { columns: string[]; rows: Row[] }>();\n for (const row of rows) {\n const columns = columnOrder.filter((c) => row[c] !== undefined);\n const sig = columns.join(',');\n let batch = batches.get(sig);\n if (!batch) {\n batch = { columns, rows: [] };\n batches.set(sig, batch);\n }\n batch.rows.push(row);\n }\n for (const { columns, rows: batchRows } of batches.values()) {\n if (columns.length === 0) {\n for (let i = 0; i < batchRows.length; i++) {\n await run(query, tableName, `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES`, []);\n }\n continue;\n }\n const params: unknown[] = [];\n const tuples = batchRows.map((row) => {\n const placeholders = columns.map((c) => {\n params.push(row[c]);\n return `$${params.length}`;\n });\n return `(${placeholders.join(', ')})`;\n });\n const sql =\n `INSERT INTO ${quoteIdent(tableName)} (${columns.map(quoteIdent).join(', ')}) ` +\n `VALUES ${tuples.join(', ')}`;\n await run(query, tableName, sql, params);\n }\n }\n}\n\nasync function run(\n query: (sql: string, params: unknown[]) => Promise<unknown>,\n tableName: string,\n sql: string,\n params: unknown[]\n): Promise<void> {\n try {\n await query(sql, params);\n } catch (err) {\n throw new TestDataError(\n `Failed to insert scenario rows into table \"${tableName}\": ${(err as Error).message}`\n );\n }\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { SqlStackError } from '../../core/errors.js';
2
+ /** Default matches docker/docker-compose.pg.yml (npm run db:pg:up). */
3
+ export const DEFAULT_ADMIN_URL = 'postgres://postgres:postgres@127.0.0.1:55432/postgres';
4
+ export class TestPostgresServerError extends SqlStackError {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'TestPostgresServerError';
8
+ }
9
+ }
10
+ export function resolveAdminUrl(options = {}) {
11
+ return options.adminUrl ?? process.env.SQLSTACK_TEST_PG_URL ?? DEFAULT_ADMIN_URL;
12
+ }
13
+ /**
14
+ * Load the `pg` driver lazily so browser/Worker bundles that never import
15
+ * sqlstack/testing do not see it. Testing is Node-only.
16
+ */
17
+ const dynamicImport =
18
+ // Indirect so neither tsc's CJS downlevel nor bundlers statically link "pg".
19
+ new Function('specifier', 'return import(specifier)');
20
+ export async function loadPg() {
21
+ // CJS path first (also covers jest's VM, where dynamic import is unavailable).
22
+ try {
23
+ if (typeof require === 'function')
24
+ return require('pg');
25
+ }
26
+ catch {
27
+ /* fall through to dynamic import */
28
+ }
29
+ try {
30
+ return await dynamicImport('pg');
31
+ }
32
+ catch {
33
+ throw new TestPostgresServerError(`sqlstack/testing requires the optional "pg" package. Install it in your project (npm i -D pg).`);
34
+ }
35
+ }
36
+ /** Create a connected admin client, with an actionable diagnostic on failure. */
37
+ export async function connectAdmin(options = {}) {
38
+ const url = resolveAdminUrl(options);
39
+ const pg = await loadPg();
40
+ const Client = pg.Client ?? pg.default?.Client;
41
+ const client = new Client({ connectionString: url });
42
+ try {
43
+ await client.connect();
44
+ }
45
+ catch (err) {
46
+ try {
47
+ await client.end();
48
+ }
49
+ catch {
50
+ /* ignore */
51
+ }
52
+ throw new TestPostgresServerError(`Cannot connect to the test PostgreSQL server at ${redact(url)}: ${err.message}\n` +
53
+ `- If you use the bundled docker server, start it with: npm run db:pg:up\n` +
54
+ `- Otherwise set SQLSTACK_TEST_PG_URL (or pass options.adminUrl) to an admin URL with ` +
55
+ `permission to CREATE/DROP DATABASE.`);
56
+ }
57
+ return client;
58
+ }
59
+ /** Hide password in URLs used inside diagnostics. */
60
+ export function redact(url) {
61
+ return url.replace(/\/\/([^:@/]+):[^@/]+@/, '//$1:***@');
62
+ }
63
+ /** Replace the database segment of a postgres URL. */
64
+ export function withDatabase(url, dbName) {
65
+ const u = new URL(url);
66
+ u.pathname = `/${dbName}`;
67
+ return u.toString();
68
+ }
69
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../../src/testing/postgres/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,uDAAuD,CAAC;AAEzF,MAAM,OAAO,uBAAwB,SAAQ,aAAa;IACxD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAOD,MAAM,UAAU,eAAe,CAAC,UAA6B,EAAE;IAC7D,OAAO,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,iBAAiB,CAAC;AACnF,CAAC;AAED;;;GAGG;AACH,MAAM,aAAa;AACjB,6EAA6E;AAC7E,IAAI,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAQ,CAAC;AAE/D,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,+EAA+E;IAC/E,IAAI,CAAC;QACH,IAAI,OAAO,OAAO,KAAK,UAAU;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,uBAAuB,CAC/B,gGAAgG,CACjG,CAAC;IACJ,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,UAA6B,EAAE;IAChE,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;QACD,MAAM,IAAI,uBAAuB,CAC/B,mDAAmD,MAAM,CAAC,GAAG,CAAC,KAAM,GAAa,CAAC,OAAO,IAAI;YAC3F,2EAA2E;YAC3E,uFAAuF;YACvF,qCAAqC,CACxC,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,MAAM,CAAC,GAAW;IAChC,OAAO,GAAG,CAAC,OAAO,CAAC,uBAAuB,EAAE,WAAW,CAAC,CAAC;AAC3D,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,MAAc;IACtD,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,QAAQ,GAAG,IAAI,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtB,CAAC","sourcesContent":["import { SqlStackError } from '../../core/errors';\n\n/** Default matches docker/docker-compose.pg.yml (npm run db:pg:up). */\nexport const DEFAULT_ADMIN_URL = 'postgres://postgres:postgres@127.0.0.1:55432/postgres';\n\nexport class TestPostgresServerError extends SqlStackError {\n constructor(message: string) {\n super(message);\n this.name = 'TestPostgresServerError';\n }\n}\n\nexport interface TestServerOptions {\n /** Admin connection URL with CREATE/DROP DATABASE privilege. */\n adminUrl?: string;\n}\n\nexport function resolveAdminUrl(options: TestServerOptions = {}): string {\n return options.adminUrl ?? process.env.SQLSTACK_TEST_PG_URL ?? DEFAULT_ADMIN_URL;\n}\n\n/**\n * Load the `pg` driver lazily so browser/Worker bundles that never import\n * sqlstack/testing do not see it. Testing is Node-only.\n */\nconst dynamicImport: (specifier: string) => Promise<any> =\n // Indirect so neither tsc's CJS downlevel nor bundlers statically link \"pg\".\n new Function('specifier', 'return import(specifier)') as any;\n\nexport async function loadPg(): Promise<any> {\n // CJS path first (also covers jest's VM, where dynamic import is unavailable).\n try {\n if (typeof require === 'function') return require('pg');\n } catch {\n /* fall through to dynamic import */\n }\n try {\n return await dynamicImport('pg');\n } catch {\n throw new TestPostgresServerError(\n `sqlstack/testing requires the optional \"pg\" package. Install it in your project (npm i -D pg).`\n );\n }\n}\n\n/** Create a connected admin client, with an actionable diagnostic on failure. */\nexport async function connectAdmin(options: TestServerOptions = {}): Promise<any> {\n const url = resolveAdminUrl(options);\n const pg = await loadPg();\n const Client = pg.Client ?? pg.default?.Client;\n const client = new Client({ connectionString: url });\n try {\n await client.connect();\n } catch (err) {\n try {\n await client.end();\n } catch {\n /* ignore */\n }\n throw new TestPostgresServerError(\n `Cannot connect to the test PostgreSQL server at ${redact(url)}: ${(err as Error).message}\\n` +\n `- If you use the bundled docker server, start it with: npm run db:pg:up\\n` +\n `- Otherwise set SQLSTACK_TEST_PG_URL (or pass options.adminUrl) to an admin URL with ` +\n `permission to CREATE/DROP DATABASE.`\n );\n }\n return client;\n}\n\n/** Hide password in URLs used inside diagnostics. */\nexport function redact(url: string): string {\n return url.replace(/\\/\\/([^:@/]+):[^@/]+@/, '//$1:***@');\n}\n\n/** Replace the database segment of a postgres URL. */\nexport function withDatabase(url: string, dbName: string): string {\n const u = new URL(url);\n u.pathname = `/${dbName}`;\n return u.toString();\n}\n"]}
@@ -0,0 +1,49 @@
1
+ /** Quote a (pre-validated) identifier for PostgreSQL. */
2
+ export function quoteIdent(name) {
3
+ return `"${name.replace(/"/g, '""')}"`;
4
+ }
5
+ function renderDefault(value) {
6
+ // String defaults are raw SQL expressions (e.g. now(), 'active', gen_random_uuid()).
7
+ if (typeof value === 'string')
8
+ return value;
9
+ if (typeof value === 'boolean')
10
+ return value ? 'TRUE' : 'FALSE';
11
+ return String(value);
12
+ }
13
+ export function generateTableDdl(table) {
14
+ const lines = [];
15
+ for (const col of table.columns) {
16
+ let line = ` ${quoteIdent(col.name)} ${col.type}`;
17
+ if (!col.nullable && !col.primary)
18
+ line += ' NOT NULL';
19
+ if (col.unique && !col.primary)
20
+ line += ' UNIQUE';
21
+ if (col.hasDefault)
22
+ line += ` DEFAULT ${renderDefault(col.default)}`;
23
+ lines.push(line);
24
+ }
25
+ if (table.primaryKey.length > 0) {
26
+ lines.push(` PRIMARY KEY (${table.primaryKey.map(quoteIdent).join(', ')})`);
27
+ }
28
+ return `CREATE TABLE ${quoteIdent(table.name)} (\n${lines.join(',\n')}\n)`;
29
+ }
30
+ /**
31
+ * Deterministic DDL for the structured tables.
32
+ *
33
+ * Tables are created in authored order without inline foreign keys; declared
34
+ * references are then added as ALTER TABLE statements so authored table
35
+ * order never has to match dependency order. Raw SQL entries are applied by
36
+ * the builder after these statements, in authored order.
37
+ */
38
+ export function generateDdl(schema) {
39
+ const statements = schema.tables.map(generateTableDdl);
40
+ for (const table of schema.tables) {
41
+ for (const col of table.columns) {
42
+ if (col.references) {
43
+ statements.push(`ALTER TABLE ${quoteIdent(table.name)} ADD CONSTRAINT ${quoteIdent(`fk_${table.name}_${col.name}`)} FOREIGN KEY (${quoteIdent(col.name)}) REFERENCES ${quoteIdent(col.references.table)} (${quoteIdent(col.references.column)})`);
44
+ }
45
+ }
46
+ }
47
+ return statements;
48
+ }
49
+ //# sourceMappingURL=ddl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ddl.js","sourceRoot":"","sources":["../../../../src/testing/schema/ddl.ts"],"names":[],"mappings":"AAEA,yDAAyD;AACzD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,SAAS,aAAa,CAAC,KAAgC;IACrD,qFAAqF;IACrF,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAChE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAsB;IACrD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAChC,IAAI,IAAI,GAAG,KAAK,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;QACnD,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,IAAI,IAAI,WAAW,CAAC;QACvD,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,IAAI,IAAI,SAAS,CAAC;QAClD,IAAI,GAAG,CAAC,UAAU;YAAE,IAAI,IAAI,YAAY,aAAa,CAAC,GAAG,CAAC,OAAQ,CAAC,EAAE,CAAC;QACtE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,kBAAkB,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,gBAAgB,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,MAA4B;IACtD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACvD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;gBACnB,UAAU,CAAC,IAAI,CACb,eAAe,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,UAAU,CAChE,MAAM,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,CAC/B,iBAAiB,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,UAAU,CAC9D,GAAG,CAAC,UAAU,CAAC,KAAK,CACrB,KAAK,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAC3C,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC","sourcesContent":["import type { NormalizedTable, NormalizedTestSchema } from './types';\n\n/** Quote a (pre-validated) identifier for PostgreSQL. */\nexport function quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction renderDefault(value: string | number | boolean): string {\n // String defaults are raw SQL expressions (e.g. now(), 'active', gen_random_uuid()).\n if (typeof value === 'string') return value;\n if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';\n return String(value);\n}\n\nexport function generateTableDdl(table: NormalizedTable): string {\n const lines: string[] = [];\n for (const col of table.columns) {\n let line = ` ${quoteIdent(col.name)} ${col.type}`;\n if (!col.nullable && !col.primary) line += ' NOT NULL';\n if (col.unique && !col.primary) line += ' UNIQUE';\n if (col.hasDefault) line += ` DEFAULT ${renderDefault(col.default!)}`;\n lines.push(line);\n }\n if (table.primaryKey.length > 0) {\n lines.push(` PRIMARY KEY (${table.primaryKey.map(quoteIdent).join(', ')})`);\n }\n return `CREATE TABLE ${quoteIdent(table.name)} (\\n${lines.join(',\\n')}\\n)`;\n}\n\n/**\n * Deterministic DDL for the structured tables.\n *\n * Tables are created in authored order without inline foreign keys; declared\n * references are then added as ALTER TABLE statements so authored table\n * order never has to match dependency order. Raw SQL entries are applied by\n * the builder after these statements, in authored order.\n */\nexport function generateDdl(schema: NormalizedTestSchema): string[] {\n const statements = schema.tables.map(generateTableDdl);\n for (const table of schema.tables) {\n for (const col of table.columns) {\n if (col.references) {\n statements.push(\n `ALTER TABLE ${quoteIdent(table.name)} ADD CONSTRAINT ${quoteIdent(\n `fk_${table.name}_${col.name}`\n )} FOREIGN KEY (${quoteIdent(col.name)}) REFERENCES ${quoteIdent(\n col.references.table\n )} (${quoteIdent(col.references.column)})`\n );\n }\n }\n }\n return statements;\n}\n"]}
@@ -0,0 +1,164 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { SqlStackError } from '../../core/errors.js';
4
+ export class TestSchemaError extends SqlStackError {
5
+ constructor(message, source) {
6
+ super(source ? `${message} (schema source: ${source})` : message);
7
+ this.name = 'TestSchemaError';
8
+ }
9
+ }
10
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
11
+ function assertIdentifier(kind, name, source) {
12
+ if (!IDENT.test(name)) {
13
+ throw new TestSchemaError(`Invalid ${kind} name ${JSON.stringify(name)}: names must match /^[A-Za-z_][A-Za-z0-9_]*$/`, source);
14
+ }
15
+ }
16
+ function parseReference(raw, where, source) {
17
+ if (typeof raw !== 'string') {
18
+ throw new TestSchemaError(`Invalid "references" at ${where}: expected a string like "table.column"`, source);
19
+ }
20
+ // Accept "table.column" or "table(column)".
21
+ let m = /^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(raw);
22
+ if (!m)
23
+ m = /^([A-Za-z_][A-Za-z0-9_]*)\(([A-Za-z_][A-Za-z0-9_]*)\)$/.exec(raw);
24
+ if (!m) {
25
+ throw new TestSchemaError(`Invalid "references" syntax ${JSON.stringify(raw)} at ${where}: use "table.column" or "table(column)"`, source);
26
+ }
27
+ return { table: m[1], column: m[2] };
28
+ }
29
+ const COLUMN_KEYS = new Set(['type', 'nullable', 'primary', 'unique', 'default', 'references']);
30
+ function normalizeColumn(tableName, name, decl, source) {
31
+ const where = `${tableName}.${name}`;
32
+ assertIdentifier('column', name, source);
33
+ let obj;
34
+ if (typeof decl === 'string') {
35
+ obj = { type: decl };
36
+ }
37
+ else if (decl && typeof decl === 'object' && !Array.isArray(decl)) {
38
+ obj = decl;
39
+ }
40
+ else {
41
+ throw new TestSchemaError(`Malformed column declaration at ${where}: expected a type string or an object`, source);
42
+ }
43
+ for (const key of Object.keys(obj)) {
44
+ if (!COLUMN_KEYS.has(key)) {
45
+ throw new TestSchemaError(`Unknown column property ${JSON.stringify(key)} at ${where}: supported properties are ${[...COLUMN_KEYS].join(', ')}`, source);
46
+ }
47
+ }
48
+ const type = obj.type;
49
+ if (typeof type !== 'string' || type.trim() === '') {
50
+ throw new TestSchemaError(`Missing or invalid "type" at ${where}: a non-empty string is required`, source);
51
+ }
52
+ for (const flag of ['nullable', 'primary', 'unique']) {
53
+ if (obj[flag] !== undefined && typeof obj[flag] !== 'boolean') {
54
+ throw new TestSchemaError(`Invalid "${flag}" at ${where}: expected a boolean`, source);
55
+ }
56
+ }
57
+ const def = obj.default;
58
+ if (def !== undefined && !['string', 'number', 'boolean'].includes(typeof def)) {
59
+ throw new TestSchemaError(`Invalid "default" at ${where}: expected a string, number, or boolean`, source);
60
+ }
61
+ const primary = obj.primary === true;
62
+ return {
63
+ name,
64
+ type: type.trim(),
65
+ nullable: obj.nullable === undefined ? !primary : obj.nullable,
66
+ primary,
67
+ unique: obj.unique === true,
68
+ hasDefault: def !== undefined,
69
+ default: def,
70
+ references: obj.references === undefined ? undefined : parseReference(obj.references, where, source),
71
+ };
72
+ }
73
+ function normalizeTable(name, decl, source) {
74
+ assertIdentifier('table', name, source);
75
+ if (!decl || typeof decl !== 'object' || Array.isArray(decl)) {
76
+ throw new TestSchemaError(`Malformed table declaration "${name}": expected an object with "columns"`, source);
77
+ }
78
+ const columnsDecl = decl.columns;
79
+ if (!columnsDecl || typeof columnsDecl !== 'object' || Array.isArray(columnsDecl)) {
80
+ throw new TestSchemaError(`Table "${name}" is missing a "columns" map`, source);
81
+ }
82
+ const columns = Object.entries(columnsDecl).map(([colName, colDecl]) => normalizeColumn(name, colName, colDecl, source));
83
+ if (columns.length === 0) {
84
+ throw new TestSchemaError(`Table "${name}" declares no columns`, source);
85
+ }
86
+ const primaryKey = columns.filter((c) => c.primary).map((c) => c.name);
87
+ return { name, columns: Object.freeze(columns), primaryKey: Object.freeze(primaryKey) };
88
+ }
89
+ function normalizeSqlEntry(raw, index, options, source) {
90
+ if (typeof raw !== 'string' || raw.trim() === '') {
91
+ throw new TestSchemaError(`Invalid sql entry #${index}: expected a non-empty string`, source);
92
+ }
93
+ const looksLikeFile = !raw.includes('\n') && /\.sql$/i.test(raw.trim());
94
+ if (!looksLikeFile) {
95
+ return { kind: 'inline', index, sql: raw };
96
+ }
97
+ const authored = raw.trim();
98
+ if (!options.sourceDir) {
99
+ throw new TestSchemaError(`sql entry #${index} references file "${authored}" but no sourceDir is available. ` +
100
+ `Pass the schema as a file path, or provide { sourceDir } so relative .sql paths can be resolved ` +
101
+ `(paths are never resolved against process.cwd() implicitly).`, source);
102
+ }
103
+ const resolvedPath = path.resolve(options.sourceDir, authored);
104
+ if (!fs.existsSync(resolvedPath)) {
105
+ throw new TestSchemaError(`sql entry #${index}: file not found: ${authored} (resolved to ${resolvedPath})`, source);
106
+ }
107
+ return { kind: 'file', index, sourcePath: authored, resolvedPath };
108
+ }
109
+ export function normalizeTestSchema(doc, options = {}) {
110
+ const source = options.sourceLabel ?? '<inline schema>';
111
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
112
+ throw new TestSchemaError('Test schema must be a YAML mapping / object', source);
113
+ }
114
+ const root = doc;
115
+ if (root.version !== 1) {
116
+ throw new TestSchemaError(`Unsupported test schema version: ${JSON.stringify(root.version)} (expected: version: 1)`, source);
117
+ }
118
+ if (root.dialect !== 'postgres') {
119
+ throw new TestSchemaError(`Unsupported test schema dialect: ${JSON.stringify(root.dialect)} (expected: dialect: postgres)`, source);
120
+ }
121
+ for (const key of Object.keys(root)) {
122
+ if (!['version', 'dialect', 'tables', 'sql'].includes(key)) {
123
+ throw new TestSchemaError(`Unknown top-level key ${JSON.stringify(key)} in test schema`, source);
124
+ }
125
+ }
126
+ let tables = [];
127
+ if (root.tables !== undefined) {
128
+ if (!root.tables || typeof root.tables !== 'object' || Array.isArray(root.tables)) {
129
+ throw new TestSchemaError('"tables" must be a mapping of table name to declaration', source);
130
+ }
131
+ tables = Object.entries(root.tables).map(([name, decl]) => normalizeTable(name, decl, source));
132
+ }
133
+ // Validate references that point at structured tables.
134
+ const byName = new Map(tables.map((t) => [t.name, t]));
135
+ for (const t of tables) {
136
+ for (const c of t.columns) {
137
+ const ref = c.references;
138
+ if (ref && byName.has(ref.table)) {
139
+ const target = byName.get(ref.table);
140
+ if (!target.columns.some((tc) => tc.name === ref.column)) {
141
+ throw new TestSchemaError(`Column ${t.name}.${c.name} references ${ref.table}.${ref.column}, but table "${ref.table}" declares no column "${ref.column}"`, source);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ let sql = [];
147
+ if (root.sql !== undefined) {
148
+ if (!Array.isArray(root.sql)) {
149
+ throw new TestSchemaError('"sql" must be a list of inline SQL strings or .sql file paths', source);
150
+ }
151
+ sql = root.sql.map((entry, i) => normalizeSqlEntry(entry, i, options, source));
152
+ }
153
+ if (tables.length === 0 && sql.length === 0) {
154
+ throw new TestSchemaError('Test schema declares no tables and no sql entries', source);
155
+ }
156
+ return Object.freeze({
157
+ version: 1,
158
+ dialect: 'postgres',
159
+ tables: Object.freeze(tables),
160
+ sql: Object.freeze(sql),
161
+ source,
162
+ });
163
+ }
164
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../../../src/testing/schema/normalize.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\nimport { SqlStackError } from '../../core/errors';\nimport type {\n NormalizedColumn,\n NormalizedReference,\n NormalizedTable,\n NormalizedTestSchema,\n ParseTestSchemaOptions,\n RawSqlEntry,\n} from './types';\n\nexport class TestSchemaError extends SqlStackError {\n constructor(message: string, source?: string) {\n super(source ? `${message} (schema source: ${source})` : message);\n this.name = 'TestSchemaError';\n }\n}\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction assertIdentifier(kind: string, name: string, source: string): void {\n if (!IDENT.test(name)) {\n throw new TestSchemaError(\n `Invalid ${kind} name ${JSON.stringify(name)}: names must match /^[A-Za-z_][A-Za-z0-9_]*$/`,\n source\n );\n }\n}\n\nfunction parseReference(raw: unknown, where: string, source: string): NormalizedReference {\n if (typeof raw !== 'string') {\n throw new TestSchemaError(`Invalid \"references\" at ${where}: expected a string like \"table.column\"`, source);\n }\n // Accept \"table.column\" or \"table(column)\".\n let m = /^([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(raw);\n if (!m) m = /^([A-Za-z_][A-Za-z0-9_]*)\\(([A-Za-z_][A-Za-z0-9_]*)\\)$/.exec(raw);\n if (!m) {\n throw new TestSchemaError(\n `Invalid \"references\" syntax ${JSON.stringify(raw)} at ${where}: use \"table.column\" or \"table(column)\"`,\n source\n );\n }\n return { table: m[1]!, column: m[2]! };\n}\n\nconst COLUMN_KEYS = new Set(['type', 'nullable', 'primary', 'unique', 'default', 'references']);\n\nfunction normalizeColumn(tableName: string, name: string, decl: unknown, source: string): NormalizedColumn {\n const where = `${tableName}.${name}`;\n assertIdentifier('column', name, source);\n let obj: Record<string, unknown>;\n if (typeof decl === 'string') {\n obj = { type: decl };\n } else if (decl && typeof decl === 'object' && !Array.isArray(decl)) {\n obj = decl as Record<string, unknown>;\n } else {\n throw new TestSchemaError(\n `Malformed column declaration at ${where}: expected a type string or an object`,\n source\n );\n }\n for (const key of Object.keys(obj)) {\n if (!COLUMN_KEYS.has(key)) {\n throw new TestSchemaError(\n `Unknown column property ${JSON.stringify(key)} at ${where}: supported properties are ${[...COLUMN_KEYS].join(', ')}`,\n source\n );\n }\n }\n const type = obj.type;\n if (typeof type !== 'string' || type.trim() === '') {\n throw new TestSchemaError(`Missing or invalid \"type\" at ${where}: a non-empty string is required`, source);\n }\n for (const flag of ['nullable', 'primary', 'unique'] as const) {\n if (obj[flag] !== undefined && typeof obj[flag] !== 'boolean') {\n throw new TestSchemaError(`Invalid \"${flag}\" at ${where}: expected a boolean`, source);\n }\n }\n const def = obj.default;\n if (def !== undefined && !['string', 'number', 'boolean'].includes(typeof def)) {\n throw new TestSchemaError(`Invalid \"default\" at ${where}: expected a string, number, or boolean`, source);\n }\n const primary = obj.primary === true;\n return {\n name,\n type: type.trim(),\n nullable: obj.nullable === undefined ? !primary : (obj.nullable as boolean),\n primary,\n unique: obj.unique === true,\n hasDefault: def !== undefined,\n default: def as string | number | boolean | undefined,\n references: obj.references === undefined ? undefined : parseReference(obj.references, where, source),\n };\n}\n\nfunction normalizeTable(name: string, decl: unknown, source: string): NormalizedTable {\n assertIdentifier('table', name, source);\n if (!decl || typeof decl !== 'object' || Array.isArray(decl)) {\n throw new TestSchemaError(`Malformed table declaration \"${name}\": expected an object with \"columns\"`, source);\n }\n const columnsDecl = (decl as Record<string, unknown>).columns;\n if (!columnsDecl || typeof columnsDecl !== 'object' || Array.isArray(columnsDecl)) {\n throw new TestSchemaError(`Table \"${name}\" is missing a \"columns\" map`, source);\n }\n const columns = Object.entries(columnsDecl as Record<string, unknown>).map(([colName, colDecl]) =>\n normalizeColumn(name, colName, colDecl, source)\n );\n if (columns.length === 0) {\n throw new TestSchemaError(`Table \"${name}\" declares no columns`, source);\n }\n const primaryKey = columns.filter((c) => c.primary).map((c) => c.name);\n return { name, columns: Object.freeze(columns), primaryKey: Object.freeze(primaryKey) };\n}\n\nfunction normalizeSqlEntry(\n raw: unknown,\n index: number,\n options: ParseTestSchemaOptions,\n source: string\n): RawSqlEntry {\n if (typeof raw !== 'string' || raw.trim() === '') {\n throw new TestSchemaError(`Invalid sql entry #${index}: expected a non-empty string`, source);\n }\n const looksLikeFile = !raw.includes('\\n') && /\\.sql$/i.test(raw.trim());\n if (!looksLikeFile) {\n return { kind: 'inline', index, sql: raw };\n }\n const authored = raw.trim();\n if (!options.sourceDir) {\n throw new TestSchemaError(\n `sql entry #${index} references file \"${authored}\" but no sourceDir is available. ` +\n `Pass the schema as a file path, or provide { sourceDir } so relative .sql paths can be resolved ` +\n `(paths are never resolved against process.cwd() implicitly).`,\n source\n );\n }\n const resolvedPath = path.resolve(options.sourceDir, authored);\n if (!fs.existsSync(resolvedPath)) {\n throw new TestSchemaError(\n `sql entry #${index}: file not found: ${authored} (resolved to ${resolvedPath})`,\n source\n );\n }\n return { kind: 'file', index, sourcePath: authored, resolvedPath };\n}\n\nexport function normalizeTestSchema(\n doc: unknown,\n options: ParseTestSchemaOptions = {}\n): NormalizedTestSchema {\n const source = options.sourceLabel ?? '<inline schema>';\n if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {\n throw new TestSchemaError('Test schema must be a YAML mapping / object', source);\n }\n const root = doc as Record<string, unknown>;\n if (root.version !== 1) {\n throw new TestSchemaError(\n `Unsupported test schema version: ${JSON.stringify(root.version)} (expected: version: 1)`,\n source\n );\n }\n if (root.dialect !== 'postgres') {\n throw new TestSchemaError(\n `Unsupported test schema dialect: ${JSON.stringify(root.dialect)} (expected: dialect: postgres)`,\n source\n );\n }\n for (const key of Object.keys(root)) {\n if (!['version', 'dialect', 'tables', 'sql'].includes(key)) {\n throw new TestSchemaError(`Unknown top-level key ${JSON.stringify(key)} in test schema`, source);\n }\n }\n\n let tables: NormalizedTable[] = [];\n if (root.tables !== undefined) {\n if (!root.tables || typeof root.tables !== 'object' || Array.isArray(root.tables)) {\n throw new TestSchemaError('\"tables\" must be a mapping of table name to declaration', source);\n }\n tables = Object.entries(root.tables as Record<string, unknown>).map(([name, decl]) =>\n normalizeTable(name, decl, source)\n );\n }\n\n // Validate references that point at structured tables.\n const byName = new Map(tables.map((t) => [t.name, t]));\n for (const t of tables) {\n for (const c of t.columns) {\n const ref = c.references;\n if (ref && byName.has(ref.table)) {\n const target = byName.get(ref.table)!;\n if (!target.columns.some((tc) => tc.name === ref.column)) {\n throw new TestSchemaError(\n `Column ${t.name}.${c.name} references ${ref.table}.${ref.column}, but table \"${ref.table}\" declares no column \"${ref.column}\"`,\n source\n );\n }\n }\n }\n }\n\n let sql: RawSqlEntry[] = [];\n if (root.sql !== undefined) {\n if (!Array.isArray(root.sql)) {\n throw new TestSchemaError('\"sql\" must be a list of inline SQL strings or .sql file paths', source);\n }\n sql = root.sql.map((entry, i) => normalizeSqlEntry(entry, i, options, source));\n }\n\n if (tables.length === 0 && sql.length === 0) {\n throw new TestSchemaError('Test schema declares no tables and no sql entries', source);\n }\n\n return Object.freeze({\n version: 1 as const,\n dialect: 'postgres' as const,\n tables: Object.freeze(tables),\n sql: Object.freeze(sql),\n source,\n });\n}\n"]}
@@ -0,0 +1,43 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { normalizeTestSchema, TestSchemaError } from './normalize.js';
5
+ /**
6
+ * Parse a test schema from a YAML string, a plain object, or a .yml/.yaml
7
+ * file path. When given a file path, relative raw .sql entries resolve
8
+ * against the file's own directory unless options.sourceDir overrides it.
9
+ */
10
+ export function parseTestSchema(input, options = {}) {
11
+ if (typeof input === 'object') {
12
+ return normalizeTestSchema(input, options);
13
+ }
14
+ if (isSchemaFilePath(input)) {
15
+ const filePath = path.resolve(input);
16
+ let text;
17
+ try {
18
+ text = fs.readFileSync(filePath, 'utf8');
19
+ }
20
+ catch (err) {
21
+ throw new TestSchemaError(`Cannot read test schema file: ${filePath} (${err.message})`);
22
+ }
23
+ return parseYamlText(text, {
24
+ sourceDir: options.sourceDir ?? path.dirname(filePath),
25
+ sourceLabel: options.sourceLabel ?? filePath,
26
+ });
27
+ }
28
+ return parseYamlText(input, options);
29
+ }
30
+ export function isSchemaFilePath(input) {
31
+ return !input.includes('\n') && /\.ya?ml$/i.test(input.trim());
32
+ }
33
+ function parseYamlText(text, options) {
34
+ let doc;
35
+ try {
36
+ doc = parseYaml(text);
37
+ }
38
+ catch (err) {
39
+ throw new TestSchemaError(`Failed to parse test schema YAML: ${err.message}`, options.sourceLabel);
40
+ }
41
+ return normalizeTestSchema(doc, options);
42
+ }
43
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../../src/testing/schema/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAsB,EACtB,UAAkC,EAAE;IAEpC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,eAAe,CACvB,iCAAiC,QAAQ,KAAM,GAAa,CAAC,OAAO,GAAG,CACxE,CAAC;QACJ,CAAC;QACD,OAAO,aAAa,CAAC,IAAI,EAAE;YACzB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;YACtD,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,QAAQ;SAC7C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,OAA+B;IAClE,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,eAAe,CACvB,qCAAsC,GAAa,CAAC,OAAO,EAAE,EAC7D,OAAO,CAAC,WAAW,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["import * as path from 'path';\nimport * as fs from 'fs';\nimport { parse as parseYaml } from 'yaml';\nimport { normalizeTestSchema, TestSchemaError } from './normalize';\nimport type { NormalizedTestSchema, ParseTestSchemaOptions } from './types';\n\n/**\n * Parse a test schema from a YAML string, a plain object, or a .yml/.yaml\n * file path. When given a file path, relative raw .sql entries resolve\n * against the file's own directory unless options.sourceDir overrides it.\n */\nexport function parseTestSchema(\n input: string | object,\n options: ParseTestSchemaOptions = {}\n): NormalizedTestSchema {\n if (typeof input === 'object') {\n return normalizeTestSchema(input, options);\n }\n if (isSchemaFilePath(input)) {\n const filePath = path.resolve(input);\n let text: string;\n try {\n text = fs.readFileSync(filePath, 'utf8');\n } catch (err) {\n throw new TestSchemaError(\n `Cannot read test schema file: ${filePath} (${(err as Error).message})`\n );\n }\n return parseYamlText(text, {\n sourceDir: options.sourceDir ?? path.dirname(filePath),\n sourceLabel: options.sourceLabel ?? filePath,\n });\n }\n return parseYamlText(input, options);\n}\n\nexport function isSchemaFilePath(input: string): boolean {\n return !input.includes('\\n') && /\\.ya?ml$/i.test(input.trim());\n}\n\nfunction parseYamlText(text: string, options: ParseTestSchemaOptions): NormalizedTestSchema {\n let doc: unknown;\n try {\n doc = parseYaml(text);\n } catch (err) {\n throw new TestSchemaError(\n `Failed to parse test schema YAML: ${(err as Error).message}`,\n options.sourceLabel\n );\n }\n return normalizeTestSchema(doc, options);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Types for the sqlstack/testing versioned YAML test-schema.
3
+ *
4
+ * The structured vocabulary is intentionally small (type / nullable /
5
+ * primary / unique / default / references). PostgreSQL type strings pass
6
+ * through; raw SQL covers everything the vocabulary cannot express.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/testing/schema/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Types for the sqlstack/testing versioned YAML test-schema.\n *\n * The structured vocabulary is intentionally small (type / nullable /\n * primary / unique / default / references). PostgreSQL type strings pass\n * through; raw SQL covers everything the vocabulary cannot express.\n */\n\n/** A column declaration after shorthand expansion (scalar string => { type }). */\nexport interface TestColumnInput {\n type: string;\n nullable?: boolean;\n primary?: boolean;\n unique?: boolean;\n /** Raw SQL default expression (emitted verbatim into DDL). */\n default?: string | number | boolean;\n /** Reference target: \"table.column\" or \"table(column)\". */\n references?: string;\n}\n\nexport interface TestTableInput {\n columns: Record<string, string | TestColumnInput>;\n}\n\n/** Root YAML document shape. */\nexport interface TestSchemaInput {\n version: number;\n dialect: string;\n tables?: Record<string, TestTableInput>;\n sql?: string[];\n}\n\n/** Normalized (immutable) representations ------------------------------- */\n\nexport interface NormalizedReference {\n readonly table: string;\n readonly column: string;\n}\n\nexport interface NormalizedColumn {\n readonly name: string;\n readonly type: string;\n readonly nullable: boolean;\n readonly primary: boolean;\n readonly unique: boolean;\n readonly hasDefault: boolean;\n readonly default?: string | number | boolean;\n readonly references?: NormalizedReference;\n}\n\nexport interface NormalizedTable {\n readonly name: string;\n /** Authored column order preserved. */\n readonly columns: readonly NormalizedColumn[];\n /** Ordered declared primary-key column names. */\n readonly primaryKey: readonly string[];\n}\n\nexport type RawSqlEntry =\n | {\n readonly kind: 'inline';\n readonly index: number;\n readonly sql: string;\n }\n | {\n readonly kind: 'file';\n readonly index: number;\n /** Path as authored in the YAML. */\n readonly sourcePath: string;\n /** Absolute resolved path. */\n readonly resolvedPath: string;\n };\n\nexport interface NormalizedTestSchema {\n readonly version: 1;\n readonly dialect: 'postgres';\n /** Authored table order preserved. */\n readonly tables: readonly NormalizedTable[];\n readonly sql: readonly RawSqlEntry[];\n /** Where the schema came from, for diagnostics. */\n readonly source: string;\n}\n\nexport interface ParseTestSchemaOptions {\n /**\n * Directory used to resolve relative .sql file entries. Required when the\n * schema contains file entries and no source file path is known. Never\n * defaults to process.cwd() implicitly.\n */\n sourceDir?: string;\n /** Label used in diagnostics (e.g. the YAML file path). */\n sourceLabel?: string;\n}\n"]}