11const cds = require ( '@sap/cds' )
22const cds_infer = require ( './infer' )
33const cqn4sql = require ( './cqn4sql' )
4+
45const _simple_queries = cds . env . features . sql_simple_queries
56const _strict_booleans = _simple_queries < 2
67
@@ -26,7 +27,8 @@ class CQN2SQLRenderer {
2627 if ( cds . env . sql . names === 'quoted' ) {
2728 this . class . prototype . name = ( name , query ) => {
2829 const e = name . id || name
29- return ( query ?. _target || this . model ?. definitions [ e ] ) ?. [ '@cds.persistence.name' ] || e
30+ const entity = query ?. _target || this . model ?. definitions [ e ]
31+ return ( ! entity ?. [ '@cds.persistence.skip' ] && entity ?. [ '@cds.persistence.name' ] ) || e
3032 }
3133 this . class . prototype . quote = ( s ) => `"${ String ( s ) . replace ( / " / g, '""' ) } "`
3234 }
@@ -76,6 +78,7 @@ class CQN2SQLRenderer {
7678 */
7779 render ( q , vars ) {
7880 const kind = q . kind || Object . keys ( q ) [ 0 ] // SELECT, INSERT, ...
81+ if ( q . _with ) this . _with = q . _with
7982 /**
8083 * @type {string } the rendered SQL string
8184 */
@@ -90,7 +93,6 @@ class CQN2SQLRenderer {
9093 if ( vars && Object . keys ( vars ) . length && ! this . values ?. length ) this . values = vars
9194 const sanitize_values = process . env . NODE_ENV === 'production' && cds . env . log . sanitize_values !== false
9295
93-
9496 if ( DEBUG && ( LOG_SQL . _debug || LOG_SQLITE . _debug ) ) {
9597 let values = sanitize_values && ( this . entries || this . values ?. length > 0 ) ? [ '***' ] : this . entries || this . values || [ ]
9698 if ( values && ! Array . isArray ( values ) ) {
@@ -257,13 +259,15 @@ class CQN2SQLRenderer {
257259
258260 // REVISIT: When selecting from an entity that is not in the model the from.where are not normalized (as cqn4sql is skipped)
259261 if ( ! where && from ?. ref ?. length === 1 && from . ref [ 0 ] ?. where ) where = from . ref [ 0 ] ?. where
260- const columns = this . SELECT_columns ( q )
262+
261263 let sql = `SELECT`
262264 if ( distinct ) sql += ` DISTINCT`
263- if ( ! _empty ( columns ) ) sql += ` ${ columns } `
264- if ( recurse ) sql += ` FROM ${ this . SELECT_recurse ( q ) } `
265- else if ( ! _empty ( from ) ) sql += ` FROM ${ this . from ( from , q ) } `
266- else sql += this . from_dummy ( )
265+ if ( recurse ) sql += this . SELECT_recurse ( q )
266+ else {
267+ sql += ` ${ this . SELECT_columns ( q ) } `
268+ if ( ! _empty ( from ) ) sql += ` FROM ${ this . from ( from , q ) } `
269+ else sql += this . from_dummy ( )
270+ }
267271 if ( ! recurse && ! _empty ( where ) ) sql += ` WHERE ${ this . where ( where ) } `
268272 if ( ! recurse && ! _empty ( groupBy ) ) sql += ` GROUP BY ${ this . groupBy ( groupBy ) } `
269273 if ( ! recurse && ! _empty ( having ) ) sql += ` HAVING ${ this . having ( having ) } `
@@ -347,10 +351,16 @@ class CQN2SQLRenderer {
347351 for ( const name in target . elements ) {
348352 const ref = { ref : [ name ] }
349353 const element = target . elements [ name ]
350- if ( element . virtual || element . value || element . isAssociation ) continue
351- if ( element [ '@Core.Computed' ] && name in availableComputedColumns ) continue
354+ if ( element . virtual || element . isAssociation ) continue
355+ if ( name in availableComputedColumns ) continue
352356 if ( name . toUpperCase ( ) in reservedColumnNames ) ref . as = `$$${ name } $$`
353- columnsIn . push ( ref )
357+ // This only supports calculated elements within the scope of the own entity
358+ if ( 'value' in element ) {
359+ const requested = columnsFiltered . find ( c => this . column_name ( c ) === element . name )
360+ if ( requested ) columnsIn . push ( requested )
361+ else continue
362+ }
363+ else columnsIn . push ( ref )
354364 const foreignkey4 = element . _foreignKey4
355365 if (
356366 from . args ||
@@ -497,13 +507,19 @@ class CQN2SQLRenderer {
497507 }
498508 }
499509
510+ const columnsQuery = cds . ql ( q ) . clone ( )
511+ columnsQuery . SELECT . columns = columns . map ( x => {
512+ if ( x . element && 'value' in x . element ) return { element : x . element , ref : [ this . column_name ( x ) ] }
513+ return x
514+ } )
515+ const recurseColumns = this . SELECT_columns ( columnsQuery )
500516 // Only apply result join if the columns contain a references which doesn't start with the source alias
501517 if ( from . args && columns . find ( c => c . ref ?. [ 0 ] === alias ) ) {
502518 graph . as = alias
503- return this . from ( setStableFrom ( from , graph ) )
519+ return ` ${ recurseColumns } FROM ${ this . from ( setStableFrom ( from , graph ) ) } `
504520 }
505521
506- return `(${ this . SELECT ( graph ) } )${ alias ? ` AS ${ this . quote ( alias ) } ` : '' } `
522+ return ` ${ recurseColumns } FROM (${ this . SELECT ( graph ) } )${ alias ? ` AS ${ this . quote ( alias ) } ` : '' } `
507523
508524 function collectDistanceTo ( where , innot = false ) {
509525 for ( let i = 0 ; i < where . length ; i ++ ) {
@@ -730,6 +746,38 @@ class CQN2SQLRenderer {
730746 return this . xpr ( { xpr } )
731747 }
732748
749+ /**
750+ * Renders a transformed where clause that maps the query target view to the source table
751+ * @param {import('./infer/cqn').source } alias
752+ * @param {import('./infer/cqn').predicate } where
753+ * @param {import('./infer/cqn').query } q
754+ * @returns SQL
755+ */
756+ where_resolved ( alias , where , q ) {
757+ const transitions = this . srv . resolve . transitions4db ( q )
758+ if ( transitions . target === transitions . queryTarget ) return this . where ( where )
759+
760+ // view and table column refs to be matched
761+ const viewCols = [ ]
762+ const tableCols = [ ]
763+
764+ // Only match key columns when possible
765+ const elements = q . _target . keys || q . _target . elements
766+ for ( const c in elements ) {
767+ if (
768+ c in elements
769+ && transitions . mapping . has ( c )
770+ && this . physical_column ( elements , c )
771+ ) {
772+ viewCols . push ( { ref : [ c ] } )
773+ tableCols . push ( transitions . mapping . get ( c ) )
774+ }
775+ }
776+ return tableCols . length > 0
777+ ? this . where ( [ { list : tableCols } , 'in' , SELECT . from ( q . _target ) . alias ( alias ) . columns ( viewCols ) . where ( where ) ] )
778+ : this . where ( where )
779+ }
780+
733781 /**
734782 * Renders a HAVING clause into generic SQL
735783 * @param {import('./infer/cqn').predicate } xpr
@@ -835,15 +883,20 @@ class CQN2SQLRenderer {
835883 if ( ! elements && ! INSERT . entries ?. length ) {
836884 return // REVISIT: mtx sends an insert statement without entries and no reference entity
837885 }
886+ const transitions = this . srv . resolve . transitions4db ( q )
838887 const columns = elements
839- ? ObjectKeys ( elements ) . filter ( c => c in elements && ! elements [ c ] . virtual && ! elements [ c ] . value && ! elements [ c ] . isAssociation )
888+ ? ObjectKeys ( elements ) . filter ( c => this . physical_column ( elements , c )
889+ && ( c = transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c )
890+ && c in transitions . target . elements
891+ && this . physical_column ( transitions . target . elements , c )
892+ )
840893 : ObjectKeys ( INSERT . entries [ 0 ] )
841894
842895 /** @type {string[] } */
843896 this . columns = columns
844897
845898 const alias = INSERT . into . as
846- const entity = this . name ( q . _target ?. name || INSERT . into . ref [ 0 ] , q )
899+ const entity = q . _target ? this . table_name ( q ) : INSERT . into . ref [ 0 ]
847900 if ( ! elements ) {
848901 this . entries = INSERT . entries . map ( e => columns . map ( c => e [ c ] ) )
849902 const param = this . param . bind ( this , { ref : [ '?' ] } )
@@ -865,8 +918,8 @@ class CQN2SQLRenderer {
865918 }
866919
867920 const extractions = this . _managed = this . managed ( columns . map ( c => ( { name : c } ) ) , elements )
868- return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ this . columns . map ( c => this . quote ( c ) )
869- } ) SELECT ${ extractions . map ( c => c . insert ) } FROM json_each(?)`)
921+ return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ this . columns . map ( c => this . quote ( transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c ) )
922+ } ) SELECT ${ extractions . slice ( 0 , columns . length ) . map ( c => c . insert ) } FROM json_each(?)`)
870923 }
871924
872925 async * INSERT_entries_stream ( entries , binaryEncoding = 'base64' ) {
@@ -972,7 +1025,7 @@ class CQN2SQLRenderer {
9721025 */
9731026 INSERT_rows ( q ) {
9741027 const { INSERT } = q
975- const entity = this . name ( q . _target ?. name || INSERT . into . ref [ 0 ] , q )
1028+ const entity = q . _target ? this . table_name ( q ) : INSERT . into . ref [ 0 ]
9761029 const alias = INSERT . into . as
9771030 const elements = q . elements || q . _target ?. elements
9781031 const columns = this . columns = INSERT . columns || cds . error `Cannot insert rows without columns or elements`
@@ -997,7 +1050,8 @@ class CQN2SQLRenderer {
9971050 . slice ( 0 , columns . length )
9981051 . map ( c => c . converter ( c . extract ) )
9991052
1000- return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ this . columns . map ( c => this . quote ( c ) )
1053+ const transitions = this . srv . resolve . transitions4db ( q )
1054+ return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ this . columns . map ( c => this . quote ( transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c ) )
10011055 } ) SELECT ${ extraction } FROM json_each(?)`)
10021056 }
10031057
@@ -1018,20 +1072,24 @@ class CQN2SQLRenderer {
10181072 */
10191073 INSERT_select ( q ) {
10201074 const { INSERT } = q
1021- const entity = this . name ( q . _target . name , q )
1075+ const entity = q . _target ? this . table_name ( q ) : INSERT . into . ref [ 0 ]
10221076 const alias = INSERT . into . as
1077+ const src = this . cqn4sql ( INSERT . from )
10231078 const elements = q . elements || q . _target ?. elements || { }
1024- let columns = ( this . columns = ( INSERT . columns || ObjectKeys ( elements ) ) . filter (
1025- c => c in elements && ! elements [ c ] . virtual && ! elements [ c ] . isAssociation ,
1026- ) )
1079+ const transitions = this . srv . resolve . transitions4db ( q , this . srv )
1080+ let columns = ( this . columns = ( INSERT . columns || src . SELECT . columns ?. map ( c => this . column_name ( c ) ) || ObjectKeys ( src . elements ) || ObjectKeys ( elements ) )
1081+ . filter ( c => this . physical_column ( elements , c )
1082+ && ( c = transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c )
1083+ && c in transitions . target . elements
1084+ && this . physical_column ( transitions . target . elements , c )
1085+ ) )
10271086
1028- const src = this . cqn4sql ( INSERT . from )
10291087 const extractions = this . _managed = this . managed ( columns . map ( c => ( { name : c , sql : `NEW.${ this . quote ( c ) } ` } ) ) , elements )
10301088 const sql = extractions . length > columns . length
10311089 ? `SELECT ${ extractions . map ( c => `${ c . insert } AS ${ this . quote ( c . name ) } ` ) } FROM (${ this . SELECT ( src ) } ) AS NEW`
10321090 : this . SELECT ( src )
10331091 if ( extractions . length > columns . length ) columns = this . columns = extractions . map ( c => c . name )
1034- this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ columns . map ( c => this . quote ( c ) ) } ) ${ sql } `
1092+ this . sql = `INSERT INTO ${ this . quote ( entity ) } ${ alias ? ' as ' + this . quote ( alias ) : '' } (${ columns . map ( c => this . quote ( transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c ) ) } ) ${ sql } `
10351093 this . entries = [ this . values ]
10361094 return this . sql
10371095 }
@@ -1085,7 +1143,7 @@ class CQN2SQLRenderer {
10851143 . join ( ' AND ' )
10861144
10871145 let columns = this . columns // this.columns is computed as part of this.INSERT
1088- const entity = this . name ( q . _target ?. name || UPSERT . into . ref [ 0 ] , q )
1146+ const entity = q . _target ? this . table_name ( q ) : this . name ( UPSERT . into . ref [ 0 ] , q )
10891147 if ( UPSERT . entries || UPSERT . rows || UPSERT . values ) {
10901148 const managed = this . _managed . slice ( 0 , columns . length )
10911149
@@ -1121,7 +1179,8 @@ class CQN2SQLRenderer {
11211179 else return true
11221180 } ) . map ( c => `${ this . quote ( c ) } = excluded.${ this . quote ( c ) } ` )
11231181
1124- return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } (${ columns . map ( c => this . quote ( c ) ) } ) ${ sql
1182+ const transitions = this . srv . resolve . transitions4db ( q )
1183+ return ( this . sql = `INSERT INTO ${ this . quote ( entity ) } (${ columns . map ( c => this . quote ( transitions . mapping . get ( c ) ?. ref ?. [ 0 ] || c ) ) } ) ${ sql
11251184 } WHERE TRUE ON CONFLICT(${ keys . map ( c => this . quote ( c ) ) } ) DO ${ updateColumns . length ? `UPDATE SET ${ updateColumns } ` : 'NOTHING' } `)
11261185 }
11271186
@@ -1134,29 +1193,36 @@ class CQN2SQLRenderer {
11341193 */
11351194 UPDATE ( q ) {
11361195 const { entity, with : _with , data, where } = q . UPDATE
1196+ const transitions = this . srv . resolve . transitions4db ( q )
11371197 const elements = q . _target ?. elements
1138- let sql = `UPDATE ${ this . quote ( this . name ( entity . ref ?. [ 0 ] || entity , q ) ) } `
1198+ let sql = `UPDATE ${ this . quote ( this . table_name ( q ) ) } `
11391199 if ( entity . as ) sql += ` AS ${ this . quote ( entity . as ) } `
11401200
1141- let columns = [ ]
1142- if ( data ) _add ( data , val => this . val ( { val } ) )
1143- if ( _with ) _add ( _with , x => this . expr ( x ) )
1144- function _add ( data , sql4 ) {
1145- for ( let c in data ) {
1146- const columnExistsInDatabase =
1147- elements && c in elements && ! elements [ c ] . virtual && ! elements [ c ] . isAssociation && ! elements [ c ] . value
1201+ const _add = ( data , sql4 ) => {
1202+ for ( let col in data ) {
1203+ const c = transitions . mapping . get ( col ) ?. ref ?. [ 0 ] || col
1204+ const columnExistsInDatabase = elements
1205+ && this . physical_column ( elements , col )
1206+ && c in transitions . target . elements
1207+ && this . physical_column ( transitions . target . elements , c )
11481208 if ( ! elements || columnExistsInDatabase ) {
1149- columns . push ( { name : c , sql : sql4 ( data [ c ] ) } )
1209+ columns . push ( { name : c , sql : sql4 ( data [ col ] , col ) } )
11501210 }
11511211 }
11521212 }
11531213
1214+ let columns = [ ]
1215+ if ( data ) _add ( data , val => this . val ( { val } ) )
1216+ if ( _with ) _add ( _with , x => this . expr ( x ) )
1217+
11541218 const extraction = this . managed ( columns , elements )
1155- . filter ( ( c , i ) => columns [ i ] || c . onUpdate )
1156- . map ( ( c , i ) => `${ this . quote ( c . name ) } =${ ! columns [ i ] ? c . onUpdate : c . sql } ` )
1219+ . filter ( ( c , i ) => {
1220+ if ( transitions . mapping . get ( c . name ) ?. ref ?. length > 1 ) return false
1221+ return columns [ i ] || c . onUpdate
1222+ } ) . map ( ( c , i ) => `${ this . quote ( transitions . mapping . get ( c . name ) ?. ref ?. [ 0 ] || c . name ) } =${ ! columns [ i ] ? c . onUpdate : c . sql } ` )
11571223
11581224 sql += ` SET ${ extraction } `
1159- if ( where ) sql += ` WHERE ${ this . where ( where ) } `
1225+ if ( where ) sql += ` WHERE ${ this . where_resolved ( entity . as , where , q ) } `
11601226 return ( this . sql = sql )
11611227 }
11621228
@@ -1168,8 +1234,9 @@ class CQN2SQLRenderer {
11681234 * @returns {string } SQL
11691235 */
11701236 DELETE ( q ) {
1171- const { DELETE : { from, where } } = q
1172- let sql = `DELETE FROM ${ this . from ( from , q ) } `
1237+ const { DELETE : { where, from } } = q
1238+ let sql = `DELETE FROM ${ this . quote ( this . table_name ( q ) ) } `
1239+ if ( from . as ) sql += ` AS ${ this . quote ( from . as ) } `
11731240 if ( where ) sql += ` WHERE ${ this . where ( where ) } `
11741241 return ( this . sql = sql )
11751242 }
@@ -1382,6 +1449,16 @@ class CQN2SQLRenderer {
13821449 return ( typeof col . as === 'string' && col . as ) || ( 'val' in col && col . val + '' ) || col . func || col . ref . at ( - 1 )
13831450 }
13841451
1452+ /**
1453+ * Calculates the Database table name of the query target
1454+ * @param {import('./infer/cqn').Query } query
1455+ * @returns {string } Database table name
1456+ */
1457+ table_name ( q ) {
1458+ const table = cds . db . resolve . table ( q . _target )
1459+ return this . name ( table . name , { _target : table } )
1460+ }
1461+
13851462 /**
13861463 * Calculates the Database name of the given name
13871464 * @param {string|import('./infer/cqn').ref } name
@@ -1489,6 +1566,10 @@ class CQN2SQLRenderer {
14891566 } )
14901567 }
14911568
1569+ physical_column ( elements , c ) {
1570+ return elements [ c ] && ! elements [ c ] . virtual && ! elements [ c ] . value && ! elements [ c ] . isAssociation
1571+ }
1572+
14921573 managed_extract ( name , element , converter ) {
14931574 const { UPSERT , INSERT } = this . cqn
14941575 const extract = ! ( INSERT ?. entries || UPSERT ?. entries ) && ( INSERT ?. rows || UPSERT ?. rows )
0 commit comments