tina4-nodejs 3.13.124 → 3.13.130

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.
@@ -915,6 +915,38 @@ var init_sqlTranslator = __esm({
915
915
  return sql;
916
916
  }
917
917
  }
918
+ /**
919
+ * Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
920
+ * target engine.
921
+ *
922
+ * ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
923
+ * that happens to contain the word `TEXT` (a column name, a string literal) is
924
+ * never rewritten. Complements `autoIncrementSyntax` (which maps the id
925
+ * keyword) so ONE portable migration — and every `Model.createTable()` DDL,
926
+ * which is also SQLite-canonical — applies on every engine instead of failing
927
+ * on Firebird/MSSQL.
928
+ *
929
+ * * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
930
+ * EXISTS`.
931
+ * * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
932
+ * rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
933
+ * * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
934
+ * column maps to `DATETIME` (matching the adapters' createTableAsync).
935
+ */
936
+ static ddlTypes(sql, engine) {
937
+ const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
938
+ if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
939
+ switch ((engine ?? "").toLowerCase()) {
940
+ case "firebird":
941
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bBLOB\s+SUB_TYPE\s+TEXT\b/gi, "\0FBTEXT\0").replace(/\bTEXT\b/gi, "BLOB SUB_TYPE TEXT").replaceAll("\0FBTEXT\0", "BLOB SUB_TYPE TEXT").replace(/\bREAL\b/gi, "DOUBLE PRECISION");
942
+ case "mssql":
943
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bTIMESTAMP\b/gi, "DATETIME2");
944
+ case "mysql":
945
+ return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
946
+ default:
947
+ return sql;
948
+ }
949
+ }
918
950
  /**
919
951
  * Convert ? placeholders to engine-specific style.
920
952
  *
@@ -12729,6 +12761,7 @@ __export(generate_exports, {
12729
12761
  parseEvery: () => parseEvery,
12730
12762
  parseFields: () => parseFields,
12731
12763
  pluralizeReserved: () => pluralizeReserved,
12764
+ resolveTable: () => resolveTable,
12732
12765
  toPascal: () => toPascal,
12733
12766
  toSnake: () => toSnake,
12734
12767
  toTableName: () => toTableName
@@ -12771,12 +12804,35 @@ function toTableName(name) {
12771
12804
  from: raw,
12772
12805
  to: safe,
12773
12806
  reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
12774
- override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
12807
+ override: `--table-name <name> (table names interpolate unquoted; forcing a reserved name is yours to quote in raw SQL)`
12775
12808
  });
12776
12809
  return safe;
12777
12810
  }
12778
12811
  return raw;
12779
12812
  }
12813
+ function resolveTable(name, flags, opts = {}) {
12814
+ const announce = opts.announce ?? false;
12815
+ const override = (flags ?? {})["table-name"];
12816
+ if (typeof override === "string" && override) {
12817
+ if (announce && SQL_RESERVED_TABLE_NAMES.has(toSnake(override))) {
12818
+ console.error(
12819
+ ` ! table_name '${override}' is a SQL reserved word. Tina4 interpolates table names UNQUOTED, so the ORM's generated SQL will fail on it -- quote it yourself in raw SQL and migrations.`
12820
+ );
12821
+ }
12822
+ return override;
12823
+ }
12824
+ const bare = toSnake(name);
12825
+ if (!SQL_RESERVED_TABLE_NAMES.has(bare)) {
12826
+ return bare;
12827
+ }
12828
+ const table2 = toTableName(name);
12829
+ if (announce) {
12830
+ console.error(
12831
+ ` \xB7 '${bare}' is a SQL reserved word; using table_name '${table2}' (Tina4 interpolates table names unquoted). Override with --table-name <name>.`
12832
+ );
12833
+ }
12834
+ return table2;
12835
+ }
12780
12836
  function resetResolution(target, input, opts) {
12781
12837
  __resolution.target = target;
12782
12838
  __resolution.input = input;
@@ -12875,10 +12931,11 @@ function printResolution() {
12875
12931
  lines.push(` migration ${b.migration_path}`);
12876
12932
  }
12877
12933
  const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
12878
- if (reserved && reserved.from && reserved.override) {
12934
+ if (reserved && reserved.from) {
12879
12935
  lines.push("");
12880
- lines.push(` To keep the raw name '${reserved.from}' as the table:`);
12881
- lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
12936
+ lines.push(` To set the table name yourself:`);
12937
+ lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} --table-name <name>`);
12938
+ lines.push(` Tina4 interpolates table names unquoted; if you force the reserved '${reserved.from}', you own the quoting in raw SQL.`);
12882
12939
  }
12883
12940
  if (b.test_paths && b.test_paths.length > 0) {
12884
12941
  lines.push("");
@@ -13014,6 +13071,7 @@ async function generate(what, name, extraArgs = []) {
13014
13071
  console.error(" Usage: tina4nodejs generate <what> <name> [options]");
13015
13072
  console.error(` Generators: ${GENERATOR_LIST}`);
13016
13073
  console.error(' Options: --fields "name:string,price:float" --model ModelName');
13074
+ console.error(" --table-name <name> force the model's table name (else derived; reserved words auto-pluralise)");
13017
13075
  console.error(" --public open a route's writes (default: secure)");
13018
13076
  console.error(' --every 5m | --cron "\u2026" service schedule');
13019
13077
  console.error(" --json emit machine-readable resolution envelope on stdout");
@@ -13064,7 +13122,7 @@ async function generateProgrammatic(what, name, extraArgs = []) {
13064
13122
  }
13065
13123
  function generateModel(name, flags, emitTest = true) {
13066
13124
  const fields = fieldsOrDefault(flags.fields || "");
13067
- const table2 = toTableName(name);
13125
+ const table2 = resolveTable(name, flags, { announce: true });
13068
13126
  const dir = resolve6("src/models");
13069
13127
  ensureDir(dir);
13070
13128
  const path8 = join15(dir, `${name}.ts`);
@@ -13115,7 +13173,7 @@ function generateRoute(name, flags, emitTest = true) {
13115
13173
  if (__resolution.target === "route") {
13116
13174
  setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
13117
13175
  }
13118
- const table2 = model ? toTableName(model) : "";
13176
+ const table2 = model ? resolveTable(model, flags, { announce: false }) : "";
13119
13177
  const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
13120
13178
  ` : "";
13121
13179
  const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
@@ -13336,7 +13394,7 @@ ${aiFill(`delete_${singular}`, {
13336
13394
  }
13337
13395
  }
13338
13396
  function generateCrud(name, flags) {
13339
- const table2 = toTableName(name);
13397
+ const table2 = resolveTable(name, flags, { announce: false });
13340
13398
  const routeName = toPlural(table2);
13341
13399
  const isPublic = Boolean(flags.public);
13342
13400
  if (!__resolution.jsonMode) console.log(`
@@ -13363,7 +13421,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
13363
13421
  table2 = tableOverride;
13364
13422
  } else {
13365
13423
  const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
13366
- table2 = toTableName(raw);
13424
+ table2 = resolveTable(raw, flags, { announce: false });
13367
13425
  }
13368
13426
  if (__resolution.target === "migration") {
13369
13427
  setResolutionField("table_name", table2);
@@ -13386,7 +13444,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
13386
13444
  const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
13387
13445
  colLines.push(` ${fname} ${info.sql}${defaultClause}`);
13388
13446
  }
13389
- colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
13447
+ colLines.push(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
13390
13448
  upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
13391
13449
  -- tina4:edit add columns beyond id + created_at
13392
13450
  ${colLines.join(",\n")}
@@ -13589,7 +13647,7 @@ void test${titleName};
13589
13647
  }
13590
13648
  function generateForm(name, flags) {
13591
13649
  const fields = fieldsOrDefault(flags.fields || "");
13592
- const table2 = toTableName(name);
13650
+ const table2 = resolveTable(name, flags, { announce: false });
13593
13651
  const routeName = toPlural(table2);
13594
13652
  const inputTypes = {
13595
13653
  string: "text",
@@ -13655,7 +13713,7 @@ function generateForm(name, flags) {
13655
13713
  }
13656
13714
  function generateView(name, flags) {
13657
13715
  const fields = fieldsOrDefault(flags.fields || "");
13658
- const table2 = toTableName(name);
13716
+ const table2 = resolveTable(name, flags, { announce: false });
13659
13717
  const routeName = toPlural(table2);
13660
13718
  const cols = fields.map(([f]) => f);
13661
13719
  const dir = resolve6("src/templates/pages");
@@ -14010,8 +14068,8 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
14010
14068
  writeFileSafe(path8, content);
14011
14069
  emitValidatorTest(name, toSnake(name), toPascal(name));
14012
14070
  }
14013
- function generateSeeder(name, _flags) {
14014
- const table2 = toTableName(name);
14071
+ function generateSeeder(name, flags) {
14072
+ const table2 = resolveTable(name, flags, { announce: false });
14015
14073
  const dir = resolve6("src/seeds");
14016
14074
  ensureDir(dir);
14017
14075
  const path8 = join15(dir, `${table2}_seeder.ts`);
@@ -14529,8 +14587,8 @@ var init_generate = __esm({
14529
14587
  "../cli/src/commands/generate.ts"() {
14530
14588
  "use strict";
14531
14589
  FIELD_TYPE_MAP = {
14532
- string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14533
- str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14590
+ string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
14591
+ str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
14534
14592
  int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14535
14593
  integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
14536
14594
  float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
@@ -14540,7 +14598,7 @@ var init_generate = __esm({
14540
14598
  bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14541
14599
  boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
14542
14600
  text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
14543
- datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
14601
+ datetime: { orm: '"datetime"', sql: "TIMESTAMP", defaultVal: "NULL" },
14544
14602
  blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
14545
14603
  };
14546
14604
  SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
@@ -14636,7 +14694,7 @@ var init_generate = __esm({
14636
14694
  TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
14637
14695
  DEFAULT_FIELDS = [["name", "string"]];
14638
14696
  GENERATORS = {
14639
- model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
14697
+ model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
14640
14698
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
14641
14699
  crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
14642
14700
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
@@ -28091,7 +28149,7 @@ function mulberry32(seed) {
28091
28149
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
28092
28150
  };
28093
28151
  }
28094
- var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, FakeData;
28152
+ var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, PRODUCT_ADJECTIVES, PRODUCT_NOUNS, FakeData;
28095
28153
  var init_fakeData = __esm({
28096
28154
  "../core/src/fakeData.ts"() {
28097
28155
  "use strict";
@@ -28353,6 +28411,59 @@ var init_fakeData = __esm({
28353
28411
  "ZAR",
28354
28412
  "INR"
28355
28413
  ];
28414
+ PRODUCT_ADJECTIVES = [
28415
+ "Wireless",
28416
+ "Organic",
28417
+ "Premium",
28418
+ "Classic",
28419
+ "Eco",
28420
+ "Smart",
28421
+ "Portable",
28422
+ "Deluxe",
28423
+ "Compact",
28424
+ "Rustic",
28425
+ "Handcrafted",
28426
+ "Vintage",
28427
+ "Modern",
28428
+ "Ergonomic",
28429
+ "Stainless",
28430
+ "Bamboo",
28431
+ "Recycled",
28432
+ "Artisan",
28433
+ "Professional",
28434
+ "Ultra",
28435
+ "Insulated",
28436
+ "Lightweight",
28437
+ "Adjustable",
28438
+ "Foldable"
28439
+ ];
28440
+ PRODUCT_NOUNS = [
28441
+ "Keyboard",
28442
+ "Coffee Beans",
28443
+ "Backpack",
28444
+ "Water Bottle",
28445
+ "Desk Lamp",
28446
+ "Headphones",
28447
+ "Notebook",
28448
+ "Sneakers",
28449
+ "Sunglasses",
28450
+ "Wallet",
28451
+ "Mug",
28452
+ "Chair",
28453
+ "Blender",
28454
+ "Speaker",
28455
+ "Charger",
28456
+ "Umbrella",
28457
+ "Toothbrush",
28458
+ "Jacket",
28459
+ "Watch",
28460
+ "Kettle",
28461
+ "Picture Frame",
28462
+ "Planter",
28463
+ "Cutlery Set",
28464
+ "Yoga Mat",
28465
+ "Phone Case"
28466
+ ];
28356
28467
  FakeData = class _FakeData {
28357
28468
  rng;
28358
28469
  seeded;
@@ -28424,6 +28535,16 @@ var init_fakeData = __esm({
28424
28535
  jobTitle() {
28425
28536
  return this.pick(JOB_TITLES);
28426
28537
  }
28538
+ /**
28539
+ * A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
28540
+ * Beans" — an adjective + noun from the product vocabulary. Deterministic
28541
+ * under a seed like every other generator (draws from the same instance
28542
+ * PRNG). Used to seed a generic `name` column on a product-ish table instead
28543
+ * of a person name (see the ORM FakeData `forField` heuristic).
28544
+ */
28545
+ product() {
28546
+ return `${this.pick(PRODUCT_ADJECTIVES)} ${this.pick(PRODUCT_NOUNS)}`;
28547
+ }
28427
28548
  paragraph(sentences = 4) {
28428
28549
  const parts = [];
28429
28550
  for (let i = 0; i < sentences; i++) {
@@ -37310,6 +37431,8 @@ var init_mysql = __esm({
37310
37431
  translateSql(sql) {
37311
37432
  let translated = SQLTranslator.concatPipesToFunc(sql);
37312
37433
  translated = SQLTranslator.ilikeToLike(translated);
37434
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
37435
+ translated = SQLTranslator.ddlTypes(translated, "mysql");
37313
37436
  return translated;
37314
37437
  }
37315
37438
  execute(sql, params) {
@@ -37731,6 +37854,9 @@ var init_mssql = __esm({
37731
37854
  let translated = SQLTranslator.limitToTop(sql);
37732
37855
  translated = SQLTranslator.concatPipesToFunc(translated);
37733
37856
  translated = SQLTranslator.ilikeToLike(translated);
37857
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
37858
+ translated = SQLTranslator.booleanToInt(translated);
37859
+ translated = SQLTranslator.ddlTypes(translated, "mssql");
37734
37860
  return translated;
37735
37861
  }
37736
37862
  execSqlPromise(sql, params) {
@@ -38361,6 +38487,8 @@ var init_firebird = __esm({
38361
38487
  let translated = SQLTranslator.limitToRows(sql);
38362
38488
  translated = SQLTranslator.booleanToInt(translated);
38363
38489
  translated = SQLTranslator.ilikeToLike(translated);
38490
+ translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
38491
+ translated = SQLTranslator.ddlTypes(translated, "firebird");
38364
38492
  return translated;
38365
38493
  }
38366
38494
  /**
@@ -44300,11 +44428,27 @@ var init_baseModel = __esm({
44300
44428
  });
44301
44429
 
44302
44430
  // src/fakeData.ts
44303
- var FakeData2;
44431
+ function isProductTable(table2) {
44432
+ const t = (table2 ?? "").toLowerCase();
44433
+ return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
44434
+ }
44435
+ var PRODUCT_TABLE_HINTS, FakeData2;
44304
44436
  var init_fakeData2 = __esm({
44305
44437
  "src/fakeData.ts"() {
44306
44438
  "use strict";
44307
44439
  init_fakeData();
44440
+ PRODUCT_TABLE_HINTS = [
44441
+ "product",
44442
+ "item",
44443
+ "catalog",
44444
+ "inventory",
44445
+ "goods",
44446
+ "merchandise",
44447
+ "sku",
44448
+ "listing",
44449
+ "stock",
44450
+ "ware"
44451
+ ];
44308
44452
  FakeData2 = class extends FakeData {
44309
44453
  constructor(seed) {
44310
44454
  super(seed);
@@ -44326,8 +44470,11 @@ var init_fakeData2 = __esm({
44326
44470
  *
44327
44471
  * @param fieldDef - An ORM FieldDefinition object
44328
44472
  * @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
44473
+ * @param table - Optional table/model name. A generic `name`/`full_name`
44474
+ * column on a product-ish table (see {@link isProductTable}) gets a
44475
+ * product name; with no table context it stays a person name (back-compat).
44329
44476
  */
44330
- forField(fieldDef, columnName) {
44477
+ forField(fieldDef, columnName, table2) {
44331
44478
  if (fieldDef.primaryKey && fieldDef.autoIncrement) {
44332
44479
  return void 0;
44333
44480
  }
@@ -44337,7 +44484,9 @@ var init_fakeData2 = __esm({
44337
44484
  const col = (columnName ?? "").toLowerCase();
44338
44485
  if (col.includes("email")) return this.email();
44339
44486
  if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
44340
- if (col === "name" || col === "full_name" || col === "fullname") return this.name();
44487
+ if (col === "name" || col === "full_name" || col === "fullname") {
44488
+ return isProductTable(table2) ? this.product() : this.name();
44489
+ }
44341
44490
  if (col === "first_name" || col === "firstname") return this.firstName();
44342
44491
  if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
44343
44492
  if (col.includes("address")) return this.address();
@@ -44424,7 +44573,7 @@ async function autoFieldMap(db, table2, fake = new FakeData2()) {
44424
44573
  continue;
44425
44574
  }
44426
44575
  const fieldType = sqlTypeToFieldType(sqlType);
44427
- fieldMap[name] = () => fake.forField({ type: fieldType }, name);
44576
+ fieldMap[name] = () => fake.forField({ type: fieldType }, name, table2);
44428
44577
  }
44429
44578
  return fieldMap;
44430
44579
  }
@@ -44570,7 +44719,7 @@ async function seedOrm(ormClass, count = 10, overrides, seed, opts, fkPools) {
44570
44719
  } else if (pools[name] && pools[name].length > 0) {
44571
44720
  attrs[name] = fake.choice(pools[name]);
44572
44721
  } else {
44573
- attrs[name] = fake.forField(def, name);
44722
+ attrs[name] = fake.forField(def, name, modelName);
44574
44723
  }
44575
44724
  }
44576
44725
  validateTypes(fields, attrs, modelName);
@@ -395,6 +395,13 @@ export class FirebirdAdapter implements DatabaseAdapter {
395
395
  let translated = SQLTranslator.limitToRows(sql);
396
396
  translated = SQLTranslator.booleanToInt(translated);
397
397
  translated = SQLTranslator.ilikeToLike(translated);
398
+ // DDL: strip AUTOINCREMENT (Firebird uses generators) and rewrite the
399
+ // SQLite-canonical column TYPES so ONE portable migration applies here —
400
+ // TEXT -> BLOB SUB_TYPE TEXT, REAL -> DOUBLE PRECISION, IF NOT EXISTS
401
+ // dropped. Both are DDL-only, so DML is untouched. Mirrors the Python
402
+ // master's firebird.py::_translate_sql.
403
+ translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
404
+ translated = SQLTranslator.ddlTypes(translated, "firebird");
398
405
  return translated;
399
406
  }
400
407
 
@@ -172,6 +172,16 @@ export class MssqlAdapter implements DatabaseAdapter {
172
172
  let translated = SQLTranslator.limitToTop(sql);
173
173
  translated = SQLTranslator.concatPipesToFunc(translated);
174
174
  translated = SQLTranslator.ilikeToLike(translated);
175
+ // DDL: AUTOINCREMENT -> IDENTITY(1,1), drop IF NOT EXISTS (unsupported), and
176
+ // TIMESTAMP -> DATETIME2 (MSSQL's TIMESTAMP is a rowversion, not a datetime)
177
+ // so ONE portable migration applies here. Both are DDL-only, so DML is
178
+ // untouched. Mirrors the Python master's mssql.py::_translate_sql.
179
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
180
+ // MSSQL has BIT, not a boolean type, so bare TRUE/FALSE must become 1/0
181
+ // (a TRUE/FALSE inside a string literal is data and is left untouched).
182
+ // Mirrors the Python master's mssql.py::_translate_sql.
183
+ translated = SQLTranslator.booleanToInt(translated);
184
+ translated = SQLTranslator.ddlTypes(translated, "mssql");
175
185
  return translated;
176
186
  }
177
187
 
@@ -148,6 +148,12 @@ export class MysqlAdapter implements DatabaseAdapter {
148
148
  let translated = SQLTranslator.concatPipesToFunc(sql);
149
149
  // MySQL uses LOWER() LIKE instead of ILIKE
150
150
  translated = SQLTranslator.ilikeToLike(translated);
151
+ // DDL: AUTOINCREMENT -> AUTO_INCREMENT and TIMESTAMP -> DATETIME (MySQL's
152
+ // TIMESTAMP carries auto-update / 2038 surprises) so ONE portable migration
153
+ // applies here. Both are DDL-only, so DML is untouched. Mirrors the Python
154
+ // master's mysql.py::_translate_sql.
155
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
156
+ translated = SQLTranslator.ddlTypes(translated, "mysql");
151
157
  return translated;
152
158
  }
153
159
 
@@ -5,6 +5,26 @@
5
5
  import { FakeData as CoreFakeData } from "../../core/src/fakeData.js";
6
6
  import type { FieldDefinition } from "./types.js";
7
7
 
8
+ // A table/model whose name contains any of these gets product names on its
9
+ // generic `name`/`full_name` column instead of a person name. Mirrors the
10
+ // Python master's `_PRODUCT_TABLE_HINTS`.
11
+ const PRODUCT_TABLE_HINTS = [
12
+ "product", "item", "catalog", "inventory", "goods", "merchandise",
13
+ "sku", "listing", "stock", "ware",
14
+ ] as const;
15
+
16
+ /**
17
+ * True when the table/model name looks like a product catalogue, so a generic
18
+ * `name` column should seed a product name, not a person name. With NO table
19
+ * context (undefined/null/empty) this is false, so the person-name default is
20
+ * kept — back-compat. Exported (not via the barrel) so seeding tests can assert
21
+ * it directly, mirroring the Python master's `_is_product_table`.
22
+ */
23
+ export function isProductTable(table?: string | null): boolean {
24
+ const t = (table ?? "").toLowerCase();
25
+ return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
26
+ }
27
+
8
28
  /**
9
29
  * ORM-aware FakeData — wraps the core FakeData and adds forField()
10
30
  * which generates appropriate fake data based on an ORM FieldDefinition.
@@ -32,8 +52,11 @@ export class FakeData extends CoreFakeData {
32
52
  *
33
53
  * @param fieldDef - An ORM FieldDefinition object
34
54
  * @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
55
+ * @param table - Optional table/model name. A generic `name`/`full_name`
56
+ * column on a product-ish table (see {@link isProductTable}) gets a
57
+ * product name; with no table context it stays a person name (back-compat).
35
58
  */
36
- forField(fieldDef: FieldDefinition, columnName?: string): unknown {
59
+ forField(fieldDef: FieldDefinition, columnName?: string, table?: string): unknown {
37
60
  // Auto-increment primary keys should not be generated
38
61
  if (fieldDef.primaryKey && fieldDef.autoIncrement) {
39
62
  return undefined;
@@ -53,7 +76,9 @@ export class FakeData extends CoreFakeData {
53
76
 
54
77
  if (col.includes("email")) return this.email();
55
78
  if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
56
- if (col === "name" || col === "full_name" || col === "fullname") return this.name();
79
+ if (col === "name" || col === "full_name" || col === "fullname") {
80
+ return isProductTable(table) ? this.product() : this.name();
81
+ }
57
82
  if (col === "first_name" || col === "firstname") return this.firstName();
58
83
  if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
59
84
  if (col.includes("address")) return this.address();
@@ -143,8 +143,10 @@ export async function autoFieldMap(
143
143
  }
144
144
  const fieldType = sqlTypeToFieldType(sqlType);
145
145
  // Bind the type + name per column; forField applies the name heuristics
146
- // (email/phone/name/...) before falling back to the type.
147
- fieldMap[name] = () => fake.forField({ type: fieldType }, name);
146
+ // (email/phone/name/...) before falling back to the type. The TABLE name is
147
+ // threaded so a generic `name` column on a product-ish table seeds a
148
+ // product name, not a person name (parity with the Python auto_field_map).
149
+ fieldMap[name] = () => fake.forField({ type: fieldType }, name, table);
148
150
  }
149
151
  return fieldMap;
150
152
  }
@@ -427,7 +429,10 @@ export async function seedOrm(
427
429
  } else if (pools[name] && pools[name].length > 0) {
428
430
  attrs[name] = fake.choice(pools[name]);
429
431
  } else {
430
- attrs[name] = fake.forField(def, name);
432
+ // Thread the MODEL name so a generic `name` column on a product-ish
433
+ // model seeds a product name, not a person name (Python seed_orm
434
+ // passes orm_class.__name__; modelName is name ?? tableName).
435
+ attrs[name] = fake.forField(def, name, modelName);
431
436
  }
432
437
  }
433
438
  validateTypes(fields, attrs, modelName);
@@ -269,6 +269,51 @@ export class SQLTranslator {
269
269
  }
270
270
  }
271
271
 
272
+ /**
273
+ * Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
274
+ * target engine.
275
+ *
276
+ * ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
277
+ * that happens to contain the word `TEXT` (a column name, a string literal) is
278
+ * never rewritten. Complements `autoIncrementSyntax` (which maps the id
279
+ * keyword) so ONE portable migration — and every `Model.createTable()` DDL,
280
+ * which is also SQLite-canonical — applies on every engine instead of failing
281
+ * on Firebird/MSSQL.
282
+ *
283
+ * * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
284
+ * EXISTS`.
285
+ * * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
286
+ * rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
287
+ * * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
288
+ * column maps to `DATETIME` (matching the adapters' createTableAsync).
289
+ */
290
+ static ddlTypes(sql: string, engine: string): string {
291
+ // Gate to DDL only, tolerating leading `-- ...` comment lines / blank lines
292
+ // that a migration file carries before its CREATE TABLE. A SELECT or INSERT
293
+ // that merely mentions a type keyword is never rewritten.
294
+ const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
295
+ if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
296
+ switch ((engine ?? "").toLowerCase()) {
297
+ case "firebird":
298
+ return sql
299
+ .replace(/\bIF\s+NOT\s+EXISTS\b/gi, "")
300
+ // Map bare TEXT -> BLOB SUB_TYPE TEXT, but leave an existing
301
+ // "BLOB SUB_TYPE TEXT" intact (it already contains the word TEXT).
302
+ .replace(/\bBLOB\s+SUB_TYPE\s+TEXT\b/gi, "\x00FBTEXT\x00")
303
+ .replace(/\bTEXT\b/gi, "BLOB SUB_TYPE TEXT")
304
+ .replaceAll("\x00FBTEXT\x00", "BLOB SUB_TYPE TEXT")
305
+ .replace(/\bREAL\b/gi, "DOUBLE PRECISION");
306
+ case "mssql":
307
+ return sql
308
+ .replace(/\bIF\s+NOT\s+EXISTS\b/gi, "")
309
+ .replace(/\bTIMESTAMP\b/gi, "DATETIME2");
310
+ case "mysql":
311
+ return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
312
+ default:
313
+ return sql;
314
+ }
315
+ }
316
+
272
317
  /**
273
318
  * Convert ? placeholders to engine-specific style.
274
319
  *
@@ -10,6 +10,35 @@ export declare function pluralizeReserved(name: string): string;
10
10
  * routes and tests all agree on the same table name.
11
11
  */
12
12
  export declare function toTableName(name: string): string;
13
+ /**
14
+ * The table name a generator uses (issue #123) — honours `--table-name` and
15
+ * speaks up instead of renaming SILENTLY. Mirrors the Python master's
16
+ * `_resolve_table` (tina4-python/tina4_python/cli/__init__.py).
17
+ *
18
+ * `announce` prints the note/warning; it is TRUE only for `generateModel` (where
19
+ * the table is born). Composite generators (crud) let the model sub-call
20
+ * announce, and generators that target an EXISTING table (route/seeder/form/view/
21
+ * migration) still honour `--table-name` but stay quiet so the note is not
22
+ * repeated — the note prints exactly once per `generate`.
23
+ *
24
+ * • `--table-name <name>` wins verbatim. If that name is ITSELF a reserved word,
25
+ * warn loudly (when announcing): Tina4 interpolates table names UNQUOTED, so
26
+ * the ORM's generated SQL will fail on it — quoting it in raw SQL + migrations
27
+ * is now the developer's job (we do NOT silently quote; identifier quoting is a
28
+ * global storage invariant, not a local fix).
29
+ * • Otherwise fall back to `toTableName` (snake + reserved-word pluralise). When
30
+ * that auto-pluralises a reserved-word class name (`Order` -> `orders`), print a
31
+ * one-line NOTE (when announcing) naming the rename and the `--table-name`
32
+ * escape hatch, so the developer is informed rather than surprised.
33
+ *
34
+ * The note/warning goes to STDERR (console.error) so a `generate … --json` run
35
+ * keeps its stdout envelope pristine for a downstream `| jq`. `toTableName`'s
36
+ * `reserved_word_pluralize` envelope transformation is UNCHANGED (this ADDS the
37
+ * announce path; it does not touch the envelope contract).
38
+ */
39
+ export declare function resolveTable(name: string, flags: Record<string, string | boolean> | undefined, opts?: {
40
+ announce?: boolean;
41
+ }): string;
13
42
  /** One transformation the resolver made — visible to the caller so an AI
14
43
  * agent (or human) knows exactly why the output differs from the input. */
15
44
  export interface ResolutionTransformation {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Lint the project's source. Exits 0 = clean, 1 = findings (parity with the
3
+ * Python master and with `tina4nodejs test`'s exit-code contract).
4
+ */
5
+ export declare function runLint(args: string[]): void;
@@ -1,3 +1,7 @@
1
+ export declare const FIRST_NAMES: string[];
2
+ export declare const LAST_NAMES: string[];
3
+ export declare const PRODUCT_ADJECTIVES: string[];
4
+ export declare const PRODUCT_NOUNS: string[];
1
5
  export declare class FakeData {
2
6
  private rng;
3
7
  private seeded;
@@ -19,6 +23,14 @@ export declare class FakeData {
19
23
  zipCode(): string;
20
24
  company(): string;
21
25
  jobTitle(): string;
26
+ /**
27
+ * A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
28
+ * Beans" — an adjective + noun from the product vocabulary. Deterministic
29
+ * under a seed like every other generator (draws from the same instance
30
+ * PRNG). Used to seed a generic `name` column on a product-ish table instead
31
+ * of a person name (see the ORM FakeData `forField` heuristic).
32
+ */
33
+ product(): string;
22
34
  paragraph(sentences?: number): string;
23
35
  sentence(words?: number): string;
24
36
  word(): string;
@@ -1,5 +1,13 @@
1
1
  import { FakeData as CoreFakeData } from "../../core/src/fakeData.js";
2
2
  import type { FieldDefinition } from "./types.js";
3
+ /**
4
+ * True when the table/model name looks like a product catalogue, so a generic
5
+ * `name` column should seed a product name, not a person name. With NO table
6
+ * context (undefined/null/empty) this is false, so the person-name default is
7
+ * kept — back-compat. Exported (not via the barrel) so seeding tests can assert
8
+ * it directly, mirroring the Python master's `_is_product_table`.
9
+ */
10
+ export declare function isProductTable(table?: string | null): boolean;
3
11
  /**
4
12
  * ORM-aware FakeData — wraps the core FakeData and adds forField()
5
13
  * which generates appropriate fake data based on an ORM FieldDefinition.
@@ -17,6 +25,9 @@ export declare class FakeData extends CoreFakeData {
17
25
  *
18
26
  * @param fieldDef - An ORM FieldDefinition object
19
27
  * @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
28
+ * @param table - Optional table/model name. A generic `name`/`full_name`
29
+ * column on a product-ish table (see {@link isProductTable}) gets a
30
+ * product name; with no table context it stays a person name (back-compat).
20
31
  */
21
- forField(fieldDef: FieldDefinition, columnName?: string): unknown;
32
+ forField(fieldDef: FieldDefinition, columnName?: string, table?: string): unknown;
22
33
  }