@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
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.BaseConnection = void 0;
10
10
  const sql_block_1 = require("../model/sql_block");
11
+ const validate_table_path_1 = require("./validate_table_path");
11
12
  class BaseConnection {
12
13
  constructor() {
13
14
  this.schemaCache = {};
@@ -47,6 +48,11 @@ class BaseConnection {
47
48
  const schemas = {};
48
49
  const errors = {};
49
50
  for (const [tableName, tablePath] of Object.entries(missing)) {
51
+ const invalid = (0, validate_table_path_1.validateCanonicalTablePath)(this.dialectName, tablePath);
52
+ if (invalid !== undefined) {
53
+ errors[tableName] = invalid;
54
+ continue;
55
+ }
50
56
  const inCache = await this.checkSchemaCache(tablePath, 'table', async () => await this.fetchTableSchema(tableName, tablePath), refreshTimestamp);
51
57
  if (inCache.schema) {
52
58
  schemas[tableName] = inCache.schema;
@@ -0,0 +1,10 @@
1
+ /** Validate against a known dialect. Returns an error string or undefined. */
2
+ export declare function validateCanonicalTablePath(dialectName: string, tablePath: string): string | undefined;
3
+ /**
4
+ * Validate against any registered dialect. Used at boundaries where the
5
+ * destination dialect isn't synchronously known (virtualMap, manifest
6
+ * entries).
7
+ */
8
+ export declare function validateCanonicalTablePathAnyDialect(tablePath: string): string | undefined;
9
+ /** Throw if `tablePath` isn't canonical SQL in any registered dialect. */
10
+ export declare function requireCanonicalTablePathAnyDialect(tablePath: string, prefix: string): void;
@@ -0,0 +1,56 @@
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.validateCanonicalTablePath = validateCanonicalTablePath;
8
+ exports.validateCanonicalTablePathAnyDialect = validateCanonicalTablePathAnyDialect;
9
+ exports.requireCanonicalTablePathAnyDialect = requireCanonicalTablePathAnyDialect;
10
+ const dialect_map_1 = require("../dialect/dialect_map");
11
+ /** Validate against a known dialect. Returns an error string or undefined. */
12
+ function validateCanonicalTablePath(dialectName, tablePath) {
13
+ let dialect;
14
+ try {
15
+ dialect = (0, dialect_map_1.getDialect)(dialectName);
16
+ }
17
+ catch {
18
+ return `tablePath '${tablePath}' cannot be validated: unknown dialect '${dialectName}'`;
19
+ }
20
+ const result = dialect.sqlValidateTableName(tablePath);
21
+ if (!result.ok) {
22
+ return `tablePath '${tablePath}' is not canonical SQL for the ${dialectName} dialect; the translator must validate before passing it here. (${result.error})`;
23
+ }
24
+ if (result.canonical !== tablePath) {
25
+ return `tablePath '${tablePath}' is not canonical SQL for the ${dialectName} dialect; the translator must validate before passing it here.`;
26
+ }
27
+ return undefined;
28
+ }
29
+ /**
30
+ * Validate against any registered dialect. Used at boundaries where the
31
+ * destination dialect isn't synchronously known (virtualMap, manifest
32
+ * entries).
33
+ */
34
+ function validateCanonicalTablePathAnyDialect(tablePath) {
35
+ let suggestion;
36
+ for (const dialect of (0, dialect_map_1.getDialects)()) {
37
+ const result = dialect.sqlValidateTableName(tablePath);
38
+ if (result.ok) {
39
+ if (result.canonical === tablePath)
40
+ return undefined;
41
+ if (suggestion === undefined)
42
+ suggestion = result.canonical;
43
+ }
44
+ }
45
+ if (suggestion !== undefined) {
46
+ return `value '${tablePath}' is not canonical SQL; did you mean '${suggestion}'?`;
47
+ }
48
+ return `value '${tablePath}' is not a valid canonical table path in any registered dialect`;
49
+ }
50
+ /** Throw if `tablePath` isn't canonical SQL in any registered dialect. */
51
+ function requireCanonicalTablePathAnyDialect(tablePath, prefix) {
52
+ const err = validateCanonicalTablePathAnyDialect(tablePath);
53
+ if (err !== undefined)
54
+ throw new Error(`${prefix}: ${err}`);
55
+ }
56
+ //# sourceMappingURL=validate_table_path.js.map
@@ -4,6 +4,9 @@ import { Dialect } from '../dialect';
4
4
  import type { DialectFunctionOverloadDef } from '../functions';
5
5
  export declare class DatabricksDialect 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;
@@ -37,9 +40,9 @@ export declare class DatabricksDialect extends Dialect {
37
40
  hasTimestamptz: boolean;
38
41
  supportsBigIntPrecision: boolean;
39
42
  maxIdentifierLength: number;
43
+ tablePathBareIdentRegex: RegExp;
40
44
  malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
41
45
  sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
42
- quoteTablePath(tablePath: string): string;
43
46
  sqlGroupSetTable(groupSetCount: number): string;
44
47
  sqlLateralJoinBag(expressions: LateralJoinExpression[]): string;
45
48
  sqlOrderBy(orderTerms: string[], obr?: OrderByRequest): string;
@@ -60,7 +63,6 @@ export declare class DatabricksDialect extends Dialect {
60
63
  sqlCreateFunction(id: string, funcText: string): string;
61
64
  sqlCreateFunctionCombineLastStage(lastStageName: string, fieldList: DialectFieldList): string;
62
65
  sqlSelectAliasAsStruct(alias: string, fieldList: DialectFieldList): string;
63
- sqlMaybeQuoteIdentifier(identifier: string): string;
64
66
  sqlCreateTableAsSelect(tableName: string, sql: string): string;
65
67
  sqlNowExpr(): string;
66
68
  sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
@@ -78,8 +80,6 @@ export declare class DatabricksDialect extends Dialect {
78
80
  sqlTimestamptzLiteral(_qi: QueryInfo, _literal: string, _timezone: string): string;
79
81
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
80
82
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
81
- sqlLiteralString(literal: string): string;
82
- sqlLiteralRegexp(literal: string): string;
83
83
  getDialectFunctionOverrides(): {
84
84
  [name: string]: DialectFunctionOverloadDef[];
85
85
  };
@@ -43,6 +43,9 @@ class DatabricksDialect extends dialect_1.Dialect {
43
43
  constructor() {
44
44
  super(...arguments);
45
45
  this.name = 'databricks';
46
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Backslash;
47
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
48
+ this.identifierQuoteChar = '`';
46
49
  this.defaultNumberType = 'DOUBLE';
47
50
  this.defaultDecimalType = 'DECIMAL';
48
51
  this.udfPrefix = '__udf';
@@ -74,6 +77,10 @@ class DatabricksDialect extends dialect_1.Dialect {
74
77
  this.hasTimestamptz = false;
75
78
  this.supportsBigIntPrecision = false;
76
79
  this.maxIdentifierLength = 255;
80
+ // Databricks bare identifiers may start with a digit, but cannot be
81
+ // entirely digits (or they lex as number literals). Verified against
82
+ // the live engine: `1foo` resolves; `$` is rejected.
83
+ this.tablePathBareIdentRegex = /^[A-Za-z0-9_]*[A-Za-z_][A-Za-z0-9_]*/;
77
84
  }
78
85
  malloyTypeToSQLType(malloyType) {
79
86
  switch (malloyType.type) {
@@ -95,7 +102,7 @@ class DatabricksDialect extends dialect_1.Dialect {
95
102
  const fields = [];
96
103
  for (const f of malloyType.fields) {
97
104
  if ((0, malloy_types_1.isAtomic)(f)) {
98
- fields.push(`${this.sqlMaybeQuoteIdentifier(f.name)}: ${this.malloyTypeToSQLType(f)}`);
105
+ fields.push(`${this.sqlQuoteIdentifier(f.name)}: ${this.malloyTypeToSQLType(f)}`);
99
106
  }
100
107
  }
101
108
  return `STRUCT<${fields.join(', ')}>`;
@@ -105,7 +112,7 @@ class DatabricksDialect extends dialect_1.Dialect {
105
112
  const fields = [];
106
113
  for (const f of malloyType.fields) {
107
114
  if ((0, malloy_types_1.isAtomic)(f)) {
108
- fields.push(`${this.sqlMaybeQuoteIdentifier(f.name)}: ${this.malloyTypeToSQLType(f)}`);
115
+ fields.push(`${this.sqlQuoteIdentifier(f.name)}: ${this.malloyTypeToSQLType(f)}`);
109
116
  }
110
117
  }
111
118
  return `ARRAY<STRUCT<${fields.join(', ')}>>`;
@@ -128,12 +135,6 @@ class DatabricksDialect extends dialect_1.Dialect {
128
135
  rawType: baseSqlType,
129
136
  });
130
137
  }
131
- quoteTablePath(tablePath) {
132
- return tablePath
133
- .split('.')
134
- .map(part => (/^[a-zA-Z_]\w*$/.test(part) ? part : `\`${part}\``))
135
- .join('.');
136
- }
137
138
  sqlGroupSetTable(groupSetCount) {
138
139
  return `LATERAL VIEW EXPLODE(SEQUENCE(0, ${groupSetCount})) group_set AS group_set`;
139
140
  }
@@ -163,7 +164,9 @@ class DatabricksDialect extends dialect_1.Dialect {
163
164
  // field names) in Databricks.
164
165
  buildNamedStructExpression(fieldList) {
165
166
  return ('named_struct(' +
166
- fieldList.map(f => `'${f.rawName}', ${f.sqlExpression}`).join(', ') +
167
+ fieldList
168
+ .map(f => `${this.sqlLiteralString(f.rawName)}, ${f.sqlExpression}`)
169
+ .join(', ') +
167
170
  ')');
168
171
  }
169
172
  sqlAggregateTurtle(groupSet, fieldList, orderBy) {
@@ -207,7 +210,9 @@ class DatabricksDialect extends dialect_1.Dialect {
207
210
  sqlCoaleseMeasuresInline(groupSet, fieldList) {
208
211
  const namedStruct = this.buildNamedStructExpression(fieldList);
209
212
  const nullStruct = 'named_struct(' +
210
- fieldList.map(f => `'${f.rawName}', NULL`).join(', ') +
213
+ fieldList
214
+ .map(f => `${this.sqlLiteralString(f.rawName)}, NULL`)
215
+ .join(', ') +
211
216
  ')';
212
217
  return `COALESCE(FIRST(CASE WHEN group_set=${groupSet} THEN ${namedStruct} END) IGNORE NULLS, ${nullStruct})`;
213
218
  }
@@ -268,7 +273,7 @@ class DatabricksDialect extends dialect_1.Dialect {
268
273
  if (childName === '__row_id') {
269
274
  return `__row_id_from_${parentAlias}`;
270
275
  }
271
- return `${parentAlias}.${this.sqlMaybeQuoteIdentifier(childName)}`;
276
+ return `${parentAlias}.${this.sqlQuoteIdentifier(childName)}`;
272
277
  }
273
278
  sqlCreateFunction(id, funcText) {
274
279
  return `CREATE TEMPORARY FUNCTION ${id}(param STRING) RETURNS STRING RETURN (\n${(0, utils_1.indent)(funcText)}\n);\n`;
@@ -279,13 +284,10 @@ class DatabricksDialect extends dialect_1.Dialect {
279
284
  }
280
285
  sqlSelectAliasAsStruct(alias, fieldList) {
281
286
  const fields = fieldList
282
- .map(f => `${alias}.${this.sqlMaybeQuoteIdentifier(f.rawName)}`)
287
+ .map(f => `${alias}.${this.sqlQuoteIdentifier(f.rawName)}`)
283
288
  .join(', ');
284
289
  return `STRUCT(${fields})`;
285
290
  }
286
- sqlMaybeQuoteIdentifier(identifier) {
287
- return '`' + identifier.replace(/`/g, '``') + '`';
288
- }
289
291
  sqlCreateTableAsSelect(tableName, sql) {
290
292
  return `CREATE TABLE ${tableName} AS ${sql}`;
291
293
  }
@@ -406,13 +408,6 @@ class DatabricksDialect extends dialect_1.Dialect {
406
408
  }
407
409
  return tableSQL;
408
410
  }
409
- sqlLiteralString(literal) {
410
- const noVirgule = literal.replace(/\\/g, '\\\\');
411
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
412
- }
413
- sqlLiteralRegexp(literal) {
414
- return "'" + literal.replace(/'/g, "''") + "'";
415
- }
416
411
  getDialectFunctionOverrides() {
417
412
  return (0, functions_1.expandOverrideMap)(function_overrides_1.DATABRICKS_MALLOY_STANDARD_OVERLOADS);
418
413
  }
@@ -1,5 +1,6 @@
1
1
  import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralExpr, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, ATimestampTypeDef, TimeExpr, TemporalFieldType } from '../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from './functions';
3
+ import type { ValidateTablePathResult } from './table-path';
3
4
  interface DialectField {
4
5
  typeDef: AtomicTypeDef;
5
6
  sqlExpression: string;
@@ -35,6 +36,18 @@ export declare const MIN_INT128: bigint;
35
36
  export declare const MAX_INT128: bigint;
36
37
  export declare const MIN_DECIMAL38: bigint;
37
38
  export declare const MAX_DECIMAL38: bigint;
39
+ /**
40
+ * Allowed values for `Dialect.stringLiteralStyle` and
41
+ * `Dialect.identifierEscapeStyle`. Subclasses set their style with
42
+ * e.g. `stringLiteralStyle = EscapeStyle.Backslash`; the `as const`
43
+ * is centralized here so dialect files stay free of it.
44
+ */
45
+ export declare const EscapeStyle: {
46
+ readonly Doubled: "doubled";
47
+ readonly Backslash: "backslash";
48
+ readonly Unset: "unset";
49
+ };
50
+ export type EscapeStyleValue = (typeof EscapeStyle)[keyof typeof EscapeStyle];
38
51
  /**
39
52
  * Data which dialect methods need in order to correctly generate SQL.
40
53
  * Initially this is just timezone related, but I made this an interface
@@ -132,7 +145,34 @@ export declare abstract class Dialect {
132
145
  abstract getDialectFunctions(): {
133
146
  [name: string]: DialectFunctionOverloadDef[];
134
147
  };
135
- abstract quoteTablePath(tablePath: string): string;
148
+ /**
149
+ * Regex matching one bare (unquoted) table-path segment for this
150
+ * dialect, anchored at the start of the input. Drives the default
151
+ * `sqlValidateTableName` along with `identifierQuoteChar` and
152
+ * `identifierEscapeStyle`.
153
+ *
154
+ * The default is strict ANSI: `[A-Za-z_][A-Za-z0-9_]*`. Override to
155
+ * widen the char set (Postgres allows `$`, MySQL allows digit-start
156
+ * with caveats, BigQuery allows dashes, …). The per-dialect regexes
157
+ * were verified by probing live engines.
158
+ */
159
+ tablePathBareIdentRegex: RegExp;
160
+ /**
161
+ * Validate a user-supplied table-path string for this dialect. On
162
+ * success, the canonical form is the SQL fragment that gets pasted
163
+ * into `FROM` clauses and stored in `StructDef.tablePath`. Canonical
164
+ * equals input verbatim except where a Malloy convenience needs
165
+ * translating into dialect SQL (today: DuckDB's file-path branch
166
+ * wraps the input in single quotes).
167
+ *
168
+ * The default implementation handles every dialect whose table-path
169
+ * grammar is a dotted sequence of `bare | quoted` segments — every
170
+ * dialect we ship except DuckDB. New dialects of that shape need
171
+ * only override `tablePathBareIdentRegex`; override
172
+ * `sqlValidateTableName` itself only if your grammar is structurally
173
+ * different.
174
+ */
175
+ sqlValidateTableName(input: string): ValidateTablePathResult;
136
176
  abstract sqlGroupSetTable(groupSetCount: number): string;
137
177
  abstract sqlAnyValue(groupSet: number, fieldName: string): string;
138
178
  abstract sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
@@ -151,7 +191,52 @@ export declare abstract class Dialect {
151
191
  abstract sqlSelectAliasAsStruct(alias: string, fieldList: DialectFieldList): string;
152
192
  sqlFinalStage(_lastStageName: string, _fields: string[]): string;
153
193
  sqlDateToString(sqlDateExp: string): string;
154
- abstract sqlMaybeQuoteIdentifier(identifier: string): string;
194
+ /**
195
+ * The character the dialect uses to quote identifiers. Most dialects
196
+ * use ANSI double-quote `"`; MySQL, BigQuery and Databricks use the
197
+ * backtick `` ` ``. The dialect must escape this character by doubling
198
+ * inside a quoted identifier.
199
+ *
200
+ * Defaults to the empty string sentinel — concrete dialects must set
201
+ * a real value (or override `sqlQuoteIdentifier`), otherwise the
202
+ * base method throws to surface the omission immediately.
203
+ */
204
+ identifierQuoteChar: string;
205
+ /**
206
+ * How the dialect escapes the closing quote inside a string literal.
207
+ * Set via `EscapeStyle` from this module:
208
+ *
209
+ * - `EscapeStyle.Doubled`: `''` escapes `'`. Backslash is a literal
210
+ * character. (ANSI standard; Postgres, DuckDB, Trino, Presto.)
211
+ * - `EscapeStyle.Backslash`: `\'` escapes `'`, `\\` escapes `\`.
212
+ * (BigQuery, Snowflake, MySQL default mode, Databricks.)
213
+ * - `EscapeStyle.Unset` (default): base methods throw if reached. A
214
+ * new dialect must set this (or override the literal methods).
215
+ *
216
+ * `sqlLiteralString` and `sqlLiteralRegexp` share this style — the
217
+ * regex engine receives whatever the SQL parser decodes, and the two
218
+ * must agree or regex patterns containing backslashes silently break.
219
+ */
220
+ stringLiteralStyle: EscapeStyleValue;
221
+ /**
222
+ * How the dialect escapes the quote character inside a quoted
223
+ * identifier. Mirrors `stringLiteralStyle`:
224
+ *
225
+ * - `EscapeStyle.Doubled`: doubling the quote char escapes it (ANSI
226
+ * standard; most dialects).
227
+ * - `EscapeStyle.Backslash`: backslash-style escape, with `\\` for
228
+ * backslash and `\<quote>` for the quote char. (BigQuery — quoted
229
+ * identifiers use string-literal escape sequences.)
230
+ * - `EscapeStyle.Unset` (default): base method throws if reached.
231
+ */
232
+ identifierEscapeStyle: EscapeStyleValue;
233
+ /**
234
+ * Wrap an identifier in the dialect's quote character, escaping any
235
+ * embedded quote characters per the dialect's `identifierEscapeStyle`.
236
+ * This is the only safe way to render a user-controlled identifier
237
+ * in SQL.
238
+ */
239
+ sqlQuoteIdentifier(identifier: string): string;
155
240
  abstract castToString(expression: string): string;
156
241
  abstract concat(...values: string[]): string;
157
242
  sqlLiteralNumber(literal: string): string;
@@ -354,8 +439,19 @@ export declare abstract class Dialect {
354
439
  * the civil time in the specified timezone
355
440
  */
356
441
  abstract sqlTimestamptzLiteral(qi: QueryInfo, literal: string, timezone: string): string;
357
- abstract sqlLiteralString(literal: string): string;
358
- abstract sqlLiteralRegexp(literal: string): string;
442
+ /**
443
+ * Render a Malloy string as a SQL string literal. The escape style is
444
+ * driven by `stringLiteralStyle`; dialects normally do not override
445
+ * this method.
446
+ */
447
+ sqlLiteralString(literal: string): string;
448
+ /**
449
+ * Render a Malloy regex literal as a SQL string literal. Defaults to
450
+ * `sqlLiteralString` — the regex engine receives whatever bytes the
451
+ * SQL parser decodes, and `sqlLiteralString` already produces a
452
+ * correctly decoding literal for both escape styles.
453
+ */
454
+ sqlLiteralRegexp(literal: string): string;
359
455
  abstract sqlLiteralArray(lit: ArrayLiteralNode): string;
360
456
  abstract sqlLiteralRecord(lit: RecordLiteralNode): string;
361
457
  /**
@@ -22,10 +22,11 @@
22
22
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.Dialect = exports.dayIndex = exports.MAX_DECIMAL38 = exports.MIN_DECIMAL38 = exports.MAX_INT128 = exports.MIN_INT128 = exports.MAX_INT64 = exports.MIN_INT64 = exports.MAX_INT32 = exports.MIN_INT32 = void 0;
25
+ exports.Dialect = exports.dayIndex = exports.EscapeStyle = exports.MAX_DECIMAL38 = exports.MIN_DECIMAL38 = exports.MAX_INT128 = exports.MIN_INT128 = exports.MAX_INT64 = exports.MIN_INT64 = exports.MAX_INT32 = exports.MIN_INT32 = void 0;
26
26
  exports.inDays = inDays;
27
27
  exports.qtz = qtz;
28
28
  const malloy_types_1 = require("../model/malloy_types");
29
+ const table_path_1 = require("./table-path");
29
30
  /*
30
31
  * Standard integer type limits.
31
32
  * Use these in dialect integerTypeMappings definitions.
@@ -42,6 +43,17 @@ exports.MAX_INT128 = BigInt('170141183460469231731687303715884105727'); // 2^127
42
43
  // Decimal(38,0) limits (for Snowflake NUMBER(38,0))
43
44
  exports.MIN_DECIMAL38 = BigInt('-99999999999999999999999999999999999999'); // -(10^38 - 1)
44
45
  exports.MAX_DECIMAL38 = BigInt('99999999999999999999999999999999999999'); // 10^38 - 1
46
+ /**
47
+ * Allowed values for `Dialect.stringLiteralStyle` and
48
+ * `Dialect.identifierEscapeStyle`. Subclasses set their style with
49
+ * e.g. `stringLiteralStyle = EscapeStyle.Backslash`; the `as const`
50
+ * is centralized here so dialect files stay free of it.
51
+ */
52
+ exports.EscapeStyle = {
53
+ Doubled: 'doubled',
54
+ Backslash: 'backslash',
55
+ Unset: 'unset',
56
+ };
45
57
  const allUnits = [
46
58
  'microsecond',
47
59
  'millisecond',
@@ -138,6 +150,57 @@ class Dialect {
138
150
  { min: BigInt(exports.MIN_INT32), max: BigInt(exports.MAX_INT32), numberType: 'integer' },
139
151
  { min: exports.MIN_INT64, max: exports.MAX_INT64, numberType: 'bigint' },
140
152
  ];
153
+ /**
154
+ * Regex matching one bare (unquoted) table-path segment for this
155
+ * dialect, anchored at the start of the input. Drives the default
156
+ * `sqlValidateTableName` along with `identifierQuoteChar` and
157
+ * `identifierEscapeStyle`.
158
+ *
159
+ * The default is strict ANSI: `[A-Za-z_][A-Za-z0-9_]*`. Override to
160
+ * widen the char set (Postgres allows `$`, MySQL allows digit-start
161
+ * with caveats, BigQuery allows dashes, …). The per-dialect regexes
162
+ * were verified by probing live engines.
163
+ */
164
+ this.tablePathBareIdentRegex = /^[A-Za-z_][A-Za-z0-9_]*/;
165
+ /**
166
+ * The character the dialect uses to quote identifiers. Most dialects
167
+ * use ANSI double-quote `"`; MySQL, BigQuery and Databricks use the
168
+ * backtick `` ` ``. The dialect must escape this character by doubling
169
+ * inside a quoted identifier.
170
+ *
171
+ * Defaults to the empty string sentinel — concrete dialects must set
172
+ * a real value (or override `sqlQuoteIdentifier`), otherwise the
173
+ * base method throws to surface the omission immediately.
174
+ */
175
+ this.identifierQuoteChar = '';
176
+ /**
177
+ * How the dialect escapes the closing quote inside a string literal.
178
+ * Set via `EscapeStyle` from this module:
179
+ *
180
+ * - `EscapeStyle.Doubled`: `''` escapes `'`. Backslash is a literal
181
+ * character. (ANSI standard; Postgres, DuckDB, Trino, Presto.)
182
+ * - `EscapeStyle.Backslash`: `\'` escapes `'`, `\\` escapes `\`.
183
+ * (BigQuery, Snowflake, MySQL default mode, Databricks.)
184
+ * - `EscapeStyle.Unset` (default): base methods throw if reached. A
185
+ * new dialect must set this (or override the literal methods).
186
+ *
187
+ * `sqlLiteralString` and `sqlLiteralRegexp` share this style — the
188
+ * regex engine receives whatever the SQL parser decodes, and the two
189
+ * must agree or regex patterns containing backslashes silently break.
190
+ */
191
+ this.stringLiteralStyle = exports.EscapeStyle.Unset;
192
+ /**
193
+ * How the dialect escapes the quote character inside a quoted
194
+ * identifier. Mirrors `stringLiteralStyle`:
195
+ *
196
+ * - `EscapeStyle.Doubled`: doubling the quote char escapes it (ANSI
197
+ * standard; most dialects).
198
+ * - `EscapeStyle.Backslash`: backslash-style escape, with `\\` for
199
+ * backslash and `\<quote>` for the quote char. (BigQuery — quoted
200
+ * identifiers use string-literal escape sequences.)
201
+ * - `EscapeStyle.Unset` (default): base method throws if reached.
202
+ */
203
+ this.identifierEscapeStyle = exports.EscapeStyle.Unset;
141
204
  }
142
205
  // Generate the lateral join bag clause for window function partitioning.
143
206
  // The expressions are dimension fields that need to be referenced by name
@@ -218,6 +281,34 @@ class Dialect {
218
281
  },
219
282
  };
220
283
  }
284
+ /**
285
+ * Validate a user-supplied table-path string for this dialect. On
286
+ * success, the canonical form is the SQL fragment that gets pasted
287
+ * into `FROM` clauses and stored in `StructDef.tablePath`. Canonical
288
+ * equals input verbatim except where a Malloy convenience needs
289
+ * translating into dialect SQL (today: DuckDB's file-path branch
290
+ * wraps the input in single quotes).
291
+ *
292
+ * The default implementation handles every dialect whose table-path
293
+ * grammar is a dotted sequence of `bare | quoted` segments — every
294
+ * dialect we ship except DuckDB. New dialects of that shape need
295
+ * only override `tablePathBareIdentRegex`; override
296
+ * `sqlValidateTableName` itself only if your grammar is structurally
297
+ * different.
298
+ */
299
+ sqlValidateTableName(input) {
300
+ if (this.identifierEscapeStyle !== exports.EscapeStyle.Doubled &&
301
+ this.identifierEscapeStyle !== exports.EscapeStyle.Backslash) {
302
+ throw new Error(`${this.name}: sqlValidateTableName requires identifierEscapeStyle ` +
303
+ 'to be set to Doubled or Backslash (or override sqlValidateTableName).');
304
+ }
305
+ return (0, table_path_1.validateDottedTablePath)(input, {
306
+ quoteChar: this.identifierQuoteChar,
307
+ escapeStyle: this.identifierEscapeStyle,
308
+ bareIdentRegex: this.tablePathBareIdentRegex,
309
+ dialectName: this.name,
310
+ });
311
+ }
221
312
  // Format a CompiledOrderBy[] into an ORDER BY clause string for use
222
313
  // inside an aggregate turtle expression. Dialects which support ORDER BY
223
314
  // inside aggregate functions can call this helper from sqlAggregateTurtle.
@@ -232,6 +323,33 @@ class Dialect {
232
323
  sqlDateToString(sqlDateExp) {
233
324
  return this.castToString(`DATE(${sqlDateExp})`);
234
325
  }
326
+ /**
327
+ * Wrap an identifier in the dialect's quote character, escaping any
328
+ * embedded quote characters per the dialect's `identifierEscapeStyle`.
329
+ * This is the only safe way to render a user-controlled identifier
330
+ * in SQL.
331
+ */
332
+ sqlQuoteIdentifier(identifier) {
333
+ const q = this.identifierQuoteChar;
334
+ if (!q) {
335
+ throw new Error(`${this.name}: identifierQuoteChar is not set. ` +
336
+ 'Set it on the dialect (e.g. \'"\' or "`"), ' +
337
+ 'or override sqlQuoteIdentifier.');
338
+ }
339
+ if (this.identifierEscapeStyle === exports.EscapeStyle.Doubled) {
340
+ return q + identifier.split(q).join(q + q) + q;
341
+ }
342
+ if (this.identifierEscapeStyle === exports.EscapeStyle.Backslash) {
343
+ const escaped = identifier
344
+ .replace(/\\/g, '\\\\')
345
+ .split(q)
346
+ .join('\\' + q);
347
+ return q + escaped + q;
348
+ }
349
+ throw new Error(`${this.name}: identifierEscapeStyle is not set. ` +
350
+ 'Set it to EscapeStyle.Doubled or EscapeStyle.Backslash on the dialect, ' +
351
+ 'or override sqlQuoteIdentifier.');
352
+ }
235
353
  sqlLiteralNumber(literal) {
236
354
  return literal;
237
355
  }
@@ -330,6 +448,32 @@ class Dialect {
330
448
  }
331
449
  return sql;
332
450
  }
451
+ /**
452
+ * Render a Malloy string as a SQL string literal. The escape style is
453
+ * driven by `stringLiteralStyle`; dialects normally do not override
454
+ * this method.
455
+ */
456
+ sqlLiteralString(literal) {
457
+ if (this.stringLiteralStyle === 'doubled') {
458
+ return "'" + literal.split("'").join("''") + "'";
459
+ }
460
+ if (this.stringLiteralStyle === 'backslash') {
461
+ const escaped = literal.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
462
+ return "'" + escaped + "'";
463
+ }
464
+ throw new Error(`${this.name}: stringLiteralStyle is not set. ` +
465
+ 'Set it to EscapeStyle.Doubled or EscapeStyle.Backslash on the dialect, ' +
466
+ 'or override sqlLiteralString.');
467
+ }
468
+ /**
469
+ * Render a Malloy regex literal as a SQL string literal. Defaults to
470
+ * `sqlLiteralString` — the regex engine receives whatever bytes the
471
+ * SQL parser decodes, and `sqlLiteralString` already produces a
472
+ * correctly decoding literal for both escape styles.
473
+ */
474
+ sqlLiteralRegexp(literal) {
475
+ return this.sqlLiteralString(literal);
476
+ }
333
477
  /**
334
478
  * The dialect has a chance to over-ride how expressions are translated. If
335
479
  * "undefined" is returned then the translation is left to the query translator.
@@ -2,6 +2,7 @@ import type { Sampling, AtomicTypeDef, RegexMatchExpr, MeasureTimeExpr, BasicAto
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
3
  import type { CompiledOrderBy, DialectFieldList, FieldReferenceType, IntegerTypeMapping } from '../dialect';
4
4
  import { PostgresBase } from '../pg_impl';
5
+ import type { ValidateTablePathResult } from '../table-path';
5
6
  export declare class DuckDBDialect extends PostgresBase {
6
7
  name: string;
7
8
  experimental: boolean;
@@ -25,7 +26,7 @@ export declare class DuckDBDialect extends PostgresBase {
25
26
  requiresExplicitUnnestOrdering: boolean;
26
27
  integerTypeMappings: IntegerTypeMapping[];
27
28
  get udfPrefix(): string;
28
- quoteTablePath(tableName: string): string;
29
+ sqlValidateTableName(input: string): ValidateTablePathResult;
29
30
  sqlGroupSetTable(groupSetCount: number): string;
30
31
  sqlAnyValue(groupSet: number, fieldName: string): string;
31
32
  sqlLiteralNumber(literal: string): string;
@@ -48,8 +49,6 @@ export declare class DuckDBDialect extends PostgresBase {
48
49
  sqlAggDistinct(key: string, values: string[], func: (valNames: string[]) => string): string;
49
50
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
50
51
  sqlOrderBy(orderTerms: string[]): string;
51
- sqlLiteralString(literal: string): string;
52
- sqlLiteralRegexp(literal: string): string;
53
52
  getDialectFunctionOverrides(): {
54
53
  [name: string]: DialectFunctionOverloadDef[];
55
54
  };
@@ -31,6 +31,7 @@ const pg_impl_1 = require("../pg_impl");
31
31
  const dialect_functions_1 = require("./dialect_functions");
32
32
  const function_overrides_1 = require("./function_overrides");
33
33
  const tiny_parser_1 = require("../tiny_parser");
34
+ const table_path_parser_1 = require("./table-path-parser");
34
35
  // need to refactor runSQL to take a SQLBlock instead of just a sql string.
35
36
  const hackSplitComment = '-- hack: split on this';
36
37
  const duckDBToMalloyTypes = {
@@ -85,9 +86,12 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
85
86
  get udfPrefix() {
86
87
  return `__udf${Math.floor(Math.random() * 100000)}`;
87
88
  }
88
- quoteTablePath(tableName) {
89
- // Quote if contains special chars that could be SQL injection or need quoting
90
- return tableName.match(/[/*:;-]/) ? `'${tableName}'` : tableName;
89
+ // DuckDB's table-path grammar is too rich for the shared ANSI parser
90
+ // (it has file-path and explicit-single-quoted-literal branches in
91
+ // addition to dotted identifier paths). See
92
+ // `duckdb/table-path-parser.ts` for the grammar.
93
+ sqlValidateTableName(input) {
94
+ return (0, table_path_parser_1.validateDuckDBTablePath)(input);
91
95
  }
92
96
  sqlGroupSetTable(groupSetCount) {
93
97
  return `CROSS JOIN (SELECT UNNEST(GENERATE_SERIES(0,${groupSetCount},1)) as group_set ) as group_set`;
@@ -174,7 +178,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
174
178
  return parentAlias;
175
179
  }
176
180
  else {
177
- return `${parentAlias}.${this.sqlMaybeQuoteIdentifier(childName)}`;
181
+ return `${parentAlias}.${this.sqlQuoteIdentifier(childName)}`;
178
182
  }
179
183
  }
180
184
  sqlUnnestPipelineHead(isSingleton, sourceSQLExpression) {
@@ -204,7 +208,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
204
208
  }
205
209
  }
206
210
  return `SELECT LIST(STRUCT_PACK(${dialectFieldList
207
- .map(d => this.sqlMaybeQuoteIdentifier(d.sqlOutputName))
211
+ .map(d => this.sqlQuoteIdentifier(d.sqlOutputName))
208
212
  .join(',')})${o}) FROM ${lastStageName}\n`;
209
213
  }
210
214
  sqlSelectAliasAsStruct(alias, dialectFieldList) {
@@ -263,12 +267,6 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
263
267
  sqlOrderBy(orderTerms) {
264
268
  return `ORDER BY ${orderTerms.map(t => `${t} NULLS LAST`).join(',')}`;
265
269
  }
266
- sqlLiteralString(literal) {
267
- return "'" + literal.replace(/'/g, "''") + "'";
268
- }
269
- sqlLiteralRegexp(literal) {
270
- return "'" + literal.replace(/'/g, "''") + "'";
271
- }
272
270
  getDialectFunctionOverrides() {
273
271
  return (0, functions_1.expandOverrideMap)(function_overrides_1.DUCKDB_MALLOY_STANDARD_OVERLOADS);
274
272
  }
@@ -297,7 +295,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
297
295
  const typeSpec = [];
298
296
  for (const f of malloyType.fields) {
299
297
  if ((0, malloy_types_1.isAtomic)(f)) {
300
- typeSpec.push(`${this.sqlMaybeQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
298
+ typeSpec.push(`${this.sqlQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
301
299
  }
302
300
  }
303
301
  return `STRUCT(${typeSpec.join(', ')})`;
@@ -307,7 +305,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
307
305
  const typeSpec = [];
308
306
  for (const f of malloyType.fields) {
309
307
  if ((0, malloy_types_1.isAtomic)(f)) {
310
- typeSpec.push(`${this.sqlMaybeQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
308
+ typeSpec.push(`${this.sqlQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
311
309
  }
312
310
  }
313
311
  return `STRUCT(${typeSpec.join(', ')})[]`;
@@ -389,7 +387,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
389
387
  return `DATE_SUB('${df.units}', ${lVal}, ${rVal})`;
390
388
  }
391
389
  sqlLiteralRecord(lit) {
392
- const pairs = Object.entries(lit.kids).map(([propName, propVal]) => `${this.sqlMaybeQuoteIdentifier(propName)}:${propVal.sql}`);
390
+ const pairs = Object.entries(lit.kids).map(([propName, propVal]) => `${this.sqlQuoteIdentifier(propName)}:${propVal.sql}`);
393
391
  return '{' + pairs.join(',') + '}';
394
392
  }
395
393
  }