@@ -110,23 +110,32 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
110110 fetchOne < GenericResult = DefaultReturnObject > (
111111 params : SelectOne
112112 ) : QueryWithExtra < GenericResultWrapper , OneResult < GenericResultWrapper , GenericResult > , IsAsync > {
113+ const queryArgs : any [ ] = [ ]
114+ const countQueryArgs : any [ ] = [ ] // Separate args for count query
115+
116+ // Ensure subQueryPlaceholders are passed to the count query as well, if they exist on params
117+ const selectParamsForCount : SelectAll = {
118+ ...params ,
119+ fields : 'count(*) as total' ,
120+ offset : undefined ,
121+ groupBy : undefined ,
122+ limit : 1 ,
123+ }
124+ if ( params . subQueryPlaceholders ) {
125+ selectParamsForCount . subQueryPlaceholders = params . subQueryPlaceholders ;
126+ }
127+
128+
129+ const mainSql = this . _select ( { ...params , limit : 1 } as SelectAll , queryArgs )
130+ const countSql = this . _select ( selectParamsForCount , countQueryArgs )
131+
113132 return new QueryWithExtra (
114133 ( q ) => {
115134 return this . execute ( q )
116135 } ,
117- this . _select ( { ...params , limit : 1 } ) ,
118- this . _select ( {
119- ...params ,
120- fields : 'count(*) as total' ,
121- offset : undefined ,
122- groupBy : undefined ,
123- limit : 1 ,
124- } ) ,
125- typeof params . where === 'object' && ! Array . isArray ( params . where ) && params . where ?. params
126- ? Array . isArray ( params . where ?. params )
127- ? params . where ?. params
128- : [ params . where ?. params ]
129- : undefined ,
136+ mainSql ,
137+ countSql ,
138+ queryArgs , // Use the populated queryArgs from the main _select call
130139 FetchTypes . ONE
131140 )
132141 }
@@ -138,6 +147,27 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
138147 ArrayResult < GenericResultWrapper , GenericResult , IsAsync , P extends { lazy : true } ? true : false > ,
139148 IsAsync
140149 > {
150+ const queryArgs : any [ ] = [ ]
151+ const countQueryArgs : any [ ] = [ ] // Separate args for count query
152+
153+ const mainQueryParams = { ...params , lazy : undefined }
154+
155+ // Ensure subQueryPlaceholders are passed to the count query as well
156+ const countQueryParams : SelectAll = {
157+ ...params ,
158+ fields : 'count(*) as total' ,
159+ offset : undefined ,
160+ groupBy : undefined ,
161+ limit : 1 ,
162+ lazy : undefined ,
163+ }
164+ if ( params . subQueryPlaceholders ) {
165+ countQueryParams . subQueryPlaceholders = params . subQueryPlaceholders ;
166+ }
167+
168+ const mainSql = this . _select ( mainQueryParams , queryArgs )
169+ const countSql = this . _select ( countQueryParams , countQueryArgs )
170+
141171 return new QueryWithExtra (
142172 ( q ) => {
143173 return params . lazy
@@ -147,20 +177,9 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
147177 > )
148178 : this . execute ( q )
149179 } ,
150- this . _select ( { ...params , lazy : undefined } ) ,
151- this . _select ( {
152- ...params ,
153- fields : 'count(*) as total' ,
154- offset : undefined ,
155- groupBy : undefined ,
156- limit : 1 ,
157- lazy : undefined ,
158- } ) ,
159- typeof params . where === 'object' && ! Array . isArray ( params . where ) && params . where ?. params
160- ? Array . isArray ( params . where ?. params )
161- ? params . where ?. params
162- : [ params . where ?. params ]
163- : undefined ,
180+ mainSql ,
181+ countSql ,
182+ queryArgs , // Use the populated queryArgs from the main _select call
164183 FetchTypes . ALL
165184 )
166185 }
@@ -415,14 +434,28 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
415434 )
416435 }
417436
418- protected _select ( params : SelectAll ) : string {
437+ protected _select ( params : SelectAll , queryArgs ?: any [ ] ) : string {
438+ const isTopLevelCall = queryArgs === undefined
439+ if ( isTopLevelCall ) {
440+ queryArgs = [ ]
441+ }
442+
443+ // This assertion tells TypeScript that queryArgs is definitely assigned after the block above.
444+ const currentQueryArgs = queryArgs !
445+
446+ const context = {
447+ subQueryPlaceholders : params . subQueryPlaceholders ,
448+ queryArgs : currentQueryArgs ,
449+ toSQLCompiler : this . _select . bind ( this ) ,
450+ }
451+
419452 return (
420453 `SELECT ${ this . _fields ( params . fields ) }
421454 FROM ${ params . tableName } ` +
422- this . _join ( params . join ) +
423- this . _where ( params . where ) +
455+ this . _join ( params . join , context ) +
456+ this . _where ( params . where , context ) +
424457 this . _groupBy ( params . groupBy ) +
425- this . _having ( params . having ) +
458+ this . _having ( params . having , context ) +
426459 this . _orderBy ( params . orderBy ) +
427460 this . _limit ( params . limit ) +
428461 this . _offset ( params . offset )
@@ -436,40 +469,109 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
436469 return value . join ( ', ' )
437470 }
438471
439- protected _where ( value ?: Where ) : string {
472+ protected _where (
473+ value : Where | undefined ,
474+ context : {
475+ subQueryPlaceholders ?: Record < string , SelectAll >
476+ queryArgs : any [ ]
477+ // Allow toSQLCompiler to be undefined for calls not originating from _select, though practically it should always be provided.
478+ toSQLCompiler ?: ( params : SelectAll , queryArgs : any [ ] ) => string
479+ }
480+ ) : string {
440481 if ( ! value ) return ''
441- let conditions = value
482+
483+ let conditionStrings : string [ ]
484+ let primitiveParams : any [ ] = [ ]
442485
443486 if ( typeof value === 'object' && ! Array . isArray ( value ) ) {
444- conditions = value . conditions
487+ conditionStrings = Array . isArray ( value . conditions ) ? value . conditions : [ value . conditions ]
488+ if ( value . params ) {
489+ primitiveParams = Array . isArray ( value . params ) ? value . params : [ value . params ]
490+ }
491+ } else if ( Array . isArray ( value ) ) {
492+ conditionStrings = value
493+ } else {
494+ // Assuming value is a single string condition
495+ conditionStrings = [ value as string ]
445496 }
446497
447- if ( typeof conditions === 'string' ) return ` WHERE ${ conditions . toString ( ) } `
448-
449- if ( ( conditions as Array < string > ) . length === 1 ) return ` WHERE ${ ( conditions as Array < string > ) [ 0 ] ! . toString ( ) } `
498+ if ( conditionStrings . length === 0 ) return ''
499+
500+ let primitiveParamIndex = 0
501+ const processedConditions : string [ ] = [ ]
502+
503+ for ( const conditionStr of conditionStrings ) {
504+ // Regex to split by token or by '?'
505+ const parts = conditionStr . split ( / ( _ _ S U B Q U E R Y _ T O K E N _ \d + _ _ | \? ) / g) . filter ( Boolean )
506+ let builtCondition = ''
507+
508+ for ( const part of parts ) {
509+ if ( part === '?' ) {
510+ if ( primitiveParamIndex >= primitiveParams . length ) {
511+ throw new Error ( 'SQL generation error: Not enough primitive parameters for "?" placeholders in WHERE clause.' )
512+ }
513+ context . queryArgs . push ( primitiveParams [ primitiveParamIndex ++ ] )
514+ builtCondition += '?'
515+ } else if ( part . startsWith ( '__SUBQUERY_TOKEN_' ) && part . endsWith ( '__' ) ) {
516+ if ( ! context . subQueryPlaceholders || ! context . toSQLCompiler ) {
517+ throw new Error ( 'SQL generation error: Subquery context not provided for token processing.' )
518+ }
519+ const subQueryParams = context . subQueryPlaceholders [ part ]
520+ if ( ! subQueryParams ) {
521+ throw new Error ( `SQL generation error: Subquery token ${ part } not found in placeholders.` )
522+ }
523+ builtCondition += context . toSQLCompiler ( subQueryParams , context . queryArgs )
524+ } else {
525+ builtCondition += part
526+ }
527+ }
528+ // Wrap each individual condition processed this way, as SelectBuilder.where() might send multiple conditions.
529+ // The original logic for multiple conditions was: `WHERE (${(conditions as Array<string>).join(') AND (')})`
530+ // So, we wrap each one and then join by AND.
531+ processedConditions . push ( `(${ builtCondition } )` )
532+ }
450533
451- if ( ( conditions as Array < string > ) . length > 1 ) {
452- return ` WHERE ( ${ ( conditions as Array < string > ) . join ( ') AND (' ) } )`
534+ if ( primitiveParamIndex < primitiveParams . length && primitiveParams . length > 0 ) { // Check primitiveParams.length to avoid error if no params were expected
535+ throw new Error ( 'SQL generation error: Too many primitive parameters provided for "?" placeholders in WHERE clause.' )
453536 }
454537
455- return ''
538+ if ( processedConditions . length === 0 ) return ''
539+ return ` WHERE ${ processedConditions . join ( ' AND ' ) } `
456540 }
457541
458- protected _join ( value ?: Join | Array < Join > ) : string {
542+ protected _join (
543+ value : Join | Array < Join > | undefined ,
544+ context : {
545+ // subQueryPlaceholders are not directly used by _join for its own structure,
546+ // but toSQLCompiler will need them if item.table is a SelectAll object
547+ // that itself has subQueryPlaceholders.
548+ subQueryPlaceholders ?: Record < string , SelectAll >
549+ queryArgs : any [ ]
550+ toSQLCompiler : ( params : SelectAll , queryArgs : any [ ] ) => string
551+ }
552+ ) : string {
459553 if ( ! value ) return ''
460554
555+ let joinArray : Join [ ]
461556 if ( ! Array . isArray ( value ) ) {
462- value = [ value ]
557+ joinArray = [ value ]
558+ } else {
559+ joinArray = value
463560 }
464561
465562 const joinQuery : Array < string > = [ ]
466- value . forEach ( ( item : Join ) => {
563+ joinArray . forEach ( ( item : Join ) => {
467564 const type = item . type ? `${ item . type } ` : ''
468- joinQuery . push (
469- `${ type } JOIN ${ typeof item . table === 'string' ? item . table : `(${ this . _select ( item . table ) } )` } ${
470- item . alias ? ` AS ${ item . alias } ` : ''
471- } ON ${ item . on } `
472- )
565+ let tableSql : string
566+ if ( typeof item . table === 'string' ) {
567+ tableSql = item . table
568+ } else {
569+ // Subquery in JOIN. item.table is SelectAll in this case.
570+ // The toSQLCompiler (this._select) will handle any '?' or tokens within this subquery,
571+ // and push its arguments to context.queryArgs.
572+ tableSql = `(${ context . toSQLCompiler ( item . table , context . queryArgs ) } )`
573+ }
574+ joinQuery . push ( `${ type } JOIN ${ tableSql } ${ item . alias ? ` AS ${ item . alias } ` : '' } ON ${ item . on } ` )
473575 } )
474576
475577 return ' ' + joinQuery . join ( ' ' )
@@ -482,11 +584,26 @@ export class QueryBuilder<GenericResultWrapper, IsAsync extends boolean = true>
482584 return ` GROUP BY ${ value . join ( ', ' ) } `
483585 }
484586
485- protected _having ( value ?: string | Array < string > ) : string {
587+ protected _having (
588+ value : Where | undefined , // Using Where type as Having structure is similar for conditions/params
589+ context : {
590+ subQueryPlaceholders ?: Record < string , SelectAll >
591+ queryArgs : any [ ]
592+ toSQLCompiler ?: ( params : SelectAll , queryArgs : any [ ] ) => string
593+ }
594+ ) : string {
486595 if ( ! value ) return ''
487- if ( typeof value === 'string' ) return ` HAVING ${ value } `
488596
489- return ` HAVING ${ value . join ( ' AND ' ) } `
597+ // Re-use the _where logic for building HAVING clause structure.
598+ // The _where method already handles token/param processing and populates context.queryArgs.
599+ const whereEquivalentString = this . _where ( value , context )
600+
601+ if ( whereEquivalentString . startsWith ( ' WHERE ' ) ) {
602+ return ` HAVING ${ whereEquivalentString . substring ( ' WHERE ' . length ) } `
603+ }
604+ // If _where returned empty (e.g., no conditions) or an unexpected format,
605+ курорт // return an empty string for HAVING as well.
606+ return ''
490607 }
491608
492609 protected _orderBy ( value ?: string | Array < string > | Record < string , string | OrderTypes > ) : string {
0 commit comments