@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.
@@ -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
- return `ST_Distance_Sphere(${col1}, ${col2}) < ${this.addParam(params, '', threshold)}`;
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
- return `${column}(${geom1}, ${geom2})`;
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));