@an5/adapters 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/an5Adapter.js +2 -1
- package/dist/base/sql.js +16 -15
- package/dist/googlesheets/helpers.js +13 -8
- package/dist/sqlite/engine.js +2 -1
- package/dotnet/Base/Core.cs +14 -24
- package/golang/base/where.go +32 -49
- package/golang/base/where_test.go +57 -0
- package/package.json +13 -5
- package/python/base/sql.py +6 -7
- package/rust/src/adapter.rs +2 -0
- package/rust/src/base/dialect.rs +38 -9
- package/rust/src/base/metadata.rs +4 -0
- package/rust/src/base/where_builder.rs +9 -28
- package/typescript/src/an5Adapter.ts +2 -1
- package/typescript/src/base/sql.ts +15 -14
- package/typescript/src/googlesheets/helpers.ts +11 -8
- package/typescript/src/sqlite/engine.ts +2 -1
package/dist/an5Adapter.js
CHANGED
|
@@ -275,7 +275,8 @@ class An5Adapter {
|
|
|
275
275
|
else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
276
276
|
this._engineType = 'mysql';
|
|
277
277
|
}
|
|
278
|
-
else if (forDialect
|
|
278
|
+
else if (forDialect === ':memory:'
|
|
279
|
+
|| forDialect.startsWith('sqlite:')
|
|
279
280
|
|| SQLITE_FILE_SUFFIXES.some((suffix) => forDialect.endsWith(suffix))) {
|
|
280
281
|
this._engineType = 'sqlite';
|
|
281
282
|
}
|
package/dist/base/sql.js
CHANGED
|
@@ -68,9 +68,10 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
68
68
|
if (!where)
|
|
69
69
|
return '';
|
|
70
70
|
const conditions = [];
|
|
71
|
+
const relations = ctx?.relationMap?.[modelName] || {};
|
|
71
72
|
const cleanWhere = {};
|
|
72
73
|
for (const [key, value] of Object.entries(where)) {
|
|
73
|
-
if (key.includes('_') &&
|
|
74
|
+
if (key.includes('_') && !relations[key] &&
|
|
74
75
|
value && typeof value === 'object' &&
|
|
75
76
|
!(value instanceof Date) &&
|
|
76
77
|
!isOperatorValue(value)) {
|
|
@@ -80,13 +81,13 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
80
81
|
cleanWhere[key] = value;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
const relations = ctx?.relationMap?.[modelName] || {};
|
|
84
84
|
const colPrefix = ctx?.colPrefix || '';
|
|
85
85
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
continue;
|
|
86
88
|
if (key === 'OR' && Array.isArray(value)) {
|
|
87
|
-
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx))
|
|
88
|
-
|
|
89
|
-
conditions.push(`(${sub.join(' OR ')})`);
|
|
89
|
+
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx) || '1=1');
|
|
90
|
+
conditions.push(sub.length > 0 ? `(${sub.join(' OR ')})` : '1=0');
|
|
90
91
|
}
|
|
91
92
|
else if (key === 'AND') {
|
|
92
93
|
const items = Array.isArray(value) ? value : [value];
|
|
@@ -96,12 +97,12 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
96
97
|
}
|
|
97
98
|
else if (key === 'NOT') {
|
|
98
99
|
const items = Array.isArray(value) ? value : [value];
|
|
99
|
-
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx))
|
|
100
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx) || '1=1');
|
|
100
101
|
if (sub.length > 0)
|
|
101
|
-
conditions.push(`NOT (${sub.join('
|
|
102
|
+
conditions.push(`NOT (${sub.join(' OR ')})`);
|
|
102
103
|
}
|
|
103
104
|
else if (relations[key] && value && typeof value === 'object') {
|
|
104
|
-
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, prefix
|
|
105
|
+
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, `${prefix}${key}_`, ctx);
|
|
105
106
|
if (clause)
|
|
106
107
|
conditions.push(clause);
|
|
107
108
|
}
|
|
@@ -220,16 +221,16 @@ function buildRelationClause(relation, value, modelName, params, dialect, prefix
|
|
|
220
221
|
const parts = [];
|
|
221
222
|
if (isMany) {
|
|
222
223
|
if (value.some !== undefined) {
|
|
223
|
-
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, prefix
|
|
224
|
+
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, `${prefix}some_`, ctx, childAlias);
|
|
224
225
|
parts.push(buildExists(inner));
|
|
225
226
|
}
|
|
226
227
|
if (value.none !== undefined) {
|
|
227
|
-
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, prefix
|
|
228
|
+
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, `${prefix}none_`, ctx, childAlias);
|
|
228
229
|
parts.push(`NOT ${buildExists(inner)}`);
|
|
229
230
|
}
|
|
230
231
|
if (value.every !== undefined) {
|
|
231
232
|
// "every" = no related row violates the condition.
|
|
232
|
-
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, prefix
|
|
233
|
+
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, `${prefix}every_`, ctx, childAlias);
|
|
233
234
|
const negated = inner ? `NOT (${inner})` : '';
|
|
234
235
|
const violated = `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${negated ? ` AND ${negated}` : ' AND 1=0'})`;
|
|
235
236
|
parts.push(`NOT ${violated}`);
|
|
@@ -237,12 +238,12 @@ function buildRelationClause(relation, value, modelName, params, dialect, prefix
|
|
|
237
238
|
}
|
|
238
239
|
else {
|
|
239
240
|
if (value.is !== undefined) {
|
|
240
|
-
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, prefix
|
|
241
|
-
parts.push(buildExists(inner));
|
|
241
|
+
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, `${prefix}is_`, ctx, childAlias);
|
|
242
|
+
parts.push(value.is === null ? `NOT ${buildExists(inner)}` : buildExists(inner));
|
|
242
243
|
}
|
|
243
244
|
if (value.isNot !== undefined) {
|
|
244
|
-
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, prefix
|
|
245
|
-
parts.push(`NOT ${buildExists(inner)}`);
|
|
245
|
+
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, `${prefix}isNot_`, ctx, childAlias);
|
|
246
|
+
parts.push(value.isNot === null ? buildExists(inner) : `NOT ${buildExists(inner)}`);
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
return parts.length > 0 ? parts.join(' AND ') : null;
|
|
@@ -25,9 +25,7 @@ function matchWhere(row, where) {
|
|
|
25
25
|
if (key.includes('_') &&
|
|
26
26
|
value && typeof value === 'object' &&
|
|
27
27
|
!(value instanceof Date) &&
|
|
28
|
-
!
|
|
29
|
-
!value.not && !value.gte &&
|
|
30
|
-
!value.lte && !value.gt && !value.lt) {
|
|
28
|
+
!['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)) {
|
|
31
29
|
Object.assign(cleanWhere, value);
|
|
32
30
|
}
|
|
33
31
|
else {
|
|
@@ -35,13 +33,16 @@ function matchWhere(row, where) {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
continue;
|
|
38
38
|
if (key === 'OR' && Array.isArray(value)) {
|
|
39
39
|
if (!value.some((v) => matchWhere(row, v)))
|
|
40
40
|
return false;
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
if (key === 'AND'
|
|
44
|
-
|
|
43
|
+
if (key === 'AND') {
|
|
44
|
+
const clauses = Array.isArray(value) ? value : [value];
|
|
45
|
+
if (!clauses.every((v) => matchWhere(row, v)))
|
|
45
46
|
return false;
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
@@ -65,21 +66,25 @@ function matchWhere(row, where) {
|
|
|
65
66
|
if (cellVal === null || cellVal === undefined)
|
|
66
67
|
return false;
|
|
67
68
|
}
|
|
69
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
70
|
+
if (matchWhere(row, { [key]: v.not }))
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
else if (normalize(cellVal) == normalize(v.not))
|
|
69
74
|
return false;
|
|
70
75
|
}
|
|
71
76
|
if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals))
|
|
72
77
|
return false;
|
|
73
78
|
if (v.contains !== undefined) {
|
|
74
|
-
if (
|
|
79
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains)))
|
|
75
80
|
return false;
|
|
76
81
|
}
|
|
77
82
|
if (v.startsWith !== undefined) {
|
|
78
|
-
if (
|
|
83
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
81
86
|
if (v.endsWith !== undefined) {
|
|
82
|
-
if (
|
|
87
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
|
|
83
88
|
return false;
|
|
84
89
|
}
|
|
85
90
|
if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte)))
|
package/dist/sqlite/engine.js
CHANGED
|
@@ -15,7 +15,8 @@ class SqliteEngine {
|
|
|
15
15
|
throw new Error('better-sqlite3 package is required for SQLite support. Run: npm install better-sqlite3');
|
|
16
16
|
this.filePath = (adapterConfig.connectionString ?? '')
|
|
17
17
|
.replace(/^sqlite:\/\/\//i, '/')
|
|
18
|
-
.replace(/^sqlite:\/\//i, '')
|
|
18
|
+
.replace(/^sqlite:\/\//i, '')
|
|
19
|
+
.replace(/^sqlite:/i, '');
|
|
19
20
|
}
|
|
20
21
|
getDb() {
|
|
21
22
|
if (!this.db)
|
package/dotnet/Base/Core.cs
CHANGED
|
@@ -259,40 +259,30 @@ public enum Dialect
|
|
|
259
259
|
{
|
|
260
260
|
var key = kv.Key;
|
|
261
261
|
var value = kv.Value;
|
|
262
|
-
if (key == "OR"
|
|
262
|
+
if (key == "OR" || key == "AND" || key == "NOT")
|
|
263
263
|
{
|
|
264
|
+
var items = value is System.Collections.IList list
|
|
265
|
+
? list : new object[] { value };
|
|
264
266
|
var subs = new List<string>();
|
|
265
267
|
var i = 0;
|
|
266
|
-
foreach (var item in
|
|
268
|
+
foreach (var item in items)
|
|
267
269
|
{
|
|
268
270
|
if (item is Dictionary<string, object> subMap)
|
|
269
271
|
{
|
|
270
|
-
var s = ParseWhere(subMap, parameters, dialect, $"{prefix}
|
|
271
|
-
|
|
272
|
+
var s = ParseWhere(subMap, parameters, dialect, $"{prefix}{key}{i++}_");
|
|
273
|
+
subs.Add(string.IsNullOrEmpty(s) ? "1=1" : s);
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
|
-
if (subs.Count
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
if (key == "AND" && value is System.Collections.IList andList && andList.Count > 0)
|
|
278
|
-
{
|
|
279
|
-
var subs = new List<string>();
|
|
280
|
-
var i = 0;
|
|
281
|
-
foreach (var item in andList)
|
|
276
|
+
if (subs.Count == 0)
|
|
282
277
|
{
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
if (key == "OR") conditions.Add("1=0");
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
{
|
|
282
|
+
var joiner = key == "AND" ? " AND " : " OR ";
|
|
283
|
+
var negation = key == "NOT" ? "NOT " : "";
|
|
284
|
+
conditions.Add(negation + "(" + string.Join(joiner, subs) + ")");
|
|
288
285
|
}
|
|
289
|
-
if (subs.Count > 0) conditions.Add("(" + string.Join(" AND ", subs) + ")");
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (key == "NOT" && value is Dictionary<string, object> notMap)
|
|
293
|
-
{
|
|
294
|
-
var s = ParseWhere(notMap, parameters, dialect, $"{prefix}not_");
|
|
295
|
-
if (!string.IsNullOrEmpty(s)) conditions.Add("NOT (" + s + ")");
|
|
296
286
|
continue;
|
|
297
287
|
}
|
|
298
288
|
|
package/golang/base/where.go
CHANGED
|
@@ -80,55 +80,41 @@ func buildWhereRec(where map[string]interface{}, dialect Dialect, args *[]interf
|
|
|
80
80
|
value := where[key]
|
|
81
81
|
|
|
82
82
|
switch key {
|
|
83
|
-
case "OR":
|
|
84
|
-
if list, ok := toSlice(value); ok && len(list) > 0 {
|
|
85
|
-
var subs []string
|
|
86
|
-
for i, item := range list {
|
|
87
|
-
if m, ok := item.(map[string]interface{}); ok && m != nil {
|
|
88
|
-
if s := buildWhereRec(m, dialect, args, ph, fmt.Sprintf("%sor%d_", prefix, i)); s != "" {
|
|
89
|
-
subs = append(subs, s)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if len(subs) > 0 {
|
|
94
|
-
conditions = append(conditions, "("+strings.Join(subs, " OR ")+")")
|
|
95
|
-
}
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
case "AND":
|
|
99
|
-
if list, ok := toSlice(value); ok && len(list) > 0 {
|
|
100
|
-
var subs []string
|
|
101
|
-
for i, item := range list {
|
|
102
|
-
if m, ok := item.(map[string]interface{}); ok && m != nil {
|
|
103
|
-
if s := buildWhereRec(m, dialect, args, ph, fmt.Sprintf("%sand%d_", prefix, i)); s != "" {
|
|
104
|
-
subs = append(subs, s)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if len(subs) > 0 {
|
|
109
|
-
conditions = append(conditions, "("+strings.Join(subs, " AND ")+")")
|
|
110
|
-
}
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
case "NOT":
|
|
83
|
+
case "OR", "AND", "NOT":
|
|
114
84
|
items := value
|
|
115
85
|
if m, ok := value.(map[string]interface{}); ok {
|
|
116
86
|
items = []interface{}{m}
|
|
117
87
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
88
|
+
list, ok := toSlice(items)
|
|
89
|
+
if !ok {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
var subs []string
|
|
93
|
+
for i, item := range list {
|
|
94
|
+
if m, ok := item.(map[string]interface{}); ok {
|
|
95
|
+
s := buildWhereRec(cleanWhere(m), dialect, args, ph, fmt.Sprintf("%s%s%d_", prefix, key, i))
|
|
96
|
+
if s == "" {
|
|
97
|
+
s = "1=1"
|
|
125
98
|
}
|
|
99
|
+
subs = append(subs, s)
|
|
126
100
|
}
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
}
|
|
102
|
+
if len(subs) == 0 {
|
|
103
|
+
if key == "OR" {
|
|
104
|
+
conditions = append(conditions, "1=0")
|
|
129
105
|
}
|
|
130
|
-
|
|
106
|
+
} else {
|
|
107
|
+
joiner := " OR "
|
|
108
|
+
if key == "AND" {
|
|
109
|
+
joiner = " AND "
|
|
110
|
+
}
|
|
111
|
+
clause := "(" + strings.Join(subs, joiner) + ")"
|
|
112
|
+
if key == "NOT" {
|
|
113
|
+
clause = "NOT " + clause
|
|
114
|
+
}
|
|
115
|
+
conditions = append(conditions, clause)
|
|
131
116
|
}
|
|
117
|
+
continue
|
|
132
118
|
}
|
|
133
119
|
|
|
134
120
|
conditions = append(conditions, buildFieldCondition(key, value, dialect, args, ph, prefix)...)
|
|
@@ -171,13 +157,10 @@ func buildFieldCondition(key string, value interface{}, dialect Dialect, args *[
|
|
|
171
157
|
case "not":
|
|
172
158
|
if val == nil {
|
|
173
159
|
parts = append(parts, col+" IS NOT NULL")
|
|
174
|
-
} else if nested, ok := val.(map[string]interface{}); ok
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if nestedCond != "" {
|
|
179
|
-
parts = append(parts, "NOT ("+nestedCond+")")
|
|
180
|
-
}
|
|
160
|
+
} else if nested, ok := val.(map[string]interface{}); ok {
|
|
161
|
+
nestedCond := buildWhereRec(map[string]interface{}{key: nested}, dialect, args, ph, key+"_not_")
|
|
162
|
+
if nestedCond != "" {
|
|
163
|
+
parts = append(parts, "NOT ("+nestedCond+")")
|
|
181
164
|
}
|
|
182
165
|
} else {
|
|
183
166
|
parts = append(parts, fmt.Sprintf("%s <> %s", col, addArg(val)))
|
|
@@ -308,4 +291,4 @@ func BuildOrderBy(orderBy interface{}, dialect Dialect) string {
|
|
|
308
291
|
return ""
|
|
309
292
|
}
|
|
310
293
|
return "ORDER BY " + strings.Join(parts, ", ")
|
|
311
|
-
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package base
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"os/exec"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"reflect"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// SQLite is provided by Python's standard library to keep the adapter driver-neutral.
|
|
13
|
+
func TestSharedQueryContract(t *testing.T) {
|
|
14
|
+
fixtures, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "query-semantics.json"))
|
|
15
|
+
if err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
var contract struct {
|
|
19
|
+
Rows []map[string]interface{} `json:"rows"`
|
|
20
|
+
Cases []struct {
|
|
21
|
+
Name string `json:"name"`
|
|
22
|
+
Where map[string]interface{} `json:"where"`
|
|
23
|
+
IDs []int `json:"ids"`
|
|
24
|
+
} `json:"cases"`
|
|
25
|
+
}
|
|
26
|
+
if err := json.Unmarshal(fixtures, &contract); err != nil {
|
|
27
|
+
t.Fatal(err)
|
|
28
|
+
}
|
|
29
|
+
python, err := exec.LookPath("python3")
|
|
30
|
+
if err != nil {
|
|
31
|
+
t.Fatal("python3 is required for the SQLite query contract")
|
|
32
|
+
}
|
|
33
|
+
for _, c := range contract.Cases {
|
|
34
|
+
t.Run(c.Name, func(t *testing.T) {
|
|
35
|
+
clause, args := BuildWhere(c.Where, DialectSqlite, func(int) string { return "?" })
|
|
36
|
+
payload, _ := json.Marshal(map[string]interface{}{"rows": contract.Rows, "clause": clause, "args": args})
|
|
37
|
+
cmd := exec.Command(python, "-c", `import json,sqlite3,sys
|
|
38
|
+
p=json.loads(sys.argv[1])
|
|
39
|
+
db=sqlite3.connect(':memory:')
|
|
40
|
+
db.execute('CREATE TABLE users (id INTEGER, score INTEGER)')
|
|
41
|
+
db.executemany('INSERT INTO users VALUES (?,?)',[(r['id'],r['score']) for r in p['rows']])
|
|
42
|
+
sql='SELECT id FROM users'+(' WHERE '+p['clause'] if p['clause'] else '')+' ORDER BY id'
|
|
43
|
+
print(json.dumps([r[0] for r in db.execute(sql,p['args'])]))`, string(payload))
|
|
44
|
+
output, err := cmd.CombinedOutput()
|
|
45
|
+
if err != nil {
|
|
46
|
+
t.Fatalf("query failed: %v\n%s", err, output)
|
|
47
|
+
}
|
|
48
|
+
var got []int
|
|
49
|
+
if err := json.Unmarshal(output, &got); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
if !reflect.DeepEqual(got, c.IDs) {
|
|
53
|
+
t.Fatalf("got %v, want %v", got, c.IDs)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@an5/adapters",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Standalone language adapters for AN5 ORM - TypeScript, Python, .NET, Go, Rust, and Google Sheets query execution.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"pretest": "npm run build",
|
|
59
59
|
"prepack": "npm run build",
|
|
60
60
|
"build:python": "(python -m build --outdir dist-py || python3 -m build --outdir dist-py)",
|
|
61
|
-
"test": "node test/unit.test.js && node test/package.test.js && node test/nbase.test.js && node --test test/version-sync.test.js",
|
|
61
|
+
"test": "node test/unit.test.js && node test/package.test.js && node test/nbase.test.js && node --test test/version-sync.test.js && node --test test/query-semantics.test.js && node test/query-relations.integration.test.js",
|
|
62
62
|
"test:package:smoke": "node scripts/package-smoke.js",
|
|
63
|
-
"test:integration:live": "node test/live-db.integration.test.js",
|
|
64
|
-
"test:python": "
|
|
63
|
+
"test:integration:live": "node test/live-db.integration.test.js && node test/query-relations.integration.test.js",
|
|
64
|
+
"test:python": "node scripts/python-tests.js",
|
|
65
65
|
"test:dotnet": "node scripts/dotnet-compile-check.js && node scripts/dotnet-sqlite-smoke.js",
|
|
66
66
|
"test:go": "cd golang && go test ./...",
|
|
67
67
|
"test:rust": "node scripts/rust-compile-check.js"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"googleapis": "^
|
|
70
|
+
"googleapis": "^183.0.0",
|
|
71
71
|
"mssql": "^12.5.5",
|
|
72
72
|
"pg": "^8.13.0"
|
|
73
73
|
},
|
|
@@ -79,5 +79,13 @@
|
|
|
79
79
|
},
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"mysql2": "^3.0.0"
|
|
85
|
+
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"mysql2": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
}
|
package/python/base/sql.py
CHANGED
|
@@ -51,10 +51,8 @@ def _parse_where(model_name: str, where: Optional[Dict], params: Dict, dialect:
|
|
|
51
51
|
|
|
52
52
|
for key, value in clean_where.items():
|
|
53
53
|
if key == "OR" and isinstance(value, list):
|
|
54
|
-
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}or{i}_") for i, v in enumerate(value)]
|
|
55
|
-
|
|
56
|
-
if sub:
|
|
57
|
-
conditions.append(f"({' OR '.join(sub)})")
|
|
54
|
+
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}or{i}_") or "1=1" for i, v in enumerate(value)]
|
|
55
|
+
conditions.append(f"({' OR '.join(sub)})" if sub else "1=0")
|
|
58
56
|
elif key == "AND":
|
|
59
57
|
items = value if isinstance(value, list) else [value]
|
|
60
58
|
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}and{i}_") for i, v in enumerate(items)]
|
|
@@ -63,10 +61,9 @@ def _parse_where(model_name: str, where: Optional[Dict], params: Dict, dialect:
|
|
|
63
61
|
conditions.append(f"({' AND '.join(sub)})")
|
|
64
62
|
elif key == "NOT":
|
|
65
63
|
items = value if isinstance(value, list) else [value]
|
|
66
|
-
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}not{i}_") for i, v in enumerate(items)]
|
|
67
|
-
sub = [s for s in sub if s]
|
|
64
|
+
sub = [_parse_where(model_name, v, params, dialect, f"{prefix}not{i}_") or "1=1" for i, v in enumerate(items)]
|
|
68
65
|
if sub:
|
|
69
|
-
conditions.append(f"NOT ({'
|
|
66
|
+
conditions.append(f"NOT ({' OR '.join(sub)})")
|
|
70
67
|
else:
|
|
71
68
|
col = _quote(key, dialect)
|
|
72
69
|
pname = _sanitize_param_name(f"{prefix}{key}")
|
|
@@ -196,6 +193,8 @@ def _quote_table(t: str, dialect: str) -> str:
|
|
|
196
193
|
# correct on MSSQL. Other dialects have to strip it and re-quote, or the
|
|
197
194
|
# previous dialect's brackets follow the name into the new dialect.
|
|
198
195
|
parts = _split_qualified(str(t))
|
|
196
|
+
if dialect == DIALECT_SQLITE and len(parts) == 2 and _strip_wrapping(_strip_wrapping(parts[0], "[", "]"), '"', '"').lower() == "dbo":
|
|
197
|
+
parts = parts[1:]
|
|
199
198
|
if not parts:
|
|
200
199
|
parts = [str(t)]
|
|
201
200
|
return ".".join(_quote(p, dialect) for p in parts)
|
package/rust/src/adapter.rs
CHANGED
|
@@ -1238,6 +1238,7 @@ mod tests {
|
|
|
1238
1238
|
|
|
1239
1239
|
#[tokio::test]
|
|
1240
1240
|
async fn resolves_physical_table() {
|
|
1241
|
+
let _guard = crate::base::metadata::TEST_METADATA_LOCK.lock().unwrap();
|
|
1241
1242
|
crate::base::set_adapter_metadata(AdapterMetadata {
|
|
1242
1243
|
model_to_table: HashMap::from([("User".to_string(), "[dbo].[Users]".to_string())]),
|
|
1243
1244
|
..Default::default()
|
|
@@ -1285,6 +1286,7 @@ mod tests {
|
|
|
1285
1286
|
|
|
1286
1287
|
#[test]
|
|
1287
1288
|
fn split_relations_uses_metadata() {
|
|
1289
|
+
let _guard = crate::base::metadata::TEST_METADATA_LOCK.lock().unwrap();
|
|
1288
1290
|
crate::base::set_adapter_metadata(AdapterMetadata {
|
|
1289
1291
|
relation_map: HashMap::from([(
|
|
1290
1292
|
"Order".to_string(),
|
package/rust/src/base/dialect.rs
CHANGED
|
@@ -35,7 +35,11 @@ impl Dialect {
|
|
|
35
35
|
|
|
36
36
|
/// Quote a single identifier (column or table part).
|
|
37
37
|
pub fn quote_identifier(&self, name: &str) -> String {
|
|
38
|
-
let raw =
|
|
38
|
+
let raw = if name.starts_with('[') && name.ends_with(']') {
|
|
39
|
+
strip_wrapping(name, "[", "]").replace("]]", "]")
|
|
40
|
+
} else if name.starts_with('"') && name.ends_with('"') {
|
|
41
|
+
strip_wrapping(name, "\"", "\"").replace("\"\"", "\"")
|
|
42
|
+
} else { name.to_string() };
|
|
39
43
|
match self {
|
|
40
44
|
Dialect::Postgres | Dialect::Sqlite => {
|
|
41
45
|
format!("\"{}\"", raw.replace('"', "\"\""))
|
|
@@ -46,15 +50,31 @@ impl Dialect {
|
|
|
46
50
|
|
|
47
51
|
/// Quote a possibly schema-qualified table name (`dbo.users`).
|
|
48
52
|
pub fn quote_table(&self, table: &str) -> String {
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
let mut parts = Vec::new();
|
|
54
|
+
let mut part = String::new();
|
|
55
|
+
let mut closing = None;
|
|
56
|
+
let mut chars = table.trim().chars().peekable();
|
|
57
|
+
while let Some(ch) = chars.next() {
|
|
58
|
+
if let Some(close) = closing {
|
|
59
|
+
part.push(ch);
|
|
60
|
+
if ch == close {
|
|
61
|
+
if chars.peek() == Some(&close) {
|
|
62
|
+
part.push(chars.next().unwrap());
|
|
63
|
+
} else { closing = None; }
|
|
64
|
+
}
|
|
65
|
+
} else if ch == '[' || ch == '"' {
|
|
66
|
+
closing = Some(if ch == '[' { ']' } else { '"' });
|
|
67
|
+
part.push(ch);
|
|
68
|
+
} else if ch == '.' {
|
|
69
|
+
parts.push(std::mem::take(&mut part));
|
|
70
|
+
} else { part.push(ch); }
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
.
|
|
72
|
+
parts.push(part);
|
|
73
|
+
if *self == Dialect::Sqlite && parts.len() == 2 {
|
|
74
|
+
let schema = strip_wrapping(strip_wrapping(parts[0].trim(), "[", "]"), "\"", "\"");
|
|
75
|
+
if schema.eq_ignore_ascii_case("dbo") { parts.remove(0); }
|
|
76
|
+
}
|
|
77
|
+
parts.iter().map(|p| self.quote_identifier(p.trim())).collect::<Vec<_>>().join(".")
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/// Positional placeholder: `$N` for Postgres, `?` otherwise.
|
|
@@ -133,6 +153,15 @@ pub fn vector_distance(a: &[f64], b: &[f64], metric: &str) -> f64 {
|
|
|
133
153
|
mod tests {
|
|
134
154
|
use super::*;
|
|
135
155
|
|
|
156
|
+
#[test]
|
|
157
|
+
fn converts_generated_table_names_for_sqlite() {
|
|
158
|
+
assert_eq!(Dialect::Sqlite.quote_table("[dbo].[users]"), "\"users\"");
|
|
159
|
+
assert_eq!(Dialect::Sqlite.quote_table("[main].[users]"), "\"main\".\"users\"");
|
|
160
|
+
assert_eq!(Dialect::Sqlite.quote_table("[dot.name]"), "\"dot.name\"");
|
|
161
|
+
assert_eq!(Dialect::Postgres.quote_table("[dbo].[a]]b]"), "\"dbo\".\"a]b\"");
|
|
162
|
+
assert_eq!(Dialect::Mssql.quote_table("[dbo].[a]]b]"), "[dbo].[a]]b]");
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
#[test]
|
|
137
166
|
fn detects_dialects() {
|
|
138
167
|
assert_eq!(
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Mirrors the Go `an5adapters/base` metadata store.
|
|
4
4
|
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
pub(crate) static TEST_METADATA_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
7
|
+
|
|
5
8
|
use std::collections::HashMap;
|
|
6
9
|
use std::sync::{OnceLock, RwLock};
|
|
7
10
|
|
|
@@ -123,6 +126,7 @@ mod tests {
|
|
|
123
126
|
fn resolves_table_with_case_fallback() {
|
|
124
127
|
let mut map = HashMap::new();
|
|
125
128
|
map.insert("User".to_string(), "[dbo].[Users]".to_string());
|
|
129
|
+
let _guard = TEST_METADATA_LOCK.lock().unwrap();
|
|
126
130
|
set_adapter_metadata(AdapterMetadata {
|
|
127
131
|
model_to_table: map,
|
|
128
132
|
..Default::default()
|
|
@@ -44,10 +44,6 @@ fn as_object(v: &Value) -> Option<&Map<String, Value>> {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
fn is_list(v: &Value) -> bool {
|
|
48
|
-
v.is_array()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
fn to_list(v: &Value) -> Vec<Value> {
|
|
52
48
|
match v {
|
|
53
49
|
Value::Array(items) => items.clone(),
|
|
@@ -113,38 +109,23 @@ fn build_where_rec(
|
|
|
113
109
|
let value = &where_obj[key];
|
|
114
110
|
|
|
115
111
|
match key.as_str() {
|
|
116
|
-
"AND" | "OR"
|
|
117
|
-
let joiner = if key == "AND" { " AND " } else { " OR " };
|
|
112
|
+
"AND" | "OR" | "NOT" => {
|
|
118
113
|
let mut subs: Vec<String> = Vec::new();
|
|
119
114
|
for item in to_list(value) {
|
|
120
115
|
if let Some(m) = as_object(&item) {
|
|
121
|
-
let s = build_where_rec(m, dialect, args, ph);
|
|
122
|
-
if
|
|
123
|
-
subs.push(s);
|
|
124
|
-
}
|
|
116
|
+
let s = build_where_rec(&clean_where(m), dialect, args, ph);
|
|
117
|
+
subs.push(if s.is_empty() { "1=1".to_string() } else { s });
|
|
125
118
|
}
|
|
126
119
|
}
|
|
127
|
-
if
|
|
128
|
-
conditions.push(
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let mut subs: Vec<String> = Vec::new();
|
|
134
|
-
for item in to_list(value) {
|
|
135
|
-
if let Some(m) = as_object(&item) {
|
|
136
|
-
let s = build_where_rec(m, dialect, args, ph);
|
|
137
|
-
if !s.is_empty() {
|
|
138
|
-
subs.push(s);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if !subs.is_empty() {
|
|
143
|
-
conditions.push(format!("NOT ({})", subs.join(" AND ")));
|
|
120
|
+
if subs.is_empty() {
|
|
121
|
+
if key == "OR" { conditions.push("1=0".to_string()); }
|
|
122
|
+
} else {
|
|
123
|
+
let joiner = if key == "AND" { " AND " } else { " OR " };
|
|
124
|
+
let negation = if key == "NOT" { "NOT " } else { "" };
|
|
125
|
+
conditions.push(format!("{}({})", negation, subs.join(joiner)));
|
|
144
126
|
}
|
|
145
127
|
continue;
|
|
146
128
|
}
|
|
147
|
-
"AND" | "OR" => continue,
|
|
148
129
|
_ => {}
|
|
149
130
|
}
|
|
150
131
|
|
|
@@ -317,7 +317,8 @@ export class An5Adapter {
|
|
|
317
317
|
} else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
318
318
|
this._engineType = 'mysql';
|
|
319
319
|
} else if (
|
|
320
|
-
forDialect
|
|
320
|
+
forDialect === ':memory:'
|
|
321
|
+
|| forDialect.startsWith('sqlite:')
|
|
321
322
|
|| SQLITE_FILE_SUFFIXES.some((suffix) => forDialect.endsWith(suffix))
|
|
322
323
|
) {
|
|
323
324
|
this._engineType = 'sqlite';
|
|
@@ -88,10 +88,11 @@ export function parseWhere(
|
|
|
88
88
|
if (!where) return '';
|
|
89
89
|
const conditions: string[] = [];
|
|
90
90
|
|
|
91
|
+
const relations = ctx?.relationMap?.[modelName] || {};
|
|
91
92
|
const cleanWhere: Record<string, any> = {};
|
|
92
93
|
for (const [key, value] of Object.entries(where)) {
|
|
93
94
|
if (
|
|
94
|
-
key.includes('_') &&
|
|
95
|
+
key.includes('_') && !relations[key] &&
|
|
95
96
|
value && typeof value === 'object' &&
|
|
96
97
|
!(value instanceof Date) &&
|
|
97
98
|
!isOperatorValue(value)
|
|
@@ -102,23 +103,23 @@ export function parseWhere(
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const relations = ctx?.relationMap?.[modelName] || {};
|
|
106
106
|
const colPrefix = ctx?.colPrefix || '';
|
|
107
107
|
|
|
108
108
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
109
|
+
if (value === undefined) continue;
|
|
109
110
|
if (key === 'OR' && Array.isArray(value)) {
|
|
110
|
-
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx))
|
|
111
|
-
|
|
111
|
+
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx) || '1=1');
|
|
112
|
+
conditions.push(sub.length > 0 ? `(${sub.join(' OR ')})` : '1=0');
|
|
112
113
|
} else if (key === 'AND') {
|
|
113
114
|
const items = Array.isArray(value) ? value : [value];
|
|
114
115
|
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}and_${i}_`, ctx)).filter(Boolean);
|
|
115
116
|
if (sub.length > 0) conditions.push(`(${sub.join(' AND ')})`);
|
|
116
117
|
} else if (key === 'NOT') {
|
|
117
118
|
const items = Array.isArray(value) ? value : [value];
|
|
118
|
-
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx))
|
|
119
|
-
if (sub.length > 0) conditions.push(`NOT (${sub.join('
|
|
119
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx) || '1=1');
|
|
120
|
+
if (sub.length > 0) conditions.push(`NOT (${sub.join(' OR ')})`);
|
|
120
121
|
} else if (relations[key] && value && typeof value === 'object') {
|
|
121
|
-
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, prefix
|
|
122
|
+
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, `${prefix}${key}_`, ctx);
|
|
122
123
|
if (clause) conditions.push(clause);
|
|
123
124
|
} else {
|
|
124
125
|
const pname = sanitizeParamName(`${prefix}${key}`);
|
|
@@ -203,28 +204,28 @@ function buildRelationClause(
|
|
|
203
204
|
|
|
204
205
|
if (isMany) {
|
|
205
206
|
if (value.some !== undefined) {
|
|
206
|
-
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, prefix
|
|
207
|
+
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, `${prefix}some_`, ctx, childAlias);
|
|
207
208
|
parts.push(buildExists(inner));
|
|
208
209
|
}
|
|
209
210
|
if (value.none !== undefined) {
|
|
210
|
-
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, prefix
|
|
211
|
+
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, `${prefix}none_`, ctx, childAlias);
|
|
211
212
|
parts.push(`NOT ${buildExists(inner)}`);
|
|
212
213
|
}
|
|
213
214
|
if (value.every !== undefined) {
|
|
214
215
|
// "every" = no related row violates the condition.
|
|
215
|
-
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, prefix
|
|
216
|
+
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, `${prefix}every_`, ctx, childAlias);
|
|
216
217
|
const negated = inner ? `NOT (${inner})` : '';
|
|
217
218
|
const violated = `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${negated ? ` AND ${negated}` : ' AND 1=0'})`;
|
|
218
219
|
parts.push(`NOT ${violated}`);
|
|
219
220
|
}
|
|
220
221
|
} else {
|
|
221
222
|
if (value.is !== undefined) {
|
|
222
|
-
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, prefix
|
|
223
|
-
parts.push(buildExists(inner));
|
|
223
|
+
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, `${prefix}is_`, ctx, childAlias);
|
|
224
|
+
parts.push(value.is === null ? `NOT ${buildExists(inner)}` : buildExists(inner));
|
|
224
225
|
}
|
|
225
226
|
if (value.isNot !== undefined) {
|
|
226
|
-
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, prefix
|
|
227
|
-
parts.push(`NOT ${buildExists(inner)}`);
|
|
227
|
+
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, `${prefix}isNot_`, ctx, childAlias);
|
|
228
|
+
parts.push(value.isNot === null ? buildExists(inner) : `NOT ${buildExists(inner)}`);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
@@ -21,9 +21,7 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
21
21
|
key.includes('_') &&
|
|
22
22
|
value && typeof value === 'object' &&
|
|
23
23
|
!(value instanceof Date) &&
|
|
24
|
-
!
|
|
25
|
-
!(value as any).not && !(value as any).gte &&
|
|
26
|
-
!(value as any).lte && !(value as any).gt && !(value as any).lt
|
|
24
|
+
!['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)
|
|
27
25
|
) {
|
|
28
26
|
Object.assign(cleanWhere, value);
|
|
29
27
|
} else {
|
|
@@ -32,12 +30,14 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
33
|
+
if (value === undefined) continue;
|
|
35
34
|
if (key === 'OR' && Array.isArray(value)) {
|
|
36
35
|
if (!value.some((v: any) => matchWhere(row, v))) return false;
|
|
37
36
|
continue;
|
|
38
37
|
}
|
|
39
|
-
if (key === 'AND'
|
|
40
|
-
|
|
38
|
+
if (key === 'AND') {
|
|
39
|
+
const clauses = Array.isArray(value) ? value : [value];
|
|
40
|
+
if (!clauses.every((v: any) => matchWhere(row, v))) return false;
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
if (key === 'NOT') {
|
|
@@ -57,17 +57,20 @@ export function matchWhere(row: Record<string, any>, where: any): boolean {
|
|
|
57
57
|
|
|
58
58
|
if (v.not !== undefined) {
|
|
59
59
|
if (v.not === null) { if (cellVal === null || cellVal === undefined) return false; }
|
|
60
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
61
|
+
if (matchWhere(row, { [key]: v.not })) return false;
|
|
62
|
+
}
|
|
60
63
|
else if (normalize(cellVal) == normalize(v.not)) return false;
|
|
61
64
|
}
|
|
62
65
|
if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals)) return false;
|
|
63
66
|
if (v.contains !== undefined) {
|
|
64
|
-
if (
|
|
67
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains))) return false;
|
|
65
68
|
}
|
|
66
69
|
if (v.startsWith !== undefined) {
|
|
67
|
-
if (
|
|
70
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith))) return false;
|
|
68
71
|
}
|
|
69
72
|
if (v.endsWith !== undefined) {
|
|
70
|
-
if (
|
|
73
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith))) return false;
|
|
71
74
|
}
|
|
72
75
|
if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte))) return false;
|
|
73
76
|
if (v.lte !== undefined && !(Number(cellVal) <= Number(v.lte))) return false;
|
|
@@ -14,7 +14,8 @@ export class SqliteEngine implements QueryEngine {
|
|
|
14
14
|
if (!BetterSqlite3) throw new Error('better-sqlite3 package is required for SQLite support. Run: npm install better-sqlite3');
|
|
15
15
|
this.filePath = (adapterConfig.connectionString ?? '')
|
|
16
16
|
.replace(/^sqlite:\/\/\//i, '/')
|
|
17
|
-
.replace(/^sqlite:\/\//i, '')
|
|
17
|
+
.replace(/^sqlite:\/\//i, '')
|
|
18
|
+
.replace(/^sqlite:/i, '');
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
private getDb(): any {
|