@objectstack/driver-sql 9.11.0 → 10.0.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 CHANGED
@@ -101,6 +101,20 @@ declare class SqlDriver implements IDataDriver {
101
101
  protected numericFields: Record<string, string[]>;
102
102
  protected dateFields: Record<string, Set<string>>;
103
103
  protected datetimeFields: Record<string, Set<string>>;
104
+ /**
105
+ * Federation read path (ADR-0015). For external objects whose physical
106
+ * remote table differs from the object name, these map between the two so
107
+ * {@link getBuilder} targets the remote table while the coercion maps above
108
+ * stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
109
+ * managed objects, so the managed query path is unchanged.
110
+ */
111
+ protected physicalTableByObject: Record<string, string>;
112
+ protected physicalSchemaByObject: Record<string, string>;
113
+ protected objectByPhysicalTable: Record<string, string>;
114
+ /** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
115
+ protected fieldColumnByObject: Record<string, Record<string, string>>;
116
+ /** External columnMap inverse: physical remote column -> logical field (for read output remap). */
117
+ protected columnFieldByObject: Record<string, Record<string, string>>;
104
118
  protected tablesWithTimestamps: Set<string>;
105
119
  /**
106
120
  * Autonumber field configs per table, captured during initObjects.
@@ -334,6 +348,30 @@ declare class SqlDriver implements IDataDriver {
334
348
  /**
335
349
  * Batch-initialise tables from an array of object definitions.
336
350
  */
351
+ /**
352
+ * DDL-free metadata registration for a federated (external) object — the
353
+ * read-path counterpart to {@link initObjects} (ADR-0015 federation).
354
+ *
355
+ * `initObjects` is gated by `assertSchemaMutable` and therefore throws for
356
+ * any non-`managed` driver, which left external objects with NO read-coercion
357
+ * metadata and the query path resolving to a table named after the object
358
+ * instead of its remote table. This populates the same coercion maps (keyed
359
+ * by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
360
+ * records the physical remote table (`external.remoteName`, optionally
361
+ * `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
362
+ * any DDL (createTable/alterTable/columnInfo). Keep the field-classification
363
+ * below in sync with initObjects() if the field-type -> storage mapping changes.
364
+ */
365
+ registerExternalObject(schema: {
366
+ name: string;
367
+ fields?: Record<string, any>;
368
+ tenancy?: any;
369
+ external?: {
370
+ remoteName?: string;
371
+ remoteSchema?: string;
372
+ columnMap?: Record<string, string>;
373
+ };
374
+ }): void;
337
375
  initObjects(objects: Array<{
338
376
  name: string;
339
377
  fields?: Record<string, any>;
@@ -439,6 +477,16 @@ declare class SqlDriver implements IDataDriver {
439
477
  * `initObjects`). Returns null when the builder is not table-scoped yet.
440
478
  */
441
479
  protected tableNameForBuilder(builder: any): string | null;
480
+ /**
481
+ * Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
482
+ * OBJECT name, but after the federation change {@link getBuilder} targets the
483
+ * physical remote table, so a builder reports the remote name. Map it back to
484
+ * the object name for external objects; identity for managed ones (no reverse
485
+ * entry). Note datetime coercion is a SQLite-only concern (see
486
+ * coerceFilterValue), and SQLite external tables are bare-named, so this is
487
+ * exact where it matters.
488
+ */
489
+ protected coercionKey(builder: any): string | null;
442
490
  /**
443
491
  * Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
444
492
  * calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
@@ -498,6 +546,19 @@ declare class SqlDriver implements IDataDriver {
498
546
  private applyContainsLike;
499
547
  protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
500
548
  protected mapSortField(field: string): string;
549
+ /**
550
+ * Physical column for a logical field on an external object that declares an
551
+ * `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
552
+ * per-site resolution) when the object has no columnMap, so managed objects
553
+ * and external objects without a columnMap are byte-for-byte unchanged.
554
+ */
555
+ protected remoteColumn(object: string | null | undefined, field: string, fallback: string): string;
556
+ /**
557
+ * Remap a write payload's logical field keys to physical remote columns for an
558
+ * external object with a columnMap. No-op otherwise. Applied AFTER formatInput
559
+ * (whose value coercion is keyed by logical field name).
560
+ */
561
+ protected applyWriteColumnMap(object: string, data: any): any;
501
562
  protected mapAggregateFunc(func: string): string;
502
563
  protected buildWindowFunction(spec: any): string;
503
564
  protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any): void;
package/dist/index.d.ts CHANGED
@@ -101,6 +101,20 @@ declare class SqlDriver implements IDataDriver {
101
101
  protected numericFields: Record<string, string[]>;
102
102
  protected dateFields: Record<string, Set<string>>;
103
103
  protected datetimeFields: Record<string, Set<string>>;
104
+ /**
105
+ * Federation read path (ADR-0015). For external objects whose physical
106
+ * remote table differs from the object name, these map between the two so
107
+ * {@link getBuilder} targets the remote table while the coercion maps above
108
+ * stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
109
+ * managed objects, so the managed query path is unchanged.
110
+ */
111
+ protected physicalTableByObject: Record<string, string>;
112
+ protected physicalSchemaByObject: Record<string, string>;
113
+ protected objectByPhysicalTable: Record<string, string>;
114
+ /** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
115
+ protected fieldColumnByObject: Record<string, Record<string, string>>;
116
+ /** External columnMap inverse: physical remote column -> logical field (for read output remap). */
117
+ protected columnFieldByObject: Record<string, Record<string, string>>;
104
118
  protected tablesWithTimestamps: Set<string>;
105
119
  /**
106
120
  * Autonumber field configs per table, captured during initObjects.
@@ -334,6 +348,30 @@ declare class SqlDriver implements IDataDriver {
334
348
  /**
335
349
  * Batch-initialise tables from an array of object definitions.
336
350
  */
351
+ /**
352
+ * DDL-free metadata registration for a federated (external) object — the
353
+ * read-path counterpart to {@link initObjects} (ADR-0015 federation).
354
+ *
355
+ * `initObjects` is gated by `assertSchemaMutable` and therefore throws for
356
+ * any non-`managed` driver, which left external objects with NO read-coercion
357
+ * metadata and the query path resolving to a table named after the object
358
+ * instead of its remote table. This populates the same coercion maps (keyed
359
+ * by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
360
+ * records the physical remote table (`external.remoteName`, optionally
361
+ * `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
362
+ * any DDL (createTable/alterTable/columnInfo). Keep the field-classification
363
+ * below in sync with initObjects() if the field-type -> storage mapping changes.
364
+ */
365
+ registerExternalObject(schema: {
366
+ name: string;
367
+ fields?: Record<string, any>;
368
+ tenancy?: any;
369
+ external?: {
370
+ remoteName?: string;
371
+ remoteSchema?: string;
372
+ columnMap?: Record<string, string>;
373
+ };
374
+ }): void;
337
375
  initObjects(objects: Array<{
338
376
  name: string;
339
377
  fields?: Record<string, any>;
@@ -439,6 +477,16 @@ declare class SqlDriver implements IDataDriver {
439
477
  * `initObjects`). Returns null when the builder is not table-scoped yet.
440
478
  */
441
479
  protected tableNameForBuilder(builder: any): string | null;
480
+ /**
481
+ * Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
482
+ * OBJECT name, but after the federation change {@link getBuilder} targets the
483
+ * physical remote table, so a builder reports the remote name. Map it back to
484
+ * the object name for external objects; identity for managed ones (no reverse
485
+ * entry). Note datetime coercion is a SQLite-only concern (see
486
+ * coerceFilterValue), and SQLite external tables are bare-named, so this is
487
+ * exact where it matters.
488
+ */
489
+ protected coercionKey(builder: any): string | null;
442
490
  /**
443
491
  * Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
444
492
  * calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
@@ -498,6 +546,19 @@ declare class SqlDriver implements IDataDriver {
498
546
  private applyContainsLike;
499
547
  protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
500
548
  protected mapSortField(field: string): string;
549
+ /**
550
+ * Physical column for a logical field on an external object that declares an
551
+ * `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
552
+ * per-site resolution) when the object has no columnMap, so managed objects
553
+ * and external objects without a columnMap are byte-for-byte unchanged.
554
+ */
555
+ protected remoteColumn(object: string | null | undefined, field: string, fallback: string): string;
556
+ /**
557
+ * Remap a write payload's logical field keys to physical remote columns for an
558
+ * external object with a columnMap. No-op otherwise. Applied AFTER formatInput
559
+ * (whose value coercion is keyed by logical field name).
560
+ */
561
+ protected applyWriteColumnMap(object: string, data: any): any;
501
562
  protected mapAggregateFunc(func: string): string;
502
563
  protected buildWindowFunction(spec: any): string;
503
564
  protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any): void;
package/dist/index.js CHANGED
@@ -86,6 +86,20 @@ var SqlDriver = class {
86
86
  this.numericFields = {};
87
87
  this.dateFields = {};
88
88
  this.datetimeFields = {};
89
+ /**
90
+ * Federation read path (ADR-0015). For external objects whose physical
91
+ * remote table differs from the object name, these map between the two so
92
+ * {@link getBuilder} targets the remote table while the coercion maps above
93
+ * stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
94
+ * managed objects, so the managed query path is unchanged.
95
+ */
96
+ this.physicalTableByObject = {};
97
+ this.physicalSchemaByObject = {};
98
+ this.objectByPhysicalTable = {};
99
+ /** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
100
+ this.fieldColumnByObject = {};
101
+ /** External columnMap inverse: physical remote column -> logical field (for read output remap). */
102
+ this.columnFieldByObject = {};
89
103
  this.tablesWithTimestamps = /* @__PURE__ */ new Set();
90
104
  /**
91
105
  * Autonumber field configs per table, captured during initObjects.
@@ -310,6 +324,13 @@ var SqlDriver = class {
310
324
  // ===================================
311
325
  async connect() {
312
326
  await this.ensureDatabaseExists();
327
+ if (this.isSqlite) {
328
+ try {
329
+ await this.knex.raw("PRAGMA auto_vacuum = INCREMENTAL");
330
+ } catch (e) {
331
+ this.logger.warn("Failed to set PRAGMA auto_vacuum=INCREMENTAL", e);
332
+ }
333
+ }
313
334
  }
314
335
  async checkHealth() {
315
336
  try {
@@ -335,7 +356,7 @@ var SqlDriver = class {
335
356
  if (query.orderBy && Array.isArray(query.orderBy)) {
336
357
  for (const item of query.orderBy) {
337
358
  if (item.field) {
338
- b.orderBy(this.mapSortField(item.field), item.order || "asc");
359
+ b.orderBy(this.remoteColumn(object, item.field, this.mapSortField(item.field)), item.order || "asc");
339
360
  }
340
361
  }
341
362
  }
@@ -412,7 +433,7 @@ var SqlDriver = class {
412
433
  this.injectTenantOnInsert(object, toInsert, options);
413
434
  await this.fillAutoNumberFields(object, toInsert, options);
414
435
  const builder = this.getBuilder(object, options);
415
- const formatted = this.formatInput(object, toInsert);
436
+ const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
416
437
  const result = await builder.insert(formatted).returning("*");
417
438
  return this.formatOutput(object, result[0]);
418
439
  }
@@ -609,8 +630,8 @@ var SqlDriver = class {
609
630
  * tenant scopes it for isolation.
610
631
  */
611
632
  async fillAutoNumberFields(object, row, options) {
612
- const tableName = import_system.StorageNameMapping.resolveTableName({ name: object });
613
- const cfgs = this.autoNumberFields[tableName] || this.autoNumberFields[object];
633
+ const tableName = this.physicalTableByObject[object] ?? import_system.StorageNameMapping.resolveTableName({ name: object });
634
+ const cfgs = this.autoNumberFields[object] || this.autoNumberFields[tableName];
614
635
  if (!cfgs || cfgs.length === 0) return;
615
636
  const parentTrx = options?.transaction;
616
637
  const timezone = options?.timezone;
@@ -644,7 +665,7 @@ var SqlDriver = class {
644
665
  this.auditMissingTenant(object, "update", options);
645
666
  const builder = this.getBuilder(object, options).where("id", id);
646
667
  this.applyTenantScope(builder, object, options);
647
- const formatted = this.formatInput(object, data);
668
+ const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
648
669
  if (this.tablesWithTimestamps.has(object)) {
649
670
  if (this.isSqlite) {
650
671
  const now = /* @__PURE__ */ new Date();
@@ -670,7 +691,7 @@ var SqlDriver = class {
670
691
  this.auditMissingTenant(object, "upsert", options);
671
692
  this.injectTenantOnInsert(object, toUpsert, options);
672
693
  await this.fillAutoNumberFields(object, toUpsert, options);
673
- const formatted = this.formatInput(object, toUpsert);
694
+ const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
674
695
  const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
675
696
  const builder = this.getBuilder(object, options);
676
697
  await builder.insert(formatted).onConflict(mergeKeys).merge();
@@ -953,6 +974,89 @@ var SqlDriver = class {
953
974
  /**
954
975
  * Batch-initialise tables from an array of object definitions.
955
976
  */
977
+ /**
978
+ * DDL-free metadata registration for a federated (external) object — the
979
+ * read-path counterpart to {@link initObjects} (ADR-0015 federation).
980
+ *
981
+ * `initObjects` is gated by `assertSchemaMutable` and therefore throws for
982
+ * any non-`managed` driver, which left external objects with NO read-coercion
983
+ * metadata and the query path resolving to a table named after the object
984
+ * instead of its remote table. This populates the same coercion maps (keyed
985
+ * by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
986
+ * records the physical remote table (`external.remoteName`, optionally
987
+ * `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
988
+ * any DDL (createTable/alterTable/columnInfo). Keep the field-classification
989
+ * below in sync with initObjects() if the field-type -> storage mapping changes.
990
+ */
991
+ registerExternalObject(schema) {
992
+ const key = schema.name;
993
+ const remoteName = schema.external?.remoteName || schema.name;
994
+ const remoteSchema = schema.external?.remoteSchema;
995
+ this.physicalTableByObject[key] = remoteName;
996
+ this.objectByPhysicalTable[remoteName] = key;
997
+ if (remoteSchema) {
998
+ if (this.isSqlite) {
999
+ this.logger.warn(
1000
+ `[sql-driver] external object "${key}" declares remoteSchema="${remoteSchema}" but SQLite has no schema namespace; ignoring (treating "${remoteName}" as a bare table).`
1001
+ );
1002
+ } else {
1003
+ this.physicalSchemaByObject[key] = remoteSchema;
1004
+ }
1005
+ }
1006
+ const columnMap = schema.external?.columnMap;
1007
+ if (columnMap && typeof columnMap === "object" && Object.keys(columnMap).length > 0) {
1008
+ const fieldToCol = {};
1009
+ const colToField = {};
1010
+ for (const [remoteCol, localField] of Object.entries(columnMap)) {
1011
+ if (typeof localField === "string" && localField) {
1012
+ fieldToCol[localField] = remoteCol;
1013
+ colToField[remoteCol] = localField;
1014
+ }
1015
+ }
1016
+ this.fieldColumnByObject[key] = fieldToCol;
1017
+ this.columnFieldByObject[key] = colToField;
1018
+ }
1019
+ const jsonCols = [];
1020
+ const booleanCols = [];
1021
+ const numericCols = [];
1022
+ const dateCols = [];
1023
+ const datetimeCols = [];
1024
+ const autoNumberCols = [];
1025
+ const tenancyDecl = schema?.tenancy;
1026
+ let tenantField = null;
1027
+ if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
1028
+ const declared = String(tenancyDecl.tenantField);
1029
+ if (schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, declared)) {
1030
+ tenantField = declared;
1031
+ }
1032
+ }
1033
+ if (!tenantField) {
1034
+ const hasOrgField = !!(schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, "organization_id"));
1035
+ tenantField = hasOrgField ? "organization_id" : null;
1036
+ }
1037
+ if (schema.fields) {
1038
+ for (const [name, field] of Object.entries(schema.fields)) {
1039
+ const type = field.type || "string";
1040
+ if (this.isJsonField(type, field)) jsonCols.push(name);
1041
+ if (type === "boolean" || type === "toggle") booleanCols.push(name);
1042
+ if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
1043
+ if (type === "date") dateCols.push(name);
1044
+ if (type === "datetime") datetimeCols.push(name);
1045
+ if (type === "auto_number" || type === "autonumber") {
1046
+ const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
1047
+ const fmt = rawFmt || "{0000}";
1048
+ autoNumberCols.push({ name, format: fmt, tokens: (0, import_data.parseAutonumberFormat)(fmt), tenantField });
1049
+ }
1050
+ }
1051
+ }
1052
+ this.jsonFields[key] = jsonCols;
1053
+ this.booleanFields[key] = booleanCols;
1054
+ this.numericFields[key] = numericCols;
1055
+ this.autoNumberFields[key] = autoNumberCols;
1056
+ this.tenantFieldByTable[key] = tenantField;
1057
+ if (dateCols.length) this.dateFields[key] = new Set(dateCols);
1058
+ if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
1059
+ }
956
1060
  async initObjects(objects) {
957
1061
  var _a, _b;
958
1062
  this.assertSchemaMutable("initObjects");
@@ -1194,7 +1298,12 @@ var SqlDriver = class {
1194
1298
  return this.knex;
1195
1299
  }
1196
1300
  getBuilder(object, options) {
1197
- let builder = this.knex(object);
1301
+ const physical = this.physicalTableByObject[object] ?? object;
1302
+ let builder = this.knex(physical);
1303
+ const remoteSchema = this.physicalSchemaByObject[object];
1304
+ if (remoteSchema) {
1305
+ builder = builder.withSchema(remoteSchema);
1306
+ }
1198
1307
  if (options?.transaction) {
1199
1308
  builder = builder.transacting(options.transaction);
1200
1309
  }
@@ -1293,6 +1402,20 @@ var SqlDriver = class {
1293
1402
  if (typeof t === "string") return t;
1294
1403
  return null;
1295
1404
  }
1405
+ /**
1406
+ * Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
1407
+ * OBJECT name, but after the federation change {@link getBuilder} targets the
1408
+ * physical remote table, so a builder reports the remote name. Map it back to
1409
+ * the object name for external objects; identity for managed ones (no reverse
1410
+ * entry). Note datetime coercion is a SQLite-only concern (see
1411
+ * coerceFilterValue), and SQLite external tables are bare-named, so this is
1412
+ * exact where it matters.
1413
+ */
1414
+ coercionKey(builder) {
1415
+ const physical = this.tableNameForBuilder(builder);
1416
+ if (physical == null) return null;
1417
+ return this.objectByPhysicalTable[physical] ?? physical;
1418
+ }
1296
1419
  /**
1297
1420
  * Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
1298
1421
  * calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
@@ -1386,7 +1509,7 @@ var SqlDriver = class {
1386
1509
  }
1387
1510
  applyFilters(builder, filters) {
1388
1511
  if (!filters) return;
1389
- const table = this.tableNameForBuilder(builder);
1512
+ const table = this.coercionKey(builder);
1390
1513
  if (!Array.isArray(filters) && typeof filters === "object") {
1391
1514
  const hasMongoOperators = Object.keys(filters).some(
1392
1515
  (k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
@@ -1397,7 +1520,7 @@ var SqlDriver = class {
1397
1520
  }
1398
1521
  for (const [key, value] of Object.entries(filters)) {
1399
1522
  if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
1400
- builder.where(key, this.coerceFilterValue(table, key, value));
1523
+ builder.where(this.remoteColumn(table, key, key), this.coerceFilterValue(table, key, value));
1401
1524
  }
1402
1525
  return;
1403
1526
  }
@@ -1413,8 +1536,9 @@ var SqlDriver = class {
1413
1536
  const [fieldRaw, op, value] = item;
1414
1537
  const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
1415
1538
  if (isCriterion) {
1416
- const field = this.mapSortField(fieldRaw);
1417
- const coerced = this.coerceFilterValue(table, field, value);
1539
+ const localField = this.mapSortField(fieldRaw);
1540
+ const field = this.remoteColumn(table, fieldRaw, localField);
1541
+ const coerced = this.coerceFilterValue(table, localField, value);
1418
1542
  const apply = (b) => {
1419
1543
  const method = nextJoin === "or" ? "orWhere" : "where";
1420
1544
  const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
@@ -1466,7 +1590,7 @@ var SqlDriver = class {
1466
1590
  }
1467
1591
  applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
1468
1592
  if (!condition || typeof condition !== "object") return;
1469
- const table = tableHint ?? this.tableNameForBuilder(builder);
1593
+ const table = tableHint ?? this.coercionKey(builder);
1470
1594
  for (const [key, value] of Object.entries(condition)) {
1471
1595
  if (key === "$and" && Array.isArray(value)) {
1472
1596
  builder.where((qb) => {
@@ -1486,10 +1610,11 @@ var SqlDriver = class {
1486
1610
  }
1487
1611
  });
1488
1612
  } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1489
- const field = this.mapSortField(key);
1613
+ const localField = this.mapSortField(key);
1614
+ const field = this.remoteColumn(table, key, localField);
1490
1615
  for (const [op, opValue] of Object.entries(value)) {
1491
1616
  const method = logicalOp === "or" ? "orWhere" : "where";
1492
- const coerced = this.coerceFilterValue(table, field, opValue);
1617
+ const coerced = this.coerceFilterValue(table, localField, opValue);
1493
1618
  switch (op) {
1494
1619
  case "$eq":
1495
1620
  builder[method](field, coerced);
@@ -1527,9 +1652,10 @@ var SqlDriver = class {
1527
1652
  }
1528
1653
  }
1529
1654
  } else {
1530
- const field = this.mapSortField(key);
1655
+ const localField = this.mapSortField(key);
1656
+ const field = this.remoteColumn(table, key, localField);
1531
1657
  const method = logicalOp === "or" ? "orWhere" : "where";
1532
- builder[method](field, this.coerceFilterValue(table, field, value));
1658
+ builder[method](field, this.coerceFilterValue(table, localField, value));
1533
1659
  }
1534
1660
  }
1535
1661
  }
@@ -1539,6 +1665,28 @@ var SqlDriver = class {
1539
1665
  if (field === "updatedAt") return "updated_at";
1540
1666
  return field;
1541
1667
  }
1668
+ /**
1669
+ * Physical column for a logical field on an external object that declares an
1670
+ * `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
1671
+ * per-site resolution) when the object has no columnMap, so managed objects
1672
+ * and external objects without a columnMap are byte-for-byte unchanged.
1673
+ */
1674
+ remoteColumn(object, field, fallback) {
1675
+ const m = object ? this.fieldColumnByObject[object] : void 0;
1676
+ return m && m[field] || fallback;
1677
+ }
1678
+ /**
1679
+ * Remap a write payload's logical field keys to physical remote columns for an
1680
+ * external object with a columnMap. No-op otherwise. Applied AFTER formatInput
1681
+ * (whose value coercion is keyed by logical field name).
1682
+ */
1683
+ applyWriteColumnMap(object, data) {
1684
+ const m = this.fieldColumnByObject[object];
1685
+ if (!m || !data || typeof data !== "object") return data;
1686
+ const out = {};
1687
+ for (const [k, v] of Object.entries(data)) out[m[k] ?? k] = v;
1688
+ return out;
1689
+ }
1542
1690
  mapAggregateFunc(func) {
1543
1691
  switch (func) {
1544
1692
  case "count":
@@ -1795,6 +1943,15 @@ var SqlDriver = class {
1795
1943
  }
1796
1944
  formatOutput(object, data) {
1797
1945
  if (!data) return data;
1946
+ const colToField = this.columnFieldByObject[object];
1947
+ if (colToField && typeof data === "object") {
1948
+ for (const [remoteCol, localField] of Object.entries(colToField)) {
1949
+ if (remoteCol !== localField && Object.prototype.hasOwnProperty.call(data, remoteCol)) {
1950
+ data[localField] = data[remoteCol];
1951
+ delete data[remoteCol];
1952
+ }
1953
+ }
1954
+ }
1798
1955
  if (this.isSqlite) {
1799
1956
  const jsonFields = this.jsonFields[object];
1800
1957
  if (jsonFields && jsonFields.length > 0) {