@@ -207,6 +207,10 @@ export class SqlDriver implements IDataDriver {
207207 protected physicalTableByObject : Record < string , string > = { } ;
208208 protected physicalSchemaByObject : Record < string , string > = { } ;
209209 protected objectByPhysicalTable : Record < string , string > = { } ;
210+ /** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
211+ protected fieldColumnByObject : Record < string , Record < string , string > > = { } ;
212+ /** External columnMap inverse: physical remote column -> logical field (for read output remap). */
213+ protected columnFieldByObject : Record < string , Record < string , string > > = { } ;
210214 protected tablesWithTimestamps : Set < string > = new Set ( ) ;
211215 /**
212216 * Autonumber field configs per table, captured during initObjects.
@@ -453,7 +457,7 @@ export class SqlDriver implements IDataDriver {
453457 if ( query . orderBy && Array . isArray ( query . orderBy ) ) {
454458 for ( const item of query . orderBy ) {
455459 if ( item . field ) {
456- b . orderBy ( this . mapSortField ( item . field ) , item . order || 'asc' ) ;
460+ b . orderBy ( this . remoteColumn ( object , item . field , this . mapSortField ( item . field ) ) , item . order || 'asc' ) ;
457461 }
458462 }
459463 }
@@ -567,7 +571,7 @@ export class SqlDriver implements IDataDriver {
567571 await this . fillAutoNumberFields ( object , toInsert , options ) ;
568572
569573 const builder = this . getBuilder ( object , options ) ;
570- const formatted = this . formatInput ( object , toInsert ) ;
574+ const formatted = this . applyWriteColumnMap ( object , this . formatInput ( object , toInsert ) ) ;
571575
572576 const result = await builder . insert ( formatted ) . returning ( '*' ) ;
573577 return this . formatOutput ( object , result [ 0 ] ) ;
@@ -881,7 +885,7 @@ export class SqlDriver implements IDataDriver {
881885 this . auditMissingTenant ( object , 'update' , options ) ;
882886 const builder = this . getBuilder ( object , options ) . where ( 'id' , id ) ;
883887 this . applyTenantScope ( builder , object , options ) ;
884- const formatted = this . formatInput ( object , data ) ;
888+ const formatted = this . applyWriteColumnMap ( object , this . formatInput ( object , data ) ) ;
885889
886890 if ( this . tablesWithTimestamps . has ( object ) ) {
887891 if ( this . isSqlite ) {
@@ -914,7 +918,7 @@ export class SqlDriver implements IDataDriver {
914918 this . injectTenantOnInsert ( object , toUpsert , options ) ;
915919 await this . fillAutoNumberFields ( object , toUpsert , options ) ;
916920
917- const formatted = this . formatInput ( object , toUpsert ) ;
921+ const formatted = this . applyWriteColumnMap ( object , this . formatInput ( object , toUpsert ) ) ;
918922 const mergeKeys = conflictKeys && conflictKeys . length > 0 ? conflictKeys : [ 'id' ] ;
919923
920924 const builder = this . getBuilder ( object , options ) ;
@@ -1283,7 +1287,7 @@ export class SqlDriver implements IDataDriver {
12831287 name : string ;
12841288 fields ?: Record < string , any > ;
12851289 tenancy ?: any ;
1286- external ?: { remoteName ?: string ; remoteSchema ?: string } ;
1290+ external ?: { remoteName ?: string ; remoteSchema ?: string ; columnMap ?: Record < string , string > } ;
12871291 } ) : void {
12881292 const key = schema . name ;
12891293 const remoteName = schema . external ?. remoteName || schema . name ;
@@ -1300,6 +1304,23 @@ export class SqlDriver implements IDataDriver {
13001304 }
13011305 }
13021306
1307+ // External columnMap (ADR-0015) is declared as { remoteColumn -> localField }.
1308+ // Keep it for read-output remap, and invert to { localField -> remoteColumn }
1309+ // for WHERE/ORDER BY/write translation. Absent => managed-identical behavior.
1310+ const columnMap = schema . external ?. columnMap ;
1311+ if ( columnMap && typeof columnMap === 'object' && Object . keys ( columnMap ) . length > 0 ) {
1312+ const fieldToCol : Record < string , string > = { } ;
1313+ const colToField : Record < string , string > = { } ;
1314+ for ( const [ remoteCol , localField ] of Object . entries ( columnMap ) ) {
1315+ if ( typeof localField === 'string' && localField ) {
1316+ fieldToCol [ localField ] = remoteCol ;
1317+ colToField [ remoteCol ] = localField ;
1318+ }
1319+ }
1320+ this . fieldColumnByObject [ key ] = fieldToCol ;
1321+ this . columnFieldByObject [ key ] = colToField ;
1322+ }
1323+
13031324 const jsonCols : string [ ] = [ ] ;
13041325 const booleanCols : string [ ] = [ ] ;
13051326 const numericCols : string [ ] = [ ] ;
@@ -1944,7 +1965,7 @@ export class SqlDriver implements IDataDriver {
19441965
19451966 for ( const [ key , value ] of Object . entries ( filters ) ) {
19461967 if ( [ 'limit' , 'offset' , 'fields' , 'orderBy' ] . includes ( key ) ) continue ;
1947- builder . where ( key , this . coerceFilterValue ( table , key , value ) as any ) ;
1968+ builder . where ( this . remoteColumn ( table , key , key ) , this . coerceFilterValue ( table , key , value ) as any ) ;
19481969 }
19491970 return ;
19501971 }
@@ -1965,8 +1986,9 @@ export class SqlDriver implements IDataDriver {
19651986 const isCriterion = typeof fieldRaw === 'string' && typeof op === 'string' ;
19661987
19671988 if ( isCriterion ) {
1968- const field = this . mapSortField ( fieldRaw ) ;
1969- const coerced = this . coerceFilterValue ( table , field , value ) ;
1989+ const localField = this . mapSortField ( fieldRaw ) ;
1990+ const field = this . remoteColumn ( table , fieldRaw , localField ) ;
1991+ const coerced = this . coerceFilterValue ( table , localField , value ) ;
19701992 const apply = ( b : any ) => {
19711993 const method = nextJoin === 'or' ? 'orWhere' : 'where' ;
19721994 const methodIn = nextJoin === 'or' ? 'orWhereIn' : 'whereIn' ;
@@ -2044,10 +2066,11 @@ export class SqlDriver implements IDataDriver {
20442066 }
20452067 } ) ;
20462068 } else if ( typeof value === 'object' && value !== null && ! Array . isArray ( value ) ) {
2047- const field = this . mapSortField ( key ) ;
2069+ const localField = this . mapSortField ( key ) ;
2070+ const field = this . remoteColumn ( table , key , localField ) ;
20482071 for ( const [ op , opValue ] of Object . entries ( value as Record < string , any > ) ) {
20492072 const method = logicalOp === 'or' ? 'orWhere' : 'where' ;
2050- const coerced = this . coerceFilterValue ( table , field , opValue ) ;
2073+ const coerced = this . coerceFilterValue ( table , localField , opValue ) ;
20512074 switch ( op ) {
20522075 case '$eq' :
20532076 ( builder as any ) [ method ] ( field , coerced ) ;
@@ -2085,9 +2108,10 @@ export class SqlDriver implements IDataDriver {
20852108 }
20862109 }
20872110 } else {
2088- const field = this . mapSortField ( key ) ;
2111+ const localField = this . mapSortField ( key ) ;
2112+ const field = this . remoteColumn ( table , key , localField ) ;
20892113 const method = logicalOp === 'or' ? 'orWhere' : 'where' ;
2090- ( builder as any ) [ method ] ( field , this . coerceFilterValue ( table , field , value ) as any ) ;
2114+ ( builder as any ) [ method ] ( field , this . coerceFilterValue ( table , localField , value ) as any ) ;
20912115 }
20922116 }
20932117 }
@@ -2100,6 +2124,30 @@ export class SqlDriver implements IDataDriver {
21002124 return field ;
21012125 }
21022126
2127+ /**
2128+ * Physical column for a logical field on an external object that declares an
2129+ * `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
2130+ * per-site resolution) when the object has no columnMap, so managed objects
2131+ * and external objects without a columnMap are byte-for-byte unchanged.
2132+ */
2133+ protected remoteColumn ( object : string | null | undefined , field : string , fallback : string ) : string {
2134+ const m = object ? this . fieldColumnByObject [ object ] : undefined ;
2135+ return ( m && m [ field ] ) || fallback ;
2136+ }
2137+
2138+ /**
2139+ * Remap a write payload's logical field keys to physical remote columns for an
2140+ * external object with a columnMap. No-op otherwise. Applied AFTER formatInput
2141+ * (whose value coercion is keyed by logical field name).
2142+ */
2143+ protected applyWriteColumnMap ( object : string , data : any ) : any {
2144+ const m = this . fieldColumnByObject [ object ] ;
2145+ if ( ! m || ! data || typeof data !== 'object' ) return data ;
2146+ const out : any = { } ;
2147+ for ( const [ k , v ] of Object . entries ( data ) ) out [ m [ k ] ?? k ] = v ;
2148+ return out ;
2149+ }
2150+
21032151 protected mapAggregateFunc ( func : string ) : string {
21042152 switch ( func ) {
21052153 case 'count' :
@@ -2412,6 +2460,21 @@ export class SqlDriver implements IDataDriver {
24122460 protected formatOutput ( object : string , data : any ) : any {
24132461 if ( ! data ) return data ;
24142462
2463+ // External columnMap (ADR-0015): rename physical remote-column keys to local
2464+ // field names BEFORE coercion (which is keyed by local field). No-op for
2465+ // managed objects and external objects without a columnMap.
2466+ const colToField = this . columnFieldByObject [ object ] ;
2467+ if ( colToField && typeof data === 'object' ) {
2468+ for ( const [ remoteCol , localField ] of Object . entries ( colToField ) ) {
2469+ if ( remoteCol !== localField && Object . prototype . hasOwnProperty . call ( data , remoteCol ) ) {
2470+ // Explicit columnMap wins: the remote column is the source of truth for
2471+ // this local field, even if a same-named native column also exists.
2472+ data [ localField ] = data [ remoteCol ] ;
2473+ delete data [ remoteCol ] ;
2474+ }
2475+ }
2476+ }
2477+
24152478 if ( this . isSqlite ) {
24162479 const jsonFields = this . jsonFields [ object ] ;
24172480 if ( jsonFields && jsonFields . length > 0 ) {
0 commit comments