leangraph 1.1.7 → 1.1.8

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.
@@ -20,6 +20,15 @@ function toSqliteParam(value) {
20
20
  function toSqliteParams(values) {
21
21
  return values.map(toSqliteParam);
22
22
  }
23
+ /**
24
+ * Escape a string for safe interpolation inside a SQL string literal.
25
+ * Used for JSON path keys, label names, and other identifiers that are
26
+ * embedded directly in SQL rather than passed as bind parameters.
27
+ * Prevents SQL injection via backtick-quoted Cypher identifiers.
28
+ */
29
+ function escSqlStr(s) {
30
+ return (s || "").replace(/'/g, "''");
31
+ }
23
32
  /**
24
33
  * Check if a string is an IANA timezone name (contains '/')
25
34
  */
@@ -314,15 +323,19 @@ export class Translator {
314
323
  * The :Label is optional and ignored (global index) - only used if no custom name provided.
315
324
  */
316
325
  translateCreateIndex(clause) {
317
- const indexName = clause.indexName || `idx_${clause.property}`;
318
- const sql = `CREATE INDEX IF NOT EXISTS ${indexName} ON nodes(json_extract(properties, '$.${clause.property}'))`;
326
+ const rawName = clause.indexName || `idx_${clause.property}`;
327
+ // Quote index name as a SQL identifier to prevent injection
328
+ const indexName = `"${rawName.replace(/"/g, '""')}"`;
329
+ const sql = `CREATE INDEX IF NOT EXISTS ${indexName} ON nodes(json_extract(properties, '$.${escSqlStr(clause.property)}'))`;
319
330
  return [{ sql, params: [] }];
320
331
  }
321
332
  /**
322
333
  * Translate DROP INDEX to SQL.
323
334
  */
324
335
  translateDropIndex(clause) {
325
- const sql = `DROP INDEX IF EXISTS ${clause.indexName}`;
336
+ // Quote index name as a SQL identifier to prevent injection
337
+ const indexName = `"${clause.indexName.replace(/"/g, '""')}"`;
338
+ const sql = `DROP INDEX IF EXISTS ${indexName}`;
326
339
  return [{ sql, params: [] }];
327
340
  }
328
341
  // ============================================================================
@@ -769,7 +782,7 @@ export class Translator {
769
782
  const withAliases = this.ctx.withAliases;
770
783
  for (const [key, value] of Object.entries(props)) {
771
784
  if (this.isParameterRef(value)) {
772
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
785
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
773
786
  params.push(this.ctx.paramValues[value.name]);
774
787
  }
775
788
  else if (this.isVariableRef(value)) {
@@ -777,7 +790,7 @@ export class Translator {
777
790
  const varName = value.name;
778
791
  if (withAliases && withAliases.has(varName)) {
779
792
  const originalExpr = withAliases.get(varName);
780
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
793
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
781
794
  if (originalExpr.type === "literal") {
782
795
  params.push(originalExpr.value);
783
796
  }
@@ -789,12 +802,12 @@ export class Translator {
789
802
  }
790
803
  }
791
804
  else {
792
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
805
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
793
806
  params.push(value);
794
807
  }
795
808
  }
796
809
  else {
797
- conditions.push(`json_extract(properties, '$.${key}') = ?`);
810
+ conditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
798
811
  params.push(value);
799
812
  }
800
813
  }
@@ -898,7 +911,7 @@ export class Translator {
898
911
  }
899
912
  if (nullKeys.length > 0) {
900
913
  // Need to merge non-null props and remove null keys
901
- const removePaths = nullKeys.map(k => `'$.${k}'`).join(', ');
914
+ const removePaths = nullKeys.map(k => `'$.${escSqlStr(k)}'`).join(', ');
902
915
  statements.push({
903
916
  sql: `UPDATE ${table} SET properties = json_remove(json_patch(properties, ?), ${removePaths}) WHERE id = ?`,
904
917
  params: [JSON.stringify(nonNullProps), varInfo.alias],
@@ -927,7 +940,7 @@ export class Translator {
927
940
  // For expressions on created nodes, translate with subquery pattern
928
941
  const { sql: exprSql, params: exprParams } = this.translateExpressionForCreatedNode(assignment.value, varInfo.alias);
929
942
  statements.push({
930
- sql: `UPDATE ${table} SET properties = json_set(properties, '$.${assignment.property}', ${exprSql}) WHERE id = ?`,
943
+ sql: `UPDATE ${table} SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', ${exprSql}) WHERE id = ?`,
931
944
  params: [...exprParams, varInfo.alias],
932
945
  });
933
946
  }
@@ -935,7 +948,7 @@ export class Translator {
935
948
  const { sql: exprSql, params: exprParams } = this.translateExpression(assignment.value);
936
949
  // Use json_set with the SQL expression directly
937
950
  statements.push({
938
- sql: `UPDATE ${table} SET properties = json_set(properties, '$.${assignment.property}', ${exprSql}) WHERE id = ?`,
951
+ sql: `UPDATE ${table} SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', ${exprSql}) WHERE id = ?`,
939
952
  params: [...exprParams, varInfo.alias],
940
953
  });
941
954
  }
@@ -945,7 +958,7 @@ export class Translator {
945
958
  // If value is null, remove the property instead of setting it to null
946
959
  if (value === null) {
947
960
  statements.push({
948
- sql: `UPDATE ${table} SET properties = json_remove(properties, '$.${assignment.property}') WHERE id = ?`,
961
+ sql: `UPDATE ${table} SET properties = json_remove(properties, '$.${escSqlStr(assignment.property)}') WHERE id = ?`,
949
962
  params: [varInfo.alias],
950
963
  });
951
964
  }
@@ -953,7 +966,7 @@ export class Translator {
953
966
  assertValidPropertyValue(value);
954
967
  // Use json_set to update the property
955
968
  statements.push({
956
- sql: `UPDATE ${table} SET properties = json_set(properties, '$.${assignment.property}', json(?)) WHERE id = ?`,
969
+ sql: `UPDATE ${table} SET properties = json_set(properties, '$.${escSqlStr(assignment.property)}', json(?)) WHERE id = ?`,
957
970
  params: [JSON.stringify(value), varInfo.alias],
958
971
  });
959
972
  }
@@ -1070,7 +1083,7 @@ export class Translator {
1070
1083
  else if (item.property) {
1071
1084
  // Remove property
1072
1085
  statements.push({
1073
- sql: `UPDATE ${table} SET properties = json_remove(properties, '$.${item.property}') WHERE id = ?`,
1086
+ sql: `UPDATE ${table} SET properties = json_remove(properties, '$.${escSqlStr(item.property)}') WHERE id = ?`,
1074
1087
  params: [varInfo.alias],
1075
1088
  });
1076
1089
  }
@@ -1404,11 +1417,11 @@ export class Translator {
1404
1417
  if (pattern.properties) {
1405
1418
  for (const [key, value] of Object.entries(pattern.properties)) {
1406
1419
  if (this.isParameterRef(value)) {
1407
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
1420
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
1408
1421
  whereParams.push(this.ctx.paramValues[value.name]);
1409
1422
  }
1410
1423
  else {
1411
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
1424
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
1412
1425
  whereParams.push(value);
1413
1426
  }
1414
1427
  }
@@ -1699,21 +1712,21 @@ export class Translator {
1699
1712
  for (const [key, value] of Object.entries(relPattern.edge.properties)) {
1700
1713
  if (this.isParameterRef(value)) {
1701
1714
  if (isOptional) {
1702
- edgeOnConditions.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${key}') = ?`);
1715
+ edgeOnConditions.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1703
1716
  edgeOnParams.push(this.ctx.paramValues[value.name]);
1704
1717
  }
1705
1718
  else {
1706
- whereParts.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${key}') = ?`);
1719
+ whereParts.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1707
1720
  whereParams.push(this.ctx.paramValues[value.name]);
1708
1721
  }
1709
1722
  }
1710
1723
  else {
1711
1724
  if (isOptional) {
1712
- edgeOnConditions.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${key}') = ?`);
1725
+ edgeOnConditions.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1713
1726
  edgeOnParams.push(value);
1714
1727
  }
1715
1728
  else {
1716
- whereParts.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${key}') = ?`);
1729
+ whereParts.push(`json_extract(${relPattern.edgeAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1717
1730
  whereParams.push(value);
1718
1731
  }
1719
1732
  }
@@ -1926,11 +1939,11 @@ export class Translator {
1926
1939
  if (isOptional && targetPattern?.properties) {
1927
1940
  for (const [key, value] of Object.entries(targetPattern.properties)) {
1928
1941
  if (this.isParameterRef(value)) {
1929
- targetOnConditions.push(`json_extract(${relPattern.targetAlias}.properties, '$.${key}') = ?`);
1942
+ targetOnConditions.push(`json_extract(${relPattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1930
1943
  targetOnParams.push(this.ctx.paramValues[value.name]);
1931
1944
  }
1932
1945
  else {
1933
- targetOnConditions.push(`json_extract(${relPattern.targetAlias}.properties, '$.${key}') = ?`);
1946
+ targetOnConditions.push(`json_extract(${relPattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
1934
1947
  targetOnParams.push(value);
1935
1948
  }
1936
1949
  }
@@ -1999,11 +2012,11 @@ export class Translator {
1999
2012
  if (sourcePattern?.properties && !sourceIsOptional) {
2000
2013
  for (const [key, value] of Object.entries(sourcePattern.properties)) {
2001
2014
  if (this.isParameterRef(value)) {
2002
- whereParts.push(`json_extract(${relPattern.sourceAlias}.properties, '$.${key}') = ?`);
2015
+ whereParts.push(`json_extract(${relPattern.sourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
2003
2016
  whereParams.push(this.ctx.paramValues[value.name]);
2004
2017
  }
2005
2018
  else {
2006
- whereParts.push(`json_extract(${relPattern.sourceAlias}.properties, '$.${key}') = ?`);
2019
+ whereParts.push(`json_extract(${relPattern.sourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
2007
2020
  whereParams.push(value);
2008
2021
  }
2009
2022
  }
@@ -2022,11 +2035,11 @@ export class Translator {
2022
2035
  if (targetPattern?.properties) {
2023
2036
  for (const [key, value] of Object.entries(targetPattern.properties)) {
2024
2037
  if (this.isParameterRef(value)) {
2025
- whereParts.push(`json_extract(${relPattern.targetAlias}.properties, '$.${key}') = ?`);
2038
+ whereParts.push(`json_extract(${relPattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
2026
2039
  whereParams.push(this.ctx.paramValues[value.name]);
2027
2040
  }
2028
2041
  else {
2029
- whereParts.push(`json_extract(${relPattern.targetAlias}.properties, '$.${key}') = ?`);
2042
+ whereParts.push(`json_extract(${relPattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
2030
2043
  whereParams.push(value);
2031
2044
  }
2032
2045
  }
@@ -2135,11 +2148,11 @@ export class Translator {
2135
2148
  if (pattern.properties) {
2136
2149
  for (const [key, value] of Object.entries(pattern.properties)) {
2137
2150
  if (this.isParameterRef(value)) {
2138
- onConditions.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2151
+ onConditions.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2139
2152
  onParams.push(this.ctx.paramValues[value.name]);
2140
2153
  }
2141
2154
  else {
2142
- onConditions.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2155
+ onConditions.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2143
2156
  onParams.push(value);
2144
2157
  }
2145
2158
  }
@@ -2164,11 +2177,11 @@ export class Translator {
2164
2177
  if (pattern.properties) {
2165
2178
  for (const [key, value] of Object.entries(pattern.properties)) {
2166
2179
  if (this.isParameterRef(value)) {
2167
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2180
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2168
2181
  whereParams.push(this.ctx.paramValues[value.name]);
2169
2182
  }
2170
2183
  else {
2171
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2184
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2172
2185
  whereParams.push(value);
2173
2186
  }
2174
2187
  }
@@ -2197,11 +2210,11 @@ export class Translator {
2197
2210
  if (pattern.properties) {
2198
2211
  for (const [key, value] of Object.entries(pattern.properties)) {
2199
2212
  if (this.isParameterRef(value)) {
2200
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2213
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2201
2214
  whereParams.push(this.ctx.paramValues[value.name]);
2202
2215
  }
2203
2216
  else {
2204
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2217
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2205
2218
  whereParams.push(value);
2206
2219
  }
2207
2220
  }
@@ -2219,11 +2232,11 @@ export class Translator {
2219
2232
  if (pattern.properties) {
2220
2233
  for (const [key, value] of Object.entries(pattern.properties)) {
2221
2234
  if (this.isParameterRef(value)) {
2222
- onConditions.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2235
+ onConditions.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2223
2236
  onParams.push(this.ctx.paramValues[value.name]);
2224
2237
  }
2225
2238
  else {
2226
- onConditions.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2239
+ onConditions.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2227
2240
  onParams.push(value);
2228
2241
  }
2229
2242
  }
@@ -2242,11 +2255,11 @@ export class Translator {
2242
2255
  if (pattern.properties) {
2243
2256
  for (const [key, value] of Object.entries(pattern.properties)) {
2244
2257
  if (this.isParameterRef(value)) {
2245
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2258
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2246
2259
  whereParams.push(this.ctx.paramValues[value.name]);
2247
2260
  }
2248
2261
  else {
2249
- whereParts.push(`json_extract(${info.alias}.properties, '$.${key}') = ?`);
2262
+ whereParts.push(`json_extract(${info.alias}.properties, '$.${escSqlStr(key)}') = ?`);
2250
2263
  whereParams.push(value);
2251
2264
  }
2252
2265
  }
@@ -3156,7 +3169,7 @@ export class Translator {
3156
3169
  const edgePropParams = [];
3157
3170
  if (edgeProperties) {
3158
3171
  for (const [key, value] of Object.entries(edgeProperties)) {
3159
- edgePropConditions.push(`json_extract(properties, '$.${key}') = ?`);
3172
+ edgePropConditions.push(`json_extract(properties, '$.${escSqlStr(key)}') = ?`);
3160
3173
  edgePropParams.push(value);
3161
3174
  }
3162
3175
  }
@@ -3179,11 +3192,11 @@ export class Translator {
3179
3192
  if (sourcePattern?.properties) {
3180
3193
  for (const [key, value] of Object.entries(sourcePattern.properties)) {
3181
3194
  if (this.isParameterRef(value)) {
3182
- sourceFilterParts.push(`json_extract(src_n.properties, '$.${key}') = ?`);
3195
+ sourceFilterParts.push(`json_extract(src_n.properties, '$.${escSqlStr(key)}') = ?`);
3183
3196
  sourceFilterParams.push(this.ctx.paramValues[value.name]);
3184
3197
  }
3185
3198
  else {
3186
- sourceFilterParts.push(`json_extract(src_n.properties, '$.${key}') = ?`);
3199
+ sourceFilterParts.push(`json_extract(src_n.properties, '$.${escSqlStr(key)}') = ?`);
3187
3200
  sourceFilterParams.push(value);
3188
3201
  }
3189
3202
  }
@@ -3408,11 +3421,11 @@ export class Translator {
3408
3421
  if (sourcePattern?.properties) {
3409
3422
  for (const [key, value] of Object.entries(sourcePattern.properties)) {
3410
3423
  if (this.isParameterRef(value)) {
3411
- whereParts.push(`json_extract(${pattern.sourceAlias}.properties, '$.${key}') = ?`);
3424
+ whereParts.push(`json_extract(${pattern.sourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3412
3425
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3413
3426
  }
3414
3427
  else {
3415
- whereParts.push(`json_extract(${pattern.sourceAlias}.properties, '$.${key}') = ?`);
3428
+ whereParts.push(`json_extract(${pattern.sourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3416
3429
  deferredWhereParams.push(value);
3417
3430
  }
3418
3431
  }
@@ -3522,11 +3535,11 @@ export class Translator {
3522
3535
  if (sourcePattern?.properties) {
3523
3536
  for (const [key, value] of Object.entries(sourcePattern.properties)) {
3524
3537
  if (this.isParameterRef(value)) {
3525
- whereParts.push(`json_extract(${varLengthSourceAlias}.properties, '$.${key}') = ?`);
3538
+ whereParts.push(`json_extract(${varLengthSourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3526
3539
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3527
3540
  }
3528
3541
  else {
3529
- whereParts.push(`json_extract(${varLengthSourceAlias}.properties, '$.${key}') = ?`);
3542
+ whereParts.push(`json_extract(${varLengthSourceAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3530
3543
  deferredWhereParams.push(value);
3531
3544
  }
3532
3545
  }
@@ -3582,11 +3595,11 @@ export class Translator {
3582
3595
  if (targetPattern?.properties) {
3583
3596
  for (const [key, value] of Object.entries(targetPattern.properties)) {
3584
3597
  if (this.isParameterRef(value)) {
3585
- whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3598
+ whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3586
3599
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3587
3600
  }
3588
3601
  else {
3589
- whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3602
+ whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3590
3603
  deferredWhereParams.push(value);
3591
3604
  }
3592
3605
  }
@@ -3627,11 +3640,11 @@ export class Translator {
3627
3640
  if (targetPattern?.properties) {
3628
3641
  for (const [key, value] of Object.entries(targetPattern.properties)) {
3629
3642
  if (this.isParameterRef(value)) {
3630
- targetOnConditions.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3643
+ targetOnConditions.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3631
3644
  allParams.push(this.ctx.paramValues[value.name]);
3632
3645
  }
3633
3646
  else {
3634
- targetOnConditions.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3647
+ targetOnConditions.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3635
3648
  allParams.push(value);
3636
3649
  }
3637
3650
  }
@@ -3672,11 +3685,11 @@ export class Translator {
3672
3685
  if (targetPattern?.properties) {
3673
3686
  for (const [key, value] of Object.entries(targetPattern.properties)) {
3674
3687
  if (this.isParameterRef(value)) {
3675
- whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3688
+ whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3676
3689
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3677
3690
  }
3678
3691
  else {
3679
- whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${key}') = ?`);
3692
+ whereParts.push(`json_extract(${varLengthTargetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3680
3693
  deferredWhereParams.push(value);
3681
3694
  }
3682
3695
  }
@@ -3839,11 +3852,11 @@ export class Translator {
3839
3852
  if (targetPattern2?.properties) {
3840
3853
  for (const [key, value] of Object.entries(targetPattern2.properties)) {
3841
3854
  if (this.isParameterRef(value)) {
3842
- whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${key}') = ?`);
3855
+ whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3843
3856
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3844
3857
  }
3845
3858
  else {
3846
- whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${key}') = ?`);
3859
+ whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3847
3860
  deferredWhereParams.push(value);
3848
3861
  }
3849
3862
  }
@@ -3903,11 +3916,11 @@ export class Translator {
3903
3916
  if (afterTargetPattern?.properties) {
3904
3917
  for (const [key, value] of Object.entries(afterTargetPattern.properties)) {
3905
3918
  if (this.isParameterRef(value)) {
3906
- whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${key}') = ?`);
3919
+ whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3907
3920
  deferredWhereParams.push(this.ctx.paramValues[value.name]);
3908
3921
  }
3909
3922
  else {
3910
- whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${key}') = ?`);
3923
+ whereParts.push(`json_extract(${pattern.targetAlias}.properties, '$.${escSqlStr(key)}') = ?`);
3911
3924
  deferredWhereParams.push(value);
3912
3925
  }
3913
3926
  }
@@ -4090,7 +4103,7 @@ export class Translator {
4090
4103
  if (!varInfo) {
4091
4104
  throw new Error(`Unknown variable: ${clause.expression.variable}`);
4092
4105
  }
4093
- jsonExpr = `json_extract(${varInfo.alias}.properties, '$.${clause.expression.property}')`;
4106
+ jsonExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(clause.expression.property)}')`;
4094
4107
  }
4095
4108
  else if (clause.expression.type === "function" || clause.expression.type === "binary") {
4096
4109
  // Function call like range(1, 10) or binary expression like (first + second)
@@ -4667,7 +4680,7 @@ export class Translator {
4667
4680
  params.push(...objectResult.params);
4668
4681
  // Access property from the result using json_extract
4669
4682
  return {
4670
- sql: `json_extract(${objectResult.sql}, '$.${expr.property}')`,
4683
+ sql: `json_extract(${objectResult.sql}, '$.${escSqlStr(expr.property)}')`,
4671
4684
  tables,
4672
4685
  params,
4673
4686
  };
@@ -4702,7 +4715,7 @@ export class Translator {
4702
4715
  }
4703
4716
  // Access property from the unwound value using json_extract
4704
4717
  return {
4705
- sql: `json_extract(${baseSql}, '$.${expr.property}')`,
4718
+ sql: `json_extract(${baseSql}, '$.${escSqlStr(expr.property)}')`,
4706
4719
  tables,
4707
4720
  params,
4708
4721
  };
@@ -4715,7 +4728,7 @@ export class Translator {
4715
4728
  tables.push(varInfo.alias);
4716
4729
  // Use -> operator to preserve JSON types (returns 'true'/'false' not 1/0)
4717
4730
  return {
4718
- sql: `${varInfo.alias}.properties -> '$.${expr.property}'`,
4731
+ sql: `${varInfo.alias}.properties -> '$.${escSqlStr(expr.property)}'`,
4719
4732
  tables,
4720
4733
  params,
4721
4734
  };
@@ -4737,7 +4750,7 @@ export class Translator {
4737
4750
  }
4738
4751
  tables.push(varInfo.alias);
4739
4752
  return {
4740
- sql: `COUNT(${distinctKeyword}json_extract(${varInfo.alias}.properties, '$.${arg.property}'))`,
4753
+ sql: `COUNT(${distinctKeyword}json_extract(${varInfo.alias}.properties, '$.${escSqlStr(arg.property)}'))`,
4741
4754
  tables,
4742
4755
  params,
4743
4756
  };
@@ -4871,7 +4884,7 @@ export class Translator {
4871
4884
  tables.push(unwindClause.alias);
4872
4885
  // UNWIND variables use the 'value' column from json_each
4873
4886
  return {
4874
- sql: `${expr.functionName}(${distinctKeyword}json_extract(${unwindClause.alias}.value, '$.${arg.property}'))`,
4887
+ sql: `${expr.functionName}(${distinctKeyword}json_extract(${unwindClause.alias}.value, '$.${escSqlStr(arg.property)}'))`,
4875
4888
  tables,
4876
4889
  params,
4877
4890
  };
@@ -4884,7 +4897,7 @@ export class Translator {
4884
4897
  tables.push(varInfo.alias);
4885
4898
  // Use json_extract for numeric properties in aggregations
4886
4899
  return {
4887
- sql: `${expr.functionName}(${distinctKeyword}json_extract(${varInfo.alias}.properties, '$.${arg.property}'))`,
4900
+ sql: `${expr.functionName}(${distinctKeyword}json_extract(${varInfo.alias}.properties, '$.${escSqlStr(arg.property)}'))`,
4888
4901
  tables,
4889
4902
  params,
4890
4903
  };
@@ -5044,7 +5057,7 @@ export class Translator {
5044
5057
  throw new Error(`Unknown variable: ${arg.variable}`);
5045
5058
  }
5046
5059
  tables.push(varInfo.alias);
5047
- valueSql = `json_extract(${varInfo.alias}.properties, '$.${arg.property}')`;
5060
+ valueSql = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(arg.property)}')`;
5048
5061
  }
5049
5062
  else if (arg.type === "variable") {
5050
5063
  // Check if this is an UNWIND variable
@@ -5145,7 +5158,7 @@ export class Translator {
5145
5158
  throw new Error(`Unknown variable: ${valueArg.variable}`);
5146
5159
  }
5147
5160
  tables.push(varInfo.alias);
5148
- valueExpr = `json_extract(${varInfo.alias}.properties, '$.${valueArg.property}')`;
5161
+ valueExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(valueArg.property)}')`;
5149
5162
  }
5150
5163
  else {
5151
5164
  const argResult = this.translateFunctionArg(valueArg);
@@ -5256,7 +5269,7 @@ END FROM (SELECT json_group_array(${valueExpr}) as sv))`,
5256
5269
  // json_quote properly escapes strings for JSON
5257
5270
  // Filter nulls: CASE WHEN value IS NULL THEN NULL ELSE json_quote(value) END
5258
5271
  // GROUP_CONCAT ignores nulls
5259
- const extractExpr = `json_extract(${varInfo.alias}.properties, '$.${arg.property}')`;
5272
+ const extractExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(arg.property)}')`;
5260
5273
  params.push(...collectOrderParams);
5261
5274
  return {
5262
5275
  sql: `COALESCE(json('[' || GROUP_CONCAT(DISTINCT CASE WHEN ${extractExpr} IS NOT NULL THEN json_quote(${extractExpr}) END${collectOrderClause}) || ']'), json('[]'))`,
@@ -5265,7 +5278,7 @@ END FROM (SELECT json_group_array(${valueExpr}) as sv))`,
5265
5278
  };
5266
5279
  }
5267
5280
  // Neo4j's collect() skips NULL values - use GROUP_CONCAT with null filtering
5268
- const extractExpr = `json_extract(${varInfo.alias}.properties, '$.${arg.property}')`;
5281
+ const extractExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(arg.property)}')`;
5269
5282
  params.push(...collectOrderParams);
5270
5283
  return {
5271
5284
  sql: `COALESCE(json('[' || GROUP_CONCAT(CASE WHEN ${extractExpr} IS NOT NULL THEN json_quote(${extractExpr}) END${collectOrderClause}) || ']'), json('[]'))`,
@@ -5751,7 +5764,7 @@ END FROM (SELECT json_group_array(${valueExpr}) as sv))`,
5751
5764
  tables.push(...argResult.tables);
5752
5765
  params.push(...argResult.params);
5753
5766
  return {
5754
- sql: `cypher_to_json_bool(json_type(${argResult.sql}, '$.${propName}') IS NOT NULL)`,
5767
+ sql: `cypher_to_json_bool(json_type(${argResult.sql}, '$.${escSqlStr(propName)}') IS NOT NULL)`,
5755
5768
  tables,
5756
5769
  params,
5757
5770
  };
@@ -9072,7 +9085,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
9072
9085
  const labelsToCheck = expr.labels || (expr.label ? [expr.label] : []);
9073
9086
  // Use EXISTS with json_each to check if label is in the array
9074
9087
  // For multiple labels, all must be present (AND)
9075
- const labelChecks = labelsToCheck.map(l => `EXISTS(SELECT 1 FROM json_each(${varInfo.alias}.label) WHERE value = '${l}')`).join(' AND ');
9088
+ const labelChecks = labelsToCheck.map(l => `EXISTS(SELECT 1 FROM json_each(${varInfo.alias}.label) WHERE value = '${escSqlStr(l)}')`).join(' AND ');
9076
9089
  return {
9077
9090
  sql: `CASE WHEN ${varInfo.alias}.id IS NULL THEN NULL ELSE cypher_to_json_bool(${labelChecks}) END`,
9078
9091
  tables,
@@ -9087,7 +9100,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
9087
9100
  params.push(...objectResult.params);
9088
9101
  // Access property from the result using json_extract
9089
9102
  return {
9090
- sql: `json_extract(${objectResult.sql}, '$.${expr.property}')`,
9103
+ sql: `json_extract(${objectResult.sql}, '$.${escSqlStr(expr.property)}')`,
9091
9104
  tables,
9092
9105
  params,
9093
9106
  };
@@ -9104,7 +9117,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
9104
9117
  if (item.type === "property") {
9105
9118
  // .property shorthand - extract property from source
9106
9119
  const key = item.property;
9107
- jsonParts.push(`'${key}', json_extract(${sourceResult.sql}, '$.${key}')`);
9120
+ jsonParts.push(`'${escSqlStr(key)}', json_extract(${sourceResult.sql}, '$.${escSqlStr(key)}')`);
9108
9121
  }
9109
9122
  else if (item.type === "literal") {
9110
9123
  // key: value syntax
@@ -9112,7 +9125,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
9112
9125
  const valueResult = this.translateExpression(item.value);
9113
9126
  tables.push(...valueResult.tables);
9114
9127
  params.push(...valueResult.params);
9115
- jsonParts.push(`'${key}', ${valueResult.sql}`);
9128
+ jsonParts.push(`'${escSqlStr(key)}', ${valueResult.sql}`);
9116
9129
  }
9117
9130
  else if (item.type === "allProperties") {
9118
9131
  // .* - project all properties (not yet fully supported, just returns the source)
@@ -10456,7 +10469,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10456
10469
  if (expr.type === "property") {
10457
10470
  const varInfo = this.ctx.variables.get(expr.variable);
10458
10471
  if (varInfo) {
10459
- return `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`;
10472
+ return `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`;
10460
10473
  }
10461
10474
  }
10462
10475
  return sql;
@@ -10467,7 +10480,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10467
10480
  // Replace -> with json_extract for numeric operations
10468
10481
  const varInfo = this.ctx.variables.get(expr.variable);
10469
10482
  if (varInfo) {
10470
- return `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`;
10483
+ return `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`;
10471
10484
  }
10472
10485
  }
10473
10486
  return sql;
@@ -10496,7 +10509,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10496
10509
  const propName = expr.property;
10497
10510
  params.push(nodeId);
10498
10511
  return {
10499
- sql: `(SELECT json_extract(properties, '$.${propName}') FROM nodes WHERE id = ?)`,
10512
+ sql: `(SELECT json_extract(properties, '$.${escSqlStr(propName)}') FROM nodes WHERE id = ?)`,
10500
10513
  params,
10501
10514
  };
10502
10515
  }
@@ -10743,7 +10756,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10743
10756
  if (expr.type === "property") {
10744
10757
  const varInfo = this.ctx.variables.get(expr.variable);
10745
10758
  if (varInfo) {
10746
- return `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`;
10759
+ return `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`;
10747
10760
  }
10748
10761
  }
10749
10762
  return sql;
@@ -10850,7 +10863,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10850
10863
  // For property access, use json_extract
10851
10864
  const varInfo = this.ctx.variables.get(listExpr.variable);
10852
10865
  if (varInfo) {
10853
- sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${listExpr.property}')`;
10866
+ sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(listExpr.property)}')`;
10854
10867
  }
10855
10868
  }
10856
10869
  // Determine what to select: the mapped expression or just the value
@@ -10990,7 +11003,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
10990
11003
  // For property access, use json_extract
10991
11004
  const varInfo = this.ctx.variables.get(listExpr.variable);
10992
11005
  if (varInfo) {
10993
- sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${listExpr.property}')`;
11006
+ sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(listExpr.property)}')`;
10994
11007
  }
10995
11008
  }
10996
11009
  // Build the WHERE clause from the filter condition
@@ -11025,7 +11038,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
11025
11038
  // For property access, use json_extract
11026
11039
  const varInfo = this.ctx.variables.get(listExpr.variable);
11027
11040
  if (varInfo) {
11028
- sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${listExpr.property}')`;
11041
+ sourceExpr = `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(listExpr.property)}')`;
11029
11042
  }
11030
11043
  }
11031
11044
  // Translate the map expression
@@ -11061,18 +11074,18 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
11061
11074
  case "property": {
11062
11075
  if (expr.variable === iterVar) {
11063
11076
  // Property access on the iterator variable: x.name
11064
- return { sql: `json_extract(${elemAlias}.value, '$.${expr.property}')`, params };
11077
+ return { sql: `json_extract(${elemAlias}.value, '$.${escSqlStr(expr.property)}')`, params };
11065
11078
  }
11066
11079
  if (expr.variable === accVar) {
11067
11080
  // Property access on accumulator (if acc is an object)
11068
- return { sql: `json_extract(${tableAlias}.acc, '$.${expr.property}')`, params };
11081
+ return { sql: `json_extract(${tableAlias}.acc, '$.${escSqlStr(expr.property)}')`, params };
11069
11082
  }
11070
11083
  // Standard property translation
11071
11084
  const varInfo = this.ctx.variables.get(expr.variable);
11072
11085
  if (varInfo) {
11073
- return { sql: `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`, params };
11086
+ return { sql: `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`, params };
11074
11087
  }
11075
- return { sql: `json_extract(${expr.variable}, '$.${expr.property}')`, params };
11088
+ return { sql: `json_extract(${expr.variable}, '$.${escSqlStr(expr.property)}')`, params };
11076
11089
  }
11077
11090
  case "literal": {
11078
11091
  if (expr.value === null)
@@ -11510,13 +11523,13 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
11510
11523
  return { sql: varResult.sql, params: varResult.params };
11511
11524
  case "property":
11512
11525
  if (expr.variable === startVar) {
11513
- return { sql: `json_extract(${startAlias}.properties, '$.${expr.property}')`, params };
11526
+ return { sql: `json_extract(${startAlias}.properties, '$.${escSqlStr(expr.property)}')`, params };
11514
11527
  }
11515
11528
  if (expr.variable === edgeVar) {
11516
- return { sql: `json_extract(${edgeAlias}.properties, '$.${expr.property}')`, params };
11529
+ return { sql: `json_extract(${edgeAlias}.properties, '$.${escSqlStr(expr.property)}')`, params };
11517
11530
  }
11518
11531
  if (expr.variable === targetVar) {
11519
- return { sql: `json_extract(${targetAlias}.properties, '$.${expr.property}')`, params };
11532
+ return { sql: `json_extract(${targetAlias}.properties, '$.${escSqlStr(expr.property)}')`, params };
11520
11533
  }
11521
11534
  // Fall through to regular translation
11522
11535
  const propResult = this.translateExpression(expr);
@@ -11595,7 +11608,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
11595
11608
  const scope = findScope(expr.variable);
11596
11609
  if (scope) {
11597
11610
  // Extract property from the JSON value in the list element
11598
- return { sql: `json_extract(${scope.tableAlias}.value, '$.${expr.property}')`, params };
11611
+ return { sql: `json_extract(${scope.tableAlias}.value, '$.${escSqlStr(expr.property)}')`, params };
11599
11612
  }
11600
11613
  // Fall through to regular translation for other variables
11601
11614
  const propResult = this.translateExpression(expr);
@@ -11979,7 +11992,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
11979
11992
  if (expr.type === "property") {
11980
11993
  const varInfo = this.ctx.variables.get(expr.variable);
11981
11994
  if (varInfo) {
11982
- return `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`;
11995
+ return `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`;
11983
11996
  }
11984
11997
  }
11985
11998
  // For literal arrays, the sql is already a json_array() call
@@ -12848,7 +12861,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
12848
12861
  const originalExpr = withAliases.get(expr.variable);
12849
12862
  const objectResult = this.translateExpression(originalExpr);
12850
12863
  return {
12851
- sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${objectResult.sql}, '$.${expr.property}')`),
12864
+ sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${objectResult.sql}, '$.${escSqlStr(expr.property)}')`),
12852
12865
  params: objectResult.params,
12853
12866
  };
12854
12867
  }
@@ -12868,7 +12881,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
12868
12881
  if (unwindClause) {
12869
12882
  // UNWIND variables use the 'value' column from json_each
12870
12883
  return {
12871
- sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${unwindClause.alias}.value, '$.${expr.property}')`),
12884
+ sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${unwindClause.alias}.value, '$.${escSqlStr(expr.property)}')`),
12872
12885
  params: [],
12873
12886
  };
12874
12887
  }
@@ -12878,7 +12891,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
12878
12891
  throw new Error(`Unknown variable: ${expr.variable}`);
12879
12892
  }
12880
12893
  return {
12881
- sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${varInfo.alias}.properties, '$.${expr.property}')`),
12894
+ sql: this.buildDateTimeWithOffsetOrderBy(`json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`),
12882
12895
  params: [],
12883
12896
  };
12884
12897
  }
@@ -13068,7 +13081,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
13068
13081
  throw new Error(`Unknown variable: ${expr.variable}`);
13069
13082
  }
13070
13083
  return {
13071
- sql: `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`,
13084
+ sql: `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`,
13072
13085
  params: [],
13073
13086
  };
13074
13087
  }
@@ -13089,7 +13102,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
13089
13102
  if (unwindClause) {
13090
13103
  // UNWIND variables use the 'value' column from json_each
13091
13104
  return {
13092
- sql: `json_extract(${unwindClause.alias}.value, '$.${expr.property}')`,
13105
+ sql: `json_extract(${unwindClause.alias}.value, '$.${escSqlStr(expr.property)}')`,
13093
13106
  params: [],
13094
13107
  };
13095
13108
  }
@@ -13099,7 +13112,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
13099
13112
  throw new Error(`Unknown variable: ${expr.variable}`);
13100
13113
  }
13101
13114
  return {
13102
- sql: `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`,
13115
+ sql: `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`,
13103
13116
  params: [],
13104
13117
  };
13105
13118
  }
@@ -13213,7 +13226,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
13213
13226
  const labelsToCheck = expr.labels || (expr.label ? [expr.label] : []);
13214
13227
  // Use EXISTS with json_each to check if label is in the array
13215
13228
  // For multiple labels, all must be present (AND)
13216
- const labelChecks = labelsToCheck.map((l) => `EXISTS(SELECT 1 FROM json_each(${varInfo.alias}.label) WHERE value = '${l}')`).join(' AND ');
13229
+ const labelChecks = labelsToCheck.map((l) => `EXISTS(SELECT 1 FROM json_each(${varInfo.alias}.label) WHERE value = '${escSqlStr(l)}')`).join(' AND ');
13217
13230
  return {
13218
13231
  sql: `(${labelChecks})`,
13219
13232
  params: [],
@@ -13256,7 +13269,7 @@ SELECT COALESCE(json_group_array(CAST(n AS INTEGER)), json_array()) FROM r)`,
13256
13269
  }
13257
13270
  tables.push(varInfo.alias);
13258
13271
  return {
13259
- sql: `json_extract(${varInfo.alias}.properties, '$.${expr.property}')`,
13272
+ sql: `json_extract(${varInfo.alias}.properties, '$.${escSqlStr(expr.property)}')`,
13260
13273
  tables,
13261
13274
  params,
13262
13275
  };