|
4 | 4 | */ |
5 | 5 |
|
6 | 6 | import { modelMapping } from 'api/services/sqlite'; |
7 | | -import { searchTables } from 'api/services/sqlite'; |
8 | 7 | import { typeOf } from '@ember/utils'; |
9 | 8 | import { underscore } from '@ember/string'; |
| 9 | +import { assert } from '@ember/debug'; |
10 | 10 |
|
11 | 11 | /** |
12 | 12 | * Takes a POJO representing a filter query and builds a SQL query. |
@@ -35,17 +35,15 @@ export function generateSQLExpressions( |
35 | 35 | addFilterConditions({ filters, parameters, conditions }); |
36 | 36 | addSearchConditions({ search, resource, tableName, parameters, conditions }); |
37 | 37 |
|
| 38 | + const selectClause = constructSelectClause(select, tableName); |
38 | 39 | const orderByClause = constructOrderByClause(resource, sort); |
39 | | - |
40 | 40 | const whereClause = conditions.length ? `WHERE ${and(conditions)}` : ''; |
41 | 41 |
|
42 | 42 | const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : ''; |
43 | 43 | if (paginationClause) { |
44 | 44 | parameters.push(pageSize, (page - 1) * pageSize); |
45 | 45 | } |
46 | 46 |
|
47 | | - const selectClause = `SELECT ${select ? select.join(', ') : '*'} FROM "${tableName}"`; |
48 | | - |
49 | 47 | return { |
50 | 48 | // Replace any empty newlines or leading whitespace on each line to be consistent with formatting |
51 | 49 | // This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution. |
@@ -142,32 +140,83 @@ function addSearchConditions({ |
142 | 140 | parameters, |
143 | 141 | conditions, |
144 | 142 | }) { |
145 | | - if (!search) { |
| 143 | + if (!search || !modelMapping[resource]) { |
146 | 144 | return; |
147 | 145 | } |
148 | 146 |
|
149 | | - if (searchTables.has(resource)) { |
150 | | - // Use the special prefix indicator "*" for full-text search |
151 | | - parameters.push(`"${search}"*`); |
152 | | - // Use a subquery to match against the FTS table with rowids as SQLite is |
153 | | - // much more efficient with FTS queries when using rowids or MATCH (or both). |
154 | | - // We could have also used a join here but a subquery is simpler. |
155 | | - conditions.push( |
156 | | - `rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`, |
| 147 | + // Use the special prefix indicator "*" for full-text search |
| 148 | + if (typeOf(search) === 'object') { |
| 149 | + if (!search?.text) { |
| 150 | + return; |
| 151 | + } |
| 152 | + |
| 153 | + parameters.push( |
| 154 | + or(search.fields.map((field) => `${field}:"${search.text}"*`)), |
157 | 155 | ); |
158 | | - return; |
| 156 | + } else { |
| 157 | + parameters.push(`"${search}"*`); |
159 | 158 | } |
160 | 159 |
|
161 | | - const fields = Object.keys(modelMapping[resource]); |
162 | | - const searchConditions = parenthetical( |
163 | | - or( |
164 | | - fields.map((field) => { |
165 | | - parameters.push(`%${search}%`); |
166 | | - return `${field}${OPERATORS['contains']}`; |
167 | | - }), |
168 | | - ), |
| 160 | + // Use a subquery to match against the FTS table with rowids as SQLite is |
| 161 | + // much more efficient with FTS queries when using rowids or MATCH (or both). |
| 162 | + // We could have also used a join here but a subquery is simpler. |
| 163 | + conditions.push( |
| 164 | + `rowid IN (SELECT rowid FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?)`, |
169 | 165 | ); |
170 | | - conditions.push(searchConditions); |
| 166 | +} |
| 167 | + |
| 168 | +function constructSelectClause(select = [{ field: '*' }], tableName) { |
| 169 | + const distinctColumns = select.filter(({ isDistinct }) => isDistinct); |
| 170 | + const nonDistinctColumns = select.filter(({ isDistinct }) => !isDistinct); |
| 171 | + |
| 172 | + const buildColumnExpression = ({ field, isCount, alias, isDistinct }) => { |
| 173 | + let column = field; |
| 174 | + |
| 175 | + // If there's just distinct with nothing else, this will be handled separately |
| 176 | + // and we will just return the column back as is |
| 177 | + if (isCount && isDistinct) { |
| 178 | + column = `count(DISTINCT ${column})`; |
| 179 | + } else if (isCount) { |
| 180 | + column = `count(${column})`; |
| 181 | + } |
| 182 | + |
| 183 | + if (alias) { |
| 184 | + column = `${column} as ${alias}`; |
| 185 | + } |
| 186 | + |
| 187 | + return column; |
| 188 | + }; |
| 189 | + |
| 190 | + let selectColumns; |
| 191 | + |
| 192 | + if (distinctColumns.length > 0) { |
| 193 | + // Check if any columns also have COUNT (or other aggregate function in the future) |
| 194 | + const hasAggregateDistinct = distinctColumns.some((col) => col.isCount); |
| 195 | + |
| 196 | + if (hasAggregateDistinct) { |
| 197 | + selectColumns = distinctColumns.map(buildColumnExpression).join(', '); |
| 198 | + |
| 199 | + // Add back in the non distinct columns |
| 200 | + if (nonDistinctColumns.length > 0) { |
| 201 | + const regularPart = nonDistinctColumns |
| 202 | + .map(buildColumnExpression) |
| 203 | + .join(', '); |
| 204 | + selectColumns = `${selectColumns}, ${regularPart}`; |
| 205 | + } |
| 206 | + } else { |
| 207 | + assert( |
| 208 | + 'Can not combine non-distincts with multi column distincts', |
| 209 | + nonDistinctColumns.length === 0, |
| 210 | + ); |
| 211 | + |
| 212 | + // Only do multi column DISTINCT with no aggregate functions |
| 213 | + selectColumns = `DISTINCT ${distinctColumns.map(buildColumnExpression).join(', ')}`; |
| 214 | + } |
| 215 | + } else { |
| 216 | + selectColumns = select.map(buildColumnExpression).join(', '); |
| 217 | + } |
| 218 | + |
| 219 | + return `SELECT ${selectColumns} FROM "${tableName}"`; |
171 | 220 | } |
172 | 221 |
|
173 | 222 | function constructOrderByClause(resource, sort) { |
|
0 commit comments