search-input-query-parser 0.1.1 → 0.1.3
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/README.md +137 -0
- package/dist/cjs/search-query-to-ilike-sql.js +208 -0
- package/dist/cjs/search-query-to-paradedb-sql.js +227 -0
- package/dist/cjs/search-query-to-sql.js +10 -1
- package/dist/cjs/search-query-to-tsvector-sql.js +200 -0
- package/dist/esm/search-query-to-ilike-sql.js +203 -0
- package/dist/esm/search-query-to-paradedb-sql.js +222 -0
- package/dist/esm/search-query-to-sql.js +3 -0
- package/dist/esm/search-query-to-tsvector-sql.js +195 -0
- package/dist/types/search-query-to-ilike-sql.d.ts +16 -0
- package/dist/types/search-query-to-paradedb-sql.d.ts +16 -0
- package/dist/types/search-query-to-sql.d.ts +3 -0
- package/dist/types/search-query-to-tsvector-sql.d.ts +16 -0
- package/package.json +1 -1
- package/src/search-query-to-ilike-sql.ts +332 -0
- package/src/search-query-to-paradedb-sql.ts +346 -0
- package/src/search-query-to-sql.test.ts +53 -55
- package/src/search-query-to-sql.ts +10 -0
- package/src/search-query-to-tsvector-sql.ts +320 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { parseSearchInputQuery, } from "./parser";
|
|
2
|
+
// Constants
|
|
3
|
+
const SPECIAL_CHARS = ["%", "_"];
|
|
4
|
+
const ESCAPE_CHAR = "\\";
|
|
5
|
+
// Helper Functions
|
|
6
|
+
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
|
+
const stripQuotes = (value) => {
|
|
8
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
9
|
+
return value.slice(1, -1);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
const cleanQuotedString = (value, stripOuterQuotes = true) => {
|
|
14
|
+
// First strip the outer quotes if requested
|
|
15
|
+
let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
|
|
16
|
+
// Replace escaped quotes with regular quotes
|
|
17
|
+
cleaned = cleaned.replace(/\\"/g, '"');
|
|
18
|
+
// Clean up any remaining escape characters
|
|
19
|
+
cleaned = cleaned.replace(/\\\\/g, "\\");
|
|
20
|
+
return cleaned;
|
|
21
|
+
};
|
|
22
|
+
// Create a new parameter placeholder and update state
|
|
23
|
+
const nextParam = (state) => {
|
|
24
|
+
const paramName = `$${state.paramCounter}`;
|
|
25
|
+
const newState = {
|
|
26
|
+
...state,
|
|
27
|
+
paramCounter: state.paramCounter + 1,
|
|
28
|
+
};
|
|
29
|
+
return [paramName, newState];
|
|
30
|
+
};
|
|
31
|
+
// Add a value to the state and return updated state
|
|
32
|
+
const addValue = (state, value) => ({
|
|
33
|
+
...state,
|
|
34
|
+
values: [...state.values, value],
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Convert a wildcard pattern to SQL
|
|
38
|
+
*/
|
|
39
|
+
const wildcardPatternToSql = (expr, state) => {
|
|
40
|
+
if (expr.prefix === "") {
|
|
41
|
+
return ["1=1", state];
|
|
42
|
+
}
|
|
43
|
+
const [paramName, newState] = nextParam(state);
|
|
44
|
+
const cleanedPrefix = cleanQuotedString(expr.prefix);
|
|
45
|
+
const langConfig = state.language || "english";
|
|
46
|
+
const conditions = state.searchableColumns.map((column) => `to_tsvector('${langConfig}', ${column})`);
|
|
47
|
+
const tsvectorCondition = `(${conditions.join(" || ")}) @@ to_tsquery('${langConfig}', ${paramName})`;
|
|
48
|
+
return [tsvectorCondition, addValue(newState, `${cleanedPrefix}:*`)];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Convert a search term to SQL conditions based on search type
|
|
52
|
+
*/
|
|
53
|
+
const searchTermToSql = (value, state) => {
|
|
54
|
+
const [paramName, newState] = nextParam(state);
|
|
55
|
+
const hasWildcard = value.endsWith("*");
|
|
56
|
+
const cleanedValue = cleanQuotedString(value);
|
|
57
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
58
|
+
const langConfig = state.language || "english";
|
|
59
|
+
const conditions = state.searchableColumns.map((column) => `to_tsvector('${langConfig}', ${column})`);
|
|
60
|
+
const tsvectorCondition = `(${conditions.join(" || ")}) @@ ${hasWildcard ? "to_tsquery" : "plainto_tsquery"}('${langConfig}', ${paramName})`;
|
|
61
|
+
return [
|
|
62
|
+
tsvectorCondition,
|
|
63
|
+
addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
|
|
64
|
+
];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Convert a field:value pair to SQL based on search type
|
|
68
|
+
*/
|
|
69
|
+
const fieldValueToSql = (field, value, state) => {
|
|
70
|
+
const [paramName, newState] = nextParam(state);
|
|
71
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
72
|
+
const hasWildcard = value.endsWith("*");
|
|
73
|
+
const cleanedValue = cleanQuotedString(value);
|
|
74
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
75
|
+
// Rest of the function remains the same...
|
|
76
|
+
switch (schema === null || schema === void 0 ? void 0 : schema.type) {
|
|
77
|
+
case "date":
|
|
78
|
+
return [
|
|
79
|
+
`${field} = ${paramName}`,
|
|
80
|
+
addValue(newState, cleanedValue),
|
|
81
|
+
];
|
|
82
|
+
case "number":
|
|
83
|
+
return [
|
|
84
|
+
`${field} = ${paramName}`,
|
|
85
|
+
addValue(newState, Number(cleanedValue)),
|
|
86
|
+
];
|
|
87
|
+
default:
|
|
88
|
+
const langConfig = state.language || "english";
|
|
89
|
+
return [
|
|
90
|
+
`to_tsvector('${langConfig}', ${field}) @@ ${hasWildcard ? "to_tsquery" : "plainto_tsquery"}('${langConfig}', ${paramName})`,
|
|
91
|
+
addValue(newState, hasWildcard ? `${baseValue}:*` : baseValue),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Convert a range expression to SQL
|
|
97
|
+
*/
|
|
98
|
+
const rangeToSql = (field, operator, value, value2, state) => {
|
|
99
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
100
|
+
const isDateField = (schema === null || schema === void 0 ? void 0 : schema.type) === "date";
|
|
101
|
+
if (operator === "BETWEEN" && value2) {
|
|
102
|
+
const [param1, state1] = nextParam(state);
|
|
103
|
+
const [param2, state2] = nextParam(state1);
|
|
104
|
+
let val1 = isDateField ? value : Number(value);
|
|
105
|
+
let val2 = isDateField ? value2 : Number(value2);
|
|
106
|
+
return [
|
|
107
|
+
`${field} BETWEEN ${param1} AND ${param2}`,
|
|
108
|
+
addValue(addValue(state2, val1), val2),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
const [paramName, newState] = nextParam(state);
|
|
112
|
+
const val = isDateField ? value : Number(value);
|
|
113
|
+
return [
|
|
114
|
+
`${field} ${operator} ${paramName}`,
|
|
115
|
+
addValue(newState, val),
|
|
116
|
+
];
|
|
117
|
+
};
|
|
118
|
+
const inExpressionToSql = (field, values, state) => {
|
|
119
|
+
let currentState = state;
|
|
120
|
+
const cleanedValues = values.map((v) => cleanQuotedString(v));
|
|
121
|
+
const paramNames = [];
|
|
122
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
123
|
+
for (const value of cleanedValues) {
|
|
124
|
+
const [paramName, newState] = nextParam(currentState);
|
|
125
|
+
paramNames.push(paramName);
|
|
126
|
+
currentState = addValue(newState, (schema === null || schema === void 0 ? void 0 : schema.type) === "number" ? Number(value) : value);
|
|
127
|
+
}
|
|
128
|
+
return [
|
|
129
|
+
`${field} IN (${paramNames
|
|
130
|
+
.map((p) => p)
|
|
131
|
+
.join(", ")})`,
|
|
132
|
+
currentState,
|
|
133
|
+
];
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Convert a binary operation (AND/OR) to SQL
|
|
137
|
+
*/
|
|
138
|
+
const binaryOpToSql = (operator, left, right, state) => {
|
|
139
|
+
const [leftText, leftState] = expressionToSql(left, state);
|
|
140
|
+
const [rightText, rightState] = expressionToSql(right, leftState);
|
|
141
|
+
return [`(${leftText} ${operator} ${rightText})`, rightState];
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Convert a single expression to SQL
|
|
145
|
+
*/
|
|
146
|
+
const expressionToSql = (expr, state) => {
|
|
147
|
+
var _a;
|
|
148
|
+
switch (expr.type) {
|
|
149
|
+
case "SEARCH_TERM":
|
|
150
|
+
return searchTermToSql(expr.value, state);
|
|
151
|
+
case "WILDCARD":
|
|
152
|
+
return wildcardPatternToSql(expr, state);
|
|
153
|
+
case "IN":
|
|
154
|
+
return inExpressionToSql(expr.field.value, expr.values.map((v) => v.value), state);
|
|
155
|
+
case "FIELD_VALUE":
|
|
156
|
+
return fieldValueToSql(expr.field.value, expr.value.value, state);
|
|
157
|
+
case "RANGE":
|
|
158
|
+
return rangeToSql(expr.field.value, expr.operator, expr.value.value, (_a = expr.value2) === null || _a === void 0 ? void 0 : _a.value, state);
|
|
159
|
+
case "AND":
|
|
160
|
+
return binaryOpToSql("AND", expr.left, expr.right, state);
|
|
161
|
+
case "OR":
|
|
162
|
+
return binaryOpToSql("OR", expr.left, expr.right, state);
|
|
163
|
+
case "NOT": {
|
|
164
|
+
const [sqlText, newState] = expressionToSql(expr.expression, state);
|
|
165
|
+
return [`NOT ${sqlText}`, newState];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
171
|
+
*/
|
|
172
|
+
export const searchQueryToTsVectorSql = (query, searchableColumns, schemas = [], options = {}) => {
|
|
173
|
+
const initialState = {
|
|
174
|
+
paramCounter: 1,
|
|
175
|
+
values: [],
|
|
176
|
+
searchableColumns,
|
|
177
|
+
schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
|
|
178
|
+
language: options.language,
|
|
179
|
+
};
|
|
180
|
+
if (!query.expression) {
|
|
181
|
+
return { text: "1=1", values: [] };
|
|
182
|
+
}
|
|
183
|
+
const [text, finalState] = expressionToSql(query.expression, initialState);
|
|
184
|
+
return { text, values: finalState.values };
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Convert a search string directly to SQL
|
|
188
|
+
*/
|
|
189
|
+
export const searchStringToTsVectorSql = (searchString, searchableColumns, schemas = [], options = {}) => {
|
|
190
|
+
const query = parseSearchInputQuery(searchString, schemas);
|
|
191
|
+
if (query.type === "SEARCH_QUERY_ERROR") {
|
|
192
|
+
throw new Error(`Parse error: ${query.errors[0].message}`);
|
|
193
|
+
}
|
|
194
|
+
return searchQueryToTsVectorSql(query, searchableColumns, schemas, options);
|
|
195
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SearchQuery, FieldSchema } from "./parser";
|
|
2
|
+
export interface SqlQueryResult {
|
|
3
|
+
text: string;
|
|
4
|
+
values: any[];
|
|
5
|
+
}
|
|
6
|
+
export interface SearchQueryOptions {
|
|
7
|
+
language?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
11
|
+
*/
|
|
12
|
+
export declare const searchQueryToIlikeSql: (query: SearchQuery, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a search string directly to SQL
|
|
15
|
+
*/
|
|
16
|
+
export declare const searchStringToIlikeSql: (searchString: string, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SearchQuery, FieldSchema } from "./parser";
|
|
2
|
+
export interface SqlQueryResult {
|
|
3
|
+
text: string;
|
|
4
|
+
values: any[];
|
|
5
|
+
}
|
|
6
|
+
export interface SearchQueryOptions {
|
|
7
|
+
language?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
11
|
+
*/
|
|
12
|
+
export declare const searchQueryToParadeDbSql: (query: SearchQuery, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a search string directly to SQL
|
|
15
|
+
*/
|
|
16
|
+
export declare const searchStringToParadeDbSql: (searchString: string, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { SearchQuery, FieldSchema } from "./parser";
|
|
2
|
+
export { searchStringToIlikeSql, searchQueryToIlikeSql, } from "./search-query-to-ilike-sql";
|
|
3
|
+
export { searchQueryToTsVectorSql, searchStringToTsVectorSql } from "./search-query-to-tsvector-sql";
|
|
4
|
+
export { searchQueryToParadeDbSql, searchStringToParadeDbSql, } from "./search-query-to-paradedb-sql";
|
|
2
5
|
export interface SqlQueryResult {
|
|
3
6
|
text: string;
|
|
4
7
|
values: any[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SearchQuery, FieldSchema } from "./parser";
|
|
2
|
+
export interface SqlQueryResult {
|
|
3
|
+
text: string;
|
|
4
|
+
values: any[];
|
|
5
|
+
}
|
|
6
|
+
export interface SearchQueryOptions {
|
|
7
|
+
language?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
11
|
+
*/
|
|
12
|
+
export declare const searchQueryToTsVectorSql: (query: SearchQuery, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a search string directly to SQL
|
|
15
|
+
*/
|
|
16
|
+
export declare const searchStringToTsVectorSql: (searchString: string, searchableColumns: string[], schemas?: FieldSchema[], options?: SearchQueryOptions) => SqlQueryResult;
|
package/package.json
CHANGED
|
@@ -0,0 +1,332 @@
|
|
|
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
|
+
language?: string; // For tsvector configuration
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SearchQueryOptions {
|
|
23
|
+
language?: string; // PostgreSQL language configuration for tsvector
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Constants
|
|
27
|
+
const SPECIAL_CHARS = ["%", "_"] as const;
|
|
28
|
+
const ESCAPE_CHAR = "\\";
|
|
29
|
+
|
|
30
|
+
// Helper Functions
|
|
31
|
+
const escapeRegExp = (str: string): string =>
|
|
32
|
+
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
|
|
34
|
+
const escapeSpecialChars = (value: string): string =>
|
|
35
|
+
SPECIAL_CHARS.reduce(
|
|
36
|
+
(escaped, char) =>
|
|
37
|
+
escaped.replace(new RegExp(escapeRegExp(char), "g"), ESCAPE_CHAR + char),
|
|
38
|
+
value
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const stripQuotes = (value: string): string => {
|
|
42
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
43
|
+
return value.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const cleanQuotedString = (
|
|
49
|
+
value: string,
|
|
50
|
+
stripOuterQuotes: boolean = true
|
|
51
|
+
): string => {
|
|
52
|
+
// First strip the outer quotes if requested
|
|
53
|
+
let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
|
|
54
|
+
|
|
55
|
+
// Replace escaped quotes with regular quotes
|
|
56
|
+
cleaned = cleaned.replace(/\\"/g, '"');
|
|
57
|
+
|
|
58
|
+
// Clean up any remaining escape characters
|
|
59
|
+
cleaned = cleaned.replace(/\\\\/g, "\\");
|
|
60
|
+
|
|
61
|
+
return cleaned;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Create a new parameter placeholder and update state
|
|
65
|
+
const nextParam = (state: SqlState): [string, SqlState] => {
|
|
66
|
+
const paramName = `$${state.paramCounter}`;
|
|
67
|
+
const newState = {
|
|
68
|
+
...state,
|
|
69
|
+
paramCounter: state.paramCounter + 1,
|
|
70
|
+
};
|
|
71
|
+
return [paramName, newState];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Add a value to the state and return updated state
|
|
75
|
+
const addValue = (state: SqlState, value: any): SqlState => ({
|
|
76
|
+
...state,
|
|
77
|
+
values: [...state.values, value],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert a wildcard pattern to SQL
|
|
82
|
+
*/
|
|
83
|
+
const wildcardPatternToSql = (
|
|
84
|
+
expr: WildcardPattern,
|
|
85
|
+
state: SqlState
|
|
86
|
+
): [string, SqlState] => {
|
|
87
|
+
if (expr.prefix === "") {
|
|
88
|
+
return ["1=1", state];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [paramName, newState] = nextParam(state);
|
|
92
|
+
const cleanedPrefix = cleanQuotedString(expr.prefix);
|
|
93
|
+
|
|
94
|
+
const escapedPrefix = escapeSpecialChars(cleanedPrefix);
|
|
95
|
+
const conditions = state.searchableColumns.map(
|
|
96
|
+
(column) => `lower(${column}) LIKE lower(${paramName})`
|
|
97
|
+
);
|
|
98
|
+
const sql =
|
|
99
|
+
conditions.length === 1
|
|
100
|
+
? conditions[0]
|
|
101
|
+
: `(${conditions.join(" OR ")})`;
|
|
102
|
+
return [sql, addValue(newState, `${escapedPrefix}%`)];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convert a search term to SQL conditions based on search type
|
|
107
|
+
*/
|
|
108
|
+
const searchTermToSql = (
|
|
109
|
+
value: string,
|
|
110
|
+
state: SqlState
|
|
111
|
+
): [string, SqlState] => {
|
|
112
|
+
const [paramName, newState] = nextParam(state);
|
|
113
|
+
const hasWildcard = value.endsWith("*");
|
|
114
|
+
const cleanedValue = cleanQuotedString(value);
|
|
115
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
116
|
+
|
|
117
|
+
// Use lower() for case-insensitive search in SQLite
|
|
118
|
+
const escapedTerm = escapeSpecialChars(baseValue);
|
|
119
|
+
const conditions = state.searchableColumns.map(
|
|
120
|
+
(column) => `lower(${column}) LIKE lower(${paramName})`
|
|
121
|
+
);
|
|
122
|
+
const sql =
|
|
123
|
+
conditions.length === 1
|
|
124
|
+
? conditions[0]
|
|
125
|
+
: `(${conditions.join(" OR ")})`;
|
|
126
|
+
|
|
127
|
+
if (hasWildcard) {
|
|
128
|
+
return [sql, addValue(newState, `${escapedTerm}%`)];
|
|
129
|
+
} else {
|
|
130
|
+
return [sql, addValue(newState, `%${escapedTerm}%`)];
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convert a field:value pair to SQL based on search type
|
|
136
|
+
*/
|
|
137
|
+
const fieldValueToSql = (
|
|
138
|
+
field: string,
|
|
139
|
+
value: string,
|
|
140
|
+
state: SqlState
|
|
141
|
+
): [string, SqlState] => {
|
|
142
|
+
const [paramName, newState] = nextParam(state);
|
|
143
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
144
|
+
const hasWildcard = value.endsWith("*");
|
|
145
|
+
const cleanedValue = cleanQuotedString(value);
|
|
146
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
147
|
+
|
|
148
|
+
// Rest of the function remains the same...
|
|
149
|
+
switch (schema?.type) {
|
|
150
|
+
case "date":
|
|
151
|
+
return [
|
|
152
|
+
`${field} = ${paramName}`,
|
|
153
|
+
addValue(newState, cleanedValue),
|
|
154
|
+
];
|
|
155
|
+
case "number":
|
|
156
|
+
return [
|
|
157
|
+
`${field} = ${paramName}`,
|
|
158
|
+
addValue(newState, Number(cleanedValue)),
|
|
159
|
+
];
|
|
160
|
+
default:
|
|
161
|
+
const escapedValue = escapeSpecialChars(baseValue);
|
|
162
|
+
return [
|
|
163
|
+
`lower(${field}) LIKE lower(${paramName})`,
|
|
164
|
+
addValue(
|
|
165
|
+
newState,
|
|
166
|
+
hasWildcard ? `${escapedValue}%` : `%${escapedValue}%`
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Convert a range expression to SQL
|
|
174
|
+
*/
|
|
175
|
+
const rangeToSql = (
|
|
176
|
+
field: string,
|
|
177
|
+
operator: string,
|
|
178
|
+
value: string,
|
|
179
|
+
value2: string | undefined,
|
|
180
|
+
state: SqlState
|
|
181
|
+
): [string, SqlState] => {
|
|
182
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
183
|
+
const isDateField = schema?.type === "date";
|
|
184
|
+
|
|
185
|
+
if (operator === "BETWEEN" && value2) {
|
|
186
|
+
const [param1, state1] = nextParam(state);
|
|
187
|
+
const [param2, state2] = nextParam(state1);
|
|
188
|
+
let val1 = isDateField ? value : Number(value);
|
|
189
|
+
let val2 = isDateField ? value2 : Number(value2);
|
|
190
|
+
return [
|
|
191
|
+
`${field} BETWEEN ${param1} AND ${param2}`,
|
|
192
|
+
addValue(addValue(state2, val1), val2),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const [paramName, newState] = nextParam(state);
|
|
197
|
+
const val = isDateField ? value : Number(value);
|
|
198
|
+
return [
|
|
199
|
+
`${field} ${operator} ${paramName}`,
|
|
200
|
+
addValue(newState, val),
|
|
201
|
+
];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const inExpressionToSql = (
|
|
205
|
+
field: string,
|
|
206
|
+
values: string[],
|
|
207
|
+
state: SqlState
|
|
208
|
+
): [string, SqlState] => {
|
|
209
|
+
let currentState = state;
|
|
210
|
+
const cleanedValues = values.map((v) => cleanQuotedString(v));
|
|
211
|
+
|
|
212
|
+
const paramNames: string[] = [];
|
|
213
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
214
|
+
|
|
215
|
+
for (const value of cleanedValues) {
|
|
216
|
+
const [paramName, newState] = nextParam(currentState);
|
|
217
|
+
paramNames.push(paramName);
|
|
218
|
+
currentState = addValue(
|
|
219
|
+
newState,
|
|
220
|
+
schema?.type === "number" ? Number(value) : value
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [
|
|
225
|
+
`${field} IN (${paramNames
|
|
226
|
+
.map((p) => p)
|
|
227
|
+
.join(", ")})`,
|
|
228
|
+
currentState,
|
|
229
|
+
];
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Convert a binary operation (AND/OR) to SQL
|
|
234
|
+
*/
|
|
235
|
+
const binaryOpToSql = (
|
|
236
|
+
operator: string,
|
|
237
|
+
left: Expression,
|
|
238
|
+
right: Expression,
|
|
239
|
+
state: SqlState
|
|
240
|
+
): [string, SqlState] => {
|
|
241
|
+
const [leftText, leftState] = expressionToSql(left, state);
|
|
242
|
+
const [rightText, rightState] = expressionToSql(right, leftState);
|
|
243
|
+
return [`(${leftText} ${operator} ${rightText})`, rightState];
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convert a single expression to SQL
|
|
248
|
+
*/
|
|
249
|
+
const expressionToSql = (
|
|
250
|
+
expr: Expression,
|
|
251
|
+
state: SqlState
|
|
252
|
+
): [string, SqlState] => {
|
|
253
|
+
switch (expr.type) {
|
|
254
|
+
case "SEARCH_TERM":
|
|
255
|
+
return searchTermToSql(expr.value, state);
|
|
256
|
+
|
|
257
|
+
case "WILDCARD":
|
|
258
|
+
return wildcardPatternToSql(expr, state);
|
|
259
|
+
|
|
260
|
+
case "IN":
|
|
261
|
+
return inExpressionToSql(
|
|
262
|
+
expr.field.value,
|
|
263
|
+
expr.values.map((v) => v.value),
|
|
264
|
+
state
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
case "FIELD_VALUE":
|
|
268
|
+
return fieldValueToSql(expr.field.value, expr.value.value, state);
|
|
269
|
+
|
|
270
|
+
case "RANGE":
|
|
271
|
+
return rangeToSql(
|
|
272
|
+
expr.field.value,
|
|
273
|
+
expr.operator,
|
|
274
|
+
expr.value.value,
|
|
275
|
+
expr.value2?.value,
|
|
276
|
+
state
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
case "AND":
|
|
280
|
+
return binaryOpToSql("AND", expr.left, expr.right, state);
|
|
281
|
+
|
|
282
|
+
case "OR":
|
|
283
|
+
return binaryOpToSql("OR", expr.left, expr.right, state);
|
|
284
|
+
|
|
285
|
+
case "NOT": {
|
|
286
|
+
const [sqlText, newState] = expressionToSql(expr.expression, state);
|
|
287
|
+
return [`NOT ${sqlText}`, newState];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
294
|
+
*/
|
|
295
|
+
export const searchQueryToIlikeSql = (
|
|
296
|
+
query: SearchQuery,
|
|
297
|
+
searchableColumns: string[],
|
|
298
|
+
schemas: FieldSchema[] = [],
|
|
299
|
+
options: SearchQueryOptions = {}
|
|
300
|
+
): SqlQueryResult => {
|
|
301
|
+
const initialState: SqlState = {
|
|
302
|
+
paramCounter: 1,
|
|
303
|
+
values: [],
|
|
304
|
+
searchableColumns,
|
|
305
|
+
schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
|
|
306
|
+
language: options.language,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (!query.expression) {
|
|
310
|
+
return { text: "1=1", values: [] };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const [text, finalState] = expressionToSql(query.expression, initialState);
|
|
314
|
+
return { text, values: finalState.values };
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Convert a search string directly to SQL
|
|
319
|
+
*/
|
|
320
|
+
export const searchStringToIlikeSql = (
|
|
321
|
+
searchString: string,
|
|
322
|
+
searchableColumns: string[],
|
|
323
|
+
schemas: FieldSchema[] = [],
|
|
324
|
+
options: SearchQueryOptions = {}
|
|
325
|
+
): SqlQueryResult => {
|
|
326
|
+
const query = parseSearchInputQuery(searchString, schemas);
|
|
327
|
+
if (query.type === "SEARCH_QUERY_ERROR") {
|
|
328
|
+
throw new Error(`Parse error: ${query.errors[0].message}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return searchQueryToIlikeSql(query, searchableColumns, schemas, options);
|
|
332
|
+
};
|