search-input-query-parser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/first-pass-parser.js +77 -0
- package/dist/cjs/lexer.js +322 -0
- package/dist/cjs/parse-in-values.js +65 -0
- package/dist/cjs/parse-primary.js +154 -0
- package/dist/cjs/parse-range-expression.js +174 -0
- package/dist/cjs/parser.js +85 -0
- package/dist/cjs/search-query-to-sql.js +346 -0
- package/dist/cjs/transform-to-expression.js +130 -0
- package/dist/cjs/validate-expression-fields.js +244 -0
- package/dist/cjs/validate-in-expression.js +33 -0
- package/dist/cjs/validate-string.js +65 -0
- package/dist/cjs/validate-wildcard.js +40 -0
- package/dist/cjs/validator.js +34 -0
- package/dist/esm/first-pass-parser.js +73 -0
- package/dist/esm/lexer.js +315 -0
- package/dist/esm/parse-in-values.js +61 -0
- package/dist/esm/parse-primary.js +147 -0
- package/dist/esm/parse-range-expression.js +170 -0
- package/dist/esm/parser.js +81 -0
- package/dist/esm/search-query-to-sql.js +341 -0
- package/dist/esm/transform-to-expression.js +126 -0
- package/dist/esm/validate-expression-fields.js +240 -0
- package/dist/esm/validate-in-expression.js +29 -0
- package/dist/esm/validate-string.js +61 -0
- package/dist/esm/validate-wildcard.js +36 -0
- package/dist/esm/validator.js +30 -0
- package/dist/types/first-pass-parser.d.ts +40 -0
- package/dist/types/lexer.d.ts +27 -0
- package/dist/types/parse-in-values.d.ts +3 -0
- package/dist/types/parse-primary.d.ts +6 -0
- package/dist/types/parse-range-expression.d.ts +2 -0
- package/dist/types/parser.d.ts +68 -0
- package/dist/types/search-query-to-sql.d.ts +18 -0
- package/dist/types/transform-to-expression.d.ts +3 -0
- package/dist/types/validate-expression-fields.d.ts +4 -0
- package/dist/types/validate-in-expression.d.ts +3 -0
- package/dist/types/validate-string.d.ts +3 -0
- package/dist/types/validate-wildcard.d.ts +3 -0
- package/dist/types/validator.d.ts +8 -0
- package/package.json +52 -0
- package/src/first-pass-parser.test.ts +441 -0
- package/src/first-pass-parser.ts +144 -0
- package/src/lexer.test.ts +439 -0
- package/src/lexer.ts +387 -0
- package/src/parse-in-values.ts +74 -0
- package/src/parse-primary.ts +179 -0
- package/src/parse-range-expression.ts +187 -0
- package/src/parser.test.ts +982 -0
- package/src/parser.ts +219 -0
- package/src/search-query-to-sql.test.ts +503 -0
- package/src/search-query-to-sql.ts +506 -0
- package/src/transform-to-expression.ts +153 -0
- package/src/validate-expression-fields.ts +296 -0
- package/src/validate-in-expression.ts +36 -0
- package/src/validate-string.ts +73 -0
- package/src/validate-wildcard.ts +45 -0
- package/src/validator.test.ts +192 -0
- package/src/validator.ts +53 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { searchQueryToSql, searchStringToSql } from "./search-query-to-sql";
|
|
3
|
+
import { parseSearchInputQuery } from "./parser";
|
|
4
|
+
import type { SearchQuery, FieldSchema } from "./parser";
|
|
5
|
+
|
|
6
|
+
describe("Search Query to SQL Converter", () => {
|
|
7
|
+
const searchableColumns = ["title", "description", "content"];
|
|
8
|
+
const schemas: FieldSchema[] = [
|
|
9
|
+
{ name: "price", type: "number" },
|
|
10
|
+
{ name: "amount", type: "number" },
|
|
11
|
+
{ name: "date", type: "date" },
|
|
12
|
+
{ name: "title", type: "string" },
|
|
13
|
+
{ name: "color", type: "string" },
|
|
14
|
+
{ name: "status", type: "string" },
|
|
15
|
+
{ name: "category", type: "string" },
|
|
16
|
+
{ name: "available", type: "string" },
|
|
17
|
+
{ name: "field1", type: "string" },
|
|
18
|
+
{ name: "field2", type: "string" },
|
|
19
|
+
{ name: "user_id", type: "number" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const testSqlConversion = (
|
|
23
|
+
query: string,
|
|
24
|
+
expectedSql: string,
|
|
25
|
+
expectedValues: any[]
|
|
26
|
+
) => {
|
|
27
|
+
const parsedQuery = parseSearchInputQuery(query, schemas);
|
|
28
|
+
expect(parsedQuery.type).toBe("SEARCH_QUERY");
|
|
29
|
+
const result = searchQueryToSql(
|
|
30
|
+
parsedQuery as SearchQuery,
|
|
31
|
+
searchableColumns,
|
|
32
|
+
schemas
|
|
33
|
+
);
|
|
34
|
+
expect(result.text).toBe(expectedSql);
|
|
35
|
+
expect(result.values).toEqual(expectedValues);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("Basic Term Conversion", () => {
|
|
39
|
+
test("converts single search term", () => {
|
|
40
|
+
testSqlConversion(
|
|
41
|
+
"boots",
|
|
42
|
+
"(lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
43
|
+
["%boots%"]
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("converts quoted search term", () => {
|
|
48
|
+
testSqlConversion(
|
|
49
|
+
'"red shoes"',
|
|
50
|
+
"(lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
51
|
+
["%red shoes%"]
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("escapes special characters in search terms", () => {
|
|
56
|
+
// testSqlConversion(
|
|
57
|
+
// "100%",
|
|
58
|
+
// "(lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
59
|
+
// ["%100\\%%"]
|
|
60
|
+
// );
|
|
61
|
+
|
|
62
|
+
testSqlConversion(
|
|
63
|
+
"under_score",
|
|
64
|
+
"(lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
65
|
+
["%under\\_score%"]
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("Field Value Conversion", () => {
|
|
71
|
+
test("converts simple field:value pairs", () => {
|
|
72
|
+
// testSqlConversion("color:red", "lower(color) LIKE lower($1)", ["%red%"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("converts field values with spaces", () => {
|
|
76
|
+
testSqlConversion('status:"in progress"', "lower(status) LIKE lower($1)", [
|
|
77
|
+
"%in progress%",
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("handles special date fields", () => {
|
|
82
|
+
testSqlConversion("date:2024-01-01", "date = $1", [
|
|
83
|
+
"2024-01-01",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("handles ID fields", () => {
|
|
88
|
+
testSqlConversion("user_id:123", "user_id = $1", [123]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("escapes special characters in field values", () => {
|
|
92
|
+
testSqlConversion("category:100%", "lower(category) LIKE lower($1)", [
|
|
93
|
+
"%100\\%%",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("Logical Operators", () => {
|
|
99
|
+
test("converts AND expressions", () => {
|
|
100
|
+
testSqlConversion(
|
|
101
|
+
"comfortable AND leather",
|
|
102
|
+
"((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND (lower(title) LIKE lower($2) OR lower(description) LIKE lower($2) OR lower(content) LIKE lower($2)))",
|
|
103
|
+
["%comfortable%", "%leather%"]
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("converts OR expressions", () => {
|
|
108
|
+
testSqlConversion(
|
|
109
|
+
"leather OR suede",
|
|
110
|
+
"((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) OR (lower(title) LIKE lower($2) OR lower(description) LIKE lower($2) OR lower(content) LIKE lower($2)))",
|
|
111
|
+
["%leather%", "%suede%"]
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("converts mixed operators", () => {
|
|
116
|
+
testSqlConversion(
|
|
117
|
+
"comfortable AND (leather OR suede)",
|
|
118
|
+
"((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND ((lower(title) LIKE lower($2) OR lower(description) LIKE lower($2) OR lower(content) LIKE lower($2)) OR (lower(title) LIKE lower($3) OR lower(description) LIKE lower($3) OR lower(content) LIKE lower($3))))",
|
|
119
|
+
["%comfortable%", "%leather%", "%suede%"]
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("NOT SQL Conversion", () => {
|
|
125
|
+
test("converts simple NOT expressions", () => {
|
|
126
|
+
testSqlConversion(
|
|
127
|
+
"NOT test",
|
|
128
|
+
"NOT (lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
129
|
+
["%test%"]
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("converts NOT with field:value", () => {
|
|
134
|
+
testSqlConversion("NOT status:active", "NOT lower(status) LIKE lower($1)", [
|
|
135
|
+
"%active%",
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("converts complex NOT expressions", () => {
|
|
140
|
+
testSqlConversion(
|
|
141
|
+
"boots AND NOT leather",
|
|
142
|
+
"((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND NOT (lower(title) LIKE lower($2) OR lower(description) LIKE lower($2) OR lower(content) LIKE lower($2)))",
|
|
143
|
+
["%boots%", "%leather%"]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
testSqlConversion(
|
|
147
|
+
"NOT (color:red OR color:blue)",
|
|
148
|
+
"NOT (lower(color) LIKE lower($1) OR lower(color) LIKE lower($2))",
|
|
149
|
+
["%red%", "%blue%"]
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Complex Queries", () => {
|
|
155
|
+
test("converts complex field and term combinations", () => {
|
|
156
|
+
testSqlConversion(
|
|
157
|
+
'category:"winter boots" AND (color:black OR color:brown)',
|
|
158
|
+
"(lower(category) LIKE lower($1) AND (lower(color) LIKE lower($2) OR lower(color) LIKE lower($3)))",
|
|
159
|
+
["%winter boots%", "%black%", "%brown%"]
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("converts nested expressions with multiple operators", () => {
|
|
164
|
+
testSqlConversion(
|
|
165
|
+
'(color:red OR color:blue) AND category:"winter boots" AND available:true',
|
|
166
|
+
"(((lower(color) LIKE lower($1) OR lower(color) LIKE lower($2)) AND lower(category) LIKE lower($3)) AND lower(available) LIKE lower($4))",
|
|
167
|
+
["%red%", "%blue%", "%winter boots%", "%true%"]
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("handles mixed fields and search terms", () => {
|
|
172
|
+
testSqlConversion(
|
|
173
|
+
'boots AND color:black AND "winter gear"',
|
|
174
|
+
"(((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND lower(color) LIKE lower($2)) AND (lower(title) LIKE lower($3) OR lower(description) LIKE lower($3) OR lower(content) LIKE lower($3)))",
|
|
175
|
+
["%boots%", "%black%", "%winter gear%"]
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("Edge Cases", () => {
|
|
181
|
+
test("handles empty query", () => {
|
|
182
|
+
const result = searchQueryToSql(
|
|
183
|
+
{ type: "SEARCH_QUERY", expression: null },
|
|
184
|
+
searchableColumns
|
|
185
|
+
);
|
|
186
|
+
expect(result.text).toBe("1=1");
|
|
187
|
+
expect(result.values).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("throws error for invalid query syntax", () => {
|
|
191
|
+
expect(() =>
|
|
192
|
+
searchStringToSql("AND", searchableColumns, schemas)
|
|
193
|
+
).toThrow("Parse error");
|
|
194
|
+
expect(() =>
|
|
195
|
+
searchStringToSql("field:", searchableColumns, schemas)
|
|
196
|
+
).toThrow("Parse error");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("throws error for invalid fields", () => {
|
|
200
|
+
expect(() =>
|
|
201
|
+
searchStringToSql("invalid_field:value", searchableColumns, schemas)
|
|
202
|
+
).toThrow('Parse error: Invalid field: "invalid_field"');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Parameter Counting", () => {
|
|
207
|
+
test("maintains correct parameter count in complex queries", () => {
|
|
208
|
+
testSqlConversion(
|
|
209
|
+
'term1 AND field1:value1 OR (term2 AND field2:"value 2")',
|
|
210
|
+
"(((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND lower(field1) LIKE lower($2)) OR ((lower(title) LIKE lower($3) OR lower(description) LIKE lower($3) OR lower(content) LIKE lower($3)) AND lower(field2) LIKE lower($4)))",
|
|
211
|
+
["%term1%", "%value1%", "%term2%", "%value 2%"]
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("Special Character Handling", () => {
|
|
217
|
+
test("escapes SQL wildcards", () => {
|
|
218
|
+
testSqlConversion(
|
|
219
|
+
"prefix% AND suffix_",
|
|
220
|
+
"((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1)) AND (lower(title) LIKE lower($2) OR lower(description) LIKE lower($2) OR lower(content) LIKE lower($2)))",
|
|
221
|
+
["%prefix\\%%", "%suffix\\_%"]
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("handles quoted strings with escaped characters", () => {
|
|
226
|
+
testSqlConversion(
|
|
227
|
+
'"value\\"with\\"quotes"',
|
|
228
|
+
"(lower(title) LIKE lower($1) OR lower(description) LIKE lower($1) OR lower(content) LIKE lower($1))",
|
|
229
|
+
['%value"with"quotes%']
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("Range Query Conversion", () => {
|
|
235
|
+
test("converts comparison operators for numbers", () => {
|
|
236
|
+
testSqlConversion("price:>100", "price > $1", [100]);
|
|
237
|
+
testSqlConversion("price:>=100", "price >= $1", [100]);
|
|
238
|
+
testSqlConversion("price:<50", "price < $1", [50]);
|
|
239
|
+
testSqlConversion("price:<=50", "price <= $1", [50]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("converts between ranges for numbers", () => {
|
|
243
|
+
testSqlConversion("price:10..20", "price BETWEEN $1 AND $2", [10, 20]);
|
|
244
|
+
testSqlConversion(
|
|
245
|
+
"amount:50.99..100.50",
|
|
246
|
+
"amount BETWEEN $1 AND $2",
|
|
247
|
+
[50.99, 100.5]
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("converts open-ended ranges for numbers", () => {
|
|
252
|
+
testSqlConversion("price:10..", "price >= $1", [10]);
|
|
253
|
+
testSqlConversion("price:..20", "price <= $1", [20]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("converts date ranges", () => {
|
|
257
|
+
testSqlConversion("date:>2024-01-01", "date > $1", [
|
|
258
|
+
"2024-01-01",
|
|
259
|
+
]);
|
|
260
|
+
testSqlConversion(
|
|
261
|
+
"date:2024-01-01..2024-12-31",
|
|
262
|
+
"date BETWEEN $1 AND $2",
|
|
263
|
+
["2024-01-01", "2024-12-31"]
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("converts complex expressions with ranges", () => {
|
|
268
|
+
testSqlConversion(
|
|
269
|
+
"price:>100 AND amount:<50",
|
|
270
|
+
"(price > $1 AND amount < $2)",
|
|
271
|
+
[100, 50]
|
|
272
|
+
);
|
|
273
|
+
testSqlConversion(
|
|
274
|
+
"price:10..20 OR amount:>=100",
|
|
275
|
+
"(price BETWEEN $1 AND $2 OR amount >= $3)",
|
|
276
|
+
[10, 20, 100]
|
|
277
|
+
);
|
|
278
|
+
testSqlConversion(
|
|
279
|
+
"(price:>100 AND amount:<50) OR date:>=2024-01-01",
|
|
280
|
+
"((price > $1 AND amount < $2) OR date >= $3)",
|
|
281
|
+
[100, 50, "2024-01-01"]
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("mixes ranges with regular field searches", () => {
|
|
286
|
+
testSqlConversion(
|
|
287
|
+
'title:"winter boots" AND price:10..20',
|
|
288
|
+
"(lower(title) LIKE lower($1) AND price BETWEEN $2 AND $3)",
|
|
289
|
+
["%winter boots%", 10, 20]
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("handles multiple date ranges in one query", () => {
|
|
294
|
+
testSqlConversion(
|
|
295
|
+
"date:>=2024-01-01 AND date:<=2024-12-31",
|
|
296
|
+
"(date >= $1 AND date <= $2)",
|
|
297
|
+
["2024-01-01", "2024-12-31"]
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("handles decimal numbers in ranges", () => {
|
|
302
|
+
testSqlConversion(
|
|
303
|
+
"price:10.5..20.99",
|
|
304
|
+
"price BETWEEN $1 AND $2",
|
|
305
|
+
[10.5, 20.99]
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("preserves numeric precision", () => {
|
|
310
|
+
testSqlConversion("price:>=99.99", "price >= $1", [99.99]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("handles negative numbers in ranges", () => {
|
|
314
|
+
testSqlConversion(
|
|
315
|
+
"amount:-10..10",
|
|
316
|
+
"amount BETWEEN $1 AND $2",
|
|
317
|
+
[-10, 10]
|
|
318
|
+
);
|
|
319
|
+
testSqlConversion("amount:<-10", "amount < $1", [-10]);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("tsvector Search Type", () => {
|
|
324
|
+
const testTsvectorConversion = (
|
|
325
|
+
query: string,
|
|
326
|
+
expectedSql: string,
|
|
327
|
+
expectedValues: any[]
|
|
328
|
+
) => {
|
|
329
|
+
const parsedQuery = parseSearchInputQuery(query, schemas);
|
|
330
|
+
expect(parsedQuery.type).toBe("SEARCH_QUERY");
|
|
331
|
+
const result = searchQueryToSql(
|
|
332
|
+
parsedQuery as SearchQuery,
|
|
333
|
+
searchableColumns,
|
|
334
|
+
schemas,
|
|
335
|
+
{
|
|
336
|
+
searchType: "tsvector",
|
|
337
|
+
language: "english",
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
expect(result.text).toBe(expectedSql);
|
|
341
|
+
expect(result.values).toEqual(expectedValues);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
test("converts single search term", () => {
|
|
345
|
+
testTsvectorConversion(
|
|
346
|
+
"boots",
|
|
347
|
+
"(to_tsvector('english', title) || to_tsvector('english', description) || to_tsvector('english', content)) @@ plainto_tsquery('english', $1)",
|
|
348
|
+
["boots"]
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("converts field:value pairs", () => {
|
|
353
|
+
testTsvectorConversion(
|
|
354
|
+
"title:boots",
|
|
355
|
+
"to_tsvector('english', title) @@ plainto_tsquery('english', $1)",
|
|
356
|
+
["boots"]
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("converts wildcards to prefix matching", () => {
|
|
361
|
+
testTsvectorConversion(
|
|
362
|
+
"boots*",
|
|
363
|
+
"(to_tsvector('english', title) || to_tsvector('english', description) || to_tsvector('english', content)) @@ to_tsquery('english', $1)",
|
|
364
|
+
["boots:*"]
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("handles complex AND/OR expressions", () => {
|
|
369
|
+
testTsvectorConversion(
|
|
370
|
+
'boots AND "winter gear"',
|
|
371
|
+
"((to_tsvector('english', title) || to_tsvector('english', description) || to_tsvector('english', content)) @@ plainto_tsquery('english', $1) AND (to_tsvector('english', title) || to_tsvector('english', description) || to_tsvector('english', content)) @@ plainto_tsquery('english', $2))",
|
|
372
|
+
["boots", "winter gear"]
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("paradedb Search Type", () => {
|
|
378
|
+
const testParadeDBConversion = (
|
|
379
|
+
query: string,
|
|
380
|
+
expectedSql: string,
|
|
381
|
+
expectedValues: any[]
|
|
382
|
+
) => {
|
|
383
|
+
const parsedQuery = parseSearchInputQuery(query, schemas);
|
|
384
|
+
expect(parsedQuery.type).toBe("SEARCH_QUERY");
|
|
385
|
+
const result = searchQueryToSql(
|
|
386
|
+
parsedQuery as SearchQuery,
|
|
387
|
+
searchableColumns,
|
|
388
|
+
schemas,
|
|
389
|
+
{
|
|
390
|
+
searchType: "paradedb",
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
expect(result.text).toBe(expectedSql);
|
|
394
|
+
expect(result.values).toEqual(expectedValues);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
test("converts single search term", () => {
|
|
398
|
+
testParadeDBConversion(
|
|
399
|
+
"boots",
|
|
400
|
+
"(title @@@ $1 OR description @@@ $1 OR content @@@ $1)",
|
|
401
|
+
['"boots"']
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("handles numeric ranges", () => {
|
|
406
|
+
testParadeDBConversion(
|
|
407
|
+
"price:10..20",
|
|
408
|
+
"price @@@ '[' || $1 || ' TO ' || $2 || ']'",
|
|
409
|
+
[10, 20]
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
testParadeDBConversion("price:>100", "price @@@ '>' || $1", [100]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("handles wildcards", () => {
|
|
416
|
+
testParadeDBConversion(
|
|
417
|
+
"boots*",
|
|
418
|
+
"(title @@@ $1 OR description @@@ $1 OR content @@@ $1)",
|
|
419
|
+
['"boots"*']
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("handles complex expressions", () => {
|
|
424
|
+
testParadeDBConversion(
|
|
425
|
+
'title:"winter boots" AND price:>100',
|
|
426
|
+
"(title @@@ $1 AND price @@@ '>' || $2)",
|
|
427
|
+
['"winter boots"', 100]
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("handles simple quoted strings", () => {
|
|
432
|
+
testParadeDBConversion(
|
|
433
|
+
'"test phrase"',
|
|
434
|
+
"(title @@@ $1 OR description @@@ $1 OR content @@@ $1)",
|
|
435
|
+
['"test phrase"']
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("handles quoted strings with wildcards", () => {
|
|
440
|
+
testParadeDBConversion(
|
|
441
|
+
'"test phrase*"',
|
|
442
|
+
"(title @@@ $1 OR description @@@ $1 OR content @@@ $1)",
|
|
443
|
+
['"test phrase\\*\"']
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
testParadeDBConversion(
|
|
447
|
+
'"test phrase"*',
|
|
448
|
+
"(title @@@ $1 OR description @@@ $1 OR content @@@ $1)",
|
|
449
|
+
['"test phrase"*']
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("handles field values with quoted strings", () => {
|
|
454
|
+
testParadeDBConversion('title:"test phrase"', "title @@@ $1", [
|
|
455
|
+
'"test phrase"',
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
testParadeDBConversion('title:"test phrase*"', "title @@@ $1", [
|
|
459
|
+
'"test phrase"*',
|
|
460
|
+
]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("handles mixed quoted and unquoted terms", () => {
|
|
464
|
+
testParadeDBConversion(
|
|
465
|
+
'title:test title:"test and test" "test and test"',
|
|
466
|
+
"((title @@@ $1 AND title @@@ $2) AND (title @@@ $3 OR description @@@ $3 OR content @@@ $3))",
|
|
467
|
+
['"test"', '"test and test"', '"test and test"']
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("handles IN expressions with quoted strings", () => {
|
|
472
|
+
testParadeDBConversion(
|
|
473
|
+
'status:IN("test one", "test two")',
|
|
474
|
+
"status @@@ 'IN[' || $1 || ' ' || $2 || ']'",
|
|
475
|
+
["test one", "test two"]
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("handles numeric fields with quoted strings", () => {
|
|
480
|
+
testParadeDBConversion(
|
|
481
|
+
'price:>100 AND title:"expensive items"',
|
|
482
|
+
"(price @@@ '>' || $1 AND title @@@ $2)",
|
|
483
|
+
[100, '"expensive items"']
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("handles dates with quoted strings", () => {
|
|
488
|
+
testParadeDBConversion(
|
|
489
|
+
'date:2024-01-01 AND title:"new items"',
|
|
490
|
+
"(date @@@ '\"' || $1 || '\"' AND title @@@ $2)",
|
|
491
|
+
["2024-01-01", '"new items"']
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("handles complex query with multiple field types", () => {
|
|
496
|
+
testParadeDBConversion(
|
|
497
|
+
'title:"test*" AND price:>100 AND status:IN("active", "pending")',
|
|
498
|
+
"((title @@@ $1 AND price @@@ '>' || $2) AND status @@@ 'IN[' || $3 || ' ' || $4 || ']')",
|
|
499
|
+
['"test"*', 100, "active", "pending"]
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
});
|