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,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRangeExpression = void 0;
4
+ const isRangeOperator = (str) => {
5
+ return [">=", ">", "<=", "<"].includes(str);
6
+ };
7
+ const parseRangeExpression = (fieldName, value, schema, position, colonIndex) => {
8
+ // Handle ..20 (less than or equal)
9
+ if (value.startsWith("..")) {
10
+ const numValue = value.slice(2);
11
+ return {
12
+ type: "RANGE",
13
+ field: {
14
+ type: "FIELD",
15
+ value: fieldName,
16
+ position,
17
+ length: colonIndex,
18
+ },
19
+ operator: "<=",
20
+ value: {
21
+ type: "VALUE",
22
+ value: numValue,
23
+ position: position + colonIndex + 3, // after colon and ..
24
+ length: numValue.length,
25
+ },
26
+ position,
27
+ length: colonIndex + 1 + value.length,
28
+ };
29
+ }
30
+ // Handle 10.. (greater than or equal)
31
+ if (value.endsWith("..")) {
32
+ const numValue = value.slice(0, -2);
33
+ return {
34
+ type: "RANGE",
35
+ field: {
36
+ type: "FIELD",
37
+ value: fieldName,
38
+ position,
39
+ length: colonIndex,
40
+ },
41
+ operator: ">=",
42
+ value: {
43
+ type: "VALUE",
44
+ value: numValue,
45
+ position: position + colonIndex + 1,
46
+ length: numValue.length,
47
+ },
48
+ position,
49
+ length: colonIndex + 1 + value.length,
50
+ };
51
+ }
52
+ // Handle date ranges with YYYY-MM-DD format
53
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === "date") {
54
+ const betweenMatch = value.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
55
+ if (betweenMatch) {
56
+ const [_, start, end] = betweenMatch;
57
+ return {
58
+ type: "RANGE",
59
+ field: {
60
+ type: "FIELD",
61
+ value: fieldName,
62
+ position,
63
+ length: colonIndex,
64
+ },
65
+ operator: "BETWEEN",
66
+ value: {
67
+ type: "VALUE",
68
+ value: start,
69
+ position: position + colonIndex + 1,
70
+ length: start.length,
71
+ },
72
+ value2: {
73
+ type: "VALUE",
74
+ value: end,
75
+ position: position + colonIndex + start.length + 3,
76
+ length: end.length,
77
+ },
78
+ position,
79
+ length: colonIndex + 1 + value.length,
80
+ };
81
+ }
82
+ }
83
+ // Handle 10..20 (between), handling floats and negative numbers
84
+ const betweenMatch = value.match(/^(-?\d*\.?\d+)\.\.(-?\d*\.?\d+)$/);
85
+ if (betweenMatch) {
86
+ const [_, start, end] = betweenMatch;
87
+ return {
88
+ type: "RANGE",
89
+ field: {
90
+ type: "FIELD",
91
+ value: fieldName,
92
+ position,
93
+ length: colonIndex,
94
+ },
95
+ operator: "BETWEEN",
96
+ value: {
97
+ type: "VALUE",
98
+ value: start,
99
+ position: position + colonIndex + 1,
100
+ length: start.length,
101
+ },
102
+ value2: {
103
+ type: "VALUE",
104
+ value: end,
105
+ position: position + colonIndex + start.length + 3,
106
+ length: end.length,
107
+ },
108
+ position,
109
+ length: colonIndex + 1 + value.length,
110
+ };
111
+ }
112
+ // Handle >100, >=100, <100, <=100
113
+ if (value.length > 1 && isRangeOperator(value.slice(0, 2))) {
114
+ const operator = value.slice(0, 2);
115
+ const numValue = value.slice(2);
116
+ return {
117
+ type: "RANGE",
118
+ field: {
119
+ type: "FIELD",
120
+ value: fieldName,
121
+ position,
122
+ length: colonIndex,
123
+ },
124
+ operator,
125
+ value: {
126
+ type: "VALUE",
127
+ value: numValue,
128
+ position: position + colonIndex + 3,
129
+ length: numValue.length,
130
+ },
131
+ position,
132
+ length: colonIndex + 1 + value.length,
133
+ };
134
+ }
135
+ if (value.length > 0 && isRangeOperator(value.slice(0, 1))) {
136
+ const operator = value.slice(0, 1);
137
+ const numValue = value.slice(1);
138
+ return {
139
+ type: "RANGE",
140
+ field: {
141
+ type: "FIELD",
142
+ value: fieldName,
143
+ position,
144
+ length: colonIndex,
145
+ },
146
+ operator,
147
+ value: {
148
+ type: "VALUE",
149
+ value: numValue,
150
+ position: position + colonIndex + 2,
151
+ length: numValue.length,
152
+ },
153
+ position,
154
+ length: colonIndex + 1 + value.length,
155
+ };
156
+ }
157
+ // If no range pattern is matched, return a regular field value
158
+ return {
159
+ type: "FIELD_VALUE",
160
+ field: {
161
+ type: "FIELD",
162
+ value: fieldName,
163
+ position,
164
+ length: colonIndex,
165
+ },
166
+ value: {
167
+ type: "VALUE",
168
+ value,
169
+ position: position + colonIndex + 1,
170
+ length: value.length,
171
+ },
172
+ };
173
+ };
174
+ exports.parseRangeExpression = parseRangeExpression;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringify = exports.parseSearchInputQuery = void 0;
4
+ const lexer_1 = require("./lexer");
5
+ const first_pass_parser_1 = require("./first-pass-parser");
6
+ const validator_1 = require("./validator");
7
+ const validate_expression_fields_1 = require("./validate-expression-fields");
8
+ const transform_to_expression_1 = require("./transform-to-expression");
9
+ // Helper function to stringify expressions
10
+ const stringify = (expr) => {
11
+ var _a;
12
+ switch (expr.type) {
13
+ case "SEARCH_TERM":
14
+ return expr.value;
15
+ case "WILDCARD":
16
+ return `${expr.prefix}*`;
17
+ case "FIELD_VALUE":
18
+ return `${expr.field.value}:${expr.value.value}`;
19
+ case "RANGE":
20
+ if (expr.operator === "BETWEEN") {
21
+ return `${expr.field.value}:${expr.value.value}..${(_a = expr.value2) === null || _a === void 0 ? void 0 : _a.value}`;
22
+ }
23
+ return `${expr.field.value}:${expr.operator}${expr.value.value}`;
24
+ case "NOT":
25
+ return `NOT (${stringify(expr.expression)})`;
26
+ case "AND":
27
+ return `(${stringify(expr.left)} AND ${stringify(expr.right)})`;
28
+ case "OR":
29
+ return `(${stringify(expr.left)} OR ${stringify(expr.right)})`;
30
+ case "IN": {
31
+ const values = expr.values.map((v) => v.value).join(",");
32
+ return `${expr.field.value}:IN(${values})`;
33
+ }
34
+ }
35
+ };
36
+ exports.stringify = stringify;
37
+ // Main parse function
38
+ const parseSearchInputQuery = (input, fieldSchemas = []) => {
39
+ try {
40
+ const tokens = (0, lexer_1.tokenize)(input);
41
+ const stream = (0, lexer_1.createStream)(tokens);
42
+ if ((0, lexer_1.currentToken)(stream).type === lexer_1.TokenType.EOF) {
43
+ return { type: "SEARCH_QUERY", expression: null };
44
+ }
45
+ const result = (0, first_pass_parser_1.parseExpression)(stream);
46
+ const finalToken = (0, lexer_1.currentToken)(result.stream);
47
+ if (finalToken.type !== lexer_1.TokenType.EOF) {
48
+ throw {
49
+ message: 'Unexpected ")"',
50
+ position: finalToken.position,
51
+ length: finalToken.length,
52
+ };
53
+ }
54
+ const errors = (0, validator_1.validateSearchQuery)(result.result);
55
+ const fieldErrors = [];
56
+ const allowedFields = fieldSchemas.map((s) => s.name.toLowerCase());
57
+ if (allowedFields.length > 0) {
58
+ const columnSet = new Set(allowedFields.map((col) => col.toLowerCase()));
59
+ const schemaMap = new Map(fieldSchemas.map((s) => [s.name.toLowerCase(), s]));
60
+ (0, validate_expression_fields_1.validateExpressionFields)(result.result, columnSet, fieldErrors, schemaMap);
61
+ }
62
+ const fieldErrorKeys = fieldErrors.map(({ position, length }) => `${position}-${length}`);
63
+ const errorsToRemove = errors.filter(({ position, length }) => fieldErrorKeys.includes(`${position}-${length}`));
64
+ const fieldErrorsFiltered = fieldErrors.filter(({ position, length }) => !errorsToRemove.some((error) => error.position === position && error.length === length));
65
+ const allErrors = [...errors, ...fieldErrorsFiltered].sort((a, b) => a.position - b.position);
66
+ if (allErrors.length > 0) {
67
+ return {
68
+ type: "SEARCH_QUERY_ERROR",
69
+ expression: null,
70
+ errors: allErrors,
71
+ };
72
+ }
73
+ const schemaMap = new Map(fieldSchemas.map((s) => [s.name.toLowerCase(), s]));
74
+ const expression = (0, transform_to_expression_1.transformToExpression)(result.result, schemaMap);
75
+ return { type: "SEARCH_QUERY", expression };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ type: "SEARCH_QUERY_ERROR",
80
+ expression: null,
81
+ errors: [error],
82
+ };
83
+ }
84
+ };
85
+ exports.parseSearchInputQuery = parseSearchInputQuery;
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.searchStringToSql = exports.searchQueryToSql = void 0;
4
+ const parser_1 = require("./parser");
5
+ // Constants
6
+ const SPECIAL_CHARS = ["%", "_"];
7
+ const ESCAPE_CHAR = "\\";
8
+ // Helper Functions
9
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
+ const escapeSpecialChars = (value) => SPECIAL_CHARS.reduce((escaped, char) => escaped.replace(new RegExp(escapeRegExp(char), "g"), ESCAPE_CHAR + char), value);
11
+ // Helper to escape special characters for ParadeDB query syntax
12
+ const escapeParadeDBChars = (value) => {
13
+ const specialChars = [
14
+ "+",
15
+ "^",
16
+ "`",
17
+ ":",
18
+ "{",
19
+ "}",
20
+ '"',
21
+ "[",
22
+ "]",
23
+ "(",
24
+ ")",
25
+ "<",
26
+ ">",
27
+ "~",
28
+ "!",
29
+ "\\",
30
+ "*",
31
+ ];
32
+ return specialChars.reduce((escaped, char) => escaped.replace(new RegExp(escapeRegExp(char), "g"), `\\${char}`), value);
33
+ };
34
+ const stripQuotes = (value) => {
35
+ if (value.startsWith('"') && value.endsWith('"')) {
36
+ return value.slice(1, -1);
37
+ }
38
+ return value;
39
+ };
40
+ const cleanQuotedString = (value, stripOuterQuotes = true) => {
41
+ // First strip the outer quotes if requested
42
+ let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
43
+ // Replace escaped quotes with regular quotes
44
+ cleaned = cleaned.replace(/\\"/g, '"');
45
+ // Clean up any remaining escape characters
46
+ cleaned = cleaned.replace(/\\\\/g, "\\");
47
+ return cleaned;
48
+ };
49
+ const isQuotedString = (value) => {
50
+ return value.startsWith('"') && value.endsWith('"');
51
+ };
52
+ const prepareParadeDBString = (value, includeWildcard = false) => {
53
+ // First clean up the string
54
+ const cleaned = cleanQuotedString(value);
55
+ // For ParadeDB, we need to:
56
+ // 1. Escape special characters (except wildcards)
57
+ // 2. Wrap in quotes
58
+ // 3. Add wildcard if needed
59
+ const escaped = escapeParadeDBChars(cleaned);
60
+ const result = `"${escaped}"`;
61
+ return includeWildcard ? `${result}*` : result;
62
+ };
63
+ // Create a new parameter placeholder and update state
64
+ const nextParam = (state) => {
65
+ const paramName = `$${state.paramCounter}`;
66
+ const newState = {
67
+ ...state,
68
+ paramCounter: state.paramCounter + 1,
69
+ };
70
+ return [paramName, newState];
71
+ };
72
+ // Add a value to the state and return updated state
73
+ const addValue = (state, value) => ({
74
+ ...state,
75
+ values: [...state.values, value],
76
+ });
77
+ /**
78
+ * Convert a wildcard pattern to SQL
79
+ */
80
+ const wildcardPatternToSql = (expr, state) => {
81
+ if (expr.prefix === "") {
82
+ return ["1=1", state];
83
+ }
84
+ const [paramName, newState] = nextParam(state);
85
+ const cleanedPrefix = cleanQuotedString(expr.prefix);
86
+ switch (state.searchType) {
87
+ case "paradedb": {
88
+ const queryValue = prepareParadeDBString(cleanedPrefix, true);
89
+ const conditions = state.searchableColumns.map((column) => `${column} @@@ ${paramName}`);
90
+ const sql = conditions.length === 1
91
+ ? conditions[0]
92
+ : `(${conditions.join(" OR ")})`;
93
+ return [sql, addValue(newState, queryValue)];
94
+ }
95
+ case "tsvector": {
96
+ const langConfig = state.language || "english";
97
+ const conditions = state.searchableColumns.map((column) => `to_tsvector('${langConfig}', ${column})`);
98
+ const tsvectorCondition = `(${conditions.join(" || ")}) @@ to_tsquery('${langConfig}', ${paramName})`;
99
+ return [tsvectorCondition, addValue(newState, `${cleanedPrefix}:*`)];
100
+ }
101
+ default: {
102
+ // ILIKE behavior
103
+ const escapedPrefix = escapeSpecialChars(cleanedPrefix);
104
+ const conditions = state.searchableColumns.map((column) => `lower(${column}) LIKE lower(${paramName})`);
105
+ const sql = conditions.length === 1
106
+ ? conditions[0]
107
+ : `(${conditions.join(" OR ")})`;
108
+ return [sql, addValue(newState, `${escapedPrefix}%`)];
109
+ }
110
+ }
111
+ };
112
+ /**
113
+ * Convert a search term to SQL conditions based on search type
114
+ */
115
+ const searchTermToSql = (value, state) => {
116
+ const [paramName, newState] = nextParam(state);
117
+ const hasWildcard = value.endsWith("*");
118
+ const isQuoted = isQuotedString(value);
119
+ const cleanedValue = cleanQuotedString(value);
120
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
121
+ switch (state.searchType) {
122
+ case "paradedb": {
123
+ const queryValue = prepareParadeDBString(baseValue, hasWildcard);
124
+ const conditions = state.searchableColumns.map((column) => `${column} @@@ ${paramName}`);
125
+ const sql = conditions.length === 1
126
+ ? conditions[0]
127
+ : `(${conditions.join(" OR ")})`;
128
+ return [sql, addValue(newState, queryValue)];
129
+ }
130
+ case "tsvector": {
131
+ const langConfig = state.language || "english";
132
+ const conditions = state.searchableColumns.map((column) => `to_tsvector('${langConfig}', ${column})`);
133
+ const tsvectorCondition = `(${conditions.join(" || ")}) @@ ${hasWildcard ? "to_tsquery" : "plainto_tsquery"}('${langConfig}', ${paramName})`;
134
+ return [
135
+ tsvectorCondition,
136
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
137
+ ];
138
+ }
139
+ case "ilike": {
140
+ // Use lower() for case-insensitive search in SQLite
141
+ const escapedTerm = escapeSpecialChars(baseValue);
142
+ const conditions = state.searchableColumns.map((column) => `lower(${column}) LIKE lower(${paramName})`);
143
+ const sql = conditions.length === 1
144
+ ? conditions[0]
145
+ : `(${conditions.join(" OR ")})`;
146
+ if (hasWildcard) {
147
+ return [sql, addValue(newState, `${escapedTerm}%`)];
148
+ }
149
+ else {
150
+ return [sql, addValue(newState, `%${escapedTerm}%`)];
151
+ }
152
+ }
153
+ }
154
+ };
155
+ /**
156
+ * Convert a field:value pair to SQL based on search type
157
+ */
158
+ const fieldValueToSql = (field, value, state) => {
159
+ const [paramName, newState] = nextParam(state);
160
+ const schema = state.schemas.get(field.toLowerCase());
161
+ const hasWildcard = value.endsWith("*");
162
+ const cleanedValue = cleanQuotedString(value);
163
+ const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
164
+ if (state.searchType === "paradedb") {
165
+ switch (schema === null || schema === void 0 ? void 0 : schema.type) {
166
+ case "date": {
167
+ // Use parameter binding for dates
168
+ const [dateParam, dateState] = nextParam(state);
169
+ return [
170
+ `${field} @@@ '"' || ${dateParam} || '"'`,
171
+ addValue(dateState, baseValue),
172
+ ];
173
+ }
174
+ case "number":
175
+ return [`${field} @@@ '${baseValue}'`, newState];
176
+ default: {
177
+ const queryValue = prepareParadeDBString(baseValue, hasWildcard);
178
+ return [`${field} @@@ ${paramName}`, addValue(newState, queryValue)];
179
+ }
180
+ }
181
+ }
182
+ // Rest of the function remains the same...
183
+ switch (schema === null || schema === void 0 ? void 0 : schema.type) {
184
+ case "date":
185
+ return [
186
+ `${field} = ${paramName}`,
187
+ addValue(newState, cleanedValue),
188
+ ];
189
+ case "number":
190
+ return [
191
+ `${field} = ${paramName}`,
192
+ addValue(newState, Number(cleanedValue)),
193
+ ];
194
+ default:
195
+ if (state.searchType === "tsvector" &&
196
+ state.searchableColumns.includes(field)) {
197
+ const langConfig = state.language || "english";
198
+ return [
199
+ `to_tsvector('${langConfig}', ${field}) @@ ${hasWildcard ? "to_tsquery" : "plainto_tsquery"}('${langConfig}', ${paramName})`,
200
+ addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
201
+ ];
202
+ }
203
+ else {
204
+ const escapedValue = escapeSpecialChars(baseValue);
205
+ return [
206
+ `lower(${field}) LIKE lower(${paramName})`,
207
+ addValue(newState, hasWildcard ? `${escapedValue}%` : `%${escapedValue}%`),
208
+ ];
209
+ }
210
+ }
211
+ };
212
+ /**
213
+ * Convert a range expression to SQL
214
+ */
215
+ const rangeToSql = (field, operator, value, value2, state) => {
216
+ const schema = state.schemas.get(field.toLowerCase());
217
+ const isDateField = (schema === null || schema === void 0 ? void 0 : schema.type) === "date";
218
+ if (state.searchType === "paradedb") {
219
+ if (operator === "BETWEEN" && value2) {
220
+ const [param1, state1] = nextParam(state);
221
+ const [param2, state2] = nextParam(state1);
222
+ let val1 = isDateField ? value : Number(value);
223
+ let val2 = isDateField ? value2 : Number(value2);
224
+ return [
225
+ `${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
226
+ addValue(addValue(state2, val1), val2),
227
+ ];
228
+ }
229
+ else {
230
+ const [paramName, newState] = nextParam(state);
231
+ const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
232
+ const val = isDateField ? value : Number(value);
233
+ return [
234
+ `${field} @@@ '${rangeOp}' || ${paramName}`,
235
+ addValue(newState, val),
236
+ ];
237
+ }
238
+ }
239
+ if (operator === "BETWEEN" && value2) {
240
+ const [param1, state1] = nextParam(state);
241
+ const [param2, state2] = nextParam(state1);
242
+ let val1 = isDateField ? value : Number(value);
243
+ let val2 = isDateField ? value2 : Number(value2);
244
+ return [
245
+ `${field} BETWEEN ${param1} AND ${param2}`,
246
+ addValue(addValue(state2, val1), val2),
247
+ ];
248
+ }
249
+ const [paramName, newState] = nextParam(state);
250
+ const val = isDateField ? value : Number(value);
251
+ return [
252
+ `${field} ${operator} ${paramName}`,
253
+ addValue(newState, val),
254
+ ];
255
+ };
256
+ const inExpressionToSql = (field, values, state) => {
257
+ let currentState = state;
258
+ const cleanedValues = values.map((v) => cleanQuotedString(v));
259
+ if (state.searchType === "paradedb") {
260
+ const paramNames = [];
261
+ for (const value of cleanedValues) {
262
+ const [paramName, newState] = nextParam(currentState);
263
+ paramNames.push(paramName);
264
+ currentState = addValue(newState, value);
265
+ }
266
+ const concatExpr = paramNames.join(" || ' ' || ");
267
+ return [`${field} @@@ 'IN[' || ${concatExpr} || ']'`, currentState];
268
+ }
269
+ const paramNames = [];
270
+ const schema = state.schemas.get(field.toLowerCase());
271
+ for (const value of cleanedValues) {
272
+ const [paramName, newState] = nextParam(currentState);
273
+ paramNames.push(paramName);
274
+ currentState = addValue(newState, (schema === null || schema === void 0 ? void 0 : schema.type) === "number" ? Number(value) : value);
275
+ }
276
+ return [
277
+ `${field} IN (${paramNames
278
+ .map((p) => p)
279
+ .join(", ")})`,
280
+ currentState,
281
+ ];
282
+ };
283
+ /**
284
+ * Convert a binary operation (AND/OR) to SQL
285
+ */
286
+ const binaryOpToSql = (operator, left, right, state) => {
287
+ const [leftText, leftState] = expressionToSql(left, state);
288
+ const [rightText, rightState] = expressionToSql(right, leftState);
289
+ return [`(${leftText} ${operator} ${rightText})`, rightState];
290
+ };
291
+ /**
292
+ * Convert a single expression to SQL
293
+ */
294
+ const expressionToSql = (expr, state) => {
295
+ var _a;
296
+ switch (expr.type) {
297
+ case "SEARCH_TERM":
298
+ return searchTermToSql(expr.value, state);
299
+ case "WILDCARD":
300
+ return wildcardPatternToSql(expr, state);
301
+ case "IN":
302
+ return inExpressionToSql(expr.field.value, expr.values.map((v) => v.value), state);
303
+ case "FIELD_VALUE":
304
+ return fieldValueToSql(expr.field.value, expr.value.value, state);
305
+ case "RANGE":
306
+ return rangeToSql(expr.field.value, expr.operator, expr.value.value, (_a = expr.value2) === null || _a === void 0 ? void 0 : _a.value, state);
307
+ case "AND":
308
+ return binaryOpToSql("AND", expr.left, expr.right, state);
309
+ case "OR":
310
+ return binaryOpToSql("OR", expr.left, expr.right, state);
311
+ case "NOT": {
312
+ const [sqlText, newState] = expressionToSql(expr.expression, state);
313
+ return [`NOT ${sqlText}`, newState];
314
+ }
315
+ }
316
+ };
317
+ /**
318
+ * Convert a SearchQuery to a SQL WHERE clause with specified search type
319
+ */
320
+ const searchQueryToSql = (query, searchableColumns, schemas = [], options = {}) => {
321
+ const initialState = {
322
+ paramCounter: 1,
323
+ values: [],
324
+ searchableColumns,
325
+ schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
326
+ searchType: options.searchType || "ilike",
327
+ language: options.language,
328
+ };
329
+ if (!query.expression) {
330
+ return { text: "1=1", values: [] };
331
+ }
332
+ const [text, finalState] = expressionToSql(query.expression, initialState);
333
+ return { text, values: finalState.values };
334
+ };
335
+ exports.searchQueryToSql = searchQueryToSql;
336
+ /**
337
+ * Convert a search string directly to SQL
338
+ */
339
+ const searchStringToSql = (searchString, searchableColumns, schemas = [], options = {}) => {
340
+ const query = (0, parser_1.parseSearchInputQuery)(searchString, schemas);
341
+ if (query.type === "SEARCH_QUERY_ERROR") {
342
+ throw new Error(`Parse error: ${query.errors[0].message}`);
343
+ }
344
+ return (0, exports.searchQueryToSql)(query, searchableColumns, schemas, options);
345
+ };
346
+ exports.searchStringToSql = searchStringToSql;