@berthojoris/mcp-mysql-server 1.40.7 → 1.42.1

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.
@@ -0,0 +1,1432 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RelationalSeederTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ class RelationalSeederTools {
10
+ constructor(security) {
11
+ this.db = connection_1.default.getInstance();
12
+ this.security = security;
13
+ this.plans = new Map();
14
+ }
15
+ async planSeedData(params) {
16
+ try {
17
+ if (!params.target_tables || !Array.isArray(params.target_tables) || params.target_tables.length === 0) {
18
+ return { status: "error", error: "target_tables must contain at least one table" };
19
+ }
20
+ const database = this.resolveDatabase(params.database);
21
+ const schema = await this.loadSchema(database);
22
+ const warnings = [];
23
+ const maxRelatedTables = this.clampNumber(params.max_related_tables, 1, 100, 25);
24
+ const targetTables = Array.from(new Set(params.target_tables));
25
+ for (const table of targetTables) {
26
+ this.assertIdentifier(table, "table");
27
+ if (!schema.tables[table]) {
28
+ return { status: "error", error: `Table '${table}' does not exist in database '${database}'` };
29
+ }
30
+ }
31
+ const includeDependencies = params.include_dependencies ?? true;
32
+ const includeChildren = params.include_children ?? false;
33
+ const childRowsPerParent = this.clampNumber(params.child_rows_per_parent, 1, 20, 2);
34
+ const maxRowsPerTable = this.clampNumber(params.max_rows_per_table, 1, 10000, 1000);
35
+ const respectExistingData = params.respect_existing_data ?? true;
36
+ const reasons = {};
37
+ const includedTables = new Set();
38
+ targetTables.forEach((table) => {
39
+ includedTables.add(table);
40
+ reasons[table] = ["target table"];
41
+ });
42
+ let changed = true;
43
+ while (changed) {
44
+ changed = false;
45
+ const currentTables = Array.from(includedTables);
46
+ if (includeDependencies) {
47
+ for (const fk of schema.foreignKeys) {
48
+ if (currentTables.includes(fk.childTable) && !includedTables.has(fk.parentTable)) {
49
+ if (includedTables.size >= maxRelatedTables) {
50
+ warnings.push(`Skipped parent table '${fk.parentTable}' because max_related_tables=${maxRelatedTables} was reached.`);
51
+ continue;
52
+ }
53
+ includedTables.add(fk.parentTable);
54
+ reasons[fk.parentTable] = reasons[fk.parentTable] || [];
55
+ reasons[fk.parentTable].push(this.formatForeignKey(fk));
56
+ changed = true;
57
+ }
58
+ }
59
+ }
60
+ if (includeChildren) {
61
+ for (const fk of schema.foreignKeys) {
62
+ if (currentTables.includes(fk.parentTable) && !includedTables.has(fk.childTable)) {
63
+ if (includedTables.size >= maxRelatedTables) {
64
+ warnings.push(`Skipped child table '${fk.childTable}' because max_related_tables=${maxRelatedTables} was reached.`);
65
+ continue;
66
+ }
67
+ includedTables.add(fk.childTable);
68
+ reasons[fk.childTable] = reasons[fk.childTable] || [];
69
+ reasons[fk.childTable].push(this.formatForeignKey(fk));
70
+ changed = true;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ const dependencyOrder = this.topologicalSort(Array.from(includedTables), schema.foreignKeys, warnings);
76
+ const rowCounts = this.calculateRowsToCreate(dependencyOrder, targetTables, schema, reasons, params.rows_per_table, childRowsPerParent, maxRowsPerTable, respectExistingData);
77
+ const seedRules = this.buildInferredSeedRules(dependencyOrder, schema, params.seed_rules || {});
78
+ const constraints = this.detectConstraints(dependencyOrder, schema);
79
+ for (const requiredColumn of constraints.required_columns) {
80
+ const rule = seedRules[requiredColumn];
81
+ if (!rule || rule.generator === "string") {
82
+ warnings.push(`Column '${requiredColumn}' is required and uses a generic generator. Review preview before execution.`);
83
+ }
84
+ }
85
+ if (this.isProductionLikeDatabase(database)) {
86
+ warnings.push(`Database '${database}' looks production-like. execute_seed_plan will block writes unless allow_production is true.`);
87
+ }
88
+ const planId = this.createPlanId();
89
+ const plan = {
90
+ plan_id: planId,
91
+ database,
92
+ dependency_order: dependencyOrder,
93
+ tables_required: rowCounts,
94
+ constraints_detected: constraints,
95
+ seed_rules: seedRules,
96
+ warnings,
97
+ requires_confirmation: params.require_confirmation ?? true,
98
+ confirm_token: this.createConfirmToken(database),
99
+ random_seed: params.random_seed ?? 42,
100
+ options: {
101
+ strategy: params.strategy || "append",
102
+ respect_existing_data: respectExistingData,
103
+ include_dependencies: includeDependencies,
104
+ include_children: includeChildren,
105
+ child_rows_per_parent: childRowsPerParent,
106
+ max_rows_per_table: maxRowsPerTable,
107
+ },
108
+ };
109
+ this.plans.set(planId, { plan, schema });
110
+ return { status: "success", data: plan };
111
+ }
112
+ catch (error) {
113
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
114
+ }
115
+ }
116
+ async generateSeedPreview(params) {
117
+ try {
118
+ const stored = this.getStoredPlan(params.plan_id);
119
+ const maxRows = this.clampNumber(params.max_preview_rows_per_table, 1, 25, 3);
120
+ const emailDomain = params.email_domain || "example.test";
121
+ const preview = this.buildPreview(stored, maxRows, emailDomain);
122
+ stored.lastPreview = preview;
123
+ return {
124
+ status: "success",
125
+ data: {
126
+ plan_id: stored.plan.plan_id,
127
+ locale: params.locale || "en_US",
128
+ realistic: params.realistic ?? true,
129
+ preview,
130
+ quality_notes: [
131
+ `Preview is deterministic from random_seed=${stored.plan.random_seed}.`,
132
+ `Email values use '${emailDomain}' unless overridden.`,
133
+ "Foreign keys are symbolic in preview and resolved during execution.",
134
+ ],
135
+ requires_confirmation: stored.plan.requires_confirmation,
136
+ confirm_token: stored.plan.confirm_token,
137
+ },
138
+ };
139
+ }
140
+ catch (error) {
141
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
142
+ }
143
+ }
144
+ async executeSeedPlan(params) {
145
+ const startedAt = Date.now();
146
+ const dryRun = params.dry_run ?? true;
147
+ const useTransaction = params.use_transaction ?? true;
148
+ const onError = params.on_error || "rollback";
149
+ try {
150
+ const stored = this.getStoredPlan(params.plan_id);
151
+ const plan = stored.plan;
152
+ const emailDomain = params.email_domain || "example.test";
153
+ if (dryRun) {
154
+ const preview = this.buildPreview(stored, 5, emailDomain);
155
+ stored.lastExecution = {
156
+ inserted: {},
157
+ insertedPrimaryKeys: {},
158
+ transaction: "dry_run",
159
+ executedAt: new Date().toISOString(),
160
+ };
161
+ return {
162
+ status: "success",
163
+ data: {
164
+ success: true,
165
+ dry_run: true,
166
+ plan_id: plan.plan_id,
167
+ transaction: "not_started",
168
+ would_insert: this.requirementsToInsertMap(plan.tables_required),
169
+ preview,
170
+ requires_confirmation: plan.requires_confirmation,
171
+ confirm_token: plan.confirm_token,
172
+ },
173
+ };
174
+ }
175
+ if (plan.requires_confirmation && params.confirm_token !== plan.confirm_token) {
176
+ return {
177
+ status: "error",
178
+ error: `execute_seed_plan requires confirm_token='${plan.confirm_token}' for non-dry-run execution.`,
179
+ };
180
+ }
181
+ if (this.isProductionLikeDatabase(plan.database) && !params.allow_production) {
182
+ return {
183
+ status: "error",
184
+ error: `Database '${plan.database}' looks production-like. Set allow_production=true and provide the confirm token to continue.`,
185
+ };
186
+ }
187
+ const transactionId = `seed_${plan.plan_id}`;
188
+ const idMap = {};
189
+ const tupleMap = {};
190
+ const inserted = {};
191
+ const insertedPrimaryKeys = {};
192
+ if (useTransaction) {
193
+ await this.db.beginTransaction(transactionId);
194
+ }
195
+ try {
196
+ await this.preloadExistingParentIds(stored, idMap, tupleMap, useTransaction ? transactionId : undefined);
197
+ for (const tableName of plan.dependency_order) {
198
+ const requirement = plan.tables_required.find((item) => item.table === tableName);
199
+ const rowsToCreate = requirement?.rows_to_create || 0;
200
+ inserted[tableName] = 0;
201
+ insertedPrimaryKeys[tableName] = [];
202
+ if (rowsToCreate <= 0) {
203
+ await this.ensureIdMapForTable(stored, tableName, idMap, tupleMap, useTransaction ? transactionId : undefined);
204
+ continue;
205
+ }
206
+ const rows = this.buildRowsForTable(stored, tableName, rowsToCreate, {
207
+ preview: false,
208
+ emailDomain,
209
+ idMap,
210
+ tupleMap,
211
+ });
212
+ for (const row of rows) {
213
+ const result = await this.insertRow(tableName, row, useTransaction ? transactionId : undefined);
214
+ inserted[tableName] += result.affectedRows;
215
+ this.trackInsertedKeys(stored, stored.schema.tables[tableName], row, result.insertId, idMap, tupleMap, insertedPrimaryKeys);
216
+ }
217
+ }
218
+ if (useTransaction) {
219
+ await this.db.commitTransaction(transactionId);
220
+ }
221
+ stored.lastExecution = {
222
+ inserted,
223
+ insertedPrimaryKeys,
224
+ transaction: useTransaction ? "committed" : "none",
225
+ executedAt: new Date().toISOString(),
226
+ };
227
+ return {
228
+ status: "success",
229
+ data: {
230
+ success: true,
231
+ dry_run: false,
232
+ transaction: useTransaction ? "committed" : "not_used",
233
+ inserted,
234
+ inserted_primary_keys: insertedPrimaryKeys,
235
+ resolved_foreign_keys: this.countResolvedForeignKeys(stored),
236
+ duration_ms: Date.now() - startedAt,
237
+ batch_size: params.batch_size || 1,
238
+ on_error: onError,
239
+ next_recommended_tool: "validate_seed_integrity",
240
+ },
241
+ };
242
+ }
243
+ catch (error) {
244
+ if (useTransaction && onError === "rollback") {
245
+ await this.db.rollbackTransaction(transactionId);
246
+ stored.lastExecution = {
247
+ inserted,
248
+ insertedPrimaryKeys,
249
+ transaction: "rolled_back",
250
+ executedAt: new Date().toISOString(),
251
+ };
252
+ }
253
+ throw error;
254
+ }
255
+ }
256
+ catch (error) {
257
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
258
+ }
259
+ }
260
+ async validateSeedIntegrity(params) {
261
+ try {
262
+ const stored = this.getStoredPlan(params.plan_id);
263
+ const plan = stored.plan;
264
+ const tables = params.tables && params.tables.length > 0 ? params.tables : plan.dependency_order;
265
+ for (const table of tables) {
266
+ this.assertIdentifier(table, "table");
267
+ if (!stored.schema.tables[table]) {
268
+ return { status: "error", error: `Unknown table '${table}' in seed validation request.` };
269
+ }
270
+ }
271
+ const checkForeignKeys = params.check_foreign_keys ?? params.check_orphans ?? true;
272
+ const checkRequired = params.check_required_columns ?? true;
273
+ const checkUnique = params.check_unique_collisions ?? true;
274
+ const checkRowCounts = params.check_row_counts ?? true;
275
+ const summary = {
276
+ tables_checked: tables.length,
277
+ foreign_key_checks: 0,
278
+ orphan_foreign_keys: 0,
279
+ unique_violations: 0,
280
+ required_column_violations: 0,
281
+ };
282
+ const rowCounts = {};
283
+ const violations = [];
284
+ if (checkForeignKeys) {
285
+ for (const fk of stored.schema.foreignKeys.filter((item) => tables.includes(item.childTable))) {
286
+ summary.foreign_key_checks++;
287
+ const count = await this.countForeignKeyOrphans(fk);
288
+ summary.orphan_foreign_keys += count;
289
+ if (count > 0) {
290
+ violations.push({
291
+ type: "foreign_key_orphan",
292
+ table: fk.childTable,
293
+ columns: fk.childColumns,
294
+ references: `${fk.parentTable}(${fk.parentColumns.join(",")})`,
295
+ count,
296
+ });
297
+ }
298
+ }
299
+ }
300
+ if (checkRequired) {
301
+ for (const table of tables) {
302
+ for (const column of this.getRequiredColumns(stored.schema.tables[table])) {
303
+ const count = await this.countNullValues(table, column.columnName);
304
+ summary.required_column_violations += count;
305
+ if (count > 0) {
306
+ violations.push({
307
+ type: "required_column_null",
308
+ table,
309
+ column: column.columnName,
310
+ count,
311
+ });
312
+ }
313
+ }
314
+ }
315
+ }
316
+ if (checkUnique) {
317
+ for (const index of stored.schema.uniqueIndexes.filter((item) => tables.includes(item.tableName))) {
318
+ const collisions = await this.findUniqueCollisions(index);
319
+ summary.unique_violations += collisions.length;
320
+ for (const collision of collisions) {
321
+ violations.push({
322
+ type: "unique_collision",
323
+ table: index.tableName,
324
+ index: index.indexName,
325
+ columns: index.columns,
326
+ sample: collision,
327
+ });
328
+ }
329
+ }
330
+ }
331
+ if (checkRowCounts) {
332
+ for (const table of tables) {
333
+ rowCounts[table] = await this.getValidationRowCount(stored, table);
334
+ }
335
+ }
336
+ return {
337
+ status: "success",
338
+ data: {
339
+ valid: summary.orphan_foreign_keys === 0 &&
340
+ summary.unique_violations === 0 &&
341
+ summary.required_column_violations === 0,
342
+ summary,
343
+ row_counts: rowCounts,
344
+ violations,
345
+ },
346
+ };
347
+ }
348
+ catch (error) {
349
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
350
+ }
351
+ }
352
+ async inferSeedRules(params) {
353
+ try {
354
+ const database = this.resolveDatabase(params.database);
355
+ const schema = await this.loadSchema(database);
356
+ const tables = this.getSelectableTables(schema, params.tables, params.max_tables);
357
+ const domain = params.domain || "auto";
358
+ const sampleSize = this.clampNumber(params.sample_size, 0, 100, 25);
359
+ const resolvedDomain = domain === "auto" ? this.detectDomainFromTables(tables) : domain;
360
+ const rules = this.buildInferredSeedRules(tables, schema, {}, resolvedDomain);
361
+ const sampleSummary = {};
362
+ const warnings = [];
363
+ if (sampleSize > 0) {
364
+ for (const table of tables) {
365
+ const samples = await this.loadSampleRows(table, sampleSize);
366
+ sampleSummary[table] = {
367
+ rows_analyzed: samples.length,
368
+ columns_analyzed: schema.tables[table].columns.length,
369
+ };
370
+ this.refineRulesFromSamples(table, schema.tables[table], samples, rules, warnings);
371
+ }
372
+ }
373
+ return {
374
+ status: "success",
375
+ data: {
376
+ database,
377
+ domain: resolvedDomain,
378
+ tables,
379
+ sample_size: sampleSize,
380
+ sample_summary: sampleSummary,
381
+ rules,
382
+ warnings,
383
+ privacy_note: "Sample values are used only to infer safe patterns, ranges, and enum-like choices; raw PII samples are not returned.",
384
+ },
385
+ };
386
+ }
387
+ catch (error) {
388
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
389
+ }
390
+ }
391
+ async seedFromTemplate(params) {
392
+ try {
393
+ const database = this.resolveDatabase(params.database);
394
+ const schema = await this.loadSchema(database);
395
+ const scale = params.scale || "small";
396
+ const detected = this.detectTemplateTables(schema, params.template, params.include, params.exclude);
397
+ if (detected.targetTables.length === 0) {
398
+ return {
399
+ status: "error",
400
+ error: `No tables matched template '${params.template}'. Provide include with concrete table names.`,
401
+ };
402
+ }
403
+ const templateRows = this.getScaleRows(params.template, scale, detected.targetTables);
404
+ const rowsPerTable = typeof params.rows_per_table === "object" || typeof params.rows_per_table === "number"
405
+ ? params.rows_per_table
406
+ : templateRows;
407
+ const seedRules = this.buildTemplateRules(params.template, detected.targetTables, schema);
408
+ const planResult = await this.planSeedData({
409
+ database,
410
+ target_tables: detected.targetTables,
411
+ rows_per_table: rowsPerTable,
412
+ include_dependencies: params.include_dependencies ?? true,
413
+ include_children: params.include_children ?? false,
414
+ respect_existing_data: params.respect_existing_data ?? true,
415
+ random_seed: params.random_seed,
416
+ seed_rules: seedRules,
417
+ require_confirmation: params.require_confirmation,
418
+ });
419
+ if (planResult.status === "error") {
420
+ return planResult;
421
+ }
422
+ return {
423
+ status: "success",
424
+ data: {
425
+ template: params.template,
426
+ scale,
427
+ database,
428
+ detected_tables: detected.detectedTables,
429
+ target_tables: detected.targetTables,
430
+ ignored_tables: detected.ignoredTables,
431
+ warnings: detected.warnings,
432
+ plan: planResult.data,
433
+ next_recommended_tool: "generate_seed_preview",
434
+ },
435
+ };
436
+ }
437
+ catch (error) {
438
+ return { status: "error", error: error instanceof Error ? error.message : String(error) };
439
+ }
440
+ }
441
+ resolveDatabase(database) {
442
+ const connectedDatabase = config_1.dbConfig.database;
443
+ if (!connectedDatabase) {
444
+ throw new Error("No database configured. Set DB_NAME or include a database in the MySQL URL.");
445
+ }
446
+ if (database && database !== connectedDatabase) {
447
+ throw new Error(`Access denied. You can only access the connected database '${connectedDatabase}'. Requested '${database}'.`);
448
+ }
449
+ return database || connectedDatabase;
450
+ }
451
+ async loadSchema(database) {
452
+ const columns = await this.db.query(`
453
+ SELECT
454
+ TABLE_NAME as tableName,
455
+ COLUMN_NAME as columnName,
456
+ DATA_TYPE as dataType,
457
+ COLUMN_TYPE as columnType,
458
+ IS_NULLABLE as isNullable,
459
+ COLUMN_DEFAULT as columnDefault,
460
+ EXTRA as extra,
461
+ COLUMN_KEY as columnKey,
462
+ ORDINAL_POSITION as ordinalPosition,
463
+ CHARACTER_MAXIMUM_LENGTH as characterMaximumLength
464
+ FROM INFORMATION_SCHEMA.COLUMNS
465
+ WHERE TABLE_SCHEMA = ?
466
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
467
+ `, [database]);
468
+ const foreignKeys = await this.db.query(`
469
+ SELECT
470
+ TABLE_NAME as childTable,
471
+ COLUMN_NAME as childColumn,
472
+ REFERENCED_TABLE_NAME as parentTable,
473
+ REFERENCED_COLUMN_NAME as parentColumn,
474
+ CONSTRAINT_NAME as constraintName
475
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
476
+ WHERE TABLE_SCHEMA = ?
477
+ AND REFERENCED_TABLE_NAME IS NOT NULL
478
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
479
+ `, [database]);
480
+ const uniqueRows = await this.db.query(`
481
+ SELECT
482
+ TABLE_NAME as tableName,
483
+ INDEX_NAME as indexName,
484
+ COLUMN_NAME as columnName,
485
+ SEQ_IN_INDEX as seqInIndex
486
+ FROM INFORMATION_SCHEMA.STATISTICS
487
+ WHERE TABLE_SCHEMA = ?
488
+ AND NON_UNIQUE = 0
489
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
490
+ `, [database]);
491
+ const tableRows = await this.db.query(`
492
+ SELECT TABLE_NAME as tableName, TABLE_ROWS as tableRows
493
+ FROM INFORMATION_SCHEMA.TABLES
494
+ WHERE TABLE_SCHEMA = ?
495
+ `, [database]);
496
+ const tables = {};
497
+ for (const rawColumn of columns) {
498
+ const column = {
499
+ tableName: rawColumn.tableName,
500
+ columnName: rawColumn.columnName,
501
+ dataType: String(rawColumn.dataType || "").toLowerCase(),
502
+ columnType: String(rawColumn.columnType || "").toLowerCase(),
503
+ isNullable: rawColumn.isNullable === "YES",
504
+ columnDefault: rawColumn.columnDefault,
505
+ extra: String(rawColumn.extra || "").toLowerCase(),
506
+ columnKey: String(rawColumn.columnKey || ""),
507
+ ordinalPosition: Number(rawColumn.ordinalPosition || 0),
508
+ characterMaximumLength: rawColumn.characterMaximumLength ? Number(rawColumn.characterMaximumLength) : undefined,
509
+ };
510
+ tables[column.tableName] = tables[column.tableName] || {
511
+ tableName: column.tableName,
512
+ columns: [],
513
+ primaryKeys: [],
514
+ };
515
+ tables[column.tableName].columns.push(column);
516
+ if (column.columnKey === "PRI") {
517
+ tables[column.tableName].primaryKeys.push(column.columnName);
518
+ }
519
+ if (column.extra.includes("auto_increment")) {
520
+ tables[column.tableName].autoIncrementColumn = column.columnName;
521
+ }
522
+ }
523
+ const uniqueIndexesByKey = {};
524
+ for (const row of uniqueRows) {
525
+ const key = `${row.tableName}.${row.indexName}`;
526
+ uniqueIndexesByKey[key] = uniqueIndexesByKey[key] || {
527
+ tableName: row.tableName,
528
+ indexName: row.indexName,
529
+ columns: [],
530
+ };
531
+ uniqueIndexesByKey[key].columns.push(row.columnName);
532
+ }
533
+ const rowCounts = {};
534
+ for (const row of tableRows) {
535
+ rowCounts[row.tableName] = Number(row.tableRows || 0);
536
+ }
537
+ const groupedForeignKeys = this.groupForeignKeys(foreignKeys);
538
+ return {
539
+ database,
540
+ tables,
541
+ foreignKeys: groupedForeignKeys,
542
+ uniqueIndexes: Object.values(uniqueIndexesByKey),
543
+ rowCounts,
544
+ };
545
+ }
546
+ groupForeignKeys(rows) {
547
+ const grouped = {};
548
+ for (const row of rows) {
549
+ const key = `${row.childTable}.${row.constraintName}`;
550
+ if (!grouped[key]) {
551
+ grouped[key] = {
552
+ childTable: row.childTable,
553
+ childColumn: row.childColumn,
554
+ childColumns: [],
555
+ parentTable: row.parentTable,
556
+ parentColumn: row.parentColumn,
557
+ parentColumns: [],
558
+ constraintName: row.constraintName,
559
+ };
560
+ }
561
+ grouped[key].childColumns.push(row.childColumn);
562
+ grouped[key].parentColumns.push(row.parentColumn);
563
+ }
564
+ return Object.values(grouped);
565
+ }
566
+ topologicalSort(tables, foreignKeys, warnings) {
567
+ const tableSet = new Set(tables);
568
+ const indegree = {};
569
+ const edges = {};
570
+ for (const table of tables) {
571
+ indegree[table] = 0;
572
+ edges[table] = [];
573
+ }
574
+ for (const fk of foreignKeys) {
575
+ if (!tableSet.has(fk.parentTable) || !tableSet.has(fk.childTable) || fk.parentTable === fk.childTable) {
576
+ continue;
577
+ }
578
+ edges[fk.parentTable].push(fk.childTable);
579
+ indegree[fk.childTable]++;
580
+ }
581
+ const queue = tables.filter((table) => indegree[table] === 0).sort();
582
+ const ordered = [];
583
+ while (queue.length > 0) {
584
+ const table = queue.shift();
585
+ ordered.push(table);
586
+ for (const child of edges[table]) {
587
+ indegree[child]--;
588
+ if (indegree[child] === 0) {
589
+ queue.push(child);
590
+ queue.sort();
591
+ }
592
+ }
593
+ }
594
+ if (ordered.length !== tables.length) {
595
+ const remaining = tables.filter((table) => !ordered.includes(table)).sort();
596
+ warnings.push(`Cycle detected in table relationships: ${remaining.join(", ")}. Remaining tables were appended after dependency ordering.`);
597
+ ordered.push(...remaining);
598
+ }
599
+ return ordered;
600
+ }
601
+ calculateRowsToCreate(orderedTables, targetTables, schema, reasons, rowsPerTable, childRowsPerParent, maxRowsPerTable, respectExistingData) {
602
+ const explicitRows = typeof rowsPerTable === "object" && rowsPerTable !== null ? rowsPerTable : {};
603
+ const baseRows = typeof rowsPerTable === "number" ? rowsPerTable : 10;
604
+ const targetSet = new Set(targetTables);
605
+ const requirements = [];
606
+ for (const table of orderedTables) {
607
+ const existingRows = schema.rowCounts[table] || 0;
608
+ let rowsToCreate = this.getExplicitOrDefaultRows(table, explicitRows, baseRows);
609
+ if (!targetSet.has(table)) {
610
+ const isChildOfIncluded = schema.foreignKeys.some((fk) => fk.childTable === table && orderedTables.includes(fk.parentTable));
611
+ const isParentDependency = schema.foreignKeys.some((fk) => fk.parentTable === table && orderedTables.includes(fk.childTable));
612
+ if (isChildOfIncluded && !isParentDependency) {
613
+ const parentFk = schema.foreignKeys.find((fk) => fk.childTable === table && orderedTables.includes(fk.parentTable));
614
+ const parentRows = parentFk
615
+ ? requirements.find((item) => item.table === parentFk.parentTable)?.rows_to_create || baseRows
616
+ : baseRows;
617
+ rowsToCreate = parentRows * childRowsPerParent;
618
+ }
619
+ else if (respectExistingData && isParentDependency && existingRows > 0) {
620
+ rowsToCreate = 0;
621
+ }
622
+ }
623
+ rowsToCreate = this.clampNumber(rowsToCreate, 0, maxRowsPerTable, baseRows);
624
+ requirements.push({
625
+ table,
626
+ rows_to_create: rowsToCreate,
627
+ reason: (reasons[table] || ["related table"]).join("; "),
628
+ existing_rows: existingRows,
629
+ });
630
+ }
631
+ return requirements;
632
+ }
633
+ buildInferredSeedRules(tables, schema, customRules, domain = "generic") {
634
+ const rules = {};
635
+ const uniqueColumns = new Set();
636
+ for (const index of schema.uniqueIndexes) {
637
+ if (index.columns.length === 1) {
638
+ uniqueColumns.add(`${index.tableName}.${index.columns[0]}`);
639
+ }
640
+ }
641
+ for (const table of tables) {
642
+ for (const column of schema.tables[table].columns) {
643
+ const key = `${table}.${column.columnName}`;
644
+ rules[key] =
645
+ customRules[key] ||
646
+ customRules[column.columnName] ||
647
+ this.getDomainRule(domain, table, column) ||
648
+ this.inferRuleForColumn(table, column, uniqueColumns.has(key));
649
+ }
650
+ }
651
+ return rules;
652
+ }
653
+ inferRuleForColumn(table, column, unique) {
654
+ const name = column.columnName.toLowerCase();
655
+ const enumValues = this.extractEnumValues(column.columnType);
656
+ if (enumValues.length > 0) {
657
+ return { generator: "choice", values: enumValues };
658
+ }
659
+ if (name.includes("email"))
660
+ return { generator: "email", domain: "example.test" };
661
+ if (name.includes("username"))
662
+ return { generator: "username", prefix: table };
663
+ if (name === "name" || name.endsWith("_name") || name.includes("fullname"))
664
+ return { generator: "person_name" };
665
+ if (name.includes("phone") || name.includes("mobile") || name.includes("telp"))
666
+ return { generator: "phone" };
667
+ if (name.includes("slug"))
668
+ return { generator: "slug", prefix: table };
669
+ if (name.includes("sku"))
670
+ return { generator: "pattern", pattern: `${table.toUpperCase().slice(0, 3)}-####` };
671
+ if (name.includes("code") || unique)
672
+ return { generator: "pattern", pattern: `${column.columnName.toUpperCase().slice(0, 4)}-####` };
673
+ if (name.includes("status"))
674
+ return { generator: "choice", values: ["active", "inactive", "pending"] };
675
+ if (name.includes("role"))
676
+ return { generator: "choice", values: ["member", "staff", "manager"] };
677
+ if (name.includes("type") || name.includes("category"))
678
+ return { generator: "choice", values: ["standard", "premium", "basic"] };
679
+ if (name.includes("price") || name.includes("amount") || name.includes("total") || name.includes("balance")) {
680
+ return { generator: "number", min: 10000, max: 500000 };
681
+ }
682
+ if (name.includes("qty") || name.includes("quantity") || name.includes("count") || name.includes("stock")) {
683
+ return { generator: "integer", min: 1, max: 20 };
684
+ }
685
+ if (name.includes("uuid"))
686
+ return { generator: "uuid" };
687
+ if (name.includes("password"))
688
+ return { generator: "string", prefix: "DummyPassword" };
689
+ if (this.isBooleanColumn(column))
690
+ return { generator: "boolean" };
691
+ if (this.isIntegerColumn(column))
692
+ return { generator: "integer", min: 1, max: 1000 };
693
+ if (this.isNumberColumn(column))
694
+ return { generator: "number", min: 1, max: 1000 };
695
+ if (column.dataType === "date")
696
+ return { generator: "date" };
697
+ if (["datetime", "timestamp"].includes(column.dataType))
698
+ return { generator: "datetime" };
699
+ if (column.dataType === "time")
700
+ return { generator: "time" };
701
+ if (column.dataType === "year")
702
+ return { generator: "year" };
703
+ if (column.dataType === "json")
704
+ return { generator: "json" };
705
+ if (column.dataType.includes("text"))
706
+ return { generator: "text", prefix: column.columnName };
707
+ return { generator: "string", prefix: column.columnName };
708
+ }
709
+ detectConstraints(tables, schema) {
710
+ const tableSet = new Set(tables);
711
+ return {
712
+ foreign_keys: schema.foreignKeys
713
+ .filter((fk) => tableSet.has(fk.childTable))
714
+ .map((fk) => this.formatForeignKey(fk)),
715
+ unique: schema.uniqueIndexes
716
+ .filter((index) => tableSet.has(index.tableName))
717
+ .map((index) => `${index.tableName}.${index.indexName}(${index.columns.join(",")})`),
718
+ required_columns: tables.flatMap((table) => this.getRequiredColumns(schema.tables[table]).map((column) => `${table}.${column.columnName}`)),
719
+ };
720
+ }
721
+ buildPreview(stored, maxRows, emailDomain) {
722
+ const preview = {};
723
+ const idMap = {};
724
+ const tupleMap = {};
725
+ for (const table of stored.plan.dependency_order) {
726
+ const requirement = stored.plan.tables_required.find((item) => item.table === table);
727
+ const count = Math.min(requirement?.rows_to_create || 0, maxRows);
728
+ preview[table] = this.buildRowsForTable(stored, table, count, {
729
+ preview: true,
730
+ emailDomain,
731
+ idMap,
732
+ tupleMap,
733
+ });
734
+ }
735
+ return preview;
736
+ }
737
+ buildRowsForTable(stored, tableName, count, context) {
738
+ const table = stored.schema.tables[tableName];
739
+ const rows = [];
740
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
741
+ const row = {};
742
+ const childForeignKeys = stored.schema.foreignKeys.filter((item) => item.childTable === tableName);
743
+ for (const fk of childForeignKeys) {
744
+ Object.assign(row, this.resolveForeignKeyTuple(stored, fk, rowIndex, context));
745
+ }
746
+ for (const column of table.columns) {
747
+ if (this.shouldSkipColumn(column)) {
748
+ continue;
749
+ }
750
+ if (row[column.columnName] !== undefined || this.isForeignKeyColumn(childForeignKeys, column.columnName)) {
751
+ continue;
752
+ }
753
+ const rule = stored.plan.seed_rules[`${tableName}.${column.columnName}`] || {};
754
+ const value = this.generateValue(stored.plan, tableName, column, rowIndex, rule, context.emailDomain);
755
+ if (value !== undefined) {
756
+ row[column.columnName] = value;
757
+ }
758
+ }
759
+ rows.push(row);
760
+ }
761
+ return rows;
762
+ }
763
+ resolveForeignKeyTuple(stored, fk, rowIndex, context) {
764
+ const parentRequirement = stored.plan.tables_required.find((item) => item.table === fk.parentTable);
765
+ const parentRows = parentRequirement?.rows_to_create || 0;
766
+ const resolved = {};
767
+ if (context.preview) {
768
+ for (let i = 0; i < fk.childColumns.length; i++) {
769
+ const parentColumn = fk.parentColumns[i];
770
+ const childColumn = fk.childColumns[i];
771
+ if (parentRows > 0 && stored.plan.dependency_order.includes(fk.parentTable)) {
772
+ resolved[childColumn] = `{{${fk.parentTable}[${rowIndex % parentRows}].${parentColumn}}}`;
773
+ }
774
+ else {
775
+ resolved[childColumn] = `{{existing ${fk.parentTable}.${parentColumn}}}`;
776
+ }
777
+ }
778
+ return resolved;
779
+ }
780
+ const signature = this.keySignature(fk.parentColumns);
781
+ const tuples = context.tupleMap[fk.parentTable]?.[signature] || [];
782
+ if (tuples.length === 0) {
783
+ throw new Error(`No parent key tuples available for ${this.formatForeignKey(fk)}`);
784
+ }
785
+ const tuple = tuples[rowIndex % tuples.length];
786
+ for (let i = 0; i < fk.childColumns.length; i++) {
787
+ resolved[fk.childColumns[i]] = tuple[fk.parentColumns[i]];
788
+ }
789
+ return resolved;
790
+ }
791
+ generateValue(plan, tableName, column, rowIndex, rule, emailDomain) {
792
+ if (rule.value !== undefined)
793
+ return rule.value;
794
+ if (rule.nullable && column.isNullable)
795
+ return null;
796
+ const generator = rule.generator || "string";
797
+ const rng = this.makeRng(plan.random_seed + this.hashString(`${tableName}.${column.columnName}`) + rowIndex);
798
+ const suffix = rowIndex + 1 + plan.random_seed;
799
+ const min = rule.min ?? 1;
800
+ const max = rule.max ?? 1000;
801
+ if (rule.values && rule.values.length > 0) {
802
+ return rule.values[Math.floor(rng() * rule.values.length)];
803
+ }
804
+ switch (generator) {
805
+ case "email":
806
+ return this.truncate(`${tableName}.${column.columnName}.${suffix}@${rule.domain || emailDomain}`.toLowerCase(), column);
807
+ case "username":
808
+ return this.truncate(`${rule.prefix || tableName}_user_${suffix}`, column);
809
+ case "person_name":
810
+ return this.truncate(this.pickPersonName(suffix), column);
811
+ case "phone":
812
+ return this.truncate(`+62812${String(10000000 + suffix).slice(-8)}`, column);
813
+ case "slug":
814
+ return this.truncate(`${rule.prefix || tableName}-${suffix}`, column);
815
+ case "pattern":
816
+ return this.truncate((rule.pattern || "SEED-####").replace(/#+/g, (match) => String(suffix).padStart(match.length, "0")), column);
817
+ case "choice":
818
+ return this.truncate(String((rule.values || ["seeded"])[Math.floor(rng() * (rule.values?.length || 1))]), column);
819
+ case "uuid":
820
+ return this.truncate(`00000000-0000-4000-8000-${String(100000000000 + suffix).slice(-12)}`, column);
821
+ case "boolean":
822
+ return rowIndex % 2 === 0;
823
+ case "integer":
824
+ return Math.floor(min + rng() * (max - min + 1));
825
+ case "number":
826
+ return Number((min + rng() * (max - min)).toFixed(2));
827
+ case "date":
828
+ return this.formatDate(new Date(Date.UTC(2026, rowIndex % 12, (rowIndex % 27) + 1)));
829
+ case "datetime":
830
+ return `${this.formatDate(new Date(Date.UTC(2026, rowIndex % 12, (rowIndex % 27) + 1)))} 10:00:00`;
831
+ case "time":
832
+ return `${String((rowIndex % 12) + 8).padStart(2, "0")}:00:00`;
833
+ case "year":
834
+ return 2026;
835
+ case "json":
836
+ return JSON.stringify({ seeded: true, index: rowIndex + 1 });
837
+ case "text":
838
+ return this.truncate(`${rule.prefix || column.columnName} seeded text ${suffix}`, column);
839
+ case "string":
840
+ default:
841
+ return this.truncate(`${rule.prefix || column.columnName}_${suffix}`, column);
842
+ }
843
+ }
844
+ async preloadExistingParentIds(stored, idMap, tupleMap, transactionId) {
845
+ for (const fk of stored.schema.foreignKeys) {
846
+ const childIncluded = stored.plan.dependency_order.includes(fk.childTable);
847
+ if (!childIncluded)
848
+ continue;
849
+ const parentRequirement = stored.plan.tables_required.find((item) => item.table === fk.parentTable);
850
+ if (parentRequirement && parentRequirement.rows_to_create > 0)
851
+ continue;
852
+ const tuples = await this.loadExistingKeyTuples(fk.parentTable, fk.parentColumns, 1000, transactionId);
853
+ this.storeTuples(fk.parentTable, fk.parentColumns, tuples, idMap, tupleMap);
854
+ }
855
+ }
856
+ async ensureIdMapForTable(stored, tableName, idMap, tupleMap, transactionId) {
857
+ const table = stored.schema.tables[tableName];
858
+ const primaryColumns = table.primaryKeys.length > 0
859
+ ? table.primaryKeys
860
+ : table.autoIncrementColumn
861
+ ? [table.autoIncrementColumn]
862
+ : [];
863
+ if (primaryColumns.length > 0) {
864
+ const signature = this.keySignature(primaryColumns);
865
+ if (!tupleMap[tableName]?.[signature]?.length) {
866
+ const tuples = await this.loadExistingKeyTuples(tableName, primaryColumns, 1000, transactionId);
867
+ this.storeTuples(tableName, primaryColumns, tuples, idMap, tupleMap);
868
+ }
869
+ }
870
+ const referencedKeys = stored.schema.foreignKeys
871
+ .filter((fk) => fk.parentTable === tableName)
872
+ .map((fk) => fk.parentColumns);
873
+ for (const columns of referencedKeys) {
874
+ const signature = this.keySignature(columns);
875
+ if (tupleMap[tableName]?.[signature]?.length)
876
+ continue;
877
+ const tuples = await this.loadExistingKeyTuples(tableName, columns, 1000, transactionId);
878
+ this.storeTuples(tableName, columns, tuples, idMap, tupleMap);
879
+ }
880
+ }
881
+ async loadExistingKeyTuples(tableName, columns, limit, transactionId) {
882
+ const selectColumns = columns.map((column) => this.security.escapeIdentifier(column)).join(", ");
883
+ const notNullChecks = columns.map((column) => `${this.security.escapeIdentifier(column)} IS NOT NULL`).join(" AND ");
884
+ const query = `SELECT ${selectColumns} FROM ${this.security.escapeIdentifier(tableName)} WHERE ${notNullChecks} LIMIT ${limit}`;
885
+ const rows = transactionId
886
+ ? await this.db.executeInTransaction(transactionId, query)
887
+ : await this.db.query(query);
888
+ return rows.map((row) => {
889
+ const tuple = {};
890
+ for (const column of columns) {
891
+ tuple[column] = row[column];
892
+ }
893
+ return tuple;
894
+ });
895
+ }
896
+ async insertRow(tableName, row, transactionId) {
897
+ const columns = Object.keys(row);
898
+ let query;
899
+ let values = [];
900
+ if (columns.length === 0) {
901
+ query = `INSERT INTO ${this.security.escapeIdentifier(tableName)} () VALUES ()`;
902
+ }
903
+ else {
904
+ values = columns.map((column) => row[column]);
905
+ const paramValidation = this.security.validateParameters(values);
906
+ if (!paramValidation.valid) {
907
+ throw new Error(`Invalid seed row value for '${tableName}': ${paramValidation.error}`);
908
+ }
909
+ values = paramValidation.sanitizedParams || [];
910
+ query = `INSERT INTO ${this.security.escapeIdentifier(tableName)} (${columns.map((column) => this.security.escapeIdentifier(column)).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
911
+ }
912
+ const result = transactionId
913
+ ? await this.db.executeInTransaction(transactionId, query, values)
914
+ : await this.db.query(query, values);
915
+ return {
916
+ insertId: result.insertId,
917
+ affectedRows: result.affectedRows || 0,
918
+ };
919
+ }
920
+ trackInsertedKeys(stored, table, row, insertId, idMap, tupleMap, insertedPrimaryKeys) {
921
+ const resolvedRow = { ...row };
922
+ if (table.autoIncrementColumn && insertId !== undefined && insertId !== 0) {
923
+ resolvedRow[table.autoIncrementColumn] = insertId;
924
+ }
925
+ const primaryColumns = table.primaryKeys.length > 0
926
+ ? table.primaryKeys
927
+ : table.autoIncrementColumn
928
+ ? [table.autoIncrementColumn]
929
+ : [];
930
+ if (primaryColumns.length > 0) {
931
+ const primaryTuple = this.tupleFromRow(resolvedRow, primaryColumns);
932
+ if (primaryTuple) {
933
+ this.storeTuples(table.tableName, primaryColumns, [primaryTuple], idMap, tupleMap);
934
+ insertedPrimaryKeys[table.tableName] = insertedPrimaryKeys[table.tableName] || [];
935
+ insertedPrimaryKeys[table.tableName].push(primaryColumns.length === 1 ? primaryTuple[primaryColumns[0]] : primaryTuple);
936
+ }
937
+ }
938
+ const referencedKeySignatures = new Set();
939
+ for (const fk of stored.schema.foreignKeys.filter((item) => item.parentTable === table.tableName)) {
940
+ const signature = this.keySignature(fk.parentColumns);
941
+ if (referencedKeySignatures.has(signature))
942
+ continue;
943
+ referencedKeySignatures.add(signature);
944
+ const tuple = this.tupleFromRow(resolvedRow, fk.parentColumns);
945
+ if (tuple) {
946
+ this.storeTuples(table.tableName, fk.parentColumns, [tuple], idMap, tupleMap);
947
+ }
948
+ }
949
+ }
950
+ async countForeignKeyOrphans(fk) {
951
+ const joinConditions = fk.childColumns
952
+ .map((childColumn, index) => `child_table.${this.security.escapeIdentifier(childColumn)} = parent_table.${this.security.escapeIdentifier(fk.parentColumns[index])}`)
953
+ .join(" AND ");
954
+ const childNotNullChecks = fk.childColumns
955
+ .map((childColumn) => `child_table.${this.security.escapeIdentifier(childColumn)} IS NOT NULL`)
956
+ .join(" AND ");
957
+ const parentNullCheck = `parent_table.${this.security.escapeIdentifier(fk.parentColumns[0])} IS NULL`;
958
+ const query = `
959
+ SELECT COUNT(*) as count
960
+ FROM ${this.security.escapeIdentifier(fk.childTable)} child_table
961
+ LEFT JOIN ${this.security.escapeIdentifier(fk.parentTable)} parent_table
962
+ ON ${joinConditions}
963
+ WHERE ${childNotNullChecks}
964
+ AND ${parentNullCheck}
965
+ `;
966
+ const rows = await this.db.query(query);
967
+ return Number(rows[0]?.count || 0);
968
+ }
969
+ async countNullValues(tableName, columnName) {
970
+ const query = `SELECT COUNT(*) as count FROM ${this.security.escapeIdentifier(tableName)} WHERE ${this.security.escapeIdentifier(columnName)} IS NULL`;
971
+ const rows = await this.db.query(query);
972
+ return Number(rows[0]?.count || 0);
973
+ }
974
+ async findUniqueCollisions(index) {
975
+ const escapedColumns = index.columns.map((column) => this.security.escapeIdentifier(column));
976
+ const nullChecks = escapedColumns.map((column) => `${column} IS NOT NULL`).join(" AND ");
977
+ const query = `
978
+ SELECT ${escapedColumns.join(", ")}, COUNT(*) as duplicate_count
979
+ FROM ${this.security.escapeIdentifier(index.tableName)}
980
+ ${nullChecks ? `WHERE ${nullChecks}` : ""}
981
+ GROUP BY ${escapedColumns.join(", ")}
982
+ HAVING COUNT(*) > 1
983
+ LIMIT 5
984
+ `;
985
+ return await this.db.query(query);
986
+ }
987
+ async getValidationRowCount(stored, tableName) {
988
+ const requirement = stored.plan.tables_required.find((item) => item.table === tableName);
989
+ const table = stored.schema.tables[tableName];
990
+ const keyColumns = table.primaryKeys.length > 0
991
+ ? table.primaryKeys
992
+ : table.autoIncrementColumn
993
+ ? [table.autoIncrementColumn]
994
+ : [];
995
+ const insertedKeys = stored.lastExecution?.insertedPrimaryKeys[tableName] || [];
996
+ if (keyColumns.length > 0 && insertedKeys.length > 0) {
997
+ const { whereClause, params } = this.buildTupleWhereClause(keyColumns, insertedKeys);
998
+ const query = `SELECT COUNT(*) as count FROM ${this.security.escapeIdentifier(tableName)} WHERE ${whereClause}`;
999
+ const rows = await this.db.query(query, params);
1000
+ return {
1001
+ expected_inserted: stored.lastExecution?.inserted[tableName] ?? requirement?.rows_to_create ?? 0,
1002
+ actual_inserted: Number(rows[0]?.count || 0),
1003
+ key_columns: keyColumns,
1004
+ };
1005
+ }
1006
+ const rows = await this.db.query(`SELECT COUNT(*) as count FROM ${this.security.escapeIdentifier(tableName)}`);
1007
+ return {
1008
+ expected_inserted: requirement?.rows_to_create ?? 0,
1009
+ actual_table_rows: Number(rows[0]?.count || 0),
1010
+ };
1011
+ }
1012
+ getRequiredColumns(table) {
1013
+ return table.columns.filter((column) => !column.isNullable &&
1014
+ column.columnDefault === null &&
1015
+ !column.extra.includes("auto_increment") &&
1016
+ !column.extra.includes("generated"));
1017
+ }
1018
+ shouldSkipColumn(column) {
1019
+ return column.extra.includes("auto_increment") || column.extra.includes("generated");
1020
+ }
1021
+ isIntegerColumn(column) {
1022
+ return ["tinyint", "smallint", "mediumint", "int", "integer", "bigint"].includes(column.dataType);
1023
+ }
1024
+ isNumberColumn(column) {
1025
+ return this.isIntegerColumn(column) || ["decimal", "numeric", "float", "double", "real"].includes(column.dataType);
1026
+ }
1027
+ isBooleanColumn(column) {
1028
+ return column.dataType === "tinyint" && /\(1\)/.test(column.columnType);
1029
+ }
1030
+ extractEnumValues(columnType) {
1031
+ if (!columnType.startsWith("enum("))
1032
+ return [];
1033
+ const values = [];
1034
+ const regex = /'((?:''|[^'])*)'/g;
1035
+ let match;
1036
+ while ((match = regex.exec(columnType)) !== null) {
1037
+ values.push(match[1].replace(/''/g, "'"));
1038
+ }
1039
+ return values;
1040
+ }
1041
+ isForeignKeyColumn(foreignKeys, columnName) {
1042
+ return foreignKeys.some((fk) => fk.childColumns.includes(columnName));
1043
+ }
1044
+ keySignature(columns) {
1045
+ return columns.join("|");
1046
+ }
1047
+ formatForeignKey(fk) {
1048
+ return `${fk.childTable}(${fk.childColumns.join(",")}) -> ${fk.parentTable}(${fk.parentColumns.join(",")})`;
1049
+ }
1050
+ tupleFromRow(row, columns) {
1051
+ const tuple = {};
1052
+ for (const column of columns) {
1053
+ if (row[column] === undefined || row[column] === null) {
1054
+ return null;
1055
+ }
1056
+ tuple[column] = row[column];
1057
+ }
1058
+ return tuple;
1059
+ }
1060
+ storeTuples(tableName, columns, tuples, idMap, tupleMap) {
1061
+ const signature = this.keySignature(columns);
1062
+ tupleMap[tableName] = tupleMap[tableName] || {};
1063
+ tupleMap[tableName][signature] = tupleMap[tableName][signature] || [];
1064
+ idMap[tableName] = idMap[tableName] || {};
1065
+ for (const tuple of tuples) {
1066
+ tupleMap[tableName][signature].push(tuple);
1067
+ for (const column of columns) {
1068
+ idMap[tableName][column] = idMap[tableName][column] || [];
1069
+ idMap[tableName][column].push(tuple[column]);
1070
+ }
1071
+ }
1072
+ }
1073
+ buildTupleWhereClause(columns, keys) {
1074
+ const clauses = [];
1075
+ const params = [];
1076
+ for (const key of keys) {
1077
+ const tuple = typeof key === "object" && key !== null
1078
+ ? key
1079
+ : columns.length === 1
1080
+ ? { [columns[0]]: key }
1081
+ : null;
1082
+ if (!tuple)
1083
+ continue;
1084
+ const tupleClauses = [];
1085
+ let valid = true;
1086
+ for (const column of columns) {
1087
+ if (tuple[column] === undefined || tuple[column] === null) {
1088
+ valid = false;
1089
+ break;
1090
+ }
1091
+ tupleClauses.push(`${this.security.escapeIdentifier(column)} = ?`);
1092
+ params.push(tuple[column]);
1093
+ }
1094
+ if (valid) {
1095
+ clauses.push(`(${tupleClauses.join(" AND ")})`);
1096
+ }
1097
+ }
1098
+ return {
1099
+ whereClause: clauses.length > 0 ? clauses.join(" OR ") : "1=0",
1100
+ params,
1101
+ };
1102
+ }
1103
+ getSelectableTables(schema, requestedTables, maxTables) {
1104
+ const limit = this.clampNumber(maxTables, 1, 200, 50);
1105
+ if (requestedTables && requestedTables.length > 0) {
1106
+ const tables = Array.from(new Set(requestedTables));
1107
+ for (const table of tables) {
1108
+ this.assertIdentifier(table, "table");
1109
+ if (!schema.tables[table]) {
1110
+ throw new Error(`Table '${table}' does not exist in database '${schema.database}'`);
1111
+ }
1112
+ }
1113
+ return tables.slice(0, limit);
1114
+ }
1115
+ return Object.keys(schema.tables).sort().slice(0, limit);
1116
+ }
1117
+ async loadSampleRows(tableName, limit) {
1118
+ const query = `SELECT * FROM ${this.security.escapeIdentifier(tableName)} LIMIT ${limit}`;
1119
+ return await this.db.query(query);
1120
+ }
1121
+ refineRulesFromSamples(tableName, table, samples, rules, warnings) {
1122
+ if (samples.length === 0)
1123
+ return;
1124
+ for (const column of table.columns) {
1125
+ const key = `${tableName}.${column.columnName}`;
1126
+ const values = samples
1127
+ .map((row) => row[column.columnName])
1128
+ .filter((value) => value !== null && value !== undefined);
1129
+ if (values.length === 0 || this.isSensitiveColumn(column.columnName)) {
1130
+ continue;
1131
+ }
1132
+ const numericValues = values
1133
+ .map((value) => Number(value))
1134
+ .filter((value) => Number.isFinite(value));
1135
+ if (numericValues.length === values.length && this.isNumberColumn(column)) {
1136
+ const min = Math.min(...numericValues);
1137
+ const max = Math.max(...numericValues);
1138
+ rules[key] = {
1139
+ ...rules[key],
1140
+ generator: this.isIntegerColumn(column) ? "integer" : "number",
1141
+ min,
1142
+ max: max > min ? max : min + 1,
1143
+ source: "sample_range",
1144
+ };
1145
+ continue;
1146
+ }
1147
+ const distinctValues = Array.from(new Set(values.map((value) => String(value)))).slice(0, 20);
1148
+ if (this.canUseChoiceFromSamples(column.columnName, distinctValues)) {
1149
+ rules[key] = {
1150
+ ...rules[key],
1151
+ generator: "choice",
1152
+ values: distinctValues,
1153
+ source: "sample_choice",
1154
+ };
1155
+ continue;
1156
+ }
1157
+ const pattern = this.inferPatternFromSamples(column, distinctValues);
1158
+ if (pattern) {
1159
+ rules[key] = {
1160
+ ...rules[key],
1161
+ generator: "pattern",
1162
+ pattern,
1163
+ source: "sample_pattern",
1164
+ };
1165
+ }
1166
+ }
1167
+ if (samples.length < 3) {
1168
+ warnings.push(`Only ${samples.length} sample row(s) available for '${tableName}', so inferred rules may be generic.`);
1169
+ }
1170
+ }
1171
+ isSensitiveColumn(columnName) {
1172
+ return /(password|token|secret|key|credential|email|phone|mobile|address|name)$/i.test(columnName);
1173
+ }
1174
+ canUseChoiceFromSamples(columnName, values) {
1175
+ if (values.length === 0 || values.length > 10)
1176
+ return false;
1177
+ if (!/(status|state|type|role|category|stage|source|method|channel|priority)$/i.test(columnName))
1178
+ return false;
1179
+ return values.every((value) => value.length <= 50 && !/@/.test(value));
1180
+ }
1181
+ inferPatternFromSamples(column, values) {
1182
+ if (values.length === 0 || !/(code|sku|number|ref|reference|invoice|receipt)/i.test(column.columnName)) {
1183
+ return null;
1184
+ }
1185
+ const prefixMatch = values
1186
+ .map((value) => value.match(/^([A-Za-z]{2,10})[-_]/)?.[1])
1187
+ .find(Boolean);
1188
+ if (!prefixMatch) {
1189
+ return null;
1190
+ }
1191
+ return `${prefixMatch.toUpperCase()}-####`;
1192
+ }
1193
+ detectDomainFromTables(tables) {
1194
+ const joined = tables.join("_").toLowerCase();
1195
+ if (/(order|product|cart|payment|shipment|invoice)/.test(joined))
1196
+ return "ecommerce";
1197
+ if (/(sale|pos|cashier|receipt|shift|register)/.test(joined))
1198
+ return "pos";
1199
+ if (/(lead|deal|opportunit|contact|account|company|activity)/.test(joined))
1200
+ return "crm";
1201
+ return "generic";
1202
+ }
1203
+ getDomainRule(domain, tableName, column) {
1204
+ const table = tableName.toLowerCase();
1205
+ const name = column.columnName.toLowerCase();
1206
+ if (domain === "ecommerce") {
1207
+ if (name.includes("sku"))
1208
+ return { generator: "pattern", pattern: "PRD-####", source: "domain_ecommerce" };
1209
+ if (name.includes("status") && table.includes("order"))
1210
+ return { generator: "choice", values: ["pending", "paid", "processing", "shipped", "cancelled"], source: "domain_ecommerce" };
1211
+ if (name.includes("status") && table.includes("payment"))
1212
+ return { generator: "choice", values: ["pending", "paid", "failed", "refunded"], source: "domain_ecommerce" };
1213
+ if (name.includes("method"))
1214
+ return { generator: "choice", values: ["cash", "bank_transfer", "card", "ewallet"], source: "domain_ecommerce" };
1215
+ if (name.includes("price") || name.includes("total") || name.includes("amount"))
1216
+ return { generator: "number", min: 10000, max: 750000, source: "domain_ecommerce" };
1217
+ if (name.includes("qty") || name.includes("quantity"))
1218
+ return { generator: "integer", min: 1, max: 5, source: "domain_ecommerce" };
1219
+ }
1220
+ if (domain === "pos") {
1221
+ if (name.includes("receipt") || name.includes("invoice"))
1222
+ return { generator: "pattern", pattern: "POS-####", source: "domain_pos" };
1223
+ if (name.includes("status"))
1224
+ return { generator: "choice", values: ["open", "paid", "void", "refunded"], source: "domain_pos" };
1225
+ if (name.includes("method"))
1226
+ return { generator: "choice", values: ["cash", "card", "qris", "ewallet"], source: "domain_pos" };
1227
+ if (name.includes("shift"))
1228
+ return { generator: "choice", values: ["morning", "afternoon", "night"], source: "domain_pos" };
1229
+ if (name.includes("price") || name.includes("total") || name.includes("amount"))
1230
+ return { generator: "number", min: 5000, max: 250000, source: "domain_pos" };
1231
+ }
1232
+ if (domain === "crm") {
1233
+ if (name.includes("stage"))
1234
+ return { generator: "choice", values: ["new", "qualified", "proposal", "won", "lost"], source: "domain_crm" };
1235
+ if (name.includes("status"))
1236
+ return { generator: "choice", values: ["new", "contacted", "qualified", "inactive"], source: "domain_crm" };
1237
+ if (name.includes("source"))
1238
+ return { generator: "choice", values: ["website", "referral", "event", "ads"], source: "domain_crm" };
1239
+ if (name.includes("company"))
1240
+ return { generator: "string", prefix: "Company", source: "domain_crm" };
1241
+ if (name.includes("amount") || name.includes("value"))
1242
+ return { generator: "number", min: 1000000, max: 100000000, source: "domain_crm" };
1243
+ }
1244
+ return undefined;
1245
+ }
1246
+ detectTemplateTables(schema, template, include, exclude) {
1247
+ const excluded = new Set(exclude || []);
1248
+ for (const table of excluded) {
1249
+ this.assertIdentifier(table, "table");
1250
+ }
1251
+ if (include && include.length > 0) {
1252
+ const targetTables = Array.from(new Set(include)).filter((table) => !excluded.has(table));
1253
+ for (const table of targetTables) {
1254
+ this.assertIdentifier(table, "table");
1255
+ if (!schema.tables[table]) {
1256
+ throw new Error(`Included table '${table}' does not exist in database '${schema.database}'`);
1257
+ }
1258
+ }
1259
+ return {
1260
+ targetTables,
1261
+ detectedTables: { included: targetTables },
1262
+ ignoredTables: Array.from(excluded),
1263
+ warnings: [],
1264
+ };
1265
+ }
1266
+ const keywords = this.getTemplateKeywords(template);
1267
+ const tables = Object.keys(schema.tables);
1268
+ const detectedTables = {};
1269
+ const targetSet = new Set();
1270
+ const warnings = [];
1271
+ for (const [role, roleKeywords] of Object.entries(keywords)) {
1272
+ const matches = tables.filter((table) => {
1273
+ const normalized = table.toLowerCase();
1274
+ return !excluded.has(table) && roleKeywords.some((keyword) => normalized.includes(keyword));
1275
+ });
1276
+ if (matches.length > 0) {
1277
+ detectedTables[role] = matches;
1278
+ matches.forEach((table) => targetSet.add(table));
1279
+ }
1280
+ else {
1281
+ warnings.push(`No table matched template role '${role}'.`);
1282
+ }
1283
+ }
1284
+ return {
1285
+ targetTables: Array.from(targetSet).sort(),
1286
+ detectedTables,
1287
+ ignoredTables: Array.from(excluded),
1288
+ warnings,
1289
+ };
1290
+ }
1291
+ getTemplateKeywords(template) {
1292
+ const templates = {
1293
+ ecommerce: {
1294
+ users: ["user", "customer", "member"],
1295
+ products: ["product", "item", "catalog"],
1296
+ orders: ["order", "cart", "checkout"],
1297
+ order_items: ["order_item", "order_detail", "line_item"],
1298
+ payments: ["payment", "transaction", "invoice"],
1299
+ shipments: ["shipment", "shipping", "delivery"],
1300
+ },
1301
+ pos: {
1302
+ customers: ["customer", "member"],
1303
+ products: ["product", "item", "inventory"],
1304
+ cashiers: ["cashier", "employee", "staff", "user"],
1305
+ sales: ["sale", "transaction", "receipt", "order"],
1306
+ sale_items: ["sale_item", "transaction_item", "receipt_item", "order_item"],
1307
+ payments: ["payment", "tender"],
1308
+ shifts: ["shift", "register"],
1309
+ },
1310
+ crm: {
1311
+ contacts: ["contact", "lead", "customer"],
1312
+ companies: ["company", "account", "organization"],
1313
+ deals: ["deal", "opportunity", "pipeline"],
1314
+ activities: ["activity", "task", "note", "interaction"],
1315
+ users: ["user", "owner", "agent"],
1316
+ },
1317
+ };
1318
+ return templates[template];
1319
+ }
1320
+ getScaleRows(template, scale, tables) {
1321
+ const baseByScale = {
1322
+ small: 10,
1323
+ medium: 50,
1324
+ large: 200,
1325
+ };
1326
+ const base = baseByScale[scale];
1327
+ const rows = {};
1328
+ for (const table of tables) {
1329
+ const normalized = table.toLowerCase();
1330
+ if (/(item|detail|line)/.test(normalized))
1331
+ rows[table] = Math.min(base * 3, 1000);
1332
+ else if (/(product|inventory|catalog)/.test(normalized))
1333
+ rows[table] = Math.min(base * 2, 1000);
1334
+ else if (/(payment|shipment|activity|task|note)/.test(normalized))
1335
+ rows[table] = base;
1336
+ else if (template === "crm" && /(deal|opportunit)/.test(normalized))
1337
+ rows[table] = base;
1338
+ else
1339
+ rows[table] = base;
1340
+ }
1341
+ return rows;
1342
+ }
1343
+ buildTemplateRules(template, tables, schema) {
1344
+ return this.buildInferredSeedRules(tables, schema, {}, template);
1345
+ }
1346
+ requirementsToInsertMap(requirements) {
1347
+ return requirements.reduce((acc, requirement) => {
1348
+ acc[requirement.table] = requirement.rows_to_create;
1349
+ return acc;
1350
+ }, {});
1351
+ }
1352
+ countResolvedForeignKeys(stored) {
1353
+ return stored.schema.foreignKeys
1354
+ .filter((fk) => stored.plan.dependency_order.includes(fk.childTable))
1355
+ .reduce((sum, fk) => {
1356
+ const requirement = stored.plan.tables_required.find((item) => item.table === fk.childTable);
1357
+ return sum + (requirement?.rows_to_create || 0);
1358
+ }, 0);
1359
+ }
1360
+ getExplicitOrDefaultRows(table, explicitRows, defaultRows) {
1361
+ return explicitRows[table] ?? defaultRows;
1362
+ }
1363
+ assertIdentifier(identifier, label) {
1364
+ const validation = this.security.validateIdentifier(identifier);
1365
+ if (!validation.valid) {
1366
+ throw new Error(`Invalid ${label} '${identifier}': ${validation.error}`);
1367
+ }
1368
+ }
1369
+ getStoredPlan(planId) {
1370
+ const stored = this.plans.get(planId);
1371
+ if (!stored) {
1372
+ throw new Error(`Seed plan '${planId}' was not found. Run plan_seed_data first in the current MCP session.`);
1373
+ }
1374
+ return stored;
1375
+ }
1376
+ createPlanId() {
1377
+ RelationalSeederTools.planCounter++;
1378
+ const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
1379
+ return `seed_plan_${timestamp}_${String(RelationalSeederTools.planCounter).padStart(3, "0")}`;
1380
+ }
1381
+ createConfirmToken(database) {
1382
+ return `CONFIRM_SEED_${database.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
1383
+ }
1384
+ isProductionLikeDatabase(database) {
1385
+ return /(^|[_-])(prod|production|live|main|primary)([_-]|$)/i.test(database);
1386
+ }
1387
+ clampNumber(value, min, max, defaultValue) {
1388
+ const numeric = Number(value);
1389
+ if (!Number.isFinite(numeric))
1390
+ return defaultValue;
1391
+ return Math.min(Math.max(Math.floor(numeric), min), max);
1392
+ }
1393
+ makeRng(seed) {
1394
+ let value = seed >>> 0;
1395
+ return () => {
1396
+ value += 0x6D2B79F5;
1397
+ let result = value;
1398
+ result = Math.imul(result ^ (result >>> 15), result | 1);
1399
+ result ^= result + Math.imul(result ^ (result >>> 7), result | 61);
1400
+ return ((result ^ (result >>> 14)) >>> 0) / 4294967296;
1401
+ };
1402
+ }
1403
+ hashString(value) {
1404
+ let hash = 0;
1405
+ for (let i = 0; i < value.length; i++) {
1406
+ hash = Math.imul(31, hash) + value.charCodeAt(i) | 0;
1407
+ }
1408
+ return Math.abs(hash);
1409
+ }
1410
+ truncate(value, column) {
1411
+ const maxLength = column.characterMaximumLength;
1412
+ if (!maxLength || value.length <= maxLength)
1413
+ return value;
1414
+ return value.slice(0, maxLength);
1415
+ }
1416
+ pickPersonName(seed) {
1417
+ const names = [
1418
+ "Budi Santoso",
1419
+ "Siti Aminah",
1420
+ "Andi Pratama",
1421
+ "Maya Lestari",
1422
+ "Dewi Anggraini",
1423
+ "Rizky Saputra",
1424
+ ];
1425
+ return names[seed % names.length];
1426
+ }
1427
+ formatDate(date) {
1428
+ return date.toISOString().slice(0, 10);
1429
+ }
1430
+ }
1431
+ exports.RelationalSeederTools = RelationalSeederTools;
1432
+ RelationalSeederTools.planCounter = 0;