@b9g/zen 0.1.0 → 0.1.2

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.
@@ -665,9 +665,126 @@ function extendZod(zodModule) {
665
665
  extendZod(z);
666
666
  var TABLE_MARKER = Symbol.for("@b9g/zen:table");
667
667
  var TABLE_META = Symbol.for("@b9g/zen:table-meta");
668
+ function isTable(value) {
669
+ return value !== null && typeof value === "object" && TABLE_MARKER in value && value[TABLE_MARKER] === true;
670
+ }
668
671
  function getTableMeta(table2) {
669
672
  return table2[TABLE_META];
670
673
  }
674
+ var VIEW_MARKER = Symbol("view");
675
+ var VIEW_META = Symbol("viewMeta");
676
+ function isView(value) {
677
+ return typeof value === "object" && value !== null && value[VIEW_MARKER] === true;
678
+ }
679
+ function getViewMeta(view2) {
680
+ return view2[VIEW_META];
681
+ }
682
+ function view(name, baseTable) {
683
+ validateIdentifier(name, "table");
684
+ if (name.includes(".")) {
685
+ throw new TableDefinitionError(
686
+ `Invalid view name "${name}": view names cannot contain "." as it conflicts with normalization prefixes`,
687
+ name
688
+ );
689
+ }
690
+ return (strings, ...templateValues) => {
691
+ const resultStrings = [];
692
+ const resultValues = [];
693
+ for (let i = 0; i < strings.length; i++) {
694
+ if (i === 0) {
695
+ resultStrings.push(strings[i]);
696
+ }
697
+ if (i < templateValues.length) {
698
+ const value = templateValues[i];
699
+ if (isSQLTemplate(value)) {
700
+ mergeFragment(resultStrings, resultValues, value, strings[i + 1]);
701
+ } else {
702
+ resultValues.push(value);
703
+ resultStrings.push(strings[i + 1]);
704
+ }
705
+ }
706
+ }
707
+ if (resultStrings.length > 0) {
708
+ resultStrings[0] = resultStrings[0].trimStart();
709
+ resultStrings[resultStrings.length - 1] = resultStrings[resultStrings.length - 1].trimEnd();
710
+ }
711
+ const whereTemplate = createTemplate(
712
+ makeTemplate(resultStrings),
713
+ resultValues
714
+ );
715
+ return createViewObject(name, baseTable, whereTemplate);
716
+ };
717
+ }
718
+ function createViewObject(name, baseTable, whereTemplate) {
719
+ const baseMeta = getTableMeta(baseTable);
720
+ const cols = new Proxy({}, {
721
+ get(_target, prop) {
722
+ if (prop in baseTable.schema.shape) {
723
+ return createTemplate(makeTemplate(["", ".", ""]), [
724
+ ident(name),
725
+ ident(prop)
726
+ ]);
727
+ }
728
+ return void 0;
729
+ },
730
+ has(_target, prop) {
731
+ return prop in baseTable.schema.shape;
732
+ },
733
+ ownKeys() {
734
+ return Object.keys(baseTable.schema.shape);
735
+ },
736
+ getOwnPropertyDescriptor(_target, prop) {
737
+ if (prop in baseTable.schema.shape) {
738
+ return { enumerable: true, configurable: true };
739
+ }
740
+ return void 0;
741
+ }
742
+ });
743
+ const primary = baseMeta.primary ? createTemplate(makeTemplate(["", ".", ""]), [
744
+ ident(name),
745
+ ident(baseMeta.primary)
746
+ ]) : null;
747
+ const viewMeta = {
748
+ baseTable,
749
+ whereTemplate
750
+ };
751
+ const tableMeta = {
752
+ ...baseMeta,
753
+ isView: true,
754
+ viewOf: baseTable.name,
755
+ derivedExprs: void 0,
756
+ derivedFields: void 0,
757
+ isDerived: void 0
758
+ };
759
+ const viewObj = {
760
+ name,
761
+ schema: baseTable.schema,
762
+ meta: tableMeta,
763
+ cols,
764
+ primary,
765
+ baseTable,
766
+ fields() {
767
+ return baseTable.fields();
768
+ },
769
+ references() {
770
+ return baseTable.references();
771
+ }
772
+ };
773
+ Object.defineProperty(viewObj, VIEW_MARKER, { value: true, enumerable: false });
774
+ Object.defineProperty(viewObj, VIEW_META, {
775
+ value: viewMeta,
776
+ enumerable: false
777
+ });
778
+ Object.defineProperty(viewObj, TABLE_MARKER, {
779
+ value: true,
780
+ enumerable: false
781
+ });
782
+ Object.defineProperty(viewObj, TABLE_META, {
783
+ value: tableMeta,
784
+ enumerable: false
785
+ });
786
+ return viewObj;
787
+ }
671
788
  function table(name, shape, options = {}) {
672
789
  validateIdentifier(name, "table");
673
790
  if (name.includes(".")) {
@@ -886,7 +1003,7 @@ function createTableObject(name, schema, zodShape, meta, options) {
886
1003
  `Table "${name}" does not have a soft delete field. Use softDelete() wrapper to mark a field.`
887
1004
  );
888
1005
  }
889
- return createTemplate(makeTemplate(["", ".", " IS NOT NULL"]), [
1006
+ return createTemplate(makeTemplate(["(", ".", " IS NOT NULL)"]), [
890
1007
  ident(name),
891
1008
  ident(softDeleteField)
892
1009
  ]);
@@ -1113,6 +1230,28 @@ function createTableObject(name, schema, zodShape, meta, options) {
1113
1230
  }
1114
1231
  }
1115
1232
  return createTemplate(makeTemplate(strings), templateValues);
1233
+ },
1234
+ get active() {
1235
+ const softDeleteField = meta.softDeleteField;
1236
+ if (!softDeleteField) {
1237
+ throw new Error(
1238
+ `Table "${name}" does not have a soft delete field. Use .db.softDelete() to mark a field for soft delete.`
1239
+ );
1240
+ }
1241
+ const whereTemplate = createTemplate(
1242
+ makeTemplate(["WHERE ", ".", " IS NULL"]),
1243
+ [ident(name), ident(softDeleteField)]
1244
+ );
1245
+ const activeViewName = `${name}_active`;
1246
+ const activeView = createViewObject(
1247
+ activeViewName,
1248
+ table2,
1249
+ whereTemplate
1250
+ );
1251
+ if (!internalMeta.activeView) {
1252
+ internalMeta.activeView = activeView;
1253
+ }
1254
+ return activeView;
1116
1255
  }
1117
1256
  };
1118
1257
  return table2;
@@ -1250,7 +1389,25 @@ function extractFieldMeta(name, zodType, dbMeta) {
1250
1389
  }
1251
1390
  return meta;
1252
1391
  }
