@malloydata/malloy 0.0.394 → 0.0.396

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.
Files changed (84) hide show
  1. package/dist/api/foundation/compile.d.ts +7 -6
  2. package/dist/api/foundation/compile.js +22 -6
  3. package/dist/api/foundation/config.d.ts +2 -3
  4. package/dist/api/foundation/config.js +23 -11
  5. package/dist/api/foundation/core.js +1 -1
  6. package/dist/api/foundation/runtime.d.ts +85 -5
  7. package/dist/api/foundation/runtime.js +204 -14
  8. package/dist/api/foundation/types.d.ts +2 -0
  9. package/dist/api/util.js +4 -0
  10. package/dist/connection/base_connection.js +6 -0
  11. package/dist/connection/validate_table_path.d.ts +10 -0
  12. package/dist/connection/validate_table_path.js +56 -0
  13. package/dist/dialect/databricks/databricks.d.ts +4 -4
  14. package/dist/dialect/databricks/databricks.js +17 -22
  15. package/dist/dialect/dialect.d.ts +100 -4
  16. package/dist/dialect/dialect.js +145 -1
  17. package/dist/dialect/duckdb/duckdb.d.ts +2 -3
  18. package/dist/dialect/duckdb/duckdb.js +12 -14
  19. package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
  20. package/dist/dialect/duckdb/table-path-parser.js +57 -0
  21. package/dist/dialect/index.d.ts +2 -0
  22. package/dist/dialect/index.js +4 -1
  23. package/dist/dialect/mysql/mysql.d.ts +4 -4
  24. package/dist/dialect/mysql/mysql.js +25 -20
  25. package/dist/dialect/pg_impl.d.ts +3 -1
  26. package/dist/dialect/pg_impl.js +6 -3
  27. package/dist/dialect/postgres/postgres.d.ts +1 -3
  28. package/dist/dialect/postgres/postgres.js +8 -16
  29. package/dist/dialect/snowflake/snowflake.d.ts +4 -4
  30. package/dist/dialect/snowflake/snowflake.js +11 -27
  31. package/dist/dialect/standardsql/standardsql.d.ts +6 -4
  32. package/dist/dialect/standardsql/standardsql.js +36 -15
  33. package/dist/dialect/table-path.d.ts +54 -0
  34. package/dist/dialect/table-path.js +144 -0
  35. package/dist/dialect/trino/trino.d.ts +0 -3
  36. package/dist/dialect/trino/trino.js +7 -20
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +4 -2
  39. package/dist/lang/ast/expressions/expr-func.js +30 -11
  40. package/dist/lang/ast/expressions/expr-given.js +1 -0
  41. package/dist/lang/ast/field-space/reference-field.js +1 -1
  42. package/dist/lang/ast/source-elements/sql-source.js +4 -0
  43. package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
  44. package/dist/lang/ast/source-elements/table-source.js +24 -19
  45. package/dist/lang/ast/statements/define-given.d.ts +1 -0
  46. package/dist/lang/ast/statements/define-given.js +7 -0
  47. package/dist/lang/ast/statements/import-statement.js +4 -0
  48. package/dist/lang/ast/types/annotation-elements.d.ts +1 -0
  49. package/dist/lang/ast/types/annotation-elements.js +10 -3
  50. package/dist/lang/ast/types/malloy-element.d.ts +1 -0
  51. package/dist/lang/ast/types/malloy-element.js +4 -0
  52. package/dist/lang/malloy-to-ast.d.ts +2 -1
  53. package/dist/lang/malloy-to-ast.js +11 -1
  54. package/dist/lang/parse-log.d.ts +2 -0
  55. package/dist/lang/parse-log.js +4 -0
  56. package/dist/lang/parse-malloy.d.ts +4 -1
  57. package/dist/lang/parse-malloy.js +63 -11
  58. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
  59. package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
  60. package/dist/lang/test/test-translator.d.ts +19 -5
  61. package/dist/lang/test/test-translator.js +15 -12
  62. package/dist/lang/translate-response.d.ts +1 -1
  63. package/dist/lang/zone.d.ts +2 -0
  64. package/dist/lang/zone.js +10 -0
  65. package/dist/model/constant_expression_compiler.js +14 -5
  66. package/dist/model/expression_compiler.js +19 -17
  67. package/dist/model/field_instance.js +7 -3
  68. package/dist/model/filter_compilers.js +1 -1
  69. package/dist/model/given_binding.js +26 -21
  70. package/dist/model/index.d.ts +1 -0
  71. package/dist/model/index.js +3 -1
  72. package/dist/model/malloy_compile_error.d.ts +13 -0
  73. package/dist/model/malloy_compile_error.js +23 -0
  74. package/dist/model/malloy_types.d.ts +2 -0
  75. package/dist/model/query_model_impl.js +9 -8
  76. package/dist/model/query_node.d.ts +5 -5
  77. package/dist/model/query_node.js +21 -16
  78. package/dist/model/query_query.js +60 -44
  79. package/dist/model/sql_compiled.d.ts +2 -4
  80. package/dist/model/sql_compiled.js +20 -18
  81. package/dist/test/test-models.js +2 -2
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +4 -4
@@ -0,0 +1,2 @@
1
+ import type { ValidateTablePathResult } from '../table-path';
2
+ export declare function validateDuckDBTablePath(input: string): ValidateTablePathResult;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateDuckDBTablePath = validateDuckDBTablePath;
8
+ const table_path_1 = require("../table-path");
9
+ const DUCKDB_FILE_PATH_RE = /^[A-Za-z0-9._\-/:?*]+$/;
10
+ const DUCKDB_SINGLE_QUOTED_RE = /^'(?:[^']|'')*'$/;
11
+ function validateDuckDBTablePath(input) {
12
+ if (input.length === 0) {
13
+ return { ok: false, error: 'DuckDB table path is empty' };
14
+ }
15
+ // Branch 1: explicit single-quoted literal.
16
+ if (input[0] === "'") {
17
+ if (DUCKDB_SINGLE_QUOTED_RE.test(input)) {
18
+ const body = input.slice(1, -1).replace(/''/g, "'");
19
+ if (body.includes(';') || body.includes('--')) {
20
+ return {
21
+ ok: false,
22
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — ` +
23
+ 'forbidden character `;` or `--` in single-quoted body.',
24
+ };
25
+ }
26
+ return { ok: true, canonical: input };
27
+ }
28
+ return {
29
+ ok: false,
30
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — ` +
31
+ 'unterminated or trailing-junk single-quoted literal.',
32
+ };
33
+ }
34
+ // Branch 2: identifier path (same grammar as ANSI dialects).
35
+ const id = (0, table_path_1.validateDottedTablePath)(input, {
36
+ quoteChar: '"',
37
+ escapeStyle: 'doubled',
38
+ bareIdentRegex: /^[A-Za-z_][A-Za-z0-9_]*/,
39
+ dialectName: 'DuckDB',
40
+ });
41
+ if (id.ok)
42
+ return id;
43
+ // Branch 3: file-path convenience.
44
+ if (DUCKDB_FILE_PATH_RE.test(input)) {
45
+ return { ok: true, canonical: `'${input}'` };
46
+ }
47
+ return {
48
+ ok: false,
49
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — expected an ` +
50
+ 'identifier path, a quoted identifier path, a single-quoted ' +
51
+ "literal ('foo.csv'), or a file-path-shaped string of letters, " +
52
+ 'digits, and `._-/:?*`. For table-valued function calls (e.g. ' +
53
+ "read_parquet('foo.parquet')) or other table expressions, use a " +
54
+ 'SQL block instead: connection.sql("""SELECT * FROM …""").',
55
+ };
56
+ }
57
+ //# sourceMappingURL=table-path-parser.js.map
@@ -14,3 +14,5 @@ export { getMalloyStandardFunctions } from './functions';
14
14
  export type { MalloyStandardFunctionImplementations } from './functions';
15
15
  export type { TinyToken } from './tiny_parser';
16
16
  export { TinyParser } from './tiny_parser';
17
+ export type { DecodeDottedTablePathResult, DottedTablePathOptions, TablePathEscapeStyle, TablePathSegment, ValidateTablePathResult, } from './table-path';
18
+ export { decodeDottedTablePath, validateDottedTablePath } from './table-path';
@@ -22,7 +22,7 @@
22
22
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.TinyParser = exports.getMalloyStandardFunctions = exports.registerDialect = exports.getDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.TrinoDialect = exports.SnowflakeDialect = exports.DuckDBDialect = exports.PostgresDialect = exports.StandardSQLDialect = exports.qtz = exports.Dialect = exports.sql = exports.literal = exports.variadicParam = exports.param = exports.spread = exports.maxScalar = exports.minAggregate = exports.minScalar = exports.overload = exports.makeParam = exports.anyExprType = exports.arg = void 0;
25
+ exports.validateDottedTablePath = exports.decodeDottedTablePath = exports.TinyParser = exports.getMalloyStandardFunctions = exports.registerDialect = exports.getDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.TrinoDialect = exports.SnowflakeDialect = exports.DuckDBDialect = exports.PostgresDialect = exports.StandardSQLDialect = exports.qtz = exports.Dialect = exports.sql = exports.literal = exports.variadicParam = exports.param = exports.spread = exports.maxScalar = exports.minAggregate = exports.minScalar = exports.overload = exports.makeParam = exports.anyExprType = exports.arg = void 0;
26
26
  var util_1 = require("./functions/util");
27
27
  Object.defineProperty(exports, "arg", { enumerable: true, get: function () { return util_1.arg; } });
28
28
  Object.defineProperty(exports, "anyExprType", { enumerable: true, get: function () { return util_1.anyExprType; } });
@@ -60,4 +60,7 @@ var functions_1 = require("./functions");
60
60
  Object.defineProperty(exports, "getMalloyStandardFunctions", { enumerable: true, get: function () { return functions_1.getMalloyStandardFunctions; } });
61
61
  var tiny_parser_1 = require("./tiny_parser");
62
62
  Object.defineProperty(exports, "TinyParser", { enumerable: true, get: function () { return tiny_parser_1.TinyParser; } });
63
+ var table_path_1 = require("./table-path");
64
+ Object.defineProperty(exports, "decodeDottedTablePath", { enumerable: true, get: function () { return table_path_1.decodeDottedTablePath; } });
65
+ Object.defineProperty(exports, "validateDottedTablePath", { enumerable: true, get: function () { return table_path_1.validateDottedTablePath; } });
63
66
  //# sourceMappingURL=index.js.map
@@ -4,6 +4,9 @@ import { Dialect } from '../dialect';
4
4
  import type { DialectFunctionOverloadDef } from '../functions';
5
5
  export declare class MySQLDialect extends Dialect {
6
6
  name: string;
7
+ stringLiteralStyle: "backslash";
8
+ identifierEscapeStyle: "doubled";
9
+ identifierQuoteChar: string;
7
10
  defaultNumberType: string;
8
11
  defaultDecimalType: string;
9
12
  udfPrefix: string;
@@ -32,9 +35,9 @@ export declare class MySQLDialect extends Dialect {
32
35
  booleanType: BooleanTypeSupport;
33
36
  orderByClause: OrderByClauseType;
34
37
  maxIdentifierLength: number;
38
+ tablePathBareIdentRegex: RegExp;
35
39
  malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
36
40
  sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
37
- quoteTablePath(tablePath: string): string;
38
41
  sqlGroupSetTable(groupSetCount: number): string;
39
42
  sqlAnyValue(_groupSet: number, fieldName: string): string;
40
43
  private mapFields;
@@ -54,7 +57,6 @@ export declare class MySQLDialect extends Dialect {
54
57
  sqlCreateFunction(id: string, funcText: string): string;
55
58
  sqlCreateFunctionCombineLastStage(lastStageName: string): string;
56
59
  sqlSelectAliasAsStruct(_alias: string, _fieldList: DialectFieldList): string;
57
- sqlMaybeQuoteIdentifier(identifier: string): string;
58
60
  sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
59
61
  sqlNowExpr(): string;
60
62
  sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
@@ -73,8 +75,6 @@ export declare class MySQLDialect extends Dialect {
73
75
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
74
76
  sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
75
77
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
76
- sqlLiteralString(literal: string): string;
77
- sqlLiteralRegexp(literal: string): string;
78
78
  getDialectFunctionOverrides(): {
79
79
  [name: string]: DialectFunctionOverloadDef[];
80
80
  };
@@ -94,6 +94,9 @@ class MySQLDialect extends dialect_1.Dialect {
94
94
  constructor() {
95
95
  super(...arguments);
96
96
  this.name = 'mysql';
97
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Backslash;
98
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
99
+ this.identifierQuoteChar = '`';
97
100
  this.defaultNumberType = 'DOUBLE PRECISION';
98
101
  this.defaultDecimalType = 'DECIMAL';
99
102
  this.udfPrefix = 'ms_temp.__udf';
@@ -121,6 +124,11 @@ class MySQLDialect extends dialect_1.Dialect {
121
124
  this.booleanType = 'simulated';
122
125
  this.orderByClause = 'ordinal';
123
126
  this.maxIdentifierLength = 64;
127
+ // MySQL bare identifiers allow `$` and may start with a digit, but
128
+ // cannot be entirely digits (or they lex as number literals). The
129
+ // regex requires at least one non-digit char somewhere in the run.
130
+ // Verified against the live engine.
131
+ this.tablePathBareIdentRegex = /^[A-Za-z0-9_$]*[A-Za-z_$][A-Za-z0-9_$]*/;
124
132
  }
125
133
  malloyTypeToSQLType(malloyType) {
126
134
  switch (malloyType.type) {
@@ -154,12 +162,6 @@ class MySQLDialect extends dialect_1.Dialect {
154
162
  rawType: baseSqlType,
155
163
  });
156
164
  }
157
- quoteTablePath(tablePath) {
158
- return tablePath
159
- .split('.')
160
- .map(part => `\`${part}\``)
161
- .join('.');
162
- }
163
165
  sqlGroupSetTable(groupSetCount) {
164
166
  return `CROSS JOIN (select number - 1 as group_set from JSON_TABLE(cast(concat("[1", repeat(",1", ${groupSetCount}), "]") as JSON),"$[*]" COLUMNS(number FOR ORDINALITY)) group_set) as group_set`;
165
167
  }
@@ -167,7 +169,11 @@ class MySQLDialect extends dialect_1.Dialect {
167
169
  return `MAX(${fieldName})`;
168
170
  }
169
171
  mapFields(fieldList) {
170
- return fieldList.map(f => `"${f.rawName}", ${f.sqlExpression}`).join(', ');
172
+ // JSON_OBJECT key is a string value. Routing rawName through
173
+ // sqlLiteralString also keeps the SQL valid under ANSI_QUOTES mode.
174
+ return fieldList
175
+ .map(f => `${this.sqlLiteralString(f.rawName)}, ${f.sqlExpression}`)
176
+ .join(', ');
171
177
  }
172
178
  sqlAggregateTurtle(groupSet, fieldList, orderBy) {
173
179
  const separator = ',';
@@ -218,7 +224,12 @@ class MySQLDialect extends dialect_1.Dialect {
218
224
  ((_a = f.typeDef.rawType) === null || _a === void 0 ? void 0 : _a.match(/json/))) {
219
225
  fType = f.typeDef.rawType.toUpperCase();
220
226
  }
221
- fields.push(`${this.sqlMaybeQuoteIdentifier(f.sqlOutputName)} ${fType} PATH "$.${f.rawName}"`);
227
+ // JSON_TABLE PATH argument is a string literal containing a
228
+ // JSONPath. Render rawName through sqlLiteralString so a single
229
+ // quote in the user-controlled field name cannot close the SQL
230
+ // string literal.
231
+ const jsonPathLit = this.sqlLiteralString('$.' + f.rawName);
232
+ fields.push(`${this.sqlQuoteIdentifier(f.sqlOutputName)} ${fType} PATH ${jsonPathLit}`);
222
233
  }
223
234
  return fields.join(',\n');
224
235
  }
@@ -266,7 +277,11 @@ class MySQLDialect extends dialect_1.Dialect {
266
277
  }
267
278
  sqlFieldReference(parentAlias, parentType, childName, childType) {
268
279
  if (parentType === 'array[scalar]' || parentType === 'record') {
269
- let ret = `JSON_UNQUOTE(JSON_EXTRACT(${parentAlias},'$.${childName}'))`;
280
+ // childName comes from user-controlled record field names; render
281
+ // the JSON path through sqlLiteralString so a single quote in the
282
+ // name cannot close the SQL string literal.
283
+ const jsonPathLit = this.sqlLiteralString('$.' + childName);
284
+ let ret = `JSON_UNQUOTE(JSON_EXTRACT(${parentAlias},${jsonPathLit}))`;
270
285
  if (parentType === 'array[scalar]') {
271
286
  ret = `JSON_UNQUOTE(${parentAlias}.\`value\`)`;
272
287
  }
@@ -280,7 +295,7 @@ class MySQLDialect extends dialect_1.Dialect {
280
295
  return `CAST(${ret} as JSON)`;
281
296
  }
282
297
  }
283
- const child = this.sqlMaybeQuoteIdentifier(childName);
298
+ const child = this.sqlQuoteIdentifier(childName);
284
299
  return `${parentAlias}.${child}`;
285
300
  }
286
301
  sqlCreateFunction(id, funcText) {
@@ -297,9 +312,6 @@ class MySQLDialect extends dialect_1.Dialect {
297
312
  // .map(name => `'${name.replace(/`/g, '')}', \`${alias}\`.${name}`)
298
313
  // .join(',')})`;
299
314
  }
300
- sqlMaybeQuoteIdentifier(identifier) {
301
- return '`' + identifier.replace(/`/g, '``') + '`';
302
- }
303
315
  // TODO: Check what this is.
304
316
  sqlCreateTableAsSelect(_tableName, _sql) {
305
317
  throw new Error('Not implemented Yet');
@@ -441,13 +453,6 @@ class MySQLDialect extends dialect_1.Dialect {
441
453
  }
442
454
  return tableSQL;
443
455
  }
444
- sqlLiteralString(literal) {
445
- const noVirgule = literal.replace(/\\/g, '\\\\');
446
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
447
- }
448
- sqlLiteralRegexp(literal) {
449
- return "'" + literal.replace(/'/g, "''") + "'";
450
- }
451
456
  getDialectFunctionOverrides() {
452
457
  return (0, functions_1.expandOverrideMap)(function_overrides_1.MYSQL_MALLOY_STANDARD_OVERLOADS);
453
458
  }
@@ -9,6 +9,9 @@ export declare const timeExtractMap: Record<string, string>;
9
9
  export declare abstract class PostgresBase extends Dialect {
10
10
  hasTimestamptz: boolean;
11
11
  supportsBigIntPrecision: boolean;
12
+ stringLiteralStyle: "doubled";
13
+ identifierEscapeStyle: "doubled";
14
+ identifierQuoteChar: string;
12
15
  sqlNowExpr(): string;
13
16
  sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
14
17
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
@@ -18,7 +21,6 @@ export declare abstract class PostgresBase extends Dialect {
18
21
  sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
19
22
  sqlLiteralRecord(_lit: RecordLiteralNode): string;
20
23
  sqlLiteralArray(lit: ArrayLiteralNode): string;
21
- sqlMaybeQuoteIdentifier(identifier: string): string;
22
24
  sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
23
25
  sql: string;
24
26
  typeDef: AtomicTypeDef;
@@ -23,6 +23,12 @@ class PostgresBase extends dialect_1.Dialect {
23
23
  this.hasTimestamptz = true;
24
24
  // Postgres-family dialects use JSON serialization which loses bigint precision
25
25
  this.supportsBigIntPrecision = false;
26
+ // All current Postgres-family dialects (Postgres, DuckDB, Trino, Presto)
27
+ // use ANSI doubled-quote literal escaping and ANSI double-quote identifiers.
28
+ // Subclasses may override.
29
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Doubled;
30
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
31
+ this.identifierQuoteChar = '"';
26
32
  }
27
33
  sqlNowExpr() {
28
34
  return 'LOCALTIMESTAMP';
@@ -96,9 +102,6 @@ class PostgresBase extends dialect_1.Dialect {
96
102
  const array = lit.kids.values.map(val => val.sql);
97
103
  return 'ARRAY[' + array.join(',') + ']';
98
104
  }
99
- sqlMaybeQuoteIdentifier(identifier) {
100
- return '"' + identifier.replace(/"/g, '""') + '"';
101
- }
102
105
  sqlConvertToCivilTime(expr, timezone, typeDef) {
103
106
  // PostgreSQL/DuckDB: AT TIME ZONE is polymorphic
104
107
  // For timestamptz (TIMESTAMPTZ): AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
@@ -27,7 +27,7 @@ export declare class PostgresDialect extends PostgresBase {
27
27
  compoundObjectInSchema: boolean;
28
28
  likeEscape: boolean;
29
29
  maxIdentifierLength: number;
30
- quoteTablePath(tablePath: string): string;
30
+ tablePathBareIdentRegex: RegExp;
31
31
  sqlGroupSetTable(groupSetCount: number): string;
32
32
  sqlAnyValue(groupSet: number, fieldName: string): string;
33
33
  mapFields(fieldList: DialectFieldList): string;
@@ -52,8 +52,6 @@ export declare class PostgresDialect extends PostgresBase {
52
52
  sqlAggDistinct(key: string, values: string[], func: (valNames: string[]) => string): string;
53
53
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
54
54
  sqlOrderBy(orderTerms: string[]): string;
55
- sqlLiteralString(literal: string): string;
56
- sqlLiteralRegexp(literal: string): string;
57
55
  getDialectFunctionOverrides(): {
58
56
  [name: string]: DialectFunctionOverloadDef[];
59
57
  };
@@ -99,12 +99,9 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
99
99
  this.compoundObjectInSchema = false;
100
100
  this.likeEscape = false;
101
101
  this.maxIdentifierLength = 63;
102
- }
103
- quoteTablePath(tablePath) {
104
- return tablePath
105
- .split('.')
106
- .map(part => `"${part}"`)
107
- .join('.');
102
+ // Postgres bare-identifier continuation allows `$` (verified against
103
+ // the live engine: `postgres.table('foo$bar')` resolves successfully).
104
+ this.tablePathBareIdentRegex = /^[A-Za-z_][A-Za-z0-9_$]*/;
108
105
  }
109
106
  sqlGroupSetTable(groupSetCount) {
110
107
  return `CROSS JOIN GENERATE_SERIES(0,${groupSetCount},1) as group_set`;
@@ -164,7 +161,8 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
164
161
  return `(${parentAlias}->>'__row_id')`;
165
162
  }
166
163
  if (parentType !== 'table') {
167
- let ret = `JSONB_EXTRACT_PATH_TEXT(${parentAlias},'${childName}')`;
164
+ const nameLit = this.sqlLiteralString(childName);
165
+ let ret = `JSONB_EXTRACT_PATH_TEXT(${parentAlias},${nameLit})`;
168
166
  switch (childType) {
169
167
  case 'string':
170
168
  break;
@@ -176,13 +174,13 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
176
174
  case 'record':
177
175
  case 'array[record]':
178
176
  case 'sql native':
179
- ret = `JSONB_EXTRACT_PATH(${parentAlias},'${childName}')`;
177
+ ret = `JSONB_EXTRACT_PATH(${parentAlias},${nameLit})`;
180
178
  break;
181
179
  }
182
180
  return ret;
183
181
  }
184
182
  else {
185
- const child = this.sqlMaybeQuoteIdentifier(childName);
183
+ const child = this.sqlQuoteIdentifier(childName);
186
184
  return `${parentAlias}.${child}`;
187
185
  }
188
186
  }
@@ -307,12 +305,6 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
307
305
  sqlOrderBy(orderTerms) {
308
306
  return `ORDER BY ${orderTerms.map(t => `${t} NULLS LAST`).join(',')}`;
309
307
  }
310
- sqlLiteralString(literal) {
311
- return "'" + literal.replace(/'/g, "''") + "'";
312
- }
313
- sqlLiteralRegexp(literal) {
314
- return "'" + literal.replace(/'/g, "''") + "'";
315
- }
316
308
  getDialectFunctionOverrides() {
317
309
  return (0, functions_1.expandOverrideMap)(function_overrides_1.POSTGRES_MALLOY_STANDARD_OVERLOADS);
318
310
  }
@@ -372,7 +364,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
372
364
  sqlLiteralRecord(lit) {
373
365
  const props = [];
374
366
  for (const [kName, kVal] of Object.entries(lit.kids)) {
375
- props.push(`'${kName}',${kVal.sql}`);
367
+ props.push(`${this.sqlLiteralString(kName)},${kVal.sql}`);
376
368
  }
377
369
  return `JSONB_BUILD_OBJECT(${props.join(', ')})`;
378
370
  }
@@ -6,6 +6,9 @@ export declare class SnowflakeDialect extends Dialect {
6
6
  name: string;
7
7
  experimental: boolean;
8
8
  hasTimestamptz: boolean;
9
+ stringLiteralStyle: "backslash";
10
+ identifierEscapeStyle: "doubled";
11
+ identifierQuoteChar: string;
9
12
  defaultNumberType: string;
10
13
  defaultDecimalType: string;
11
14
  udfPrefix: string;
@@ -26,8 +29,8 @@ export declare class SnowflakeDialect extends Dialect {
26
29
  supportsQualify: boolean;
27
30
  supportsPipelinesInViews: boolean;
28
31
  supportsComplexFilteredSources: boolean;
32
+ tablePathBareIdentRegex: RegExp;
29
33
  integerTypeMappings: IntegerTypeMapping[];
30
- quoteTablePath(tablePath: string): string;
31
34
  sqlGroupSetTable(groupSetCount: number): string;
32
35
  sqlAnyValue(groupSet: number, fieldName: string): string;
33
36
  mapFields(fieldList: DialectFieldList): string;
@@ -45,7 +48,6 @@ export declare class SnowflakeDialect extends Dialect {
45
48
  sqlCreateFunction(_id: string, _funcText: string): string;
46
49
  sqlCreateFunctionCombineLastStage(_lastStageName: string): string;
47
50
  sqlSelectAliasAsStruct(alias: string): string;
48
- sqlMaybeQuoteIdentifier(identifier: string): string;
49
51
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
50
52
  sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
51
53
  sql: string;
@@ -65,8 +67,6 @@ export declare class SnowflakeDialect extends Dialect {
65
67
  sqlRegexpMatch(compare: RegexMatchExpr): string;
66
68
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
67
69
  sqlOrderBy(orderTerms: string[]): string;
68
- sqlLiteralString(literal: string): string;
69
- sqlLiteralRegexp(literal: string): string;
70
70
  getDialectFunctionOverrides(): {
71
71
  [name: string]: DialectFunctionOverloadDef[];
72
72
  };
@@ -78,6 +78,9 @@ class SnowflakeDialect extends dialect_1.Dialect {
78
78
  this.name = 'snowflake';
79
79
  this.experimental = false;
80
80
  this.hasTimestamptz = true;
81
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Backslash;
82
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
83
+ this.identifierQuoteChar = '"';
81
84
  this.defaultNumberType = 'NUMBER';
82
85
  this.defaultDecimalType = 'NUMBER';
83
86
  this.udfPrefix = '__udf';
@@ -98,21 +101,14 @@ class SnowflakeDialect extends dialect_1.Dialect {
98
101
  this.supportsQualify = false;
99
102
  this.supportsPipelinesInViews = false;
100
103
  this.supportsComplexFilteredSources = false;
104
+ // Snowflake bare-identifier continuation allows `$` (verified against
105
+ // the live engine).
106
+ this.tablePathBareIdentRegex = /^[A-Za-z_][A-Za-z0-9_$]*/;
101
107
  // Snowflake uses NUMBER(38,0) for all integers - can exceed JS Number precision
102
108
  this.integerTypeMappings = [
103
109
  { min: dialect_1.MIN_DECIMAL38, max: dialect_1.MAX_DECIMAL38, numberType: 'bigint' },
104
110
  ];
105
111
  }
106
- quoteTablePath(tablePath) {
107
- // Quote with double quotes if contains dangerous characters
108
- if (tablePath.match(/[;-]/)) {
109
- return tablePath
110
- .split('.')
111
- .map(part => `"${part}"`)
112
- .join('.');
113
- }
114
- return tablePath;
115
- }
116
112
  sqlGroupSetTable(groupSetCount) {
117
113
  return `CROSS JOIN (SELECT index as group_set FROM TABLE(FLATTEN(ARRAY_GENERATE_RANGE(0, ${groupSetCount + 1}))))`;
118
114
  }
@@ -126,7 +122,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
126
122
  }
127
123
  mapFieldsForObjectConstruct(fieldList) {
128
124
  return fieldList
129
- .map(f => `'${f.rawName}', (${f.sqlExpression})`)
125
+ .map(f => `${this.sqlLiteralString(f.rawName)}, (${f.sqlExpression})`)
130
126
  .join(', ');
131
127
  }
132
128
  sqlAggregateTurtle(groupSet, fieldList, orderBy) {
@@ -152,7 +148,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
152
148
  return `COALESCE(ARRAY_AGG(CASE WHEN group_set=${groupSet} THEN OBJECT_CONSTRUCT_KEEP_NULL(${fields}) END)[0], OBJECT_CONSTRUCT_KEEP_NULL(${nullValues}))`;
153
149
  }
154
150
  sqlUnnestAlias(source, alias, _fieldList, _needDistinctKey, isArray, _isInNestedPipeline) {
155
- const as = this.sqlMaybeQuoteIdentifier(alias);
151
+ const as = this.sqlQuoteIdentifier(alias);
156
152
  if (isArray) {
157
153
  return `LEFT JOIN lateral flatten(input => ${source}) as ${as}`;
158
154
  }
@@ -202,7 +198,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
202
198
  return 'UUID_STRING()';
203
199
  }
204
200
  sqlFieldReference(parentAlias, parentType, childName, childType) {
205
- const sqlName = this.sqlMaybeQuoteIdentifier(childName);
201
+ const sqlName = this.sqlQuoteIdentifier(childName);
206
202
  if (childName === '__row_id') {
207
203
  return `"${parentAlias}".INDEX::varchar`;
208
204
  }
@@ -250,9 +246,6 @@ class SnowflakeDialect extends dialect_1.Dialect {
250
246
  sqlSelectAliasAsStruct(alias) {
251
247
  return `OBJECT_CONSTRUCT_KEEP_NULL(${alias}.*)`;
252
248
  }
253
- sqlMaybeQuoteIdentifier(identifier) {
254
- return '"' + identifier.replace(/"/g, '""') + '"';
255
- }
256
249
  sqlCreateTableAsSelect(tableName, sql) {
257
250
  return `
258
251
  CREATE TEMP TABLE IF NOT EXISTS \`${tableName}\`
@@ -425,14 +418,6 @@ ${(0, utils_1.indent)(sql)}
425
418
  sqlOrderBy(orderTerms) {
426
419
  return `ORDER BY ${orderTerms.map(t => `${t} NULLS LAST`).join(',')}`;
427
420
  }
428
- sqlLiteralString(literal) {
429
- const noVirgule = literal.replace(/\\/g, '\\\\');
430
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
431
- }
432
- sqlLiteralRegexp(literal) {
433
- const noVirgule = literal.replace(/\\/g, '\\\\');
434
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
435
- }
436
421
  getDialectFunctionOverrides() {
437
422
  return (0, functions_1.expandOverrideMap)(function_overrides_1.SNOWFLAKE_MALLOY_STANDARD_OVERLOADS);
438
423
  }
@@ -457,7 +442,7 @@ ${(0, utils_1.indent)(sql)}
457
442
  var _a;
458
443
  if ((0, malloy_types_1.isAtomic)(f)) {
459
444
  const name = (_a = f.as) !== null && _a !== void 0 ? _a : f.name;
460
- const oneSchema = `${this.sqlMaybeQuoteIdentifier(name)} ${this.malloyTypeToSQLType(f)}`;
445
+ const oneSchema = `${this.sqlQuoteIdentifier(name)} ${this.malloyTypeToSQLType(f)}`;
461
446
  ret.push(oneSchema);
462
447
  }
463
448
  return ret;
@@ -521,9 +506,8 @@ ${(0, utils_1.indent)(sql)}
521
506
  const rowVals = [];
522
507
  for (const f of lit.typeDef.fields) {
523
508
  const name = (_a = f.as) !== null && _a !== void 0 ? _a : f.name;
524
- const propName = `'${name}'`;
525
509
  const propVal = (_c = (_b = (0, malloy_types_1.safeRecordGet)(lit.kids, name)) === null || _b === void 0 ? void 0 : _b.sql) !== null && _c !== void 0 ? _c : 'internal-error-record-literal';
526
- rowVals.push(`${propName},${propVal}`);
510
+ rowVals.push(`${this.sqlLiteralString(name)},${propVal}`);
527
511
  }
528
512
  return `OBJECT_CONSTRUCT_KEEP_NULL(${rowVals.join(',')})`;
529
513
  }
@@ -4,6 +4,9 @@ import type { CompiledOrderBy, DialectFieldList, IntegerTypeMapping, OrderByRequ
4
4
  import { Dialect, type LateralJoinExpression } from '../dialect';
5
5
  export declare class StandardSQLDialect extends Dialect {
6
6
  name: string;
7
+ stringLiteralStyle: "backslash";
8
+ identifierEscapeStyle: "backslash";
9
+ identifierQuoteChar: string;
7
10
  experimental: boolean;
8
11
  defaultNumberType: string;
9
12
  defaultDecimalType: string;
@@ -29,7 +32,9 @@ export declare class StandardSQLDialect extends Dialect {
29
32
  supportsHyperLogLog: boolean;
30
33
  likeEscape: boolean;
31
34
  integerTypeMappings: IntegerTypeMapping[];
32
- quoteTablePath(tablePath: string): string;
35
+ private bqRejectBacktick;
36
+ sqlQuoteIdentifier(identifier: string): string;
37
+ tablePathBareIdentRegex: RegExp;
33
38
  needsCivilTimeComputation(typeDef: AtomicTypeDef, truncateTo: TimestampUnit | undefined, offsetUnit: TimestampUnit | undefined, qi: QueryInfo): {
34
39
  needed: boolean;
35
40
  tz: string | undefined;
@@ -50,7 +55,6 @@ export declare class StandardSQLDialect extends Dialect {
50
55
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
51
56
  sqlCreateFunctionCombineLastStage(lastStageName: string): string;
52
57
  sqlSelectAliasAsStruct(alias: string): string;
53
- sqlMaybeQuoteIdentifier(identifier: string): string;
54
58
  sqlNowExpr(): string;
55
59
  sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
56
60
  sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
@@ -68,8 +72,6 @@ export declare class StandardSQLDialect extends Dialect {
68
72
  sqlTimestamptzLiteral(_qi: QueryInfo, _literal: string, _timezone: string): string;
69
73
  sqlMeasureTimeExpr(measure: MeasureTimeExpr): string;
70
74
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
71
- sqlLiteralString(literal: string): string;
72
- sqlLiteralRegexp(literal: string): string;
73
75
  getDialectFunctionOverrides(): {
74
76
  [name: string]: DialectFunctionOverloadDef[];
75
77
  };
@@ -76,6 +76,9 @@ class StandardSQLDialect extends dialect_1.Dialect {
76
76
  constructor() {
77
77
  super(...arguments);
78
78
  this.name = 'standardsql';
79
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Backslash;
80
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Backslash;
81
+ this.identifierQuoteChar = '`';
79
82
  this.experimental = false;
80
83
  this.defaultNumberType = 'FLOAT64';
81
84
  this.defaultDecimalType = 'NUMERIC';
@@ -101,13 +104,42 @@ class StandardSQLDialect extends dialect_1.Dialect {
101
104
  this.integerTypeMappings = [
102
105
  { min: dialect_1.MIN_INT64, max: dialect_1.MAX_INT64, numberType: 'bigint' },
103
106
  ];
107
+ // BigQuery bare-identifier continuation allows dashes (verified
108
+ // against the live engine: `proj-foo.dataset.table` resolves to a
109
+ // table reference, both bare and inside per-segment backticks). The
110
+ // base `sqlValidateTableName` handles every shape we accept —
111
+ // bare-dotted, whole-backticked, and per-segment-backticked — because
112
+ // its grammar is `Segment ('.' Segment)*` and a segment is either
113
+ // bare or quoted with this dialect's `identifierQuoteChar` /
114
+ // `identifierEscapeStyle` (`` ` `` / Backslash). The whole-path form
115
+ // (`` `proj.dataset.table` ``) is accepted naturally as a single
116
+ // quoted segment.
117
+ //
118
+ // `*` is intentionally NOT in this regex. BigQuery's parser only
119
+ // accepts `*` inside backticks (wildcard tables must be quoted, e.g.
120
+ // `` `dataset.events_*` ``). Bare wildcards would fail at the engine,
121
+ // so we reject them up front and require the user to type the
122
+ // backticks they'd need anyway.
123
+ this.tablePathBareIdentRegex = /^[A-Za-z_][A-Za-z0-9_-]*/;
104
124
  }
105
125
  sqlLateralJoinBag(expressions) {
106
126
  const fields = expressions.map(e => `${e.sql} as ${e.name}`);
107
127
  return `LEFT JOIN UNNEST([STRUCT(${fields.join(',\n')})]) as __lateral_join_bag\n`;
108
128
  }
109
- quoteTablePath(tablePath) {
110
- return `\`${tablePath}\``;
129
+ // BigQuery's parser accepts `\`` as a backtick escape inside quoted
130
+ // identifiers, but BigQuery's schema layer rejects field/table names
131
+ // containing a literal backtick. Refuse here so the error names the
132
+ // dialect; the rest of the escape (backslash-doubling) is handled by
133
+ // the base via identifierEscapeStyle.
134
+ // Reference: https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical
135
+ bqRejectBacktick(name, kind) {
136
+ if (name.includes('`')) {
137
+ throw new Error(`BigQuery ${kind} cannot contain a backtick: ${JSON.stringify(name)}`);
138
+ }
139
+ }
140
+ sqlQuoteIdentifier(identifier) {
141
+ this.bqRejectBacktick(identifier, 'identifier');
142
+ return super.sqlQuoteIdentifier(identifier);
111
143
  }
112
144
  needsCivilTimeComputation(typeDef, truncateTo, offsetUnit, qi) {
113
145
  // In addition to using "civil" space for units where a query time zone is
@@ -193,7 +225,7 @@ class StandardSQLDialect extends dialect_1.Dialect {
193
225
  return 'GENERATE_UUID()';
194
226
  }
195
227
  sqlFieldReference(parentAlias, _parentType, childName, _childType) {
196
- const child = this.sqlMaybeQuoteIdentifier(childName);
228
+ const child = this.sqlQuoteIdentifier(childName);
197
229
  return `${parentAlias}.${child}`;
198
230
  }
199
231
  sqlUnnestPipelineHead(isSingleton, sourceSQLExpression) {
@@ -223,9 +255,6 @@ ${(0, utils_1.indent)(sql)}
223
255
  sqlSelectAliasAsStruct(alias) {
224
256
  return `(SELECT AS STRUCT ${alias}.*)`;
225
257
  }
226
- sqlMaybeQuoteIdentifier(identifier) {
227
- return '`' + identifier + '`';
228
- }
229
258
  sqlNowExpr() {
230
259
  return 'CURRENT_TIMESTAMP()';
231
260
  }
@@ -358,14 +387,6 @@ ${(0, utils_1.indent)(sql)}
358
387
  }
359
388
  return tableSQL;
360
389
  }
361
- sqlLiteralString(literal) {
362
- const noVirgule = literal.replace(/\\/g, '\\\\');
363
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
364
- }
365
- sqlLiteralRegexp(literal) {
366
- const noVirgule = literal.replace(/\\/g, '\\\\');
367
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
368
- }
369
390
  getDialectFunctionOverrides() {
370
391
  return (0, functions_1.expandOverrideMap)(function_overrides_1.STANDARDSQL_MALLOY_STANDARD_OVERLOADS);
371
392
  }
@@ -432,7 +453,7 @@ ${(0, utils_1.indent)(sql)}
432
453
  const ents = [];
433
454
  for (const [name, val] of Object.entries(lit.kids)) {
434
455
  const expr = val.sql || 'internal-error-literal-record';
435
- ents.push(`${expr} AS ${this.sqlMaybeQuoteIdentifier(name)}`);
456
+ ents.push(`${expr} AS ${this.sqlQuoteIdentifier(name)}`);
436
457
  }
437
458
  return `STRUCT(${ents.join(',')})`;
438
459
  }