@objectstack/driver-sql 9.9.1 → 9.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +65 -8
- package/dist/index.d.ts +65 -8
- package/dist/index.js +189 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +189 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/sql-driver.ts
|
|
2
|
+
import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
|
|
2
3
|
import { StorageNameMapping } from "@objectstack/spec/system";
|
|
3
4
|
import { ExternalSchemaModeViolationError } from "@objectstack/spec/shared";
|
|
4
5
|
import knex from "knex";
|
|
@@ -11,9 +12,12 @@ var JSON_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
|
11
12
|
"json",
|
|
12
13
|
"object",
|
|
13
14
|
"array",
|
|
15
|
+
"record",
|
|
14
16
|
"image",
|
|
15
17
|
"file",
|
|
16
18
|
"avatar",
|
|
19
|
+
"video",
|
|
20
|
+
"audio",
|
|
17
21
|
"location",
|
|
18
22
|
"address",
|
|
19
23
|
"composite",
|
|
@@ -23,6 +27,18 @@ var JSON_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
|
23
27
|
"repeater",
|
|
24
28
|
"vector"
|
|
25
29
|
]);
|
|
30
|
+
var NUMERIC_SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
31
|
+
"integer",
|
|
32
|
+
"int",
|
|
33
|
+
"float",
|
|
34
|
+
"number",
|
|
35
|
+
"currency",
|
|
36
|
+
"percent",
|
|
37
|
+
"summary",
|
|
38
|
+
"rating",
|
|
39
|
+
"slider",
|
|
40
|
+
"progress"
|
|
41
|
+
]);
|
|
26
42
|
var SqlDriver = class {
|
|
27
43
|
constructor(config) {
|
|
28
44
|
// IDataDriver metadata
|
|
@@ -30,6 +46,7 @@ var SqlDriver = class {
|
|
|
30
46
|
this.version = "1.0.0";
|
|
31
47
|
this.jsonFields = {};
|
|
32
48
|
this.booleanFields = {};
|
|
49
|
+
this.numericFields = {};
|
|
33
50
|
this.dateFields = {};
|
|
34
51
|
this.datetimeFields = {};
|
|
35
52
|
this.tablesWithTimestamps = /* @__PURE__ */ new Set();
|
|
@@ -50,6 +67,14 @@ var SqlDriver = class {
|
|
|
50
67
|
this.autoNumberFields = {};
|
|
51
68
|
/** Whether the sequences table has been ensured this process. */
|
|
52
69
|
this.sequencesTableReady = false;
|
|
70
|
+
/**
|
|
71
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
72
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
73
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
74
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
75
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
76
|
+
*/
|
|
77
|
+
this.sequencesHasKeyHash = false;
|
|
53
78
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
54
79
|
this.sequencesTableEnsurePromise = null;
|
|
55
80
|
/**
|
|
@@ -357,6 +382,15 @@ var SqlDriver = class {
|
|
|
357
382
|
/**
|
|
358
383
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
359
384
|
* the first call (cached via `sequencesTableReady`).
|
|
385
|
+
*
|
|
386
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
387
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
388
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
389
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
390
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
391
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
392
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
393
|
+
* keep their single global counter (backward compatible).
|
|
360
394
|
*/
|
|
361
395
|
async ensureSequencesTable() {
|
|
362
396
|
if (this.sequencesTableReady) return;
|
|
@@ -368,18 +402,15 @@ var SqlDriver = class {
|
|
|
368
402
|
const exists = await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
369
403
|
if (!exists) {
|
|
370
404
|
try {
|
|
371
|
-
await this.
|
|
372
|
-
|
|
373
|
-
t.string("tenant_id").notNullable();
|
|
374
|
-
t.string("field").notNullable();
|
|
375
|
-
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
376
|
-
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
377
|
-
t.primary(["object", "tenant_id", "field"]);
|
|
378
|
-
});
|
|
405
|
+
await this.createSequencesTable(SEQUENCES_TABLE);
|
|
406
|
+
this.sequencesHasKeyHash = true;
|
|
379
407
|
} catch (err) {
|
|
380
408
|
const stillMissing = !await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
381
409
|
if (stillMissing) throw err;
|
|
410
|
+
await this.ensureSequencesKeyHashShape();
|
|
382
411
|
}
|
|
412
|
+
} else {
|
|
413
|
+
await this.ensureSequencesKeyHashShape();
|
|
383
414
|
}
|
|
384
415
|
this.sequencesTableReady = true;
|
|
385
416
|
})();
|
|
@@ -389,6 +420,71 @@ var SqlDriver = class {
|
|
|
389
420
|
this.sequencesTableEnsurePromise = null;
|
|
390
421
|
}
|
|
391
422
|
}
|
|
423
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
424
|
+
sequenceKeyHash(object, tenantId, field, scope) {
|
|
425
|
+
return createHash("sha256").update(`${object}${tenantId}${field}${scope}`).digest("hex");
|
|
426
|
+
}
|
|
427
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
428
|
+
async createSequencesTable(table) {
|
|
429
|
+
await this.knex.schema.createTable(table, (t) => {
|
|
430
|
+
t.string("key_hash", 64).notNullable().primary();
|
|
431
|
+
t.string("object").notNullable();
|
|
432
|
+
t.string("tenant_id").notNullable();
|
|
433
|
+
t.string("field").notNullable();
|
|
434
|
+
t.string("scope", 1024).notNullable().defaultTo("");
|
|
435
|
+
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
436
|
+
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
441
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
442
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
443
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
444
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
445
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
446
|
+
*
|
|
447
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
448
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
449
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
450
|
+
*/
|
|
451
|
+
async ensureSequencesKeyHashShape() {
|
|
452
|
+
if (await this.knex.schema.hasColumn(SEQUENCES_TABLE, "key_hash")) {
|
|
453
|
+
this.sequencesHasKeyHash = true;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const hasScope = await this.knex.schema.hasColumn(SEQUENCES_TABLE, "scope");
|
|
457
|
+
const TMP = `${SEQUENCES_TABLE}__rebuild`;
|
|
458
|
+
try {
|
|
459
|
+
const rows = await this.knex(SEQUENCES_TABLE).select("*");
|
|
460
|
+
await this.knex.schema.dropTableIfExists(TMP);
|
|
461
|
+
await this.createSequencesTable(TMP);
|
|
462
|
+
const migrated = rows.map((r) => {
|
|
463
|
+
const scope = hasScope && r.scope != null ? String(r.scope) : "";
|
|
464
|
+
return {
|
|
465
|
+
key_hash: this.sequenceKeyHash(String(r.object), String(r.tenant_id), String(r.field), scope),
|
|
466
|
+
object: r.object,
|
|
467
|
+
tenant_id: r.tenant_id,
|
|
468
|
+
field: r.field,
|
|
469
|
+
scope,
|
|
470
|
+
last_value: r.last_value ?? 0,
|
|
471
|
+
updated_at: r.updated_at ?? this.knex.fn.now()
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
if (migrated.length > 0) await this.knex(TMP).insert(migrated);
|
|
475
|
+
await this.knex.schema.dropTable(SEQUENCES_TABLE);
|
|
476
|
+
await this.knex.schema.renameTable(TMP, SEQUENCES_TABLE);
|
|
477
|
+
this.sequencesHasKeyHash = true;
|
|
478
|
+
} catch (err) {
|
|
479
|
+
this.sequencesHasKeyHash = false;
|
|
480
|
+
await this.knex.schema.dropTableIfExists(TMP).catch(() => {
|
|
481
|
+
});
|
|
482
|
+
this.logger.warn(
|
|
483
|
+
`[autonumber] Failed to migrate ${SEQUENCES_TABLE} to the key_hash shape. Fixed-prefix autonumbers keep working; date/{field}/per-parent formats will error until the table is migrated.`,
|
|
484
|
+
{ error: String(err) }
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
392
488
|
/**
|
|
393
489
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
394
490
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -426,10 +522,16 @@ var SqlDriver = class {
|
|
|
426
522
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
427
523
|
* matching standard sequence semantics.
|
|
428
524
|
*/
|
|
429
|
-
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx) {
|
|
525
|
+
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx, scope = "") {
|
|
430
526
|
await this.ensureSequencesTable();
|
|
431
527
|
const resolvedTenantId = tenantField && tenantId ? String(tenantId) : GLOBAL_TENANT;
|
|
432
|
-
|
|
528
|
+
if (scope !== "" && !this.sequencesHasKeyHash) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Cannot generate a per-scope autonumber for "${object}.${field}": the ${SEQUENCES_TABLE} table is still the legacy shape. Migrate it to the key_hash shape before using date/{field}/per-parent formats.`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const key = this.sequencesHasKeyHash ? { key_hash: this.sequenceKeyHash(tableName, resolvedTenantId, field, scope) } : { object: tableName, tenant_id: resolvedTenantId, field };
|
|
534
|
+
const insertRow = this.sequencesHasKeyHash ? { ...key, object: tableName, tenant_id: resolvedTenantId, field, scope } : { ...key };
|
|
433
535
|
const runner = parentTrx ?? this.knex;
|
|
434
536
|
return runner.transaction(async (trx) => {
|
|
435
537
|
let existing;
|
|
@@ -449,7 +551,7 @@ var SqlDriver = class {
|
|
|
449
551
|
);
|
|
450
552
|
const initial = seedMax + 1;
|
|
451
553
|
try {
|
|
452
|
-
await trx(SEQUENCES_TABLE).insert({ ...
|
|
554
|
+
await trx(SEQUENCES_TABLE).insert({ ...insertRow, last_value: initial });
|
|
453
555
|
return initial;
|
|
454
556
|
} catch (err) {
|
|
455
557
|
existing = await trx(SEQUENCES_TABLE).where(key).forUpdate().first();
|
|
@@ -462,31 +564,43 @@ var SqlDriver = class {
|
|
|
462
564
|
});
|
|
463
565
|
}
|
|
464
566
|
/**
|
|
465
|
-
* For each `auto_number` field
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
567
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
568
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
569
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
570
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
571
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
572
|
+
* tenant scopes it for isolation.
|
|
469
573
|
*/
|
|
470
574
|
async fillAutoNumberFields(object, row, options) {
|
|
471
575
|
const tableName = StorageNameMapping.resolveTableName({ name: object });
|
|
472
576
|
const cfgs = this.autoNumberFields[tableName] || this.autoNumberFields[object];
|
|
473
577
|
if (!cfgs || cfgs.length === 0) return;
|
|
474
578
|
const parentTrx = options?.transaction;
|
|
579
|
+
const timezone = options?.timezone;
|
|
580
|
+
const now = /* @__PURE__ */ new Date();
|
|
475
581
|
for (const cfg of cfgs) {
|
|
476
582
|
if (row[cfg.name] !== void 0 && row[cfg.name] !== null && row[cfg.name] !== "") continue;
|
|
583
|
+
const missing = missingFieldValues(cfg.tokens, row);
|
|
584
|
+
if (missing.length > 0) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`Cannot generate autonumber "${object}.${cfg.name}" (format "${cfg.format}"): referenced field(s) [${missing.join(", ")}] are empty on the record. Fields interpolated into an autonumber format must be set before the record is created.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
477
589
|
const rowTenant = cfg.tenantField ? row[cfg.tenantField] : void 0;
|
|
478
590
|
const optTenant = options?.tenantId;
|
|
479
591
|
const tenantId = rowTenant != null && rowTenant !== "" ? String(rowTenant) : optTenant != null && optTenant !== "" ? String(optTenant) : null;
|
|
592
|
+
const probe = renderAutonumber({ tokens: cfg.tokens, seq: 0, record: row, now, timezone });
|
|
480
593
|
const next = await this.getNextSequenceValue(
|
|
481
594
|
object,
|
|
482
595
|
tableName,
|
|
483
596
|
cfg.name,
|
|
484
|
-
|
|
597
|
+
probe.prefix,
|
|
485
598
|
cfg.tenantField,
|
|
486
599
|
tenantId,
|
|
487
|
-
parentTrx
|
|
600
|
+
parentTrx,
|
|
601
|
+
probe.scope
|
|
488
602
|
);
|
|
489
|
-
row[cfg.name] =
|
|
603
|
+
row[cfg.name] = renderAutonumber({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
490
604
|
}
|
|
491
605
|
}
|
|
492
606
|
async update(object, id, data, options) {
|
|
@@ -810,6 +924,7 @@ var SqlDriver = class {
|
|
|
810
924
|
const tableName = StorageNameMapping.resolveTableName(obj);
|
|
811
925
|
const jsonCols = [];
|
|
812
926
|
const booleanCols = [];
|
|
927
|
+
const numericCols = [];
|
|
813
928
|
const autoNumberCols = [];
|
|
814
929
|
const tenancyDecl = obj?.tenancy;
|
|
815
930
|
let tenantField = null;
|
|
@@ -829,9 +944,12 @@ var SqlDriver = class {
|
|
|
829
944
|
if (this.isJsonField(type, field)) {
|
|
830
945
|
jsonCols.push(name);
|
|
831
946
|
}
|
|
832
|
-
if (type === "boolean") {
|
|
947
|
+
if (type === "boolean" || type === "toggle") {
|
|
833
948
|
booleanCols.push(name);
|
|
834
949
|
}
|
|
950
|
+
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) {
|
|
951
|
+
numericCols.push(name);
|
|
952
|
+
}
|
|
835
953
|
if (type === "date") {
|
|
836
954
|
((_a = this.dateFields)[tableName] ?? (_a[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
837
955
|
}
|
|
@@ -841,15 +959,14 @@ var SqlDriver = class {
|
|
|
841
959
|
if (type === "auto_number" || type === "autonumber") {
|
|
842
960
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
843
961
|
const fmt = rawFmt || "{0000}";
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
const prefix = m ? fmt.slice(0, m.index ?? 0) : fmt;
|
|
847
|
-
autoNumberCols.push({ name, format: fmt, prefix, padWidth, tenantField });
|
|
962
|
+
const tokens = parseAutonumberFormat(fmt);
|
|
963
|
+
autoNumberCols.push({ name, format: fmt, tokens, tenantField });
|
|
848
964
|
}
|
|
849
965
|
}
|
|
850
966
|
}
|
|
851
967
|
this.jsonFields[tableName] = jsonCols;
|
|
852
968
|
this.booleanFields[tableName] = booleanCols;
|
|
969
|
+
this.numericFields[tableName] = numericCols;
|
|
853
970
|
this.autoNumberFields[tableName] = autoNumberCols;
|
|
854
971
|
this.tenantFieldByTable[tableName] = tenantField;
|
|
855
972
|
let exists = await this.knex.schema.hasTable(tableName);
|
|
@@ -1201,11 +1318,35 @@ var SqlDriver = class {
|
|
|
1201
1318
|
return null;
|
|
1202
1319
|
};
|
|
1203
1320
|
if (isDatetime) {
|
|
1321
|
+
if (!this.isSqlite) return value;
|
|
1204
1322
|
const ms = toMs(value);
|
|
1205
1323
|
return ms == null ? value : ms;
|
|
1206
1324
|
}
|
|
1207
1325
|
return this.toDateOnly(value);
|
|
1208
1326
|
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Public, dialect-correct temporal filter-value coercion for callers that
|
|
1329
|
+
* build SQL *outside* the normal `find()`/`applyFilters()` path — chiefly the
|
|
1330
|
+
* analytics native-SQL strategy, which compiles a raw `SELECT … WHERE col >= $N`
|
|
1331
|
+
* and binds the value directly, bypassing `coerceFilterValue`.
|
|
1332
|
+
*
|
|
1333
|
+
* Given a logical object (table) name, a field name and a filter value
|
|
1334
|
+
* (typically an ISO date/datetime string from a dashboard relative-date
|
|
1335
|
+
* token like `{12_months_ago}`), this returns the value in the column's
|
|
1336
|
+
* on-disk storage form:
|
|
1337
|
+
* - SQLite `Field.datetime` → epoch milliseconds (INTEGER), so the
|
|
1338
|
+
* comparison matches the stored integer rather than failing a
|
|
1339
|
+
* TEXT-vs-INTEGER affinity compare.
|
|
1340
|
+
* - `Field.date` (any dialect) → `YYYY-MM-DD` text.
|
|
1341
|
+
* - Native-timestamp dialects / non-temporal fields → value unchanged.
|
|
1342
|
+
*
|
|
1343
|
+
* This is a thin, intentionally narrow wrapper over the same `coerceFilterValue`
|
|
1344
|
+
* the driver already uses, so there is exactly one source of truth for the
|
|
1345
|
+
* storage convention and the analytics path can never drift from CRUD.
|
|
1346
|
+
*/
|
|
1347
|
+
temporalFilterValue(objectName, field, value) {
|
|
1348
|
+
return this.coerceFilterValue(objectName, field, value);
|
|
1349
|
+
}
|
|
1209
1350
|
applyFilters(builder, filters) {
|
|
1210
1351
|
if (!filters) return;
|
|
1211
1352
|
const table = this.tableNameForBuilder(builder);
|
|
@@ -1427,9 +1568,23 @@ var SqlDriver = class {
|
|
|
1427
1568
|
case "number":
|
|
1428
1569
|
case "currency":
|
|
1429
1570
|
case "percent":
|
|
1571
|
+
// `rating`/`slider`/`progress` are authored as numeric scalars (a star
|
|
1572
|
+
// count, a slider position, a percent-of-completion). Without an explicit
|
|
1573
|
+
// case they fell to `default → table.string`, giving the column TEXT
|
|
1574
|
+
// affinity so SQLite coerced the written number to a string ('4' not 4) —
|
|
1575
|
+
// a silent type-fidelity leak the value-loss tests didn't catch. REAL
|
|
1576
|
+
// affinity round-trips them as JS numbers (#field-zoo).
|
|
1577
|
+
case "rating":
|
|
1578
|
+
case "slider":
|
|
1579
|
+
case "progress":
|
|
1430
1580
|
col = table.float(name);
|
|
1431
1581
|
break;
|
|
1582
|
+
// `toggle` is a boolean rendered as a switch. Same leak as above (TEXT
|
|
1583
|
+
// affinity stored '1'); a boolean column gives NUMERIC affinity and the
|
|
1584
|
+
// `booleanFields` read-coercion below converts the stored 1/0 back to a
|
|
1585
|
+
// real JS boolean.
|
|
1432
1586
|
case "boolean":
|
|
1587
|
+
case "toggle":
|
|
1433
1588
|
col = table.boolean(name);
|
|
1434
1589
|
break;
|
|
1435
1590
|
case "date":
|
|
@@ -1441,22 +1596,6 @@ var SqlDriver = class {
|
|
|
1441
1596
|
case "time":
|
|
1442
1597
|
col = table.time(name);
|
|
1443
1598
|
break;
|
|
1444
|
-
case "json":
|
|
1445
|
-
case "object":
|
|
1446
|
-
case "array":
|
|
1447
|
-
case "image":
|
|
1448
|
-
case "file":
|
|
1449
|
-
case "avatar":
|
|
1450
|
-
case "location":
|
|
1451
|
-
case "address":
|
|
1452
|
-
case "composite":
|
|
1453
|
-
case "multiselect":
|
|
1454
|
-
case "checkboxes":
|
|
1455
|
-
case "tags":
|
|
1456
|
-
case "repeater":
|
|
1457
|
-
case "vector":
|
|
1458
|
-
col = table.json(name);
|
|
1459
|
-
break;
|
|
1460
1599
|
case "lookup":
|
|
1461
1600
|
col = table.string(name);
|
|
1462
1601
|
if (field.reference_to) {
|
|
@@ -1474,7 +1613,7 @@ var SqlDriver = class {
|
|
|
1474
1613
|
return;
|
|
1475
1614
|
// Virtual — no column
|
|
1476
1615
|
default:
|
|
1477
|
-
col = table.string(name);
|
|
1616
|
+
col = JSON_COLUMN_TYPES.has(type) ? table.json(name) : table.string(name);
|
|
1478
1617
|
}
|
|
1479
1618
|
if (col) {
|
|
1480
1619
|
if (field.unique) col.unique();
|
|
@@ -1639,6 +1778,16 @@ var SqlDriver = class {
|
|
|
1639
1778
|
}
|
|
1640
1779
|
}
|
|
1641
1780
|
}
|
|
1781
|
+
const numericFields = this.numericFields[object];
|
|
1782
|
+
if (numericFields && numericFields.length > 0) {
|
|
1783
|
+
for (const field of numericFields) {
|
|
1784
|
+
const v = data[field];
|
|
1785
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
1786
|
+
const n = Number(v);
|
|
1787
|
+
if (!Number.isNaN(n)) data[field] = n;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1642
1791
|
}
|
|
1643
1792
|
const dateFields = this.dateFields[object];
|
|
1644
1793
|
if (dateFields && dateFields.size > 0) {
|