1253
- function decodeData(table2, data) {
1392
+ function inferFieldType(schema) {
1393
+ let core = schema;
1394
+ while (typeof core.unwrap === "function") {
1395
+ if (core instanceof z.ZodArray || core instanceof z.ZodObject || core instanceof z.ZodDate || core instanceof z.ZodBoolean || core instanceof z.ZodNumber) {
1396
+ break;
1397
+ }
1398
+ core = core.unwrap();
1399
+ }
1400
+ if (core instanceof z.ZodDate)
1401
+ return "datetime";
1402
+ if (core instanceof z.ZodBoolean)
1403
+ return "boolean";
1404
+ if (core instanceof z.ZodObject || core instanceof z.ZodArray)
1405
+ return "json";
1406
+ if (core instanceof z.ZodNumber)
1407
+ return "real";
1408
+ return "text";
1409
+ }
1410
+ function decodeData(table2, data, driver) {
1254
1411
  if (!data)
1255
1412
  return data;
1256
1413
  const decoded = {};
@@ -1260,10 +1417,13 @@ function decodeData(table2, data) {
1260
1417
  const fieldSchema = shape?.[key];
1261
1418
  if (fieldMeta?.decode && typeof fieldMeta.decode === "function") {
1262
1419
  decoded[key] = fieldMeta.decode(value);
1420
+ } else if (driver?.decodeValue && fieldSchema) {
1421
+ const fieldType = inferFieldType(fieldSchema);
1422
+ decoded[key] = driver.decodeValue(value, fieldType);
1263
1423
  } else if (fieldSchema) {
1264
1424
  let core = fieldSchema;
1265
1425
  while (typeof core.unwrap === "function") {
1266
- if (core instanceof z.ZodArray || core instanceof z.ZodObject) {
1426
+ if (core instanceof z.ZodArray || core instanceof z.ZodObject || core instanceof z.ZodBoolean || core instanceof z.ZodDate) {
1267
1427
  break;
1268
1428
  }
1269
1429
  core = core.unwrap();
@@ -1280,9 +1440,20 @@ function decodeData(table2, data) {
1280
1440
  } else {
1281
1441
  decoded[key] = value;
1282
1442
  }
1443
+ } else if (core instanceof z.ZodBoolean) {
1444
+ if (typeof value === "number") {
1445
+ decoded[key] = value !== 0;
1446
+ } else if (typeof value === "string") {
1447
+ decoded[key] = value !== "0" && value !== "";
1448
+ } else if (typeof value === "boolean") {
1449
+ decoded[key] = value;
1450
+ } else {
1451
+ decoded[key] = value;
1452
+ }
1283
1453
  } else if (core instanceof z.ZodDate) {
1284
1454
  if (typeof value === "string") {
1285
- const date = new Date(value);
1455
+ const normalized = value.includes("T") ? value : value.replace(" ", "T") + "Z";
1456
+ const date = new Date(normalized);
1286
1457
  if (isNaN(date.getTime())) {
1287
1458
  throw new Error(
1288
1459
  `Invalid date value for field "${key}": "${value}" cannot be parsed as a valid date`
@@ -1306,6 +1477,18 @@ function decodeData(table2, data) {
1306
1477
  decoded[key] = value;
1307
1478
  }
1308
1479
  }
1480
+ const derive = getTableMeta(table2).derive;
1481
+ if (derive) {
1482
+ for (const [propName, deriveFn] of Object.entries(derive)) {
1483
+ Object.defineProperty(decoded, propName, {
1484
+ get() {
1485
+ return deriveFn(this);
1486
+ },
1487
+ enumerable: false,
1488
+ configurable: true
1489
+ });
1490
+ }
1491
+ }
1309
1492
  return decoded;
1310
1493
  }
1311
1494
 
@@ -1340,7 +1523,12 @@ export {
1340
1523
  makeTemplate,
1341
1524
  validateWithStandardSchema,
1342
1525
  extendZod,
1526
+ isTable,
1343
1527
  getTableMeta,
1528
+ isView,
1529
+ getViewMeta,
1530
+ view,
1344
1531
  table,
1532
+ inferFieldType,
1345
1533
  decodeData
1346
1534
  };
@@ -0,0 +1,63 @@
1
+ import {
2
+ isSQLBuiltin,
3
+ isSQLIdentifier,
4
+ resolveSQLBuiltin
5
+ } from "./chunk-BEX6VPES.js";
6
+
7
+ // src/impl/sql.ts
8
+ function quoteIdent(name, dialect) {
9
+ if (dialect === "mysql") {
10
+ return `\`${name.replace(/`/g, "``")}\``;
11
+ }
12
+ return `"${name.replace(/"/g, '""')}"`;
13
+ }
14
+ function placeholder(index, dialect) {
15
+ if (dialect === "postgresql") {
16
+ return `$${index}`;
17
+ }
18
+ return "?";
19
+ }
20
+ function renderDDL(strings, values, dialect) {
21
+ let sql = "";
22
+ for (let i = 0; i < strings.length; i++) {
23
+ sql += strings[i];
24
+ if (i < values.length) {
25
+ const value = values[i];
26
+ if (isSQLBuiltin(value)) {
27
+ sql += resolveSQLBuiltin(value);
28
+ } else if (isSQLIdentifier(value)) {
29
+ sql += quoteIdent(value.name, dialect);
30
+ } else {
31
+ sql += inlineLiteral(value, dialect);
32
+ }
33
+ }
34
+ }
35
+ return sql;
36
+ }
37
+ function inlineLiteral(value, dialect) {
38
+ if (value === null || value === void 0) {
39
+ return "NULL";
40
+ }
41
+ if (typeof value === "boolean") {
42
+ if (dialect === "sqlite") {
43
+ return value ? "1" : "0";
44
+ }
45
+ return value ? "TRUE" : "FALSE";
46
+ }
47
+ if (typeof value === "number") {
48
+ return String(value);
49
+ }
50
+ if (typeof value === "string") {
51
+ return `'${value.replace(/'/g, "''")}'`;
52
+ }
53
+ if (value instanceof Date) {
54
+ return `'${value.toISOString()}'`;
55
+ }
56
+ return `'${String(value).replace(/'/g, "''")}'`;
57
+ }
58
+
59
+ export {
60
+ quoteIdent,
61
+ placeholder,
62
+ renderDDL
63
+ };
@@ -0,0 +1,11 @@
1
+ import {
2
+ generateColumnDDL,
3
+ generateDDL,
4
+ generateViewDDL
5
+ } from "./chunk-ARUUB3H4.js";
6
+ import "./chunk-BEX6VPES.js";
7
+ export {
8
+ generateColumnDDL,
9
+ generateDDL,
10
+ generateViewDDL
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/zen",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The simple database client. Define tables. Write SQL. Get objects.",
5
5
  "keywords": [
6
6
  "database",
@@ -12,7 +12,16 @@
12
12
  "postgres",
13
13
  "mysql"
14
14
  ],
15
+ "author": "Brian Kim <briankimdev@gmail.com>",
15
16
  "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bikeshaving/zen.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/bikeshaving/zen/issues"
23
+ },
24
+ "homepage": "https://zendb.org",
16
25
  "dependencies": {
17
26
  "zod": "^4.0.0"
18
27
  },
package/src/bun.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Unified driver supporting PostgreSQL, MySQL, and SQLite via Bun's built-in SQL.
5
5
  * Zero dependencies - uses native Bun implementation.
6
6
  */
7
- import type { Driver, Table, EnsureResult } from "./zen.js";
7
+ import type { Driver, Table, View, EnsureResult } from "./zen.js";
8
8
  /**
9
9
  * Bun driver using Bun's built-in SQL.
10
10
  * Supports PostgreSQL, MySQL, and SQLite with automatic dialect detection.
@@ -34,9 +34,20 @@ export default class BunDriver implements Driver {
34
34
  run(strings: TemplateStringsArray, values: unknown[]): Promise<number>;
35
35
  val<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
36
36
  close(): Promise<void>;
37
+ /**
38
+ * Encode a JS value for database insertion.
39
+ * Dialect-aware: PostgreSQL uses native types, SQLite/MySQL need conversion.
40
+ */
41
+ encodeValue(value: unknown, fieldType: string): unknown;
42
+ /**
43
+ * Decode a database value to JS.
44
+ * Dialect-aware: handles differences in how values are returned.
45
+ */
46
+ decodeValue(value: unknown, fieldType: string): unknown;
37
47
  transaction<T>(fn: (txDriver: Driver) => Promise<T>): Promise<T>;
38
48
  withMigrationLock<T>(fn: () => Promise<T>): Promise<T>;
39
49
  ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
50
+ ensureView<T extends View<any>>(viewObj: T): Promise<EnsureResult>;
40
51
  ensureConstraints<T extends Table<any>>(table: T): Promise<EnsureResult>;
41
52
  /**
42
53
  * Optional introspection: list columns for a table.
package/src/bun.js CHANGED
@@ -3,13 +3,15 @@ import {
3
3
  placeholder,
4
4
  quoteIdent,
5
5
  renderDDL
6
- } from "../chunk-2IEEEMRN.js";
6
+ } from "../chunk-NBXBBEMA.js";
7
7
  import {
8
- generateDDL
9
- } from "../chunk-QXGEP5PB.js";
8
+ generateDDL,
9
+ generateViewDDL
10
+ } from "../chunk-ARUUB3H4.js";
10
11
  import {
12
+ getTableMeta,
11
13
  resolveSQLBuiltin
12
- } from "../chunk-56M5Z3A6.js";
14
+ } from "../chunk-BEX6VPES.js";
13
15
 
14
16
  // src/bun.ts
15
17
  import { SQL } from "bun";
@@ -227,13 +229,103 @@ var BunDriver = class {
227
229
  async close() {
228
230
  await this.#sql.close();
229
231
  }
232
+ // ==========================================================================
233
+ // Type Encoding/Decoding
234
+ // ==========================================================================
235
+ /**
236
+ * Encode a JS value for database insertion.
237
+ * Dialect-aware: PostgreSQL uses native types, SQLite/MySQL need conversion.
238
+ */
239
+ encodeValue(value, fieldType) {
240
+ if (value === null || value === void 0) {
241
+ return value;
242
+ }
243
+ switch (fieldType) {
244
+ case "datetime":
245
+ if (value instanceof Date && !isNaN(value.getTime())) {
246
+ if (this.#dialect === "postgresql") {
247
+ return value;
248
+ } else if (this.#dialect === "mysql") {
249
+ return value.toISOString().replace("T", " ").slice(0, 23);
250
+ } else {
251
+ return value.toISOString();
252
+ }
253
+ }
254
+ return value;
255
+ case "boolean":
256
+ if (this.#dialect === "postgresql") {
257
+ return value;
258
+ }
259
+ return value ? 1 : 0;
260
+ case "json":
261
+ return JSON.stringify(value);
262
+ default:
263
+ return value;
264
+ }
265
+ }
266
+ /**
267
+ * Decode a database value to JS.
268
+ * Dialect-aware: handles differences in how values are returned.
269
+ */
270
+ decodeValue(value, fieldType) {
271
+ if (value === null || value === void 0) {
272
+ return value;
273
+ }
274
+ switch (fieldType) {
275
+ case "datetime":
276
+ if (value instanceof Date) {
277
+ if (isNaN(value.getTime())) {
278
+ throw new Error(`Invalid Date object received from database`);
279
+ }
280
+ return value;
281
+ }
282
+ if (typeof value === "string") {
283
+ let date;
284
+ if (this.#dialect === "mysql") {
285
+ const normalized = value.includes("T") ? value : value.replace(" ", "T") + "Z";
286
+ date = new Date(normalized);
287
+ } else {
288
+ date = new Date(value);
289
+ }
290
+ if (isNaN(date.getTime())) {
291
+ throw new Error(
292
+ `Invalid date value: "${value}" cannot be parsed as a valid date`
293
+ );
294
+ }
295
+ return date;
296
+ }
297
+ return value;
298
+ case "boolean":
299
+ if (this.#dialect === "postgresql") {
300
+ return value;
301
+ }
302
+ if (typeof value === "number") {
303
+ return value !== 0;
304
+ }
305
+ if (typeof value === "string") {
306
+ return value !== "0" && value !== "";
307
+ }
308
+ return value;
309
+ case "json":
310
+ if (typeof value === "string") {
311
+ return JSON.parse(value);
312
+ }
313
+ return value;
314
+ default:
315
+ return value;
316
+ }
317
+ }
230
318
  async transaction(fn) {
231
319
  const dialect = this.#dialect;
232
320
  const handleError = this.#handleError.bind(this);
233
321
  const supportsReturning = this.supportsReturning;
322
+ const encodeValue = this.encodeValue.bind(this);
323
+ const decodeValue = this.decodeValue.bind(this);
234
324
  return await this.#sql.transaction(async (txSql) => {
235
325
  const txDriver = {
236
326
  supportsReturning,
327
+ encodeValue,
328
+ decodeValue,
237
329
  all: async (strings, values) => {
238
330
  try {
239
331
  const { sql, params } = buildSQL(strings, values, dialect);
@@ -359,6 +451,9 @@ var BunDriver = class {
359
451
  step = 4;
360
452
  await this.#checkMissingConstraints(table);
361
453
  }
454
+ step = 5;
455
+ const viewApplied = await this.#ensureViews(table);
456
+ applied = applied || viewApplied;
362
457
  return { applied };
363
458
  } catch (error) {
364
459
  if (error instanceof SchemaDriftError || error instanceof EnsureError) {
@@ -371,6 +466,19 @@ var BunDriver = class {
371
466
  );
372
467
  }
373
468
  }
469
+ async ensureView(viewObj) {
470
+ await this.#ensureSqliteInit();
471
+ const ddlTemplate = generateViewDDL(viewObj, { dialect: this.#dialect });
472
+ const ddlSQL = renderDDL(
473
+ ddlTemplate[0],
474
+ ddlTemplate.slice(1),
475
+ this.#dialect
476
+ );
477
+ for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
478
+ await this.#sql.unsafe(stmt.trim(), []);
479
+ }
480
+ return { applied: true };
481
+ }
374
482
  async ensureConstraints(table) {
375
483
  await this.#ensureSqliteInit();
376
484
  const tableName = table.name;
@@ -634,7 +742,7 @@ var BunDriver = class {
634
742
  return applied;
635
743
  }
636
744
  async #addColumn(table, fieldName) {
637
- const { generateColumnDDL } = await import("../ddl-NAJM37GQ.js");
745
+ const { generateColumnDDL } = await import("../ddl-OT6HPLQY.js");
638
746
  const zodType = table.schema.shape[fieldName];
639
747
  const fieldMeta = table.meta.fields[fieldName] || {};
640
748
  const colTemplate = generateColumnDDL(
@@ -673,6 +781,30 @@ var BunDriver = class {
673
781
  }
674
782
  return applied;
675
783
  }
784
+ /**
785
+ * Ensure the active view exists for this table (if it has soft delete).
786
+ * Creates the view using generateViewDDL.
787
+ */
788
+ async #ensureViews(table) {
789
+ const meta = getTableMeta(table);
790
+ if (meta.softDeleteField && !meta.activeView) {
791
+ void table.active;
792
+ }
793
+ const activeView = meta.activeView;
794
+ if (!activeView) {
795
+ return false;
796
+ }
797
+ const ddlTemplate = generateViewDDL(activeView, { dialect: this.#dialect });
798
+ const ddlSQL = renderDDL(
799
+ ddlTemplate[0],
800
+ ddlTemplate.slice(1),
801
+ this.#dialect
802
+ );
803
+ for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
804
+ await this.#sql.unsafe(stmt.trim(), []);
805
+ }
806
+ return true;
807
+ }
676
808
  async #createIndex(tableName, indexName, columns, unique) {
677
809
  const uniqueKw = unique ? "UNIQUE " : "";
678
810
  const colList = columns.map((c) => quoteIdent(c, this.#dialect)).join(", ");
package/src/mysql.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * Requires: mysql2
8
8
  */
9
9
  import type { Driver, Table, EnsureResult } from "./zen.js";
10
+ import type { View } from "./impl/table.js";
10
11
  /**
11
12
  * Options for the mysql adapter.
12
13
  */
@@ -46,6 +47,16 @@ export default class MySQLDriver implements Driver {
46
47
  run(strings: TemplateStringsArray, values: unknown[]): Promise<number>;
47
48
  val<T>(strings: TemplateStringsArray, values: unknown[]): Promise<T | null>;
48
49
  close(): Promise<void>;
50
+ /**
51
+ * Encode a JS value for database insertion.
52
+ * MySQL: needs conversion for Date and boolean.
53
+ */
54
+ encodeValue(value: unknown, fieldType: string): unknown;
55
+ /**
56
+ * Decode a database value to JS.
57
+ * MySQL: handles 0/1 booleans and datetime strings.
58
+ */
59
+ decodeValue(value: unknown, fieldType: string): unknown;
49
60
  transaction<T>(fn: (txDriver: Driver) => Promise<T>): Promise<T>;
50
61
  withMigrationLock<T>(fn: () => Promise<T>): Promise<T>;
51
62
  /**
@@ -54,6 +65,7 @@ export default class MySQLDriver implements Driver {
54
65
  * Throws SchemaDriftError if constraints are missing.
55
66
  */
56
67
  ensureTable<T extends Table<any>>(table: T): Promise<EnsureResult>;
68
+ ensureView<T extends View<any>>(viewObj: T): Promise<EnsureResult>;
57
69
  /**
58
70
  * Ensure constraints exist on the table.
59
71
  * Applies unique and foreign key constraints with preflight checks.