@carbonorm/carbonnode 3.9.3 → 3.9.5
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/index.cjs.js +55 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +55 -3
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sqlBuilders.complex.test.ts +26 -5
- package/src/api/orm/builders/AggregateBuilder.ts +17 -1
- package/src/api/orm/builders/ConditionBuilder.ts +42 -2
|
@@ -19,11 +19,6 @@ const Parcel_Sales = {
|
|
|
19
19
|
SALE_DATE: 'parcel_sales.sale_date',
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
|
-
const Parcel_Building_Details = {
|
|
23
|
-
TABLE_NAME: 'parcel_building_details',
|
|
24
|
-
PARCEL_ID: 'parcel_building_details.parcel_id',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
22
|
/**
|
|
28
23
|
* Complex SELECT coverage focused on WHERE operators, JOIN chains, ORDER, and pagination.
|
|
29
24
|
*/
|
|
@@ -56,6 +51,7 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
56
51
|
const { sql, params } = qb.build('actor');
|
|
57
52
|
|
|
58
53
|
// SQL fragments
|
|
54
|
+
// noinspection SqlResolve
|
|
59
55
|
expect(sql).toContain('SELECT actor.actor_id, actor.first_name FROM `actor`');
|
|
60
56
|
expect(sql).toContain('WHERE');
|
|
61
57
|
expect(sql).toMatch(/\(actor\.first_name\) LIKE \?/);
|
|
@@ -131,6 +127,7 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
131
127
|
|
|
132
128
|
const { sql, params } = qb.build('actor');
|
|
133
129
|
|
|
130
|
+
// noinspection SqlResolve
|
|
134
131
|
expect(sql).toContain('SELECT DISTINCT actor.first_name, COUNT(actor.actor_id) AS cnt FROM `actor`');
|
|
135
132
|
expect(sql).toContain('GROUP BY actor.first_name');
|
|
136
133
|
expect(sql).toContain('HAVING');
|
|
@@ -238,6 +235,7 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
238
235
|
|
|
239
236
|
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
240
237
|
|
|
238
|
+
// noinspection SqlResolve
|
|
241
239
|
expect(sql).toContain('SELECT property_units.unit_id, property_units.location, pu_target.location FROM `property_units`');
|
|
242
240
|
expect(sql).toContain('INNER JOIN `parcel_sales` AS `ps`');
|
|
243
241
|
expect(sql).toContain('INNER JOIN `parcel_building_details` AS `pbd`');
|
|
@@ -404,6 +402,29 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
404
402
|
expect(params.slice(-1)[0]).toBe('POINT(-104.8967729 39.3976764)');
|
|
405
403
|
});
|
|
406
404
|
|
|
405
|
+
it('orders by distance to ST_GeomFromText(WKT, 4326) using literal WKT', () => {
|
|
406
|
+
const config = buildParcelConfig();
|
|
407
|
+
|
|
408
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
409
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
410
|
+
[C6C.PAGINATION]: {
|
|
411
|
+
[C6C.LIMIT]: 10,
|
|
412
|
+
[C6C.ORDER]: {
|
|
413
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
414
|
+
Property_Units.LOCATION,
|
|
415
|
+
[C6C.ST_GEOMFROMTEXT, ['POINT(-104.8967729 39.3976764)', 4326]],
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
} as any, false);
|
|
420
|
+
|
|
421
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
422
|
+
expect(sql).toContain(
|
|
423
|
+
"ORDER BY ST_Distance_Sphere(property_units.location, ST_GEOMFROMTEXT('POINT(-104.8967729 39.3976764)', 4326))",
|
|
424
|
+
);
|
|
425
|
+
expect(params).not.toContain('POINT(-104.8967729 39.3976764)');
|
|
426
|
+
});
|
|
427
|
+
|
|
407
428
|
it('leaves normal table joins unaffected', () => {
|
|
408
429
|
const config = buildTestConfig();
|
|
409
430
|
|
|
@@ -43,6 +43,11 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const F = String(fn).toUpperCase();
|
|
46
|
+
const isGeomFromText = F === C6C.ST_GEOMFROMTEXT.toUpperCase();
|
|
47
|
+
|
|
48
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
49
|
+
args = args[0];
|
|
50
|
+
}
|
|
46
51
|
|
|
47
52
|
// Parameter placeholder helper: [C6C.PARAM, value]
|
|
48
53
|
if (F === C6C.PARAM) {
|
|
@@ -79,7 +84,7 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
79
84
|
const identifierPathRegex = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
80
85
|
|
|
81
86
|
const argList = args
|
|
82
|
-
.map(arg => {
|
|
87
|
+
.map((arg, index) => {
|
|
83
88
|
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
84
89
|
if (typeof arg === 'string') {
|
|
85
90
|
if (identifierPathRegex.test(arg)) {
|
|
@@ -88,6 +93,17 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
88
93
|
}
|
|
89
94
|
// Treat numeric-looking strings as literals, not identifier paths
|
|
90
95
|
if (isNumericString(arg)) return arg;
|
|
96
|
+
|
|
97
|
+
if (isGeomFromText && index === 0) {
|
|
98
|
+
const trimmed = arg.trim();
|
|
99
|
+
const alreadyQuoted = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2;
|
|
100
|
+
if (alreadyQuoted) {
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
const escaped = arg.replace(/'/g, "''");
|
|
104
|
+
return `'${escaped}'`;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
return arg;
|
|
92
108
|
}
|
|
93
109
|
return String(arg);
|
|
@@ -187,10 +187,30 @@ export abstract class ConditionBuilder<
|
|
|
187
187
|
this.OPERATORS.has(column) &&
|
|
188
188
|
Array.isArray(op)
|
|
189
189
|
) {
|
|
190
|
+
// Helper to serialize operand which may be a qualified identifier or a nested function array
|
|
191
|
+
const serializeOperand = (arg: any): string => {
|
|
192
|
+
const identifierPathRegex = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
193
|
+
if (Array.isArray(arg)) {
|
|
194
|
+
// Delegate to aggregate builder to handle nested functions/params
|
|
195
|
+
// @ts-ignore - buildAggregateField is defined upstream in AggregateBuilder
|
|
196
|
+
return this.buildAggregateField(arg, params);
|
|
197
|
+
}
|
|
198
|
+
if (typeof arg === 'string') {
|
|
199
|
+
if (identifierPathRegex.test(arg)) {
|
|
200
|
+
this.assertValidIdentifier(arg, 'WHERE argument');
|
|
201
|
+
return arg;
|
|
202
|
+
}
|
|
203
|
+
return arg;
|
|
204
|
+
}
|
|
205
|
+
return String(arg);
|
|
206
|
+
};
|
|
207
|
+
|
|
190
208
|
if (column === C6C.ST_DISTANCE_SPHERE) {
|
|
191
209
|
const [col1, col2] = op;
|
|
192
210
|
const threshold = Array.isArray(value) ? value[0] : value;
|
|
193
|
-
|
|
211
|
+
const left = serializeOperand(col1);
|
|
212
|
+
const right = serializeOperand(col2);
|
|
213
|
+
return `ST_Distance_Sphere(${left}, ${right}) < ${this.addParam(params, '', threshold)}`;
|
|
194
214
|
}
|
|
195
215
|
if ([
|
|
196
216
|
C6C.ST_CONTAINS,
|
|
@@ -203,7 +223,9 @@ export abstract class ConditionBuilder<
|
|
|
203
223
|
C6C.ST_TOUCHES
|
|
204
224
|
].includes(column)) {
|
|
205
225
|
const [geom1, geom2] = op;
|
|
206
|
-
|
|
226
|
+
const left = serializeOperand(geom1);
|
|
227
|
+
const right = serializeOperand(geom2);
|
|
228
|
+
return `${column}(${left}, ${right})`;
|
|
207
229
|
}
|
|
208
230
|
}
|
|
209
231
|
|
|
@@ -308,6 +330,24 @@ export abstract class ConditionBuilder<
|
|
|
308
330
|
const numeric = entries.filter(([k]) => !isNaN(Number(k)));
|
|
309
331
|
|
|
310
332
|
const processEntry = (k: string, v: any) => {
|
|
333
|
+
// Operator-as-key handling, e.g., { [C6C.ST_DISTANCE_SPHERE]: [arg1, arg2, threshold] }
|
|
334
|
+
if (typeof k === 'string' && this.OPERATORS.has(k) && Array.isArray(v)) {
|
|
335
|
+
if (k === C6C.ST_DISTANCE_SPHERE) {
|
|
336
|
+
// Accept either [arg1, arg2, threshold] or [[arg1, arg2], threshold]
|
|
337
|
+
let args: any[];
|
|
338
|
+
let threshold: any;
|
|
339
|
+
if (Array.isArray(v[0]) && v.length >= 2) {
|
|
340
|
+
args = v[0];
|
|
341
|
+
threshold = v[1];
|
|
342
|
+
} else {
|
|
343
|
+
args = v.slice(0, 2);
|
|
344
|
+
threshold = v[2];
|
|
345
|
+
}
|
|
346
|
+
subParts.push(addCondition(k, args as any, threshold));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
311
351
|
if (typeof v === 'object' && v !== null && Object.keys(v).length === 1) {
|
|
312
352
|
const [op, val] = Object.entries(v)[0];
|
|
313
353
|
subParts.push(addCondition(k, op, val));
|