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
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Search Input Query
|
|
2
|
+
|
|
3
|
+
## Parser
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
npm install search-input-query-parser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Basic Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
parseSearchInputQuery,
|
|
16
|
+
type FieldSchema
|
|
17
|
+
} from 'search-input-query-parser';
|
|
18
|
+
|
|
19
|
+
// Define your field schemas
|
|
20
|
+
const schemas: FieldSchema[] = [
|
|
21
|
+
{ name: 'title', type: 'string' },
|
|
22
|
+
{ name: 'price', type: 'number' },
|
|
23
|
+
{ name: 'date', type: 'date' }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Parse a search query
|
|
27
|
+
const query = 'title:"winter boots" AND price:>100';
|
|
28
|
+
const result = parseSearchInputQuery(query, schemas);
|
|
29
|
+
|
|
30
|
+
if (result.type === 'SEARCH_QUERY') {
|
|
31
|
+
// Handle successful parse where the expression is in AST format
|
|
32
|
+
console.log(result.expression);
|
|
33
|
+
} else {
|
|
34
|
+
// Handle validation errors
|
|
35
|
+
console.log(result.errors);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
## SQL Conversion
|
|
39
|
+
|
|
40
|
+
The parser can convert search queries into SQL WHERE clauses using three different search strategies:
|
|
41
|
+
|
|
42
|
+
1. ILIKE - Case-insensitive pattern matching
|
|
43
|
+
2. tsvector - PostgreSQL full-text search
|
|
44
|
+
3. ParadeDB - BM25-based full-text search
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import {
|
|
50
|
+
searchStringToIlikeSql,
|
|
51
|
+
searchStringToTsVectorSql,
|
|
52
|
+
searchStringToParadeDBSql,
|
|
53
|
+
type FieldSchema
|
|
54
|
+
} from 'search-input-query-parser';
|
|
55
|
+
|
|
56
|
+
// Define searchable columns and schemas
|
|
57
|
+
const searchableColumns = ['title', 'description'];
|
|
58
|
+
const schemas: FieldSchema[] = [
|
|
59
|
+
{ name: 'title', type: 'string' },
|
|
60
|
+
{ name: 'price', type: 'number' },
|
|
61
|
+
{ name: 'date', type: 'date' }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Convert a search query to SQL
|
|
65
|
+
const query = 'winter boots AND price:>100';
|
|
66
|
+
|
|
67
|
+
// Using ILIKE (case-insensitive pattern matching)
|
|
68
|
+
const ilikeResult = searchStringToIlikeSql(query, searchableColumns, schemas);
|
|
69
|
+
// Result:
|
|
70
|
+
// {
|
|
71
|
+
// text: "((lower(title) LIKE lower($1) OR lower(description) LIKE lower($1)) AND price > $2)",
|
|
72
|
+
// values: ["%winter boots%", 100]
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
// Using tsvector (PostgreSQL full-text search)
|
|
76
|
+
const tsvectorResult = searchStringToTsVectorSql(query, searchableColumns, schemas);
|
|
77
|
+
// Result:
|
|
78
|
+
// {
|
|
79
|
+
// text: "((to_tsvector('english', title) || to_tsvector('english', description)) @@ plainto_tsquery('english', $1) AND price > $2)",
|
|
80
|
+
// values: ["winter boots", 100]
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
// Using ParadeDB (BM25 search)
|
|
84
|
+
const paradedbResult = searchStringToParadeDBSql(query, searchableColumns, schemas);
|
|
85
|
+
// Result:
|
|
86
|
+
// {
|
|
87
|
+
// text: "((title @@@ $1 OR description @@@ $1) AND price @@@ '>' || $2)",
|
|
88
|
+
// values: ['"winter boots"', 100]
|
|
89
|
+
// }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Search Types Comparison
|
|
93
|
+
|
|
94
|
+
| Feature | ILIKE | tsvector | ParadeDB |
|
|
95
|
+
|---------|-------|----------|----------|
|
|
96
|
+
| Case Sensitivity | Case-insensitive | Case-insensitive | Case-sensitive |
|
|
97
|
+
| Pattern Matching | Simple wildcards | Language-aware tokens | BM25 ranking |
|
|
98
|
+
| Performance | Slower on large datasets | Fast with proper indexes | Fast with proper indexes |
|
|
99
|
+
| Setup Required | None | PostgreSQL extension | ParadeDB extension |
|
|
100
|
+
| Best For | Simple searches, small datasets | Advanced text search | Relevance-based search |
|
|
101
|
+
|
|
102
|
+
### Configuration Options
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Common options for all search types
|
|
106
|
+
interface SearchQueryOptions {
|
|
107
|
+
language?: string; // Language for text analysis (default: 'english')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Example with options
|
|
111
|
+
const result = searchStringToTsVectorSql(
|
|
112
|
+
query,
|
|
113
|
+
searchableColumns,
|
|
114
|
+
schemas,
|
|
115
|
+
{
|
|
116
|
+
language: 'spanish'
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Using with Raw SQL
|
|
122
|
+
|
|
123
|
+
The converters return objects with `text` (the WHERE clause) and `values` (the parameter values):
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { searchStringToIlikeSql } from 'search-input-query-parser';
|
|
127
|
+
|
|
128
|
+
const base = 'SELECT * FROM products';
|
|
129
|
+
const { text, values } = searchStringToIlikeSql(query, searchableColumns, schemas);
|
|
130
|
+
const fullQuery = `${base} WHERE ${text}`;
|
|
131
|
+
|
|
132
|
+
// Using with node-postgres
|
|
133
|
+
const result = await client.query(fullQuery, values);
|
|
134
|
+
|
|
135
|
+
// Using with Prisma
|
|
136
|
+
const result = await prisma.$queryRaw`${Prisma.raw(base)} WHERE ${Prisma.raw(text)}`;
|
|
137
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchStringToIlikeSql = exports.searchQueryToIlikeSql = 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
|
+
const stripQuotes = (value) => {
|
|
12
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
13
|
+
return value.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const cleanQuotedString = (value, stripOuterQuotes = true) => {
|
|
18
|
+
// First strip the outer quotes if requested
|
|
19
|
+
let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
|
|
20
|
+
// Replace escaped quotes with regular quotes
|
|
21
|
+
cleaned = cleaned.replace(/\\"/g, '"');
|
|
22
|
+
// Clean up any remaining escape characters
|
|
23
|
+
cleaned = cleaned.replace(/\\\\/g, "\\");
|
|
24
|
+
return cleaned;
|
|
25
|
+
};
|
|
26
|
+
// Create a new parameter placeholder and update state
|
|
27
|
+
const nextParam = (state) => {
|
|
28
|
+
const paramName = `$${state.paramCounter}`;
|
|
29
|
+
const newState = {
|
|
30
|
+
...state,
|
|
31
|
+
paramCounter: state.paramCounter + 1,
|
|
32
|
+
};
|
|
33
|
+
return [paramName, newState];
|
|
34
|
+
};
|
|
35
|
+
// Add a value to the state and return updated state
|
|
36
|
+
const addValue = (state, value) => ({
|
|
37
|
+
...state,
|
|
38
|
+
values: [...state.values, value],
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Convert a wildcard pattern to SQL
|
|
42
|
+
*/
|
|
43
|
+
const wildcardPatternToSql = (expr, state) => {
|
|
44
|
+
if (expr.prefix === "") {
|
|
45
|
+
return ["1=1", state];
|
|
46
|
+
}
|
|
47
|
+
const [paramName, newState] = nextParam(state);
|
|
48
|
+
const cleanedPrefix = cleanQuotedString(expr.prefix);
|
|
49
|
+
const escapedPrefix = escapeSpecialChars(cleanedPrefix);
|
|
50
|
+
const conditions = state.searchableColumns.map((column) => `lower(${column}) LIKE lower(${paramName})`);
|
|
51
|
+
const sql = conditions.length === 1
|
|
52
|
+
? conditions[0]
|
|
53
|
+
: `(${conditions.join(" OR ")})`;
|
|
54
|
+
return [sql, addValue(newState, `${escapedPrefix}%`)];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Convert a search term to SQL conditions based on search type
|
|
58
|
+
*/
|
|
59
|
+
const searchTermToSql = (value, state) => {
|
|
60
|
+
const [paramName, newState] = nextParam(state);
|
|
61
|
+
const hasWildcard = value.endsWith("*");
|
|
62
|
+
const cleanedValue = cleanQuotedString(value);
|
|
63
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
64
|
+
// Use lower() for case-insensitive search in SQLite
|
|
65
|
+
const escapedTerm = escapeSpecialChars(baseValue);
|
|
66
|
+
const conditions = state.searchableColumns.map((column) => `lower(${column}) LIKE lower(${paramName})`);
|
|
67
|
+
const sql = conditions.length === 1
|
|
68
|
+
? conditions[0]
|
|
69
|
+
: `(${conditions.join(" OR ")})`;
|
|
70
|
+
if (hasWildcard) {
|
|
71
|
+
return [sql, addValue(newState, `${escapedTerm}%`)];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return [sql, addValue(newState, `%${escapedTerm}%`)];
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Convert a field:value pair to SQL based on search type
|
|
79
|
+
*/
|
|
80
|
+
const fieldValueToSql = (field, value, state) => {
|
|
81
|
+
const [paramName, newState] = nextParam(state);
|
|
82
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
83
|
+
const hasWildcard = value.endsWith("*");
|
|
84
|
+
const cleanedValue = cleanQuotedString(value);
|
|
85
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
86
|
+
// Rest of the function remains the same...
|
|
87
|
+
switch (schema === null || schema === void 0 ? void 0 : schema.type) {
|
|
88
|
+
case "date":
|
|
89
|
+
return [
|
|
90
|
+
`${field} = ${paramName}`,
|
|
91
|
+
addValue(newState, cleanedValue),
|
|
92
|
+
];
|
|
93
|
+
case "number":
|
|
94
|
+
return [
|
|
95
|
+
`${field} = ${paramName}`,
|
|
96
|
+
addValue(newState, Number(cleanedValue)),
|
|
97
|
+
];
|
|
98
|
+
default:
|
|
99
|
+
const escapedValue = escapeSpecialChars(baseValue);
|
|
100
|
+
return [
|
|
101
|
+
`lower(${field}) LIKE lower(${paramName})`,
|
|
102
|
+
addValue(newState, hasWildcard ? `${escapedValue}%` : `%${escapedValue}%`),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Convert a range expression to SQL
|
|
108
|
+
*/
|
|
109
|
+
const rangeToSql = (field, operator, value, value2, state) => {
|
|
110
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
111
|
+
const isDateField = (schema === null || schema === void 0 ? void 0 : schema.type) === "date";
|
|
112
|
+
if (operator === "BETWEEN" && value2) {
|
|
113
|
+
const [param1, state1] = nextParam(state);
|
|
114
|
+
const [param2, state2] = nextParam(state1);
|
|
115
|
+
let val1 = isDateField ? value : Number(value);
|
|
116
|
+
let val2 = isDateField ? value2 : Number(value2);
|
|
117
|
+
return [
|
|
118
|
+
`${field} BETWEEN ${param1} AND ${param2}`,
|
|
119
|
+
addValue(addValue(state2, val1), val2),
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
const [paramName, newState] = nextParam(state);
|
|
123
|
+
const val = isDateField ? value : Number(value);
|
|
124
|
+
return [
|
|
125
|
+
`${field} ${operator} ${paramName}`,
|
|
126
|
+
addValue(newState, val),
|
|
127
|
+
];
|
|
128
|
+
};
|
|
129
|
+
const inExpressionToSql = (field, values, state) => {
|
|
130
|
+
let currentState = state;
|
|
131
|
+
const cleanedValues = values.map((v) => cleanQuotedString(v));
|
|
132
|
+
const paramNames = [];
|
|
133
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
134
|
+
for (const value of cleanedValues) {
|
|
135
|
+
const [paramName, newState] = nextParam(currentState);
|
|
136
|
+
paramNames.push(paramName);
|
|
137
|
+
currentState = addValue(newState, (schema === null || schema === void 0 ? void 0 : schema.type) === "number" ? Number(value) : value);
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
`${field} IN (${paramNames
|
|
141
|
+
.map((p) => p)
|
|
142
|
+
.join(", ")})`,
|
|
143
|
+
currentState,
|
|
144
|
+
];
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Convert a binary operation (AND/OR) to SQL
|
|
148
|
+
*/
|
|
149
|
+
const binaryOpToSql = (operator, left, right, state) => {
|
|
150
|
+
const [leftText, leftState] = expressionToSql(left, state);
|
|
151
|
+
const [rightText, rightState] = expressionToSql(right, leftState);
|
|
152
|
+
return [`(${leftText} ${operator} ${rightText})`, rightState];
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Convert a single expression to SQL
|
|
156
|
+
*/
|
|
157
|
+
const expressionToSql = (expr, state) => {
|
|
158
|
+
var _a;
|
|
159
|
+
switch (expr.type) {
|
|
160
|
+
case "SEARCH_TERM":
|
|
161
|
+
return searchTermToSql(expr.value, state);
|
|
162
|
+
case "WILDCARD":
|
|
163
|
+
return wildcardPatternToSql(expr, state);
|
|
164
|
+
case "IN":
|
|
165
|
+
return inExpressionToSql(expr.field.value, expr.values.map((v) => v.value), state);
|
|
166
|
+
case "FIELD_VALUE":
|
|
167
|
+
return fieldValueToSql(expr.field.value, expr.value.value, state);
|
|
168
|
+
case "RANGE":
|
|
169
|
+
return rangeToSql(expr.field.value, expr.operator, expr.value.value, (_a = expr.value2) === null || _a === void 0 ? void 0 : _a.value, state);
|
|
170
|
+
case "AND":
|
|
171
|
+
return binaryOpToSql("AND", expr.left, expr.right, state);
|
|
172
|
+
case "OR":
|
|
173
|
+
return binaryOpToSql("OR", expr.left, expr.right, state);
|
|
174
|
+
case "NOT": {
|
|
175
|
+
const [sqlText, newState] = expressionToSql(expr.expression, state);
|
|
176
|
+
return [`NOT ${sqlText}`, newState];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
182
|
+
*/
|
|
183
|
+
const searchQueryToIlikeSql = (query, searchableColumns, schemas = [], options = {}) => {
|
|
184
|
+
const initialState = {
|
|
185
|
+
paramCounter: 1,
|
|
186
|
+
values: [],
|
|
187
|
+
searchableColumns,
|
|
188
|
+
schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
|
|
189
|
+
language: options.language,
|
|
190
|
+
};
|
|
191
|
+
if (!query.expression) {
|
|
192
|
+
return { text: "1=1", values: [] };
|
|
193
|
+
}
|
|
194
|
+
const [text, finalState] = expressionToSql(query.expression, initialState);
|
|
195
|
+
return { text, values: finalState.values };
|
|
196
|
+
};
|
|
197
|
+
exports.searchQueryToIlikeSql = searchQueryToIlikeSql;
|
|
198
|
+
/**
|
|
199
|
+
* Convert a search string directly to SQL
|
|
200
|
+
*/
|
|
201
|
+
const searchStringToIlikeSql = (searchString, searchableColumns, schemas = [], options = {}) => {
|
|
202
|
+
const query = (0, parser_1.parseSearchInputQuery)(searchString, schemas);
|
|
203
|
+
if (query.type === "SEARCH_QUERY_ERROR") {
|
|
204
|
+
throw new Error(`Parse error: ${query.errors[0].message}`);
|
|
205
|
+
}
|
|
206
|
+
return (0, exports.searchQueryToIlikeSql)(query, searchableColumns, schemas, options);
|
|
207
|
+
};
|
|
208
|
+
exports.searchStringToIlikeSql = searchStringToIlikeSql;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchStringToParadeDbSql = exports.searchQueryToParadeDbSql = void 0;
|
|
4
|
+
const parser_1 = require("./parser");
|
|
5
|
+
// Helper Functions
|
|
6
|
+
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7
|
+
// Helper to escape special characters for ParadeDB query syntax
|
|
8
|
+
const escapeParadeDBChars = (value) => {
|
|
9
|
+
const specialChars = [
|
|
10
|
+
"+",
|
|
11
|
+
"^",
|
|
12
|
+
"`",
|
|
13
|
+
":",
|
|
14
|
+
"{",
|
|
15
|
+
"}",
|
|
16
|
+
'"',
|
|
17
|
+
"[",
|
|
18
|
+
"]",
|
|
19
|
+
"(",
|
|
20
|
+
")",
|
|
21
|
+
"<",
|
|
22
|
+
">",
|
|
23
|
+
"~",
|
|
24
|
+
"!",
|
|
25
|
+
"\\",
|
|
26
|
+
"*",
|
|
27
|
+
];
|
|
28
|
+
return specialChars.reduce((escaped, char) => escaped.replace(new RegExp(escapeRegExp(char), "g"), `\\${char}`), value);
|
|
29
|
+
};
|
|
30
|
+
const stripQuotes = (value) => {
|
|
31
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
32
|
+
return value.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const cleanQuotedString = (value, stripOuterQuotes = true) => {
|
|
37
|
+
// First strip the outer quotes if requested
|
|
38
|
+
let cleaned = stripOuterQuotes ? stripQuotes(value) : value;
|
|
39
|
+
// Replace escaped quotes with regular quotes
|
|
40
|
+
cleaned = cleaned.replace(/\\"/g, '"');
|
|
41
|
+
// Clean up any remaining escape characters
|
|
42
|
+
cleaned = cleaned.replace(/\\\\/g, "\\");
|
|
43
|
+
return cleaned;
|
|
44
|
+
};
|
|
45
|
+
const prepareParadeDBString = (value, includeWildcard = false) => {
|
|
46
|
+
// First clean up the string
|
|
47
|
+
const cleaned = cleanQuotedString(value);
|
|
48
|
+
// For ParadeDB, we need to:
|
|
49
|
+
// 1. Escape special characters (except wildcards)
|
|
50
|
+
// 2. Wrap in quotes
|
|
51
|
+
// 3. Add wildcard if needed
|
|
52
|
+
const escaped = escapeParadeDBChars(cleaned);
|
|
53
|
+
const result = `"${escaped}"`;
|
|
54
|
+
return includeWildcard ? `${result}*` : result;
|
|
55
|
+
};
|
|
56
|
+
// Create a new parameter placeholder and update state
|
|
57
|
+
const nextParam = (state) => {
|
|
58
|
+
const paramName = `$${state.paramCounter}`;
|
|
59
|
+
const newState = {
|
|
60
|
+
...state,
|
|
61
|
+
paramCounter: state.paramCounter + 1,
|
|
62
|
+
};
|
|
63
|
+
return [paramName, newState];
|
|
64
|
+
};
|
|
65
|
+
// Add a value to the state and return updated state
|
|
66
|
+
const addValue = (state, value) => ({
|
|
67
|
+
...state,
|
|
68
|
+
values: [...state.values, value],
|
|
69
|
+
});
|
|
70
|
+
/**
|
|
71
|
+
* Convert a wildcard pattern to SQL
|
|
72
|
+
*/
|
|
73
|
+
const wildcardPatternToSql = (expr, state) => {
|
|
74
|
+
if (expr.prefix === "") {
|
|
75
|
+
return ["1=1", state];
|
|
76
|
+
}
|
|
77
|
+
const [paramName, newState] = nextParam(state);
|
|
78
|
+
const cleanedPrefix = cleanQuotedString(expr.prefix);
|
|
79
|
+
const queryValue = prepareParadeDBString(cleanedPrefix, true);
|
|
80
|
+
const conditions = state.searchableColumns.map((column) => `${column} @@@ ${paramName}`);
|
|
81
|
+
const sql = conditions.length === 1
|
|
82
|
+
? conditions[0]
|
|
83
|
+
: `(${conditions.join(" OR ")})`;
|
|
84
|
+
return [sql, addValue(newState, queryValue)];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Convert a search term to SQL conditions based on search type
|
|
88
|
+
*/
|
|
89
|
+
const searchTermToSql = (value, state) => {
|
|
90
|
+
const [paramName, newState] = nextParam(state);
|
|
91
|
+
const hasWildcard = value.endsWith("*");
|
|
92
|
+
const cleanedValue = cleanQuotedString(value);
|
|
93
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
94
|
+
const queryValue = prepareParadeDBString(baseValue, hasWildcard);
|
|
95
|
+
const conditions = state.searchableColumns.map((column) => `${column} @@@ ${paramName}`);
|
|
96
|
+
const sql = conditions.length === 1
|
|
97
|
+
? conditions[0]
|
|
98
|
+
: `(${conditions.join(" OR ")})`;
|
|
99
|
+
return [sql, addValue(newState, queryValue)];
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Convert a field:value pair to SQL based on search type
|
|
103
|
+
*/
|
|
104
|
+
const fieldValueToSql = (field, value, state) => {
|
|
105
|
+
const [paramName, newState] = nextParam(state);
|
|
106
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
107
|
+
const hasWildcard = value.endsWith("*");
|
|
108
|
+
const cleanedValue = cleanQuotedString(value);
|
|
109
|
+
const baseValue = hasWildcard ? cleanedValue.slice(0, -1) : cleanedValue;
|
|
110
|
+
switch (schema === null || schema === void 0 ? void 0 : schema.type) {
|
|
111
|
+
case "date": {
|
|
112
|
+
// Use parameter binding for dates
|
|
113
|
+
const [dateParam, dateState] = nextParam(state);
|
|
114
|
+
return [
|
|
115
|
+
`${field} @@@ '"' || ${dateParam} || '"'`,
|
|
116
|
+
addValue(dateState, baseValue),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
case "number":
|
|
120
|
+
return [`${field} @@@ '${baseValue}'`, newState];
|
|
121
|
+
default: {
|
|
122
|
+
const queryValue = prepareParadeDBString(baseValue, hasWildcard);
|
|
123
|
+
return [`${field} @@@ ${paramName}`, addValue(newState, queryValue)];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Convert a range expression to SQL
|
|
129
|
+
*/
|
|
130
|
+
const rangeToSql = (field, operator, value, value2, state) => {
|
|
131
|
+
const schema = state.schemas.get(field.toLowerCase());
|
|
132
|
+
const isDateField = (schema === null || schema === void 0 ? void 0 : schema.type) === "date";
|
|
133
|
+
if (operator === "BETWEEN" && value2) {
|
|
134
|
+
const [param1, state1] = nextParam(state);
|
|
135
|
+
const [param2, state2] = nextParam(state1);
|
|
136
|
+
let val1 = isDateField ? value : Number(value);
|
|
137
|
+
let val2 = isDateField ? value2 : Number(value2);
|
|
138
|
+
return [
|
|
139
|
+
`${field} @@@ '[' || ${param1} || ' TO ' || ${param2} || ']'`,
|
|
140
|
+
addValue(addValue(state2, val1), val2),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const [paramName, newState] = nextParam(state);
|
|
145
|
+
const rangeOp = operator.replace(">=", ">=").replace("<=", "<=");
|
|
146
|
+
const val = isDateField ? value : Number(value);
|
|
147
|
+
return [
|
|
148
|
+
`${field} @@@ '${rangeOp}' || ${paramName}`,
|
|
149
|
+
addValue(newState, val),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const inExpressionToSql = (field, values, state) => {
|
|
154
|
+
let currentState = state;
|
|
155
|
+
const cleanedValues = values.map((v) => cleanQuotedString(v));
|
|
156
|
+
const paramNames = [];
|
|
157
|
+
for (const value of cleanedValues) {
|
|
158
|
+
const [paramName, newState] = nextParam(currentState);
|
|
159
|
+
paramNames.push(paramName);
|
|
160
|
+
currentState = addValue(newState, value);
|
|
161
|
+
}
|
|
162
|
+
const concatExpr = paramNames.join(" || ' ' || ");
|
|
163
|
+
return [`${field} @@@ 'IN[' || ${concatExpr} || ']'`, currentState];
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Convert a binary operation (AND/OR) to SQL
|
|
167
|
+
*/
|
|
168
|
+
const binaryOpToSql = (operator, left, right, state) => {
|
|
169
|
+
const [leftText, leftState] = expressionToSql(left, state);
|
|
170
|
+
const [rightText, rightState] = expressionToSql(right, leftState);
|
|
171
|
+
return [`(${leftText} ${operator} ${rightText})`, rightState];
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Convert a single expression to SQL
|
|
175
|
+
*/
|
|
176
|
+
const expressionToSql = (expr, state) => {
|
|
177
|
+
var _a;
|
|
178
|
+
switch (expr.type) {
|
|
179
|
+
case "SEARCH_TERM":
|
|
180
|
+
return searchTermToSql(expr.value, state);
|
|
181
|
+
case "WILDCARD":
|
|
182
|
+
return wildcardPatternToSql(expr, state);
|
|
183
|
+
case "IN":
|
|
184
|
+
return inExpressionToSql(expr.field.value, expr.values.map((v) => v.value), state);
|
|
185
|
+
case "FIELD_VALUE":
|
|
186
|
+
return fieldValueToSql(expr.field.value, expr.value.value, state);
|
|
187
|
+
case "RANGE":
|
|
188
|
+
return rangeToSql(expr.field.value, expr.operator, expr.value.value, (_a = expr.value2) === null || _a === void 0 ? void 0 : _a.value, state);
|
|
189
|
+
case "AND":
|
|
190
|
+
return binaryOpToSql("AND", expr.left, expr.right, state);
|
|
191
|
+
case "OR":
|
|
192
|
+
return binaryOpToSql("OR", expr.left, expr.right, state);
|
|
193
|
+
case "NOT": {
|
|
194
|
+
const [sqlText, newState] = expressionToSql(expr.expression, state);
|
|
195
|
+
return [`NOT ${sqlText}`, newState];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Convert a SearchQuery to a SQL WHERE clause with specified search type
|
|
201
|
+
*/
|
|
202
|
+
const searchQueryToParadeDbSql = (query, searchableColumns, schemas = [], options = {}) => {
|
|
203
|
+
const initialState = {
|
|
204
|
+
paramCounter: 1,
|
|
205
|
+
values: [],
|
|
206
|
+
searchableColumns,
|
|
207
|
+
schemas: new Map(schemas.map((s) => [s.name.toLowerCase(), s])),
|
|
208
|
+
language: options.language,
|
|
209
|
+
};
|
|
210
|
+
if (!query.expression) {
|
|
211
|
+
return { text: "1=1", values: [] };
|
|
212
|
+
}
|
|
213
|
+
const [text, finalState] = expressionToSql(query.expression, initialState);
|
|
214
|
+
return { text, values: finalState.values };
|
|
215
|
+
};
|
|
216
|
+
exports.searchQueryToParadeDbSql = searchQueryToParadeDbSql;
|
|
217
|
+
/**
|
|
218
|
+
* Convert a search string directly to SQL
|
|
219
|
+
*/
|
|
220
|
+
const searchStringToParadeDbSql = (searchString, searchableColumns, schemas = [], options = {}) => {
|
|
221
|
+
const query = (0, parser_1.parseSearchInputQuery)(searchString, schemas);
|
|
222
|
+
if (query.type === "SEARCH_QUERY_ERROR") {
|
|
223
|
+
throw new Error(`Parse error: ${query.errors[0].message}`);
|
|
224
|
+
}
|
|
225
|
+
return (0, exports.searchQueryToParadeDbSql)(query, searchableColumns, schemas, options);
|
|
226
|
+
};
|
|
227
|
+
exports.searchStringToParadeDbSql = searchStringToParadeDbSql;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.searchStringToSql = exports.searchQueryToSql = void 0;
|
|
3
|
+
exports.searchStringToSql = exports.searchQueryToSql = exports.searchStringToParadeDbSql = exports.searchQueryToParadeDbSql = exports.searchStringToTsVectorSql = exports.searchQueryToTsVectorSql = exports.searchQueryToIlikeSql = exports.searchStringToIlikeSql = void 0;
|
|
4
4
|
const parser_1 = require("./parser");
|
|
5
|
+
var search_query_to_ilike_sql_1 = require("./search-query-to-ilike-sql");
|
|
6
|
+
Object.defineProperty(exports, "searchStringToIlikeSql", { enumerable: true, get: function () { return search_query_to_ilike_sql_1.searchStringToIlikeSql; } });
|
|
7
|
+
Object.defineProperty(exports, "searchQueryToIlikeSql", { enumerable: true, get: function () { return search_query_to_ilike_sql_1.searchQueryToIlikeSql; } });
|
|
8
|
+
var search_query_to_tsvector_sql_1 = require("./search-query-to-tsvector-sql");
|
|
9
|
+
Object.defineProperty(exports, "searchQueryToTsVectorSql", { enumerable: true, get: function () { return search_query_to_tsvector_sql_1.searchQueryToTsVectorSql; } });
|
|
10
|
+
Object.defineProperty(exports, "searchStringToTsVectorSql", { enumerable: true, get: function () { return search_query_to_tsvector_sql_1.searchStringToTsVectorSql; } });
|
|
11
|
+
var search_query_to_paradedb_sql_1 = require("./search-query-to-paradedb-sql");
|
|
12
|
+
Object.defineProperty(exports, "searchQueryToParadeDbSql", { enumerable: true, get: function () { return search_query_to_paradedb_sql_1.searchQueryToParadeDbSql; } });
|
|
13
|
+
Object.defineProperty(exports, "searchStringToParadeDbSql", { enumerable: true, get: function () { return search_query_to_paradedb_sql_1.searchStringToParadeDbSql; } });
|
|
5
14
|
// Constants
|
|
6
15
|
const SPECIAL_CHARS = ["%", "_"];
|
|
7
16
|
const ESCAPE_CHAR = "\\";
|