@@ -96,7 +96,7 @@ function cqn4sql(originalQuery, model) {
9696
9797 // Transform the existing where, prepend table aliases, and so on...
9898 if ( where ) {
99- transformedProp . where = getTransformedTokenStream ( where )
99+ transformedProp . where = getTransformedTokenStream ( where , { prop : 'where' } )
100100 }
101101
102102 // Transform the from clause: association path steps turn into `WHERE EXISTS` subqueries.
@@ -191,7 +191,7 @@ function cqn4sql(originalQuery, model) {
191191
192192 // Like the WHERE clause, aliases from the SELECT list are not accessible for `group by`/`having` (in most DB's)
193193 if ( having ) {
194- transformedQuery . SELECT . having = getTransformedTokenStream ( having )
194+ transformedQuery . SELECT . having = getTransformedTokenStream ( having , { prop : 'having' } )
195195 }
196196
197197 if ( groupBy ) {
@@ -314,7 +314,7 @@ function cqn4sql(originalQuery, model) {
314314 lhs . args . push ( arg )
315315 alreadySeen . set ( nextAssoc . $refLink . alias , true )
316316 if ( nextAssoc . where ) {
317- const filter = getTransformedTokenStream ( nextAssoc . where , nextAssoc . $refLink )
317+ const filter = getTransformedTokenStream ( nextAssoc . where , { $baseLink : nextAssoc . $refLink } )
318318 lhs . on = [
319319 ...( hasLogicalOr ( lhs . on ) ? [ asXpr ( lhs . on ) ] : lhs . on ) ,
320320 'and' ,
@@ -521,14 +521,14 @@ function cqn4sql(originalQuery, model) {
521521 }
522522 }
523523
524- function resolveCalculatedElement ( column , omitAlias = false , baseLink = null ) {
524+ function resolveCalculatedElement ( column , omitAlias = false , $ baseLink = null ) {
525525 let value
526526
527527 if ( column . $refLinks ) {
528528 const { $refLinks } = column
529529 value = $refLinks [ $refLinks . length - 1 ] . definition . value
530530 if ( column . $refLinks . length > 1 ) {
531- baseLink =
531+ $ baseLink =
532532 [ ...$refLinks ] . reverse ( ) . find ( $refLink => $refLink . definition . isAssociation ) ||
533533 // if there is no association in the path, the table alias is the base link
534534 // TA might refer to subquery -> we need to propagate the alias to all paths of the calc element
@@ -541,13 +541,13 @@ function cqn4sql(originalQuery, model) {
541541
542542 let res
543543 if ( ref ) {
544- res = getTransformedTokenStream ( [ value ] , baseLink ) [ 0 ]
544+ res = getTransformedTokenStream ( [ value ] , { $ baseLink } ) [ 0 ]
545545 } else if ( xpr ) {
546- res = { xpr : getTransformedTokenStream ( value . xpr , baseLink ) }
546+ res = { xpr : getTransformedTokenStream ( value . xpr , { $ baseLink } ) }
547547 } else if ( val !== undefined ) {
548548 res = { val }
549549 } else if ( func ) {
550- res = { args : getTransformedFunctionArgs ( value . args , baseLink ) , func : value . func }
550+ res = { args : getTransformedFunctionArgs ( value . args , $ baseLink) , func : value . func }
551551 }
552552 if ( ! omitAlias ) res . as = column . as || column . name || column . flatName
553553 return res
@@ -1018,7 +1018,7 @@ function cqn4sql(originalQuery, model) {
10181018 * the result.
10191019 */
10201020 if ( inOrderBy && flatColumns . length > 1 )
1021- throw new Error ( `" ${ getFullName ( leaf ) } " can't be used in order by as it expands to multiple fields ` )
1021+ throw new Error ( `Structured element “ ${ getFullName ( leaf ) } ” expands to multiple fields and can't be used in order by` )
10221022 flatColumns . forEach ( fc => {
10231023 if ( col . nulls ) fc . nulls = col . nulls
10241024 if ( col . sort ) fc . sort = col . sort
@@ -1182,7 +1182,7 @@ function cqn4sql(originalQuery, model) {
11821182 if ( column . val || column . func || column . SELECT ) return [ column ]
11831183
11841184 const structsAreUnfoldedAlready = model . meta . unfolded ?. includes ( 'structs' )
1185- let { baseName, columnAlias = column . as , tableAlias } = names
1185+ let { baseName, columnAlias = column . as , tableAlias } = names || { }
11861186 const { exclude, replace } = excludeAndReplace || { }
11871187 const { $refLinks, flatName, isJoinRelevant } = column
11881188 let firstNonJoinRelevantAssoc , stepAfterAssoc
@@ -1360,14 +1360,18 @@ function cqn4sql(originalQuery, model) {
13601360 * @param {object[] } tokenStream - The token stream to transform. Each token in the stream is an
13611361 * object representing a CQN construct such as a column, an operator,
13621362 * or a subquery.
1363- * @param {object } [$baseLink=null] - The context in which the `ref`s in the token stream are resolvable.
1364- * It serves as the reference point for resolving associations in
1365- * statements like `{…} WHERE exists assoc[exists anotherAssoc]`.
1366- * Here, the $baseLink for `anotherAssoc` would be `assoc`.
1363+ * @param {object } [context] - Optional context object.
1364+ * @param {object } [context.$baseLink] - The context in which the `ref`s in the token stream are resolvable.
1365+ * It serves as the reference point for resolving associations in
1366+ * statements like `{…} WHERE exists assoc[exists anotherAssoc]`.
1367+ * Here, the $baseLink for `anotherAssoc` would be `assoc`.
1368+ * @param {string } [context.prop] - The query property which holds the token stream which shall be
1369+ * transformed by this function, e.g. "where".
13671370 * @returns {object[] } - The transformed token stream.
13681371 */
1369- function getTransformedTokenStream ( tokenStream , $baseLink = null ) {
1372+ function getTransformedTokenStream ( tokenStream , context = { } ) {
13701373 const transformedTokenStream = [ ]
1374+ const { $baseLink, /* prop */ } = context
13711375 for ( let i = 0 ; i < tokenStream . length ; i ++ ) {
13721376 const token = tokenStream [ i ]
13731377 if ( token === 'exists' ) {
@@ -1453,7 +1457,7 @@ function cqn4sql(originalQuery, model) {
14531457 if ( list . every ( e => e . val ) )
14541458 // no need for transformation
14551459 transformedTokenStream . push ( { list } )
1456- else transformedTokenStream . push ( { list : getTransformedTokenStream ( list , $baseLink ) } )
1460+ else transformedTokenStream . push ( { list : getTransformedTokenStream ( list , { $baseLink, prop : 'list' } ) } )
14571461 }
14581462 } else if ( tokenStream . length === 1 && token . val && $baseLink ) {
14591463 // infix filter - OData variant w/o mentioning key --> flatten out and compare each leaf to token.val
@@ -1509,13 +1513,13 @@ function cqn4sql(originalQuery, model) {
15091513 i = indexRhs // jump to next relevant index
15101514 } else {
15111515 // reject associations in expression, except if we are in an infix filter -> $baseLink is set
1512- assertNoStructInXpr ( token , $baseLink )
1516+ assertNoStructInXpr ( token , context )
15131517 // reject virtual elements in expressions as they will lead to a sql error down the line
15141518 if ( lhsDef ?. virtual ) throw new Error ( `Virtual elements are not allowed in expressions` )
15151519
15161520 let result = is_regexp ( token ?. val ) ? token : copy ( token ) // REVISIT: too expensive! //
15171521 if ( token . ref ) {
1518- const { definition } = token . $refLinks [ token . $refLinks . length - 1 ]
1522+ const { definition } = token . $refLinks . at ( - 1 )
15191523 // Add definition to result
15201524 setElementOnColumns ( result , definition )
15211525 if ( isCalculatedOnRead ( definition ) ) {
@@ -1536,7 +1540,15 @@ function cqn4sql(originalQuery, model) {
15361540 const lastAssoc =
15371541 token . isJoinRelevant && [ ...token . $refLinks ] . reverse ( ) . find ( l => l . definition . isAssociation )
15381542 const tableAlias = getTableAlias ( token , ( ! lastAssoc ?. onlyForeignKeyAccess && lastAssoc ) || $baseLink )
1539- if ( ( ! $baseLink || lastAssoc ) && token . isJoinRelevant ) {
1543+ if ( isAssocOrStruct ( definition ) ) {
1544+ const flat = getFlatColumnsFor ( token , { tableAlias : $baseLink ?. alias || tableAlias } )
1545+ if ( flat . length === 0 )
1546+ throw new Error ( `Structured element “${ getFullName ( definition ) } ” expands to nothing and can't be used in expressions` )
1547+ else if ( flat . length > 1 && context . prop !== 'list' ) // only acceptable in `list`
1548+ throw new Error ( `Structured element “${ getFullName ( definition ) } ” expands to multiple fields and can't be used in expressions` )
1549+ transformedTokenStream . push ( ...flat )
1550+ continue
1551+ } else if ( ( ! $baseLink || lastAssoc ) && token . isJoinRelevant ) {
15401552 let name = calculateElementName ( token )
15411553 result . ref = [ tableAlias , name ]
15421554 } else if ( tableAlias ) {
@@ -1549,7 +1561,7 @@ function cqn4sql(originalQuery, model) {
15491561 result = transformSubquery ( token )
15501562 } else {
15511563 if ( token . xpr ) {
1552- result . xpr = getTransformedTokenStream ( token . xpr , $baseLink )
1564+ result . xpr = getTransformedTokenStream ( token . xpr , { $baseLink } )
15531565 }
15541566 if ( token . func && token . args ) {
15551567 result . args = getTransformedFunctionArgs ( token . args , $baseLink )
@@ -1661,11 +1673,15 @@ function cqn4sql(originalQuery, model) {
16611673 }
16621674 }
16631675
1664- function assertNoStructInXpr ( token , inInfixFilter = false ) {
1665- if ( ! inInfixFilter && token . $refLinks ?. [ token . $refLinks . length - 1 ] . definition . target )
1666- // REVISIT: let this through if not requested otherwise
1676+ function assertNoStructInXpr ( token , context ) {
1677+ const definition = token . $refLinks ?. at ( - 1 ) . definition
1678+ if ( ! definition ) return
1679+ const rejectStructs = context && ( context . prop in { where : 1 , having : 1 } )
1680+ // unmanaged is always forbidden
1681+ // expanding a ref in a `where`/`having` context
1682+ if ( ( rejectStructs && definition ?. target ) || definition ?. on )
16671683 rejectAssocInExpression ( )
1668- if ( isStructured ( token . $refLinks ?. [ token . $refLinks . length - 1 ] . definition ) )
1684+ if ( rejectStructs && isStructured ( definition ) )
16691685 // REVISIT: let this through if not requested otherwise
16701686 rejectStructInExpression ( )
16711687
@@ -1771,7 +1787,7 @@ function cqn4sql(originalQuery, model) {
17711787
17721788 // only append infix filter to outer where if it is the leaf of the from ref
17731789 if ( refReverse [ 0 ] . where )
1774- filterConditions . push ( getTransformedTokenStream ( refReverse [ 0 ] . where , $ refLinksReverse[ 0 ] ) )
1790+ filterConditions . push ( getTransformedTokenStream ( refReverse [ 0 ] . where , { $baseLink : $ refLinksReverse[ 0 ] } ) )
17751791
17761792 if ( existingWhere . length > 0 ) filterConditions . push ( existingWhere )
17771793 if ( whereExistsSubSelects . length > 0 ) {
@@ -2209,7 +2225,7 @@ function cqn4sql(originalQuery, model) {
22092225 }
22102226
22112227 if ( customWhere ) {
2212- const filter = getTransformedTokenStream ( customWhere , next )
2228+ const filter = getTransformedTokenStream ( customWhere , { $baseLink : next } )
22132229 const wrappedFilter = hasLogicalOr ( filter ) ? [ asXpr ( filter ) ] : filter
22142230 on . push ( 'and' , ...wrappedFilter )
22152231 }
@@ -2315,10 +2331,10 @@ function cqn4sql(originalQuery, model) {
23152331 function getTransformedFunctionArgs ( args , $baseLink = null ) {
23162332 let result = null
23172333 if ( Array . isArray ( args ) ) {
2318- result = args . map ( t => {
2334+ result = args . flatMap ( t => {
23192335 if ( ! t . val )
23202336 // this must not be touched
2321- return getTransformedTokenStream ( [ t ] , $baseLink ) [ 0 ]
2337+ return getTransformedTokenStream ( [ t ] , { $baseLink } )
23222338 return t
23232339 } )
23242340 } else if ( typeof args === 'object' ) {
@@ -2327,7 +2343,7 @@ function cqn4sql(originalQuery, model) {
23272343 const t = args [ prop ]
23282344 if ( ! t . val )
23292345 // this must not be touched
2330- result [ prop ] = getTransformedTokenStream ( [ t ] , $baseLink ) [ 0 ]
2346+ result [ prop ] = getTransformedTokenStream ( [ t ] , { $baseLink } ) [ 0 ]
23312347 else result [ prop ] = t
23322348 }
23332349 }
0 commit comments