ddlseed 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +163 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +168 -0
- package/dist/generator.d.ts +18 -0
- package/dist/generator.js +381 -0
- package/dist/graph.d.ts +7 -0
- package/dist/graph.js +47 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +637 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +2 -0
- package/dist/writers.d.ts +10 -0
- package/dist/writers.js +127 -0
- package/package.json +49 -4
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { Faker, en } from '@faker-js/faker';
|
|
2
|
+
import { topologicalOrder } from './graph.js';
|
|
3
|
+
import { DdlSeedError } from './types.js';
|
|
4
|
+
const REF_DATE_DEFAULT = new Date('2026-01-01T00:00:00Z');
|
|
5
|
+
const MAX_ATTEMPTS = 40;
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Validation
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
export function validateSchema(schema) {
|
|
10
|
+
const byKey = new Map(schema.tables.map(t => [t.key, t]));
|
|
11
|
+
const resolved = new Map();
|
|
12
|
+
for (const table of schema.tables) {
|
|
13
|
+
const colKeys = new Set(table.columns.map(c => c.key));
|
|
14
|
+
if (table.primaryKey.length === 0) {
|
|
15
|
+
throw new DdlSeedError(`Table "${table.name}" has no primary key. ddlseed requires a primary key to keep rows consistent.`);
|
|
16
|
+
}
|
|
17
|
+
for (const pk of table.primaryKey) {
|
|
18
|
+
if (!colKeys.has(pk))
|
|
19
|
+
throw new DdlSeedError(`Table "${table.name}": primary key column "${pk}" not found.`);
|
|
20
|
+
}
|
|
21
|
+
const fks = [];
|
|
22
|
+
for (const fk of table.foreignKeys) {
|
|
23
|
+
for (const c of fk.columns) {
|
|
24
|
+
if (!colKeys.has(c))
|
|
25
|
+
throw new DdlSeedError(`Table "${table.name}": foreign key column "${c}" not found.`);
|
|
26
|
+
}
|
|
27
|
+
const parent = byKey.get(fk.refTable.toLowerCase());
|
|
28
|
+
if (!parent) {
|
|
29
|
+
throw new DdlSeedError(`Table "${table.name}" references unknown table "${fk.refTable}".`);
|
|
30
|
+
}
|
|
31
|
+
let refCols = fk.refColumns;
|
|
32
|
+
if (refCols.length === 0) {
|
|
33
|
+
if (parent.primaryKey.length !== 1) {
|
|
34
|
+
throw new DdlSeedError(`Table "${table.name}" references "${parent.name}" without columns, but the parent has a composite primary key. Specify the referenced columns explicitly.`);
|
|
35
|
+
}
|
|
36
|
+
refCols = parent.primaryKey;
|
|
37
|
+
}
|
|
38
|
+
const parentCols = new Set(parent.columns.map(c => c.key));
|
|
39
|
+
for (const rc of refCols) {
|
|
40
|
+
if (!parentCols.has(rc))
|
|
41
|
+
throw new DdlSeedError(`Table "${table.name}" references unknown column "${parent.name}.${rc}".`);
|
|
42
|
+
}
|
|
43
|
+
fks.push({ columns: fk.columns, refTable: parent, refColumns: refCols });
|
|
44
|
+
}
|
|
45
|
+
resolved.set(table, fks);
|
|
46
|
+
}
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Value generation
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const EXACT_NAME_HINTS = {
|
|
53
|
+
state: f => f.location.state(),
|
|
54
|
+
status: f => f.helpers.arrayElement(['active', 'inactive', 'pending', 'archived']),
|
|
55
|
+
language: f => f.helpers.arrayElement(['en', 'es', 'fr', 'de', 'pt', 'it', 'ja']),
|
|
56
|
+
locale: f => f.helpers.arrayElement(['en_US', 'es_ES', 'fr_FR', 'de_DE']),
|
|
57
|
+
gender: f => f.person.sex(),
|
|
58
|
+
currency: f => f.finance.currency().code,
|
|
59
|
+
country_code: f => f.location.countryCode(),
|
|
60
|
+
};
|
|
61
|
+
function generateValue(col, f, ctx) {
|
|
62
|
+
const key = col.key;
|
|
63
|
+
// enum columns: pick from declared values
|
|
64
|
+
if (col.kind === 'enum' && col.enumValues.length) {
|
|
65
|
+
return f.helpers.arrayElement(col.enumValues);
|
|
66
|
+
}
|
|
67
|
+
// ---- name heuristics (exact, then contains) ----
|
|
68
|
+
const exact = EXACT_NAME_HINTS[key];
|
|
69
|
+
if (exact)
|
|
70
|
+
return exact(f);
|
|
71
|
+
if (/_id$/.test(key) && col.kind === 'int')
|
|
72
|
+
return f.number.int({ min: 1, max: Math.max(ctx.rows, 1) });
|
|
73
|
+
if (key.includes('email'))
|
|
74
|
+
return withLength(f.internet.email().toLowerCase(), col.length);
|
|
75
|
+
if (key.includes('first_name') || key === 'firstname')
|
|
76
|
+
return withLength(f.person.firstName(), col.length);
|
|
77
|
+
if (key.includes('last_name') || key === 'lastname' || key.includes('surname'))
|
|
78
|
+
return withLength(f.person.lastName(), col.length);
|
|
79
|
+
if (key === 'name' || key.includes('full_name'))
|
|
80
|
+
return withLength(f.person.fullName(), col.length);
|
|
81
|
+
if (key.includes('username') || key.includes('user_name') || key.includes('nickname') || key === 'handle')
|
|
82
|
+
return withLength(f.internet.username(), col.length);
|
|
83
|
+
if (key.includes('city') || key.includes('town'))
|
|
84
|
+
return withLength(f.location.city(), col.length);
|
|
85
|
+
if (key.includes('country'))
|
|
86
|
+
return withLength(f.location.country(), col.length);
|
|
87
|
+
if (key.includes('address') || key.includes('street'))
|
|
88
|
+
return withLength(f.location.streetAddress(), col.length);
|
|
89
|
+
if (key.includes('zip') || key.includes('postal'))
|
|
90
|
+
return withLength(f.location.zipCode(), col.length);
|
|
91
|
+
if (key.includes('phone') || key.includes('mobile') || key === 'tel')
|
|
92
|
+
return withLength(f.phone.number(), col.length);
|
|
93
|
+
if (key.includes('url') || key.includes('website') || key === 'link' || key.includes('homepage'))
|
|
94
|
+
return withLength(f.internet.url(), col.length);
|
|
95
|
+
if (key.includes('password'))
|
|
96
|
+
return withLength(f.internet.password({ length: 12 }), col.length);
|
|
97
|
+
if (key.includes('company') || key.includes('organization') || key.includes('organisation') || key.includes('brand') || key.includes('manufacturer'))
|
|
98
|
+
return withLength(f.company.name(), col.length);
|
|
99
|
+
if (key.includes('description') || key.includes('bio') || key.includes('content') || key.includes('body') || key.includes('comment') || key.includes('summary') || key.includes('excerpt'))
|
|
100
|
+
return withLength(f.lorem.paragraph(), col.length);
|
|
101
|
+
if (key.includes('title') || key.includes('subject') || key.includes('headline'))
|
|
102
|
+
return withLength(f.lorem.words({ min: 2, max: 4 }), col.length);
|
|
103
|
+
if (key === 'slug' || key.includes('slug'))
|
|
104
|
+
return f.helpers.slugify(f.lorem.words({ min: 2, max: 3 })).slice(0, col.length ?? 80);
|
|
105
|
+
if (key.includes('price') || key.includes('amount') || key.includes('total') || key.includes('balance') || key.includes('salary') || key.includes('cost') || key.includes('fee'))
|
|
106
|
+
return f.number.float({ min: 1, max: 1000, fractionDigits: 2 });
|
|
107
|
+
if (key === 'age')
|
|
108
|
+
return f.number.int({ min: 18, max: 90 });
|
|
109
|
+
if (key.includes('qty') || key.includes('quantity') || key.includes('stock') || key.includes('count'))
|
|
110
|
+
return f.number.int({ min: 1, max: 100 });
|
|
111
|
+
if (key.includes('rating'))
|
|
112
|
+
return f.number.float({ min: 0, max: 5, fractionDigits: 1 });
|
|
113
|
+
if (key.includes('uuid') || key.includes('guid'))
|
|
114
|
+
return f.string.uuid();
|
|
115
|
+
if (key.includes('token') || key.includes('hash') || key.includes('secret') || key.includes('api_key') || key === 'key')
|
|
116
|
+
return f.string.alphanumeric({ length: 32 });
|
|
117
|
+
if (key.includes('color') || key.includes('colour'))
|
|
118
|
+
return f.color.human();
|
|
119
|
+
if (key.includes('version'))
|
|
120
|
+
return `${f.number.int({ min: 1, max: 3 })}.${f.number.int({ min: 0, max: 9 })}.${f.number.int({ min: 0, max: 9 })}`;
|
|
121
|
+
if (key.includes('percent'))
|
|
122
|
+
return f.number.float({ min: 0, max: 100, fractionDigits: 1 });
|
|
123
|
+
if (key.includes('ip'))
|
|
124
|
+
return f.internet.ip();
|
|
125
|
+
if (key.includes('lat'))
|
|
126
|
+
return parseFloat(f.location.latitude({ min: -60, max: 60 }).toFixed(6));
|
|
127
|
+
if (key.includes('lon') || key.includes('lng'))
|
|
128
|
+
return parseFloat(f.location.longitude({ min: -120, max: 120 }).toFixed(6));
|
|
129
|
+
if (key.includes('code'))
|
|
130
|
+
return f.string.alphanumeric({ length: 8, casing: 'upper' });
|
|
131
|
+
if (/^is_|^has_|_enabled$|_verified$|^active$|^verified$/.test(key))
|
|
132
|
+
return f.datatype.boolean();
|
|
133
|
+
// ---- by type ----
|
|
134
|
+
switch (col.kind) {
|
|
135
|
+
case 'int':
|
|
136
|
+
return f.number.int({ min: 0, max: col.length ? Math.min(col.length, 1_000_000) : 1000 });
|
|
137
|
+
case 'float':
|
|
138
|
+
return f.number.float({ min: 0, max: 1000, fractionDigits: col.scale ?? 2 });
|
|
139
|
+
case 'bool':
|
|
140
|
+
return f.datatype.boolean();
|
|
141
|
+
case 'date':
|
|
142
|
+
return f.date.recent({ days: 730, refDate: ctx.refDate });
|
|
143
|
+
case 'datetime':
|
|
144
|
+
return f.date.recent({ days: 730, refDate: ctx.refDate });
|
|
145
|
+
case 'time': {
|
|
146
|
+
const d = f.date.recent({ days: 730, refDate: ctx.refDate });
|
|
147
|
+
return d.toISOString().slice(11, 19);
|
|
148
|
+
}
|
|
149
|
+
case 'uuid':
|
|
150
|
+
return f.string.uuid();
|
|
151
|
+
case 'json':
|
|
152
|
+
return { key: f.lorem.word(), value: f.number.int({ min: 0, max: 99 }) };
|
|
153
|
+
case 'blob':
|
|
154
|
+
return f.string.alphanumeric({ length: 16 });
|
|
155
|
+
case 'text':
|
|
156
|
+
case 'unknown':
|
|
157
|
+
default: {
|
|
158
|
+
const base = f.lorem.paragraph();
|
|
159
|
+
return withLength(base, col.length);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function withLength(v, length) {
|
|
164
|
+
if (length && length > 0 && v.length > length)
|
|
165
|
+
return v.slice(0, length);
|
|
166
|
+
return v;
|
|
167
|
+
}
|
|
168
|
+
export function generateData(schema, options = {}) {
|
|
169
|
+
const rowsPerTable = options.rows ?? 50;
|
|
170
|
+
const seed = options.seed ?? 42;
|
|
171
|
+
const refDateRaw = options.refDate ?? REF_DATE_DEFAULT;
|
|
172
|
+
const refDate = refDateRaw instanceof Date ? refDateRaw : new Date(refDateRaw);
|
|
173
|
+
const resolvedFks = validateSchema(schema);
|
|
174
|
+
const order = topologicalOrder(schema);
|
|
175
|
+
const f = new Faker({ locale: [en] });
|
|
176
|
+
f.seed(seed);
|
|
177
|
+
const ctx = { f, rows: rowsPerTable, refDate };
|
|
178
|
+
/** committed rows per table, keyed by table.key -> array of internal rows */
|
|
179
|
+
const committed = new Map();
|
|
180
|
+
for (const t of schema.tables)
|
|
181
|
+
committed.set(t.key, []);
|
|
182
|
+
const result = new Map();
|
|
183
|
+
for (const table of order) {
|
|
184
|
+
const fks = resolvedFks.get(table);
|
|
185
|
+
const pkSet = new Set();
|
|
186
|
+
const uniqueSets = new Map();
|
|
187
|
+
const uniqueGroups = [];
|
|
188
|
+
if (table.primaryKey.length > 1)
|
|
189
|
+
uniqueGroups.push(table.primaryKey);
|
|
190
|
+
for (const cols of table.uniqueConstraints)
|
|
191
|
+
uniqueGroups.push(cols);
|
|
192
|
+
for (const col of table.columns) {
|
|
193
|
+
if (col.unique && !uniqueGroups.some(g => g.length === 1 && g[0] === col.key)) {
|
|
194
|
+
uniqueGroups.push([col.key]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const g of uniqueGroups)
|
|
198
|
+
uniqueSets.set(g.join('\u0000'), new Set());
|
|
199
|
+
const tableRows = [];
|
|
200
|
+
for (let i = 0; i < rowsPerTable; i++) {
|
|
201
|
+
const row = generateRow(table, fks, i, ctx, tableRows, pkSet, uniqueSets, uniqueGroups);
|
|
202
|
+
tableRows.push(row);
|
|
203
|
+
// commit: update uniqueness bookkeeping
|
|
204
|
+
if (table.primaryKey.length === 1) {
|
|
205
|
+
pkSet.add(keyOf(row, table.primaryKey[0]));
|
|
206
|
+
}
|
|
207
|
+
for (const g of uniqueGroups) {
|
|
208
|
+
const sig = tupleKey(row, g);
|
|
209
|
+
if (sig !== null)
|
|
210
|
+
uniqueSets.get(g.join('\u0000')).add(sig);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
committed.set(table.key, tableRows);
|
|
214
|
+
// convert internal (key-keyed) rows to output rows (name-keyed)
|
|
215
|
+
const outRows = tableRows.map(r => {
|
|
216
|
+
const o = {};
|
|
217
|
+
for (const col of table.columns)
|
|
218
|
+
o[col.name] = r[col.key];
|
|
219
|
+
return o;
|
|
220
|
+
});
|
|
221
|
+
result.set(table.key, outRows);
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
function keyOf(row, colKey) {
|
|
226
|
+
const v = row[colKey];
|
|
227
|
+
return v === null || v === undefined ? 'null' : String(v instanceof Date ? v.toISOString() : v);
|
|
228
|
+
}
|
|
229
|
+
function tupleKey(row, cols) {
|
|
230
|
+
const parts = [];
|
|
231
|
+
for (const c of cols) {
|
|
232
|
+
const v = row[c];
|
|
233
|
+
if (v === null || v === undefined)
|
|
234
|
+
return null; // nulls don't collide in SQL UNIQUE
|
|
235
|
+
parts.push(keyOf(row, c));
|
|
236
|
+
}
|
|
237
|
+
return parts.join('\u0000');
|
|
238
|
+
}
|
|
239
|
+
function generateRow(table, fks, index, ctx, committedRows, pkSet, uniqueSets, uniqueGroups) {
|
|
240
|
+
let row = draftRow(table, fks, index, ctx, committedRows, false);
|
|
241
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
242
|
+
if (satisfies(row, table, pkSet, uniqueSets, uniqueGroups))
|
|
243
|
+
return row;
|
|
244
|
+
row = draftRow(table, fks, index, ctx, committedRows, attempt > 0);
|
|
245
|
+
}
|
|
246
|
+
// deterministic fallback: force uniqueness with the row index
|
|
247
|
+
forceUnique(row, table, index, uniqueSets, uniqueGroups, table.primaryKey.length > 1 ? table.primaryKey : null);
|
|
248
|
+
return row;
|
|
249
|
+
}
|
|
250
|
+
function satisfies(row, table, pkSet, uniqueSets, uniqueGroups) {
|
|
251
|
+
if (table.primaryKey.length === 1) {
|
|
252
|
+
if (pkSet.has(keyOf(row, table.primaryKey[0])))
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
for (const g of uniqueGroups) {
|
|
256
|
+
const sig = tupleKey(row, g);
|
|
257
|
+
if (sig === null)
|
|
258
|
+
continue;
|
|
259
|
+
if (uniqueSets.get(g.join('\u0000')).has(sig))
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
function draftRow(table, fks, index, ctx, committedRows, isRetry) {
|
|
265
|
+
const row = {};
|
|
266
|
+
// ---- pass 1: primary key ----
|
|
267
|
+
if (table.primaryKey.length === 1) {
|
|
268
|
+
const pk = table.columns.find(c => c.key === table.primaryKey[0]);
|
|
269
|
+
if (pk.kind === 'int') {
|
|
270
|
+
row[pk.key] = index + 1; // deterministic, always unique
|
|
271
|
+
}
|
|
272
|
+
else if (!isRetry || row[pk.key] === undefined) {
|
|
273
|
+
row[pk.key] = generateValue(pk, ctx.f, ctx);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
for (const pkKey of table.primaryKey) {
|
|
278
|
+
const col = table.columns.find(c => c.key === pkKey);
|
|
279
|
+
if (col.kind === 'int') {
|
|
280
|
+
row[col.key] = index + 1;
|
|
281
|
+
}
|
|
282
|
+
else if (!isRetry || row[col.key] === undefined) {
|
|
283
|
+
row[col.key] = generateValue(col, ctx.f, ctx);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// ---- pass 2: foreign keys ----
|
|
288
|
+
for (const fk of fks) {
|
|
289
|
+
const pool = getRefPool(fk, committedRows);
|
|
290
|
+
const allNullable = fk.columns.every(c => table.columns.find(col => col.key === c).notNull === false);
|
|
291
|
+
if (pool.length === 0) {
|
|
292
|
+
if (fk.refTable.key === table.key) {
|
|
293
|
+
// self-reference with no committed rows: point the row to itself (valid once inserted)
|
|
294
|
+
const own = ownPkValuesFor(table, fk, index, row);
|
|
295
|
+
if (own === null && allNullable) {
|
|
296
|
+
for (const c of fk.columns)
|
|
297
|
+
row[c] = null;
|
|
298
|
+
}
|
|
299
|
+
else if (own !== null) {
|
|
300
|
+
fk.columns.forEach((c, idx) => { row[c] = own[idx]; });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
fk.columns.forEach((c, idx) => { row[c] = idx === 0 ? 1 : 1; });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// referenced table produced no rows (rows=0 edge) -> null if allowed
|
|
308
|
+
if (allNullable)
|
|
309
|
+
for (const c of fk.columns)
|
|
310
|
+
row[c] = null;
|
|
311
|
+
else
|
|
312
|
+
for (const c of fk.columns)
|
|
313
|
+
row[c] = 1;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (allNullable && ctx.f.number.int({ min: 1, max: 10 }) === 1) {
|
|
318
|
+
for (const c of fk.columns)
|
|
319
|
+
row[c] = null;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const tuple = ctx.f.helpers.arrayElement(pool);
|
|
323
|
+
fk.columns.forEach((c, idx) => { row[c] = tuple[idx]; });
|
|
324
|
+
}
|
|
325
|
+
// ---- pass 3: the rest ----
|
|
326
|
+
for (const col of table.columns) {
|
|
327
|
+
if (row[col.key] !== undefined)
|
|
328
|
+
continue;
|
|
329
|
+
const isPk = table.primaryKey.includes(col.key);
|
|
330
|
+
const nullable = !col.notNull && !isPk;
|
|
331
|
+
if (nullable && ctx.f.number.int({ min: 1, max: 10 }) === 1) {
|
|
332
|
+
row[col.key] = null;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
row[col.key] = generateValue(col, ctx.f, ctx);
|
|
336
|
+
}
|
|
337
|
+
return row;
|
|
338
|
+
}
|
|
339
|
+
function ownPkValuesFor(table, fk, index, row) {
|
|
340
|
+
// self-FK usually references own PK
|
|
341
|
+
const vals = [];
|
|
342
|
+
for (const rc of fk.refColumns) {
|
|
343
|
+
const v = row[rc];
|
|
344
|
+
if (v === undefined)
|
|
345
|
+
return null;
|
|
346
|
+
vals.push(v);
|
|
347
|
+
}
|
|
348
|
+
return vals;
|
|
349
|
+
}
|
|
350
|
+
function getRefPool(fk, committedRows) {
|
|
351
|
+
return committedRows.map(r => fk.refColumns.map(rc => r[rc]));
|
|
352
|
+
}
|
|
353
|
+
function forceUnique(row, table, index, uniqueSets, uniqueGroups, compositePk) {
|
|
354
|
+
const groups = [...uniqueGroups];
|
|
355
|
+
if (compositePk)
|
|
356
|
+
groups.push(compositePk);
|
|
357
|
+
for (const g of groups) {
|
|
358
|
+
const sig = tupleKey(row, g);
|
|
359
|
+
if (sig === null)
|
|
360
|
+
continue;
|
|
361
|
+
const set = uniqueSets.get(g.join('\u0000'));
|
|
362
|
+
if (set && !set.has(sig))
|
|
363
|
+
continue;
|
|
364
|
+
for (const colKey of g) {
|
|
365
|
+
const col = table.columns.find(c => c.key === colKey);
|
|
366
|
+
const v = row[colKey];
|
|
367
|
+
if (typeof v === 'number')
|
|
368
|
+
row[colKey] = v + index + 1;
|
|
369
|
+
else if (typeof v === 'string')
|
|
370
|
+
row[colKey] = `${v}-${index}`;
|
|
371
|
+
else if (v instanceof Date)
|
|
372
|
+
row[colKey] = new Date(v.getTime() + index * 1000);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Convenience: ordered table names for writers
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
export function generationOrder(schema) {
|
|
380
|
+
return topologicalOrder(schema);
|
|
381
|
+
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Schema, TableDef } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orders tables so that every table comes after the tables its foreign keys
|
|
4
|
+
* reference (self-references are ignored). Independent tables keep their
|
|
5
|
+
* declaration order. Throws on FK cycles.
|
|
6
|
+
*/
|
|
7
|
+
export declare function topologicalOrder(schema: Schema): TableDef[];
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DdlSeedError } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orders tables so that every table comes after the tables its foreign keys
|
|
4
|
+
* reference (self-references are ignored). Independent tables keep their
|
|
5
|
+
* declaration order. Throws on FK cycles.
|
|
6
|
+
*/
|
|
7
|
+
export function topologicalOrder(schema) {
|
|
8
|
+
const byKey = new Map();
|
|
9
|
+
for (const t of schema.tables)
|
|
10
|
+
byKey.set(t.key, t);
|
|
11
|
+
const deps = new Map();
|
|
12
|
+
for (const t of schema.tables) {
|
|
13
|
+
const set = new Set();
|
|
14
|
+
for (const fk of t.foreignKeys) {
|
|
15
|
+
const ref = fk.refTable.toLowerCase();
|
|
16
|
+
if (ref !== t.key)
|
|
17
|
+
set.add(ref);
|
|
18
|
+
}
|
|
19
|
+
deps.set(t.key, set);
|
|
20
|
+
}
|
|
21
|
+
const order = [];
|
|
22
|
+
const emitted = new Set();
|
|
23
|
+
const remaining = [...schema.tables]; // declaration order preserved
|
|
24
|
+
while (remaining.length > 0) {
|
|
25
|
+
// find first table whose deps are all emitted
|
|
26
|
+
const idx = remaining.findIndex(t => {
|
|
27
|
+
const d = deps.get(t.key);
|
|
28
|
+
for (const dep of d) {
|
|
29
|
+
if (!byKey.has(dep)) {
|
|
30
|
+
throw new DdlSeedError(`Table "${t.name}" references unknown table "${dep}" in a foreign key.`);
|
|
31
|
+
}
|
|
32
|
+
if (!emitted.has(dep))
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
if (idx === -1) {
|
|
38
|
+
const stuck = remaining.map(t => t.name).join(', ');
|
|
39
|
+
throw new DdlSeedError(`Circular foreign-key dependency between tables: ${stuck}. ` +
|
|
40
|
+
'ddlseed cannot order these tables for insertion.');
|
|
41
|
+
}
|
|
42
|
+
const [t] = remaining.splice(idx, 1);
|
|
43
|
+
emitted.add(t.key);
|
|
44
|
+
order.push(t);
|
|
45
|
+
}
|
|
46
|
+
return order;
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseSchema, detectDialect } from './parser.js';
|
|
2
|
+
export { topologicalOrder } from './graph.js';
|
|
3
|
+
export { generateData, validateSchema } from './generator.js';
|
|
4
|
+
export { toSql, toJson, toCsv, toCsvAll } from './writers.js';
|
|
5
|
+
export { DdlSeedError } from './types.js';
|
|
6
|
+
export type { ColumnDef, Dialect, ForeignKey, Row, Schema, TableDef } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseSchema, detectDialect } from './parser.js';
|
|
2
|
+
export { topologicalOrder } from './graph.js';
|
|
3
|
+
export { generateData, validateSchema } from './generator.js';
|
|
4
|
+
export { toSql, toJson, toCsv, toCsvAll } from './writers.js';
|
|
5
|
+
export { DdlSeedError } from './types.js';
|
package/dist/parser.d.ts
ADDED