@carbonorm/carbonnode 3.9.0 → 3.9.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/dist/api/C6Constants.d.ts +2 -0
- package/dist/index.cjs.js +49 -6
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +49 -6
- 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 +103 -0
- package/src/api/C6Constants.ts +2 -1
- package/src/api/orm/builders/AggregateBuilder.ts +32 -1
- package/src/api/orm/builders/PaginationBuilder.ts +9 -2
|
@@ -301,6 +301,109 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
301
301
|
expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Unknown table or alias 'missing_alias'/);
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
it('orders by distance to a literal ST_Point with numeric string coords', () => {
|
|
305
|
+
const config = buildParcelConfig();
|
|
306
|
+
|
|
307
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
308
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
309
|
+
[C6C.PAGINATION]: {
|
|
310
|
+
[C6C.LIMIT]: 200,
|
|
311
|
+
[C6C.ORDER]: {
|
|
312
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
313
|
+
Property_Units.LOCATION,
|
|
314
|
+
[C6C.ST_POINT, ['-104.8967729', '39.3976764']],
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
} as any, false);
|
|
319
|
+
|
|
320
|
+
const { sql } = qb.build(Property_Units.TABLE_NAME);
|
|
321
|
+
expect(sql).toContain('ORDER BY ST_Distance_Sphere(property_units.location, ST_POINT(-104.8967729, 39.3976764))');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('orders by distance to ST_SRID(ST_Point(lng, lat), 4326)', () => {
|
|
325
|
+
const config = buildParcelConfig();
|
|
326
|
+
|
|
327
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
328
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
329
|
+
[C6C.PAGINATION]: {
|
|
330
|
+
[C6C.LIMIT]: 50,
|
|
331
|
+
[C6C.ORDER]: {
|
|
332
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
333
|
+
Property_Units.LOCATION,
|
|
334
|
+
[C6C.ST_SRID, [C6C.ST_POINT, [10, 20]], 4326],
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
} as any, false);
|
|
339
|
+
|
|
340
|
+
const { sql } = qb.build(Property_Units.TABLE_NAME);
|
|
341
|
+
expect(sql).toContain('ORDER BY ST_Distance_Sphere(property_units.location, ST_SRID(ST_POINT(10, 20), 4326))');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('orders by distance using placeholders via PARAM inside nested ST_Point', () => {
|
|
345
|
+
const config = buildParcelConfig();
|
|
346
|
+
|
|
347
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
348
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
349
|
+
[C6C.PAGINATION]: {
|
|
350
|
+
[C6C.LIMIT]: 25,
|
|
351
|
+
[C6C.ORDER]: {
|
|
352
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
353
|
+
Property_Units.LOCATION,
|
|
354
|
+
[C6C.ST_SRID, [C6C.ST_POINT, [[C6C.PARAM, 10], [C6C.PARAM, 20]]], 4326],
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
} as any, false);
|
|
359
|
+
|
|
360
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
361
|
+
expect(sql).toContain('ORDER BY ST_Distance_Sphere(property_units.location, ST_SRID(ST_POINT(?, ?), 4326))');
|
|
362
|
+
expect(params.slice(-2)).toEqual([10, 20]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('orders by distance using named params via PARAM inside nested ST_Point', () => {
|
|
366
|
+
const config = buildParcelConfig();
|
|
367
|
+
|
|
368
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
369
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
370
|
+
[C6C.PAGINATION]: {
|
|
371
|
+
[C6C.LIMIT]: 25,
|
|
372
|
+
[C6C.ORDER]: {
|
|
373
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
374
|
+
Property_Units.LOCATION,
|
|
375
|
+
[C6C.ST_SRID, [C6C.ST_POINT, [[C6C.PARAM, 10], [C6C.PARAM, 20]]], 4326],
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
} as any, true);
|
|
380
|
+
|
|
381
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
382
|
+
expect(sql).toMatch(/ST_SRID\(ST_POINT\(:param0, :param1\), 4326\)/);
|
|
383
|
+
expect(params).toEqual({ param0: 10, param1: 20 });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('orders by distance to ST_GeomFromText(WKT, 4326) using PARAM for WKT', () => {
|
|
387
|
+
const config = buildParcelConfig();
|
|
388
|
+
|
|
389
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
390
|
+
[C6C.SELECT]: [Property_Units.UNIT_ID],
|
|
391
|
+
[C6C.PAGINATION]: {
|
|
392
|
+
[C6C.LIMIT]: 10,
|
|
393
|
+
[C6C.ORDER]: {
|
|
394
|
+
[C6C.ST_DISTANCE_SPHERE]: [
|
|
395
|
+
Property_Units.LOCATION,
|
|
396
|
+
[C6C.ST_GEOMFROMTEXT, [[C6C.PARAM, 'POINT(-104.8967729 39.3976764)'], 4326]],
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
} as any, false);
|
|
401
|
+
|
|
402
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
403
|
+
expect(sql).toContain('ORDER BY ST_Distance_Sphere(property_units.location, ST_GEOMFROMTEXT(?, 4326))');
|
|
404
|
+
expect(params.slice(-1)[0]).toBe('POINT(-104.8967729 39.3976764)');
|
|
405
|
+
});
|
|
406
|
+
|
|
304
407
|
it('leaves normal table joins unaffected', () => {
|
|
305
408
|
const config = buildTestConfig();
|
|
306
409
|
|
package/src/api/C6Constants.ts
CHANGED
|
@@ -101,6 +101,7 @@ export const C6Constants = {
|
|
|
101
101
|
SECOND_MICROSECOND: 'SECOND_MICROSECOND',
|
|
102
102
|
SELECT: 'SELECT',
|
|
103
103
|
SUBSELECT: 'SUBSELECT',
|
|
104
|
+
PARAM: 'PARAM',
|
|
104
105
|
|
|
105
106
|
// MySQL Spatial Functions
|
|
106
107
|
ST_AREA: 'ST_Area',
|
|
@@ -188,4 +189,4 @@ export const C6Constants = {
|
|
|
188
189
|
};
|
|
189
190
|
|
|
190
191
|
|
|
191
|
-
export const C6C = C6Constants;
|
|
192
|
+
export const C6C = C6Constants;
|
|
@@ -21,6 +21,19 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
21
21
|
throw new Error('Invalid SELECT field entry');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// If the array represents a tuple/literal list (e.g., [lng, lat]) rather than a
|
|
25
|
+
// function call like [FN, ...args], serialize the list as a comma-separated
|
|
26
|
+
// literal sequence so parent calls (like ORDER BY FN(<here>)) can embed it.
|
|
27
|
+
const isNumericString = (s: string) => /^-?\d+(?:\.\d+)?$/.test(String(s).trim());
|
|
28
|
+
if (typeof field[0] !== 'string' || isNumericString(field[0])) {
|
|
29
|
+
return field
|
|
30
|
+
.map((arg) => {
|
|
31
|
+
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
32
|
+
return String(arg);
|
|
33
|
+
})
|
|
34
|
+
.join(', ');
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
let [fn, ...args] = field;
|
|
25
38
|
let alias: string | undefined;
|
|
26
39
|
|
|
@@ -31,6 +44,17 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
31
44
|
|
|
32
45
|
const F = String(fn).toUpperCase();
|
|
33
46
|
|
|
47
|
+
// Parameter placeholder helper: [C6C.PARAM, value]
|
|
48
|
+
if (F === C6C.PARAM) {
|
|
49
|
+
if (!params) {
|
|
50
|
+
throw new Error('PARAM requires parameter tracking.');
|
|
51
|
+
}
|
|
52
|
+
const value = args[0];
|
|
53
|
+
// Use empty column context; ORDER/SELECT literals have no column typing.
|
|
54
|
+
// @ts-ignore addParam is provided by ConditionBuilder in our hierarchy.
|
|
55
|
+
return this.addParam(params, '', value);
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
if (F === C6C.SUBSELECT) {
|
|
35
59
|
if (!params) {
|
|
36
60
|
throw new Error('Scalar subselects in SELECT require parameter tracking.');
|
|
@@ -52,11 +76,18 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
|
|
|
52
76
|
return expr;
|
|
53
77
|
}
|
|
54
78
|
|
|
79
|
+
const identifierPathRegex = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
80
|
+
|
|
55
81
|
const argList = args
|
|
56
82
|
.map(arg => {
|
|
57
83
|
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
58
84
|
if (typeof arg === 'string') {
|
|
59
|
-
|
|
85
|
+
if (identifierPathRegex.test(arg)) {
|
|
86
|
+
this.assertValidIdentifier(arg, 'SELECT expression');
|
|
87
|
+
return arg;
|
|
88
|
+
}
|
|
89
|
+
// Treat numeric-looking strings as literals, not identifier paths
|
|
90
|
+
if (isNumericString(arg)) return arg;
|
|
60
91
|
return arg;
|
|
61
92
|
}
|
|
62
93
|
return String(arg);
|
|
@@ -30,11 +30,18 @@ export abstract class PaginationBuilder<G extends OrmGenerics> extends JoinBuild
|
|
|
30
30
|
}
|
|
31
31
|
// FUNCTION CALL: val is an array of args
|
|
32
32
|
if (Array.isArray(val)) {
|
|
33
|
+
const identifierPathRegex = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
|
+
const isNumericString = (s: string) => /^-?\d+(?:\.\d+)?$/.test(s.trim());
|
|
33
35
|
const args = val
|
|
34
36
|
.map((arg) => {
|
|
35
37
|
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
36
|
-
if (typeof arg === 'string'
|
|
37
|
-
|
|
38
|
+
if (typeof arg === 'string') {
|
|
39
|
+
if (identifierPathRegex.test(arg)) {
|
|
40
|
+
this.assertValidIdentifier(arg, 'ORDER BY argument');
|
|
41
|
+
return arg;
|
|
42
|
+
}
|
|
43
|
+
// numeric-looking strings should be treated as literals
|
|
44
|
+
if (isNumericString(arg)) return arg;
|
|
38
45
|
return arg;
|
|
39
46
|
}
|
|
40
47
|
return String(arg);
|