@@ -31,12 +31,14 @@ export function generateSQLExpressions(
3131 const conditions = [ ] ;
3232 const parameters = [ ] ;
3333 const tableName = underscore ( resource ) ;
34+ const joins = [ ] ;
3435
35- addFilterConditions ( { filters, parameters, conditions } ) ;
36+ addFilterConditions ( { filters, parameters, conditions, joins , tableName } ) ;
3637 addSearchConditions ( { search, resource, tableName, parameters, conditions } ) ;
3738
3839 const selectClause = constructSelectClause ( select , tableName ) ;
39- const orderByClause = constructOrderByClause ( resource , sort ) ;
40+ const joinClause = constructJoinClause ( joins ) ;
41+ const orderByClause = constructOrderByClause ( resource , tableName , sort ) ;
4042 const whereClause = conditions . length ? `WHERE ${ and ( conditions ) } ` : '' ;
4143
4244 const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '' ;
@@ -49,6 +51,7 @@ export function generateSQLExpressions(
4951 // This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
5052 sql : `
5153 ${ selectClause }
54+ ${ joinClause }
5255 ${ whereClause }
5356 ${ orderByClause }
5457 ${ paginationClause } `
@@ -58,7 +61,13 @@ export function generateSQLExpressions(
5861 } ;
5962}
6063
61- function addFilterConditions ( { filters, parameters, conditions } ) {
64+ function addFilterConditions ( {
65+ filters,
66+ parameters,
67+ conditions,
68+ joins,
69+ tableName,
70+ } ) {
6271 if ( ! filters ) {
6372 return ;
6473 }
@@ -69,7 +78,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
6978 ? filterArrayOrObject
7079 : filterArrayOrObject . values ;
7180
72- if ( ! filterValueArray || ! filterValueArray . length || key === 'subqueries ' ) {
81+ if ( ! filterValueArray || ! filterValueArray . length || key === 'joins ' ) {
7382 continue ;
7483 }
7584
@@ -86,8 +95,13 @@ function addFilterConditions({ filters, parameters, conditions }) {
8695 allOperatorsEqual
8796 ) {
8897 const operation = firstOperator === 'equals' ? 'in' : 'notIn' ;
98+ const nullOperator =
99+ firstOperator === 'equals' ? 'IS NULL' : 'IS NOT NULL' ;
89100 const values = filterValueArray
90- . filter ( ( f ) => f )
101+ . filter (
102+ ( filterObjValue ) =>
103+ filterObjValue && Object . values ( filterObjValue ) [ 0 ] !== null ,
104+ )
91105 . map ( ( filterObjValue ) => {
92106 let value = Object . values ( filterObjValue ) [ 0 ] ;
93107 if ( typeOf ( value ) === 'date' ) {
@@ -96,7 +110,14 @@ function addFilterConditions({ filters, parameters, conditions }) {
96110 parameters . push ( value ) ;
97111 return value ;
98112 } ) ;
99- const filterCondition = `${ key } ${ OPERATORS [ operation ] ( values ) } ` ;
113+
114+ // Add a check for null values as an IN clause will mistakenly use `=` for null values.
115+ // We'll add an extra OR clause to handle nulls separately.
116+ const isNullValue = filterValueArray . some (
117+ ( filterObjValue ) => Object . values ( filterObjValue ) [ 0 ] === null ,
118+ ) ;
119+
120+ const filterCondition = `"${ tableName } ".${ key } ${ OPERATORS [ operation ] ( values ) } ${ isNullValue ? ` OR "${ tableName } ".${ key } ${ nullOperator } ` : '' } ` ;
100121 conditions . push ( parenthetical ( filterCondition ) ) ;
101122 continue ;
102123 }
@@ -106,6 +127,16 @@ function addFilterConditions({ filters, parameters, conditions }) {
106127 . map ( ( filterObjValue ) => {
107128 let [ operation , value ] = Object . entries ( filterObjValue ) [ 0 ] ;
108129
130+ // Handle null values: convert equals/notEquals to IS NULL or IS NOT NULL
131+ if ( value === null ) {
132+ if ( operation === 'equals' ) {
133+ return `"${ tableName } ".${ key } IS NULL` ;
134+ }
135+ if ( operation === 'notEquals' ) {
136+ return `"${ tableName } ".${ key } IS NOT NULL` ;
137+ }
138+ }
139+
109140 // SQLite needs to be working with ISO strings
110141 if ( typeOf ( value ) === 'date' ) {
111142 value = value . toISOString ( ) ;
@@ -118,7 +149,7 @@ function addFilterConditions({ filters, parameters, conditions }) {
118149 parameters . push ( value ) ;
119150 }
120151
121- return `${ key } ${ OPERATORS [ operation ] } ` ;
152+ return `" ${ tableName } ". ${ key } ${ OPERATORS [ operation ] } ` ;
122153 } ) ;
123154
124155 const { logicalOperator } = filterArrayOrObject ;
@@ -132,19 +163,40 @@ function addFilterConditions({ filters, parameters, conditions }) {
132163 ) ;
133164 }
134165
135- if ( filters . subqueries ?. length > 0 ) {
136- filters . subqueries . forEach ( ( subquery ) => {
137- const { resource, query, select } = subquery ;
138- const { sql, parameters : subqueryParams } = generateSQLExpressions (
166+ if ( filters . joins ?. length > 0 ) {
167+ const tableNameIndex = { } ;
168+
169+ filters . joins . forEach ( ( join ) => {
170+ const {
139171 resource,
140172 query,
141- {
142- select,
143- } ,
144- ) ;
173+ joinFrom = 'id' ,
174+ joinOn,
175+ joinType = 'INNER' ,
176+ } = join ;
177+ const joinTableName = underscore ( resource ) ;
178+ tableNameIndex [ resource ] ??= 1 ;
179+ // Alias in the possible scenario we join the same table more than once
180+ const alias = `${ joinTableName } ${ tableNameIndex [ resource ] } ` ;
181+
182+ joins . push ( {
183+ type : joinType ,
184+ table : joinTableName ,
185+ alias,
186+ condition : `"${ tableName } ".${ joinFrom } = ${ alias } .${ joinOn } ` ,
187+ } ) ;
188+
189+ // Add the conditions from the join query
190+ if ( query ?. filters ) {
191+ addFilterConditions ( {
192+ filters : query . filters ,
193+ parameters,
194+ conditions,
195+ tableName : alias ,
196+ } ) ;
197+ }
145198
146- conditions . push ( `id IN (${ sql } )` ) ;
147- parameters . push ( ...subqueryParams ) ;
199+ tableNameIndex [ resource ] ++ ;
148200 } ) ;
149201 }
150202}
@@ -177,7 +229,7 @@ function addSearchConditions({
177229 // much more efficient with FTS queries when using rowids or MATCH (or both).
178230 // We could have also used a join here but a subquery is simpler.
179231 conditions . push (
180- `rowid IN (SELECT rowid FROM ${ tableName } _fts WHERE ${ tableName } _fts MATCH ?)` ,
232+ `" ${ tableName } ". rowid IN (SELECT rowid FROM ${ tableName } _fts WHERE ${ tableName } _fts MATCH ?)` ,
181233 ) ;
182234}
183235
@@ -186,7 +238,7 @@ function constructSelectClause(select = [{ field: '*' }], tableName) {
186238 const nonDistinctColumns = select . filter ( ( { isDistinct } ) => ! isDistinct ) ;
187239
188240 const buildColumnExpression = ( { field, isCount, alias, isDistinct } ) => {
189- let column = field ;
241+ let column = field === '*' ? field : `" ${ tableName } ". ${ field } ` ;
190242
191243 // If there's just distinct with nothing else, this will be handled separately
192244 // and we will just return the column back as is
@@ -235,11 +287,27 @@ function constructSelectClause(select = [{ field: '*' }], tableName) {
235287 return `SELECT ${ selectColumns } FROM "${ tableName } "` ;
236288}
237289
238- function constructOrderByClause ( resource , sort ) {
239- const defaultOrderByClause = 'ORDER BY created_time DESC' ;
290+ function constructJoinClause ( joins ) {
291+ if ( ! joins || joins . length === 0 ) {
292+ return '' ;
293+ }
294+
295+ return joins
296+ . map (
297+ ( { type, table, alias, condition } ) =>
298+ `${ type } JOIN "${ table } " ${ alias } ON ${ condition } ` ,
299+ )
300+ . join ( ' ' ) ;
301+ }
302+
303+ function constructOrderByClause ( resource , tableName , sort ) {
304+ const defaultOrderByClause = `ORDER BY "${ tableName } ".created_time DESC` ;
240305
241306 const { attributes, customSort, direction, isCoalesced } = sort ;
242307 const sortDirection = direction === 'desc' ? 'DESC' : 'ASC' ;
308+ const attributesWithTableName = attributes ?. map (
309+ ( attribute ) => `"${ tableName } ".${ attribute } ` ,
310+ ) ;
243311
244312 // We have to check if the attributes are valid for the resource
245313 // as we can't use parameterized queries for ORDER BY
@@ -255,21 +323,21 @@ function constructOrderByClause(resource, sort) {
255323 } ,
256324 '' ,
257325 ) ;
258- return `ORDER BY CASE ${ attributes . join ( ', ' ) } ${ whenClauses } END ${ sortDirection } ` ;
326+ return `ORDER BY CASE ${ attributesWithTableName . join ( ', ' ) } ${ whenClauses } END ${ sortDirection } ` ;
259327 } else if ( attributes ?. length > 0 ) {
260- const commaSeparatedVals = attributes . join ( ', ' ) ;
328+ const commaSeparatedVals = attributesWithTableName . join ( ', ' ) ;
261329
262330 // In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
263331 // Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
264332 if ( isCoalesced ) {
265- return `ORDER BY COALESCE(${ attributes . join ( ', ' ) } ) COLLATE NOCASE ${ sortDirection } , COALESCE(${ commaSeparatedVals } ) ${ sortDirection } ` ;
333+ return `ORDER BY COALESCE(${ commaSeparatedVals } ) COLLATE NOCASE ${ sortDirection } , COALESCE(${ commaSeparatedVals } ) ${ sortDirection } ` ;
266334 }
267335
268336 const attributesWithNoCollate = attributes
269- . map ( ( attr ) => `${ attr } COLLATE NOCASE ${ sortDirection } ` )
337+ . map ( ( attr ) => `" ${ tableName } ". ${ attr } COLLATE NOCASE ${ sortDirection } ` )
270338 . join ( ', ' ) ;
271339 const attributesWithDirection = attributes
272- . map ( ( attr ) => `${ attr } ${ sortDirection } ` )
340+ . map ( ( attr ) => `" ${ tableName } ". ${ attr } ${ sortDirection } ` )
273341 . join ( ', ' ) ;
274342 return `ORDER BY ${ attributesWithNoCollate } , ${ attributesWithDirection } ` ;
275343 } else if ( modelMapping [ resource ] ?. created_time ) {
0 commit comments