@malloydata/malloy 0.0.393 → 0.0.395

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 (71) hide show
  1. package/dist/api/foundation/config.d.ts +2 -3
  2. package/dist/api/foundation/config.js +23 -11
  3. package/dist/api/foundation/core.d.ts +0 -4
  4. package/dist/api/foundation/core.js +14 -11
  5. package/dist/api/foundation/runtime.js +21 -1
  6. package/dist/api/util.js +4 -0
  7. package/dist/connection/base_connection.js +6 -0
  8. package/dist/connection/validate_table_path.d.ts +10 -0
  9. package/dist/connection/validate_table_path.js +56 -0
  10. package/dist/dialect/databricks/databricks.d.ts +4 -4
  11. package/dist/dialect/databricks/databricks.js +17 -22
  12. package/dist/dialect/dialect.d.ts +100 -4
  13. package/dist/dialect/dialect.js +145 -1
  14. package/dist/dialect/duckdb/duckdb.d.ts +2 -3
  15. package/dist/dialect/duckdb/duckdb.js +12 -14
  16. package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
  17. package/dist/dialect/duckdb/table-path-parser.js +57 -0
  18. package/dist/dialect/index.d.ts +2 -0
  19. package/dist/dialect/index.js +4 -1
  20. package/dist/dialect/mysql/mysql.d.ts +4 -4
  21. package/dist/dialect/mysql/mysql.js +25 -20
  22. package/dist/dialect/pg_impl.d.ts +3 -1
  23. package/dist/dialect/pg_impl.js +6 -3
  24. package/dist/dialect/postgres/postgres.d.ts +1 -3
  25. package/dist/dialect/postgres/postgres.js +8 -16
  26. package/dist/dialect/snowflake/snowflake.d.ts +4 -4
  27. package/dist/dialect/snowflake/snowflake.js +11 -27
  28. package/dist/dialect/standardsql/standardsql.d.ts +6 -4
  29. package/dist/dialect/standardsql/standardsql.js +36 -15
  30. package/dist/dialect/table-path.d.ts +54 -0
  31. package/dist/dialect/table-path.js +144 -0
  32. package/dist/dialect/trino/trino.d.ts +0 -3
  33. package/dist/dialect/trino/trino.js +7 -20
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.js +4 -2
  36. package/dist/lang/ast/expressions/expr-compare.d.ts +15 -0
  37. package/dist/lang/ast/expressions/expr-compare.js +82 -2
  38. package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
  39. package/dist/lang/ast/source-elements/table-source.js +20 -19
  40. package/dist/lang/ast/statements/define-given.d.ts +2 -1
  41. package/dist/lang/ast/statements/define-given.js +52 -1
  42. package/dist/lang/ast/types/malloy-element.js +2 -0
  43. package/dist/lang/lib/Malloy/MalloyParser.d.ts +188 -167
  44. package/dist/lang/lib/Malloy/MalloyParser.js +2582 -2442
  45. package/dist/lang/lib/Malloy/MalloyParserListener.d.ts +24 -0
  46. package/dist/lang/lib/Malloy/MalloyParserVisitor.d.ts +15 -0
  47. package/dist/lang/malloy-to-ast.d.ts +9 -2
  48. package/dist/lang/malloy-to-ast.js +37 -2
  49. package/dist/lang/parse-log.d.ts +23 -0
  50. package/dist/lang/parse-log.js +6 -0
  51. package/dist/lang/parse-malloy.js +37 -7
  52. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
  53. package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
  54. package/dist/lang/test/expr-to-str.js +3 -0
  55. package/dist/lang/translate-response.d.ts +1 -1
  56. package/dist/model/expression_compiler.js +38 -11
  57. package/dist/model/filter_compilers.js +1 -1
  58. package/dist/model/given_binding.d.ts +15 -0
  59. package/dist/model/given_binding.js +35 -0
  60. package/dist/model/inline_expr.d.ts +30 -0
  61. package/dist/model/inline_expr.js +184 -0
  62. package/dist/model/malloy_types.d.ts +19 -1
  63. package/dist/model/query_model_impl.js +7 -7
  64. package/dist/model/query_query.d.ts +1 -1
  65. package/dist/model/query_query.js +37 -33
  66. package/dist/model/sql_compiled.d.ts +2 -4
  67. package/dist/model/sql_compiled.js +14 -15
  68. package/dist/test/test-models.js +2 -2
  69. package/dist/version.d.ts +1 -1
  70. package/dist/version.js +1 -1
  71. package/package.json +4 -4
@@ -40,9 +40,7 @@ export declare class Manifest {
40
40
  */
41
41
  get strict(): boolean;
42
42
  set strict(value: boolean);
43
- /**
44
- * Add or replace a manifest entry. Also marks it as touched.
45
- */
43
+ /** Add or replace a manifest entry. Also marks it as touched. */
46
44
  update(buildId: BuildID, entry: BuildManifestEntry): void;
47
45
  /**
48
46
  * Mark an existing entry as active without changing it.
@@ -257,3 +255,4 @@ export declare class MalloyConfig {
257
255
  */
258
256
  readOverlay(overlayName: string, ...path: string[]): Promise<unknown>;
259
257
  }
258
+ export declare function isBuildManifestEntry(value: unknown): value is BuildManifestEntry;
@@ -5,10 +5,12 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.MalloyConfig = exports.Manifest = void 0;
8
+ exports.isBuildManifestEntry = isBuildManifestEntry;
8
9
  const config_compile_1 = require("./config_compile");
9
10
  const config_resolve_1 = require("./config_resolve");
10
11
  const config_lookup_1 = require("./config_lookup");
11
12
  const config_overlays_1 = require("./config_overlays");
13
+ const validate_table_path_1 = require("../../connection/validate_table_path");
12
14
  /**
13
15
  * In-memory manifest store. Reads, updates, and serializes manifest data.
14
16
  *
@@ -61,10 +63,9 @@ class Manifest {
61
63
  set strict(value) {
62
64
  this._manifest.strict = value;
63
65
  }
64
- /**
65
- * Add or replace a manifest entry. Also marks it as touched.
66
- */
66
+ /** Add or replace a manifest entry. Also marks it as touched. */
67
67
  update(buildId, entry) {
68
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
68
69
  this._manifest.entries[buildId] = entry;
69
70
  this._touched.add(buildId);
70
71
  }
@@ -102,9 +103,10 @@ class Manifest {
102
103
  // Old format: {buildId: {tableName}, ...} (flat record, no "entries" key)
103
104
  const rawEntries = isRecord(parsed['entries']) ? parsed['entries'] : parsed;
104
105
  for (const [key, val] of Object.entries(rawEntries)) {
105
- if (key !== 'strict' && isBuildManifestEntry(val)) {
106
- this._manifest.entries[key] = val;
107
- }
106
+ if (key === 'strict' || !isBuildManifestEntry(val))
107
+ continue;
108
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(val.tableName, `Manifest entry '${key}'`);
109
+ this._manifest.entries[key] = val;
108
110
  }
109
111
  }
110
112
  }
@@ -166,7 +168,7 @@ class MalloyConfig {
166
168
  this._managedLookup = (0, config_lookup_1.buildManagedLookup)(prepared.compiledConnections, mergedOverlays, log);
167
169
  this._connections = this._managedLookup;
168
170
  this._overlays = mergedOverlays;
169
- this.virtualMap = toVirtualMap(prepared.virtualMap);
171
+ this.virtualMap = toVirtualMap(prepared.virtualMap, log);
170
172
  this.configURL = configURL;
171
173
  this.rootDirectory = rootDirectory;
172
174
  this.manifestPath = prepared.manifestPath;
@@ -441,9 +443,10 @@ function validateURLString(value, fieldName, log) {
441
443
  }
442
444
  /**
443
445
  * Convert the raw virtualMap POJO shape (a dict of dicts of strings) into
444
- * the runtime Map-of-Maps representation that Runtime consumes.
446
+ * the runtime Map-of-Maps representation. Invalid entries are dropped and
447
+ * logged — they'd be pasted into FROM clauses downstream otherwise.
445
448
  */
446
- function toVirtualMap(raw) {
449
+ function toVirtualMap(raw, log) {
447
450
  if (!isRecord(raw))
448
451
  return undefined;
449
452
  const outer = new Map();
@@ -452,9 +455,18 @@ function toVirtualMap(raw) {
452
455
  continue;
453
456
  const innerMap = new Map();
454
457
  for (const [virtualName, tablePath] of Object.entries(inner)) {
455
- if (typeof tablePath === 'string') {
456
- innerMap.set(virtualName, tablePath);
458
+ if (typeof tablePath !== 'string')
459
+ continue;
460
+ const invalid = (0, validate_table_path_1.validateCanonicalTablePathAnyDialect)(tablePath);
461
+ if (invalid !== undefined) {
462
+ log.push({
463
+ message: `virtualMap entry '${connName}.${virtualName}': ${invalid}`,
464
+ severity: 'error',
465
+ code: 'config-validation',
466
+ });
467
+ continue;
457
468
  }
469
+ innerMap.set(virtualName, tablePath);
458
470
  }
459
471
  if (innerMap.size > 0)
460
472
  outer.set(connName, innerMap);
@@ -278,10 +278,6 @@ export declare class Model implements Taggable {
278
278
  * The givens this model surfaces, keyed by caller-facing surface name.
279
279
  * Used by whole-model parameter-editor UIs to render input widgets for
280
280
  * every given the model can accept.
281
- *
282
- * Internal-only givens (declared but never surfaced into the namespace,
283
- * resolved purely via defaults) are NOT in this map — the caller has no
284
- * way to set them, so listing them would mislead a UI.
285
281
  */
286
282
  get givens(): ReadonlyMap<string, Given>;
287
283
  tagParse(spec?: TagParseSpec): MalloyTagParse;
@@ -782,10 +782,6 @@ class Model {
782
782
  * The givens this model surfaces, keyed by caller-facing surface name.
783
783
  * Used by whole-model parameter-editor UIs to render input widgets for
784
784
  * every given the model can accept.
785
- *
786
- * Internal-only givens (declared but never surfaced into the namespace,
787
- * resolved purely via defaults) are NOT in this map — the caller has no
788
- * way to set them, so listing them would mislead a UI.
789
785
  */
790
786
  get givens() {
791
787
  var _a, _b;
@@ -799,9 +795,9 @@ class Model {
799
795
  if ((_b = (_a = this.runtimeContext) === null || _a === void 0 ? void 0 : _a.finalizedGivens) === null || _b === void 0 ? void 0 : _b.has(surfaceName))
800
796
  continue;
801
797
  const decl = givens[entry.id];
802
- if (!decl)
803
- continue;
804
- out.set(surfaceName, new Given(surfaceName, entry.id, decl));
798
+ if (decl && !decl.inline) {
799
+ out.set(surfaceName, new Given(surfaceName, entry.id, decl));
800
+ }
805
801
  }
806
802
  return out;
807
803
  }
@@ -1105,7 +1101,7 @@ class PersistSource {
1105
1101
  const sd = this.persistableDef;
1106
1102
  const queryModel = this.model.queryModel;
1107
1103
  if (sd.type === 'sql_select') {
1108
- return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {}, path => this.dialect.quoteTablePath(path), (query, opts) => queryModel.compileQuery(query, opts).sql);
1104
+ return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {}, (query, opts) => queryModel.compileQuery(query, opts).sql);
1109
1105
  }
1110
1106
  else {
1111
1107
  const compiled = queryModel.compileQuery(sd.query, options);
@@ -1197,11 +1193,18 @@ class PreparedQuery {
1197
1193
  */
1198
1194
  getPreparedResult(options) {
1199
1195
  const queryModel = this._model.queryModel;
1196
+ // Build the resolved-givens map in two phases:
1197
+ // 1. caller-supplied values (resolveSuppliedGivens)
1198
+ // 2. inline-given defaults eager-evaluated against the map
1199
+ // Result is undefined when no values land — preserves the previous
1200
+ // "no givens path" downstream.
1201
+ const resolved = (options === null || options === void 0 ? void 0 : options.givens)
1202
+ ? (0, given_binding_1.resolveSuppliedGivens)(options.givens, this._modelDef)
1203
+ : new Map();
1204
+ (0, given_binding_1.evaluateInlineGivens)(resolved, this._modelDef);
1200
1205
  const prepareResultOptions = {
1201
1206
  ...options,
1202
- resolvedGivens: (options === null || options === void 0 ? void 0 : options.givens)
1203
- ? (0, given_binding_1.resolveSuppliedGivens)(options.givens, this._modelDef)
1204
- : undefined,
1207
+ resolvedGivens: resolved.size > 0 ? resolved : undefined,
1205
1208
  };
1206
1209
  const translatedQuery = queryModel.compileQuery(this._query, prepareResultOptions);
1207
1210
  return new PreparedResult({
@@ -7,6 +7,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.ExploreMaterializer = exports.PreparedResultMaterializer = exports.QueryMaterializer = exports.ModelMaterializer = exports.SingleConnectionRuntime = exports.ConnectionRuntime = exports.Runtime = void 0;
8
8
  const model_1 = require("../../model");
9
9
  const dialect_1 = require("../../dialect");
10
+ const validate_table_path_1 = require("../../connection/validate_table_path");
11
+ const config_1 = require("./config");
10
12
  const row_data_utils_1 = require("../../api/row_data_utils");
11
13
  const readers_1 = require("./readers");
12
14
  const core_1 = require("./core");
@@ -221,6 +223,12 @@ class Runtime {
221
223
  if (!isBuildManifestShape(parsed)) {
222
224
  throw new Error('manifest is not an object with an "entries" map');
223
225
  }
226
+ for (const [buildId, entry] of Object.entries(parsed.entries)) {
227
+ if (!(0, config_1.isBuildManifestEntry)(entry)) {
228
+ throw new Error(`Manifest entry '${buildId}' is missing a string tableName`);
229
+ }
230
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
231
+ }
224
232
  return parsed;
225
233
  }
226
234
  catch (e) {
@@ -502,7 +510,7 @@ class SingleConnectionRuntime extends Runtime {
502
510
  }
503
511
  // quote a column name
504
512
  quote(column) {
505
- return (0, dialect_1.getDialect)(this.connection.dialectName).sqlMaybeQuoteIdentifier(column);
513
+ return (0, dialect_1.getDialect)(this.connection.dialectName).sqlQuoteIdentifier(column);
506
514
  }
507
515
  get dialect() {
508
516
  return (0, dialect_1.getDialect)(this.connection.dialectName);
@@ -834,6 +842,11 @@ class QueryMaterializer extends FluentState {
834
842
  // Pass EMPTY_BUILD_MANIFEST in options to explicitly suppress manifest substitution.
835
843
  const explicitManifest = mergedOptions.buildManifest !== undefined;
836
844
  let buildManifest = (_a = mergedOptions.buildManifest) !== null && _a !== void 0 ? _a : (await this.runtime._resolveBuildManifest());
845
+ if (buildManifest) {
846
+ for (const [buildId, entry] of Object.entries(buildManifest.entries)) {
847
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(entry.tableName, `Manifest entry '${buildId}'`);
848
+ }
849
+ }
837
850
  // If we have a manifest with entries, compute connectionDigests for lookups.
838
851
  // TODO: This is inefficient - we call getBuildPlan just to find connection names.
839
852
  // Consider adding a listConnections() method to LookupConnection, or caching this.
@@ -866,6 +879,13 @@ class QueryMaterializer extends FluentState {
866
879
  }
867
880
  // Use virtualMap from options if provided, otherwise fall back to Runtime's.
868
881
  const virtualMap = (_b = mergedOptions.virtualMap) !== null && _b !== void 0 ? _b : this.runtime.virtualMap;
882
+ if (virtualMap) {
883
+ for (const [connName, inner] of virtualMap) {
884
+ for (const [virtualName, tablePath] of inner) {
885
+ (0, validate_table_path_1.requireCanonicalTablePathAnyDialect)(tablePath, `virtualMap entry '${connName}.${virtualName}'`);
886
+ }
887
+ }
888
+ }
869
889
  // Per-query supply for a finalized given is rejected at API entry
870
890
  // — the finalized-givens set is the runtime's "this can't be
871
891
  // overridden by the caller" guarantee. Fail before any IO so misuse
package/dist/api/util.js CHANGED
@@ -13,6 +13,7 @@ exports.mapData = mapData;
13
13
  exports.wrapResult = wrapResult;
14
14
  exports.nodeToLiteralValue = nodeToLiteralValue;
15
15
  exports.mapLogs = mapLogs;
16
+ const validate_table_path_1 = require("../connection/validate_table_path");
16
17
  const to_stable_1 = require("../to_stable");
17
18
  const row_data_utils_1 = require("./row_data_utils");
18
19
  function wrapLegacyInfoConnection(connection) {
@@ -31,6 +32,9 @@ function wrapLegacyInfoConnection(connection) {
31
32
  };
32
33
  },
33
34
  async fetchSchemaForTable(tableName) {
35
+ const invalid = (0, validate_table_path_1.validateCanonicalTablePath)(connection.dialectName, tableName);
36
+ if (invalid !== undefined)
37
+ throw new Error(invalid);
34
38
  const key = `${connection.name}:${tableName}`;
35
39
  const result = await connection.fetchSchemaForTables({ [key]: tableName }, {});
36
40
  const table = result.schemas[key];
@@ -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
  /**