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.
Files changed (58) hide show
  1. package/dist/cjs/first-pass-parser.js +77 -0
  2. package/dist/cjs/lexer.js +322 -0
  3. package/dist/cjs/parse-in-values.js +65 -0
  4. package/dist/cjs/parse-primary.js +154 -0
  5. package/dist/cjs/parse-range-expression.js +174 -0
  6. package/dist/cjs/parser.js +85 -0
  7. package/dist/cjs/search-query-to-sql.js +346 -0
  8. package/dist/cjs/transform-to-expression.js +130 -0
  9. package/dist/cjs/validate-expression-fields.js +244 -0
  10. package/dist/cjs/validate-in-expression.js +33 -0
  11. package/dist/cjs/validate-string.js +65 -0
  12. package/dist/cjs/validate-wildcard.js +40 -0
  13. package/dist/cjs/validator.js +34 -0
  14. package/dist/esm/first-pass-parser.js +73 -0
  15. package/dist/esm/lexer.js +315 -0
  16. package/dist/esm/parse-in-values.js +61 -0
  17. package/dist/esm/parse-primary.js +147 -0
  18. package/dist/esm/parse-range-expression.js +170 -0
  19. package/dist/esm/parser.js +81 -0
  20. package/dist/esm/search-query-to-sql.js +341 -0
  21. package/dist/esm/transform-to-expression.js +126 -0
  22. package/dist/esm/validate-expression-fields.js +240 -0
  23. package/dist/esm/validate-in-expression.js +29 -0
  24. package/dist/esm/validate-string.js +61 -0
  25. package/dist/esm/validate-wildcard.js +36 -0
  26. package/dist/esm/validator.js +30 -0
  27. package/dist/types/first-pass-parser.d.ts +40 -0
  28. package/dist/types/lexer.d.ts +27 -0
  29. package/dist/types/parse-in-values.d.ts +3 -0
  30. package/dist/types/parse-primary.d.ts +6 -0
  31. package/dist/types/parse-range-expression.d.ts +2 -0
  32. package/dist/types/parser.d.ts +68 -0
  33. package/dist/types/search-query-to-sql.d.ts +18 -0
  34. package/dist/types/transform-to-expression.d.ts +3 -0
  35. package/dist/types/validate-expression-fields.d.ts +4 -0
  36. package/dist/types/validate-in-expression.d.ts +3 -0
  37. package/dist/types/validate-string.d.ts +3 -0
  38. package/dist/types/validate-wildcard.d.ts +3 -0
  39. package/dist/types/validator.d.ts +8 -0
  40. package/package.json +52 -0
  41. package/src/first-pass-parser.test.ts +441 -0
  42. package/src/first-pass-parser.ts +144 -0
  43. package/src/lexer.test.ts +439 -0
  44. package/src/lexer.ts +387 -0
  45. package/src/parse-in-values.ts +74 -0
  46. package/src/parse-primary.ts +179 -0
  47. package/src/parse-range-expression.ts +187 -0
  48. package/src/parser.test.ts +982 -0
  49. package/src/parser.ts +219 -0
  50. package/src/search-query-to-sql.test.ts +503 -0
  51. package/src/search-query-to-sql.ts +506 -0
  52. package/src/transform-to-expression.ts +153 -0
  53. package/src/validate-expression-fields.ts +296 -0
  54. package/src/validate-in-expression.ts +36 -0
  55. package/src/validate-string.ts +73 -0
  56. package/src/validate-wildcard.ts +45 -0
  57. package/src/validator.test.ts +192 -0
  58. package/src/validator.ts +53 -0
@@ -0,0 +1,506 @@
1
+ import {
2
+ Expression,
3
+ SearchQuery,
4
+ parseSearchInputQuery,
5
+ FieldSchema,
6
+ WildcardPattern,
7
+ } from "./parser";
8
+
9
+ export interface SqlQueryResult {
10
+ text: string;
11
+ values: any[];
12
+ }
13
+
14
+ interface SqlState {
15
+ paramCounter: number;
16
+ values: any[];
17
+ searchableColumns: string[];
18
+ schemas: Map<string, FieldSchema>;
19
+ searchType: SearchType;
20
+ language?: string; // For tsvector configuration
21
+ }
22
+
23
+ export type SearchType = "ilike" | "tsvector" | "paradedb";
24
+
25
+ export interface SearchQueryOptions {
26
+ searchType?: SearchType;
27
+ language?: string; // PostgreSQL language configuration for tsvector
28
+ }
29
+
30
+ // Constants
31
+ const SPECIAL_CHARS = ["%", "_"] as const;
32
+ const ESCAPE_CHAR = "\\";
33
+
34
+ // Helper Functions
35
+ const escapeRegExp = (str: string): string =>
36
+ str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+
38
+ const escapeSpecialChars = (value: string): string =>
39
+ SPECIAL_CHARS.reduce(
40
+ (escaped, char) =>
41
+ escaped.replace(new RegExp(escapeRegExp(char), "g"), ESCAPE_CHAR + char),
42
+ value
43
+ );
44
+
45
+ // Helper to escape special characters for ParadeDB query syntax
46
+ const escapeParadeDBChars = (value: string): string => {
47
+ const specialChars = [
48
+ "+",
49
+ "^",
50
+ "`",
51
+ ":",
52
+ "{",
53
+ "}",
54
+ '"',
55
+ "[",
56
+ "]",
57
+ "(",
58
+ ")",
59
+ "<",
60
+ ">",
61
+ "~",
62
+ "!",
63
+ "\\",
64
+ "*",
65
+ ];
66
+ return specialChars.reduce(
67
+ (escaped, char) =>
68
+ escaped.replace(new RegExp(escapeRegExp(char), "g"), `\\${char}`),
69
+ value
70
+ );
71
+ };
72
+
73
+ const stripQuotes = (value: string): string => {
74
+ if (value.startsWith('"') && value.endsWith('"')) {
75
+ return value.slice(1, -1);
76
+ }
77
+ return value;
78
+ };
79
+
80
+ const cleanQuotedString = (
81
+ value: string,
82
+ stripOuterQuotes: boolean = true
83
+ ): string => {
84
+ // First strip the outer quotes if requested
85
+ let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
86
+
87
+ // Replace escaped quotes with regular quotes
88
+ cleaned = cleaned.replace(/\\"/g, '"');
89
+
90
+ // Clean up any remaining escape characters
91
+ cleaned = cleaned.replace(/\\\\/g, "\\");
92
+
93
+ return cleaned;
94
+ };
95
+
96
+ const isQuotedString = (value: string): boolean => {
97
+ return value.startsWith('"') && value.endsWith('"');
98
+ };
99
+
100
+ const prepareParadeDBString = (
101
+ value: string,
102
+ includeWildcard: boolean = false
103
+ ): string => {
104
+ // First clean up the string
105
+ const cleaned = cleanQuotedString(value);
106
+
107
+ // For ParadeDB, we need to:
108
+ // 1. Escape special characters (except wildcards)
109
+ // 2. Wrap in quotes
110
+ // 3. Add wildcard if needed
111
+ const escaped = escapeParadeDBChars(cleaned);
112
+ const result = `"${escaped}"`;
113
+ return includeWildcard ? `${result}*` : result;
114
+ };
115
+
116
+ // Create a new parameter placeholder and update state
117
+ const nextParam = (state: SqlState): [string, SqlState] => {
118
+ const paramName = `$${state.paramCounter}`;
119
+ const newState = {
120
+ ...state,
121
+ paramCounter: state.paramCounter + 1,
122
+ };
123
+ return [paramName, newState];
124
+ };
125
+
126
+ // Add a value to the state and return updated state
127
+ const addValue = (state: SqlState, value: any): SqlState => ({
128
+ ...state,
129
+ values: [...state.values, value],
130
+ });
131
+
132
+ /**
133
+ * Convert a wildcard pattern to SQL
134
+ */
135
+ const wildcardPatternToSql = (
136
+ expr: WildcardPattern,
137
+ state: SqlState
138
+ ): [string, SqlState] => {
139
+ if (expr.prefix === "") {
140
+ return ["1=1", state];
141
+ }
142
+
143
+ const [paramName, newState] = nextParam(state);
144
+ const cleanedPrefix = cleanQuotedString(expr.prefix);
145
+
146
+ switch (state.searchType) {
147
+ case "paradedb": {
148
+ const queryValue = prepareParadeDBString(cleanedPrefix, true);
149
+ const conditions = state.searchableColumns.map(
150
+ (column) => `${column} @@@ ${paramName}`
151
+ );
152
+ const sql =
153
+ conditions.length === 1
154
+ ? conditions[0]
155
+ : `(${conditions.join(" OR ")})`;
156
+ return [sql, addValue(newState, queryValue)];
157
+ }
158
+ case "tsvector": {
159
+ const langConfig = state.language || "english";
160
+ const conditions = state.searchableColumns.map(
161
+ (column) => `to_tsvector('${langConfig}', ${column})`
162
+ );
163
+ const tsvectorCondition = `(${conditions.join(
164
+ " || "
165
+ )}) @@ to_tsquery('${langConfig}', ${paramName})`;
166
+ return [tsvectorCondition, addValue(newState, `${cleanedPrefix}:*`)];
167
+ }
168
+ default: {
169
+ // ILIKE behavior
170
+ const escapedPrefix = escapeSpecialChars(cleanedPrefix);
171
+ const conditions = state.searchableColumns.map(
172
+ (column) => `lower(${column}) LIKE lower(${paramName})`
173
+ );
174
+ const sql =
175
+ conditions.length === 1
176
+ ? conditions[0]
177
+ : `(${conditions.join(" OR ")})`;
178
+ return [sql, addValue(newState, `${escapedPrefix}%`)];
179
+ }
180
+ }
181
+ };
182
+
183
+ /**
184
+ * Convert a search term to SQL conditions based on search type
185
+ */
186
+ const searchTermToSql = (
187
+ value: string,
188
+ state: SqlState
189
+ ): [string, SqlState] => {
190
+ const [paramName, newState] = nextParam(state);
191
+ const hasWildcard = value.endsWith("*");
192
+ const isQuoted = isQuotedString(value);
193
+ const cleanedValue = cleanQuotedString(value);
194
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
195
+
196
+ switch (state.searchType) {
197
+ case "paradedb": {
198
+ const queryValue = prepareParadeDBString(baseValue, hasWildcard);
199
+ const conditions = state.searchableColumns.map(
200
+ (column) => `${column} @@@ ${paramName}`
201
+ );
202
+ const sql =
203
+ conditions.length === 1
204
+ ? conditions[0]
205
+ : `(${conditions.join(" OR ")})`;
206
+ return [sql, addValue(newState, queryValue)];
207
+ }
208
+ case "tsvector": {
209
+ const langConfig = state.language || "english";
210
+ const conditions = state.searchableColumns.map(
211
+ (column) => `to_tsvector('${langConfig}', ${column})`
212
+ );
213
+ const tsvectorCondition = `(${conditions.join(" || ")}) @@ ${
214
+ hasWildcard ? "to_tsquery" : "plainto_tsquery"
215
+ }('${langConfig}', ${paramName})`;
216
+ return [
217
+ tsvectorCondition,
218
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
219
+ ];
220
+ }
221
+ case "ilike": {
222
+ // Use lower() for case-insensitive search in SQLite
223
+ const escapedTerm = escapeSpecialChars(baseValue);
224
+ const conditions = state.searchableColumns.map(
225
+ (column) => `lower(${column}) LIKE lower(${paramName})`
226
+ );
227
+ const sql =
228
+ conditions.length === 1
229
+ ? conditions[0]
230
+ : `(${conditions.join(" OR ")})`;
231
+
232
+ if (hasWildcard) {
233
+ return [sql, addValue(newState, `${escapedTerm}%`)];
234
+ } else {
235
+ return [sql, addValue(newState, `%${escapedTerm}%`)];
236
+ }
237
+ }
238
+ }
239
+ };
240
+
241
+ /**
242
+ * Convert a field:value pair to SQL based on search type
243
+ */
244
+ const fieldValueToSql = (
245
+ field: string,
246
+ value: string,
247
+ state: SqlState
248
+ ): [string, SqlState] => {
249
+ const [paramName, newState] = nextParam(state);
250
+ const schema = state.schemas.get(field.toLowerCase());
251
+ const hasWildcard = value.endsWith("*");
252
+ const cleanedValue = cleanQuotedString(value);
253
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
254
+
255
+ if (state.searchType === "paradedb") {
256
+ switch (schema?.type) {
257
+ case "date": {
258
+ // Use parameter binding for dates
259
+ const [dateParam, dateState] = nextParam(state);
260
+ return [
261
+ `${field} @@@ '"' || ${dateParam} || '"'`,
262
+ addValue(dateState, baseValue),
263
+ ];
264
+ }
265
+ case "number":
266
+ return [`${field} @@@ '${baseValue}'`, newState];
267
+ default: {
268
+ const queryValue = prepareParadeDBString(baseValue, hasWildcard);
269
+ return [`${field} @@@ ${paramName}`, addValue(newState, queryValue)];
270
+ }
271
+ }
272
+ }
273
+
274
+ // Rest of the function remains the same...
275
+ switch (schema?.type) {
276
+ case "date":
277
+ return [
278
+ `${field} = ${paramName}`,
279
+ addValue(newState, cleanedValue),
280
+ ];
281
+ case "number":
282
+ return [
283
+ `${field} = ${paramName}`,
284
+ addValue(newState, Number(cleanedValue)),
285
+ ];
286
+ default:
287
+ if (
288
+ state.searchType === "tsvector" &&
289
+ state.searchableColumns.includes(field)
290
+ ) {
291
+ const langConfig = state.language || "english";
292
+ return [
293
+ `to_tsvector('${langConfig}', ${field}) @@ ${
294
+ hasWildcard ? "to_tsquery" : "plainto_tsquery"
295
+ }('${langConfig}', ${paramName})`,
296
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
297
+ ];
298
+ } else {
299
+ const escapedValue = escapeSpecialChars(baseValue);
300
+ return [
301
+ `lower(${field}) LIKE lower(${paramName})`,
302
+ addValue(
303
+ newState,
304
+ hasWildcard ? `${escapedValue}%` : `%${escapedValue}%`
305
+ ),
306
+ ];
307
+ }
308
+ }
309
+ };
310
+
311
+ /**
312
+ * Convert a range expression to SQL
313
+ */
314
+ const rangeToSql = (
315
+ field: string,
316
+ operator: string,
317
+ value: string,
318
+ value2: string | undefined,
319
+ state: SqlState
320
+ ): [string, SqlState] => {
321
+ const schema = state.schemas.get(field.toLowerCase());
322
+ const isDateField = schema?.type === "date";
323
+
324
+ if (state.searchType === "paradedb") {
325
+ if (operator === "BETWEEN" && value2) {
326
+ const [param1, state1] = nextParam(state);
327
+ const [param2, state2] = nextParam(state1);
328
+ let val1 = isDateField ? value : Number(value);
329
+ let val2 = isDateField ? value2 : Number(value2);
330
+ return [
331
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
332
+ addValue(addValue(state2, val1), val2),
333
+ ];
334
+ } else {
335
+ const [paramName, newState] = nextParam(state);
336
+ const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
337
+ const val = isDateField ? value : Number(value);
338
+ return [
339
+ `${field} @@@ '${rangeOp}' || ${paramName}`,
340
+ addValue(newState, val),
341
+ ];
342
+ }
343
+ }
344
+
345
+ if (operator === "BETWEEN" && value2) {
346
+ const [param1, state1] = nextParam(state);
347
+ const [param2, state2] = nextParam(state1);
348
+ let val1 = isDateField ? value : Number(value);
349
+ let val2 = isDateField ? value2 : Number(value2);
350
+ return [
351
+ `${field} BETWEEN ${param1} AND ${param2}`,
352
+ addValue(addValue(state2, val1), val2),
353
+ ];
354
+ }
355
+
356
+ const [paramName, newState] = nextParam(state);
357
+ const val = isDateField ? value : Number(value);
358
+ return [
359
+ `${field} ${operator} ${paramName}`,
360
+ addValue(newState, val),
361
+ ];
362
+ };
363
+
364
+ const inExpressionToSql = (
365
+ field: string,
366
+ values: string[],
367
+ state: SqlState
368
+ ): [string, SqlState] => {
369
+ let currentState = state;
370
+ const cleanedValues = values.map((v) => cleanQuotedString(v));
371
+
372
+ if (state.searchType === "paradedb") {
373
+ const paramNames: string[] = [];
374
+
375
+ for (const value of cleanedValues) {
376
+ const [paramName, newState] = nextParam(currentState);
377
+ paramNames.push(paramName);
378
+ currentState = addValue(newState, value);
379
+ }
380
+
381
+ const concatExpr = paramNames.join(" || ' ' || ");
382
+ return [`${field} @@@ 'IN[' || ${concatExpr} || ']'`, currentState];
383
+ }
384
+
385
+ const paramNames: string[] = [];
386
+ const schema = state.schemas.get(field.toLowerCase());
387
+
388
+ for (const value of cleanedValues) {
389
+ const [paramName, newState] = nextParam(currentState);
390
+ paramNames.push(paramName);
391
+ currentState = addValue(
392
+ newState,
393
+ schema?.type === "number" ? Number(value) : value
394
+ );
395
+ }
396
+
397
+ return [
398
+ `${field} IN (${paramNames
399
+ .map((p) => p)
400
+ .join(", ")})`,
401
+ currentState,
402
+ ];
403
+ };
404
+
405
+ /**
406
+ * Convert a binary operation (AND/OR) to SQL
407
+ */
408
+ const binaryOpToSql = (
409
+ operator: string,
410
+ left: Expression,
411
+ right: Expression,
412
+ state: SqlState
413
+ ): [string, SqlState] => {
414
+ const [leftText, leftState] = expressionToSql(left, state);
415
+ const [rightText, rightState] = expressionToSql(right, leftState);
416
+ return [`(${leftText} ${operator} ${rightText})`, rightState];
417
+ };
418
+
419
+ /**
420
+ * Convert a single expression to SQL
421
+ */
422
+ const expressionToSql = (
423
+ expr: Expression,
424
+ state: SqlState
425
+ ): [string, SqlState] => {
426
+ switch (expr.type) {
427
+ case "SEARCH_TERM":
428
+ return searchTermToSql(expr.value, state);
429
+
430
+ case "WILDCARD":
431
+ return wildcardPatternToSql(expr, state);
432
+
433
+ case "IN":
434
+ return inExpressionToSql(
435
+ expr.field.value,
436
+ expr.values.map((v) => v.value),
437
+ state
438
+ );
439
+
440
+ case "FIELD_VALUE":
441
+ return fieldValueToSql(expr.field.value, expr.value.value, state);
442
+
443
+ case "RANGE":
444
+ return rangeToSql(
445
+ expr.field.value,
446
+ expr.operator,
447
+ expr.value.value,
448
+ expr.value2?.value,
449
+ state
450
+ );
451
+
452
+ case "AND":
453
+ return binaryOpToSql("AND", expr.left, expr.right, state);
454
+
455
+ case "OR":
456
+ return binaryOpToSql("OR", expr.left, expr.right, state);
457
+
458
+ case "NOT": {
459
+ const [sqlText, newState] = expressionToSql(expr.expression, state);
460
+ return [`NOT ${sqlText}`, newState];
461
+ }
462
+ }
463
+ };
464
+
465
+ /**
466
+ * Convert a SearchQuery to a SQL WHERE clause with specified search type
467
+ */
468
+ export const searchQueryToSql = (
469
+ query: SearchQuery,
470
+ searchableColumns: string[],
471
+ schemas: FieldSchema[] = [],
472
+ options: SearchQueryOptions = {}
473
+ ): SqlQueryResult => {
474
+ const initialState: SqlState = {
475
+ paramCounter: 1,
476
+ values: [],
477
+ searchableColumns,
478
+ schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
479
+ searchType: options.searchType || "ilike",
480
+ language: options.language,
481
+ };
482
+
483
+ if (!query.expression) {
484
+ return { text: "1=1", values: [] };
485
+ }
486
+
487
+ const [text, finalState] = expressionToSql(query.expression, initialState);
488
+ return { text, values: finalState.values };
489
+ };
490
+
491
+ /**
492
+ * Convert a search string directly to SQL
493
+ */
494
+ export const searchStringToSql = (
495
+ searchString: string,
496
+ searchableColumns: string[],
497
+ schemas: FieldSchema[] = [],
498
+ options: SearchQueryOptions = {}
499
+ ): SqlQueryResult => {
500
+ const query = parseSearchInputQuery(searchString, schemas);
501
+ if (query.type === "SEARCH_QUERY_ERROR") {
502
+ throw new Error(`Parse error: ${query.errors[0].message}`);
503
+ }
504
+
505
+ return searchQueryToSql(query, searchableColumns, schemas, options);
506
+ };
@@ -0,0 +1,153 @@
1
+ import { FirstPassExpression } from "./first-pass-parser";
2
+ import { parseRangeExpression } from "./parse-range-expression";
3
+ import { FieldSchema, Expression, Value } from "./parser";
4
+
5
+ // Helper to transform FirstPassExpression into Expression
6
+ export const transformToExpression = (
7
+ expr: FirstPassExpression,
8
+ schemas: Map<string, FieldSchema>
9
+ ): Expression => {
10
+ switch (expr.type) {
11
+ case "NOT":
12
+ return {
13
+ type: "NOT",
14
+ expression: transformToExpression(expr.expression, schemas),
15
+ position: expr.position,
16
+ length: expr.length,
17
+ };
18
+
19
+ case "WILDCARD":
20
+ // Check if this is part of a field:value pattern by looking at the prefix
21
+ const colonIndex = expr.prefix.indexOf(":");
22
+ if (colonIndex !== -1) {
23
+ const field = expr.prefix.substring(0, colonIndex).trim();
24
+ const prefix = expr.prefix.substring(colonIndex + 1).trim();
25
+
26
+ return {
27
+ type: "FIELD_VALUE",
28
+ field: {
29
+ type: "FIELD",
30
+ value: field,
31
+ position: expr.position - colonIndex - 1, // Adjust for the field part
32
+ length: colonIndex,
33
+ },
34
+ value: {
35
+ type: "VALUE",
36
+ value: prefix + "*", // Preserve the wildcard in the value
37
+ position: expr.position,
38
+ length: prefix.length + 1,
39
+ },
40
+ };
41
+ }
42
+
43
+ // If not a field:value pattern, return as a wildcard search term
44
+ return {
45
+ type: "WILDCARD",
46
+ prefix: expr.prefix,
47
+ quoted: expr.quoted,
48
+ position: expr.position,
49
+ length: expr.length,
50
+ };
51
+
52
+ case "STRING": {
53
+ // Check if the string is a field:value pattern
54
+ const colonIndex = expr.value.indexOf(":");
55
+ if (colonIndex !== -1) {
56
+ const field = expr.value.substring(0, colonIndex).trim();
57
+ let value = expr.value.substring(colonIndex + 1).trim();
58
+ // Remove quotes if present
59
+ value =
60
+ value.startsWith('"') && value.endsWith('"')
61
+ ? value.slice(1, -1)
62
+ : value;
63
+
64
+ const schema = schemas.get(field.toLowerCase());
65
+
66
+ // Check for range patterns when we have a numeric or date field
67
+ if (schema && (schema.type === "number" || schema.type === "date")) {
68
+ return parseRangeExpression(
69
+ field,
70
+ value,
71
+ schema,
72
+ expr.position,
73
+ colonIndex
74
+ );
75
+ }
76
+
77
+ return {
78
+ type: "FIELD_VALUE",
79
+ field: {
80
+ type: "FIELD",
81
+ value: field,
82
+ position: expr.position,
83
+ length: colonIndex,
84
+ },
85
+ value: {
86
+ type: "VALUE",
87
+ value,
88
+ position: expr.position + colonIndex + 1,
89
+ length: value.length,
90
+ },
91
+ };
92
+ }
93
+
94
+ return {
95
+ type: "SEARCH_TERM",
96
+ value: expr.value,
97
+ position: expr.position,
98
+ length: expr.length,
99
+ };
100
+ }
101
+
102
+ case "AND":
103
+ return {
104
+ type: "AND",
105
+ left: transformToExpression(expr.left, schemas),
106
+ right: transformToExpression(expr.right, schemas),
107
+ position: expr.position,
108
+ length: expr.length,
109
+ };
110
+
111
+ case "OR":
112
+ return {
113
+ type: "OR",
114
+ left: transformToExpression(expr.left, schemas),
115
+ right: transformToExpression(expr.right, schemas),
116
+ position: expr.position,
117
+ length: expr.length,
118
+ };
119
+
120
+ case "IN": {
121
+ const schema = schemas.get(expr.field.toLowerCase());
122
+ const transformedValues: Value[] = expr.values.map((value, index) => {
123
+ let transformedValue = value;
124
+
125
+ // Handle type conversion based on schema
126
+ if (schema?.type === "number") {
127
+ transformedValue = String(Number(value));
128
+ }
129
+
130
+ return {
131
+ type: "VALUE",
132
+ value: transformedValue,
133
+ position:
134
+ expr.position + expr.field.length + 3 + index * (value.length + 1), // +3 for ":IN"
135
+ length: value.length,
136
+ };
137
+ });
138
+
139
+ return {
140
+ type: "IN",
141
+ field: {
142
+ type: "FIELD",
143
+ value: expr.field,
144
+ position: expr.position,
145
+ length: expr.field.length,
146
+ },
147
+ values: transformedValues,
148
+ position: expr.position,
149
+ length: expr.length,
150
+ };
151
+ }
152
+ }
153
+ };