@malloydata/malloy 0.0.394 → 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 (50) 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.js +1 -1
  4. package/dist/api/foundation/runtime.js +21 -1
  5. package/dist/api/util.js +4 -0
  6. package/dist/connection/base_connection.js +6 -0
  7. package/dist/connection/validate_table_path.d.ts +10 -0
  8. package/dist/connection/validate_table_path.js +56 -0
  9. package/dist/dialect/databricks/databricks.d.ts +4 -4
  10. package/dist/dialect/databricks/databricks.js +17 -22
  11. package/dist/dialect/dialect.d.ts +100 -4
  12. package/dist/dialect/dialect.js +145 -1
  13. package/dist/dialect/duckdb/duckdb.d.ts +2 -3
  14. package/dist/dialect/duckdb/duckdb.js +12 -14
  15. package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
  16. package/dist/dialect/duckdb/table-path-parser.js +57 -0
  17. package/dist/dialect/index.d.ts +2 -0
  18. package/dist/dialect/index.js +4 -1
  19. package/dist/dialect/mysql/mysql.d.ts +4 -4
  20. package/dist/dialect/mysql/mysql.js +25 -20
  21. package/dist/dialect/pg_impl.d.ts +3 -1
  22. package/dist/dialect/pg_impl.js +6 -3
  23. package/dist/dialect/postgres/postgres.d.ts +1 -3
  24. package/dist/dialect/postgres/postgres.js +8 -16
  25. package/dist/dialect/snowflake/snowflake.d.ts +4 -4
  26. package/dist/dialect/snowflake/snowflake.js +11 -27
  27. package/dist/dialect/standardsql/standardsql.d.ts +6 -4
  28. package/dist/dialect/standardsql/standardsql.js +36 -15
  29. package/dist/dialect/table-path.d.ts +54 -0
  30. package/dist/dialect/table-path.js +144 -0
  31. package/dist/dialect/trino/trino.d.ts +0 -3
  32. package/dist/dialect/trino/trino.js +7 -20
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +4 -2
  35. package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
  36. package/dist/lang/ast/source-elements/table-source.js +20 -19
  37. package/dist/lang/parse-log.d.ts +1 -0
  38. package/dist/lang/parse-malloy.js +37 -7
  39. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
  40. package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
  41. package/dist/lang/translate-response.d.ts +1 -1
  42. package/dist/model/filter_compilers.js +1 -1
  43. package/dist/model/query_model_impl.js +7 -7
  44. package/dist/model/query_query.js +37 -33
  45. package/dist/model/sql_compiled.d.ts +2 -4
  46. package/dist/model/sql_compiled.js +14 -15
  47. package/dist/test/test-models.js +2 -2
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +4 -4
@@ -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
  }
@@ -0,0 +1,54 @@
1
+ export type ValidateTablePathResult = {
2
+ ok: true;
3
+ canonical: string;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ };
8
+ export interface TablePathSegment {
9
+ /** Decoded segment value: delimiters stripped, escapes unescaped. */
10
+ value: string;
11
+ /** Whether the segment appeared in quoted form in the input. */
12
+ quoted: boolean;
13
+ }
14
+ export type DecodeDottedTablePathResult = {
15
+ ok: true;
16
+ segments: TablePathSegment[];
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ };
21
+ export type TablePathEscapeStyle = 'doubled' | 'backslash';
22
+ export interface DottedTablePathOptions {
23
+ /** Delimiter for quoted segments (`"`, `` ` ``, …). */
24
+ quoteChar: string;
25
+ /**
26
+ * How a literal `quoteChar` is encoded inside a quoted body:
27
+ * - 'doubled': `qq` inside body escapes one literal `q`.
28
+ * - 'backslash': `\X` is a two-character escape; unescaped `q` closes.
29
+ */
30
+ escapeStyle: TablePathEscapeStyle;
31
+ /**
32
+ * Regex matching one bare segment, anchored at the start of the input.
33
+ * Must NOT have global/sticky flags; the parser calls `.match()` on
34
+ * `input.slice(i)` and expects the match to start at position 0.
35
+ */
36
+ bareIdentRegex: RegExp;
37
+ /** Used in error messages only. */
38
+ dialectName: string;
39
+ }
40
+ /**
41
+ * Parse `input` as a dotted table path and require end-of-input. On
42
+ * success, returns the decoded segment values — delimiters stripped,
43
+ * escape sequences unescaped — so callers that need the segments
44
+ * (connection metadata lookups) and callers that only need
45
+ * accept/reject (`validateDottedTablePath`) share one parser.
46
+ */
47
+ export declare function decodeDottedTablePath(input: string, opts: DottedTablePathOptions): DecodeDottedTablePathResult;
48
+ /**
49
+ * Validate `input` as a dotted table path. On success the canonical
50
+ * form is the input verbatim. See `decodeDottedTablePath` for the
51
+ * underlying parser; this is the validate-only wrapper that doesn't
52
+ * expose segment internals.
53
+ */
54
+ export declare function validateDottedTablePath(input: string, opts: DottedTablePathOptions): ValidateTablePathResult;
@@ -0,0 +1,144 @@
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.decodeDottedTablePath = decodeDottedTablePath;
8
+ exports.validateDottedTablePath = validateDottedTablePath;
9
+ /**
10
+ * Parse `input` as a dotted table path and require end-of-input. On
11
+ * success, returns the decoded segment values — delimiters stripped,
12
+ * escape sequences unescaped — so callers that need the segments
13
+ * (connection metadata lookups) and callers that only need
14
+ * accept/reject (`validateDottedTablePath`) share one parser.
15
+ */
16
+ function decodeDottedTablePath(input, opts) {
17
+ const { quoteChar, escapeStyle, bareIdentRegex, dialectName } = opts;
18
+ if (input.length === 0) {
19
+ return { ok: false, error: `${dialectName} table path is empty` };
20
+ }
21
+ const segments = [];
22
+ let i = 0;
23
+ while (true) {
24
+ let segValue;
25
+ let segQuoted;
26
+ if (input[i] === quoteChar) {
27
+ const result = consumeQuotedSegment(input, i, quoteChar, escapeStyle);
28
+ if (result === null) {
29
+ return {
30
+ ok: false,
31
+ error: `Invalid ${dialectName} table path: ${JSON.stringify(input)} — ` +
32
+ 'unterminated quoted segment',
33
+ };
34
+ }
35
+ segValue = result.decoded;
36
+ segQuoted = true;
37
+ i = result.end;
38
+ }
39
+ else {
40
+ const m = input.slice(i).match(bareIdentRegex);
41
+ if (!m || m.index !== 0 || m[0].length === 0) {
42
+ return {
43
+ ok: false,
44
+ error: `Invalid ${dialectName} table path: ${JSON.stringify(input)} — ` +
45
+ `invalid segment at position ${i}`,
46
+ };
47
+ }
48
+ segValue = m[0];
49
+ segQuoted = false;
50
+ i += m[0].length;
51
+ }
52
+ // Defense-in-depth: `;` and `--` are forbidden in any decoded segment,
53
+ // even a legally-quoted one. Real table names don't contain them.
54
+ if (segValue.includes(';') || segValue.includes('--')) {
55
+ return {
56
+ ok: false,
57
+ error: `Invalid ${dialectName} table path: segment ${JSON.stringify(segValue)} ` +
58
+ 'contains forbidden character; even when quoted, table-path ' +
59
+ 'segments may not contain `;` or `--`.',
60
+ };
61
+ }
62
+ segments.push({ value: segValue, quoted: segQuoted });
63
+ if (i === input.length)
64
+ return { ok: true, segments };
65
+ if (input[i] !== '.') {
66
+ return {
67
+ ok: false,
68
+ error: `Invalid ${dialectName} table path: ${JSON.stringify(input)} — ` +
69
+ `expected '.' at position ${i}`,
70
+ };
71
+ }
72
+ i++;
73
+ if (i === input.length) {
74
+ return {
75
+ ok: false,
76
+ error: `Invalid ${dialectName} table path: ${JSON.stringify(input)} — ` +
77
+ 'trailing dot',
78
+ };
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * Validate `input` as a dotted table path. On success the canonical
84
+ * form is the input verbatim. See `decodeDottedTablePath` for the
85
+ * underlying parser; this is the validate-only wrapper that doesn't
86
+ * expose segment internals.
87
+ */
88
+ function validateDottedTablePath(input, opts) {
89
+ const result = decodeDottedTablePath(input, opts);
90
+ if (!result.ok)
91
+ return result;
92
+ return { ok: true, canonical: input };
93
+ }
94
+ /**
95
+ * Read past a quoted segment starting at `input[i]` (must be `quoteChar`).
96
+ * Returns the segment body (delimiters stripped, escape sequences
97
+ * unescaped) and the index just after the closing quote, or `null` if
98
+ * the segment is unterminated.
99
+ */
100
+ function consumeQuotedSegment(input, i, quoteChar, escapeStyle) {
101
+ // input[i] === quoteChar
102
+ let j = i + 1;
103
+ let decoded = '';
104
+ while (j < input.length) {
105
+ if (escapeStyle === 'backslash' && input[j] === '\\') {
106
+ if (j + 1 >= input.length)
107
+ return null;
108
+ decoded += decodeBackslashEscape(input[j + 1]);
109
+ j += 2;
110
+ continue;
111
+ }
112
+ if (input[j] === quoteChar) {
113
+ if (escapeStyle === 'doubled' && input[j + 1] === quoteChar) {
114
+ decoded += quoteChar;
115
+ j += 2;
116
+ continue;
117
+ }
118
+ return { decoded, end: j + 1 };
119
+ }
120
+ decoded += input[j];
121
+ j++;
122
+ }
123
+ return null;
124
+ }
125
+ /**
126
+ * Decode a single character after a backslash. We intentionally accept
127
+ * any character — we're a translator-time grammar check, not a strict
128
+ * lexical conformance test for any particular engine's quoted-identifier
129
+ * escape table. The engine will surface its own errors at bind time if
130
+ * it doesn't recognize a particular sequence.
131
+ */
132
+ function decodeBackslashEscape(c) {
133
+ switch (c) {
134
+ case 'n':
135
+ return '\n';
136
+ case 't':
137
+ return '\t';
138
+ case 'r':
139
+ return '\r';
140
+ default:
141
+ return c;
142
+ }
143
+ }
144
+ //# sourceMappingURL=table-path.js.map
@@ -30,7 +30,6 @@ export declare class TrinoDialect extends PostgresBase {
30
30
  supportsTempTables: boolean;
31
31
  supportsCountApprox: boolean;
32
32
  supportsHyperLogLog: boolean;
33
- quoteTablePath(tablePath: string): string;
34
33
  sqlGroupSetTable(groupSetCount: number): string;
35
34
  exprToSQL(qi: QueryInfo, df: Expr): string | undefined;
36
35
  sqlAnyValue(groupSet: number, fieldName: string): string;
@@ -62,8 +61,6 @@ export declare class TrinoDialect extends PostgresBase {
62
61
  sqlRegexpMatch(reCmp: RegexMatchExpr): string;
63
62
  sqlMeasureTimeExpr(mf: MeasureTimeExpr): string;
64
63
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
65
- sqlLiteralString(literal: string): string;
66
- sqlLiteralRegexp(literal: string): string;
67
64
  getDialectFunctionOverrides(): {
68
65
  [name: string]: DialectFunctionOverloadDef[];
69
66
  };
@@ -200,16 +200,9 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
200
200
  WITH
201
201
  WITHIN`.split(/\s/);
202
202
  }
203
- quoteTablePath(tablePath) {
204
- // Quote with double quotes if contains dangerous characters
205
- if (tablePath.match(/[;-]/)) {
206
- return tablePath
207
- .split('.')
208
- .map(part => `"${part}"`)
209
- .join('.');
210
- }
211
- return tablePath;
212
- }
203
+ // Trino bare identifier is strict ANSI (`[A-Za-z_][A-Za-z0-9_]*`),
204
+ // which matches the Dialect default no override needed. Verified
205
+ // against the live engine.
213
206
  sqlGroupSetTable(groupSetCount) {
214
207
  return `CROSS JOIN (SELECT row_number() OVER() -1 group_set FROM UNNEST(SEQUENCE(0,${groupSetCount})))`;
215
208
  }
@@ -300,7 +293,7 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
300
293
  if (childName === '__row_id') {
301
294
  return `__row_id_from_${parentAlias}`;
302
295
  }
303
- return `${parentAlias}.${this.sqlMaybeQuoteIdentifier(childName)}`;
296
+ return `${parentAlias}.${this.sqlQuoteIdentifier(childName)}`;
304
297
  }
305
298
  sqlUnnestPipelineHead(isSingleton, sourceSQLExpression) {
306
299
  let p = sourceSQLExpression;
@@ -471,12 +464,6 @@ ${(0, utils_1.indent)(sql)}
471
464
  }
472
465
  return tableSQL;
473
466
  }
474
- sqlLiteralString(literal) {
475
- return "'" + literal.replace(/'/g, "''") + "'";
476
- }
477
- sqlLiteralRegexp(literal) {
478
- return "'" + literal.replace(/'/g, "''") + "'";
479
- }
480
467
  getDialectFunctionOverrides() {
481
468
  return (0, functions_1.expandOverrideMap)(function_overrides_1.TRINO_MALLOY_STANDARD_OVERLOADS);
482
469
  }
@@ -503,7 +490,7 @@ ${(0, utils_1.indent)(sql)}
503
490
  const typeSpec = [];
504
491
  for (const f of malloyType.fields) {
505
492
  if ((0, malloy_types_1.isAtomic)(f)) {
506
- typeSpec.push(`${this.sqlMaybeQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
493
+ typeSpec.push(`${this.sqlQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
507
494
  }
508
495
  }
509
496
  return `ROW(${typeSpec.join(',')})`;
@@ -515,7 +502,7 @@ ${(0, utils_1.indent)(sql)}
515
502
  const typeSpec = [];
516
503
  for (const f of malloyType.fields) {
517
504
  if ((0, malloy_types_1.isAtomic)(f)) {
518
- typeSpec.push(`${this.sqlMaybeQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
505
+ typeSpec.push(`${this.sqlQuoteIdentifier(f.name)} ${this.malloyTypeToSQLType(f)}`);
519
506
  }
520
507
  }
521
508
  return `ARRAY<ROW(${typeSpec.join(',')})>`;
@@ -603,7 +590,7 @@ ${(0, utils_1.indent)(sql)}
603
590
  const name = (_a = f.as) !== null && _a !== void 0 ? _a : f.name;
604
591
  rowVals.push((_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');
605
592
  const elType = this.malloyTypeToSQLType(f);
606
- rowTypes.push(`${this.sqlMaybeQuoteIdentifier(name)} ${elType}`);
593
+ rowTypes.push(`${this.sqlQuoteIdentifier(name)} ${elType}`);
607
594
  }
608
595
  }
609
596
  return `CAST(ROW(${rowVals.join(',')}) AS ROW(${rowTypes.join(',')}))`;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { DuckDBDialect, StandardSQLDialect, TrinoDialect, PostgresDialect, SnowflakeDialect, MySQLDialect, DatabricksDialect, registerDialect, arg, qtz, overload, minScalar, anyExprType, minAggregate, maxScalar, sql, makeParam, param, variadicParam, literal, spread, Dialect, } from './dialect';
2
- export type { DialectFieldList, DialectFunctionOverloadDef, QueryInfo, MalloyStandardFunctionImplementations, DefinitionBlueprint, DefinitionBlueprintMap, OverloadedDefinitionBlueprint, } from './dialect';
1
+ export { DuckDBDialect, StandardSQLDialect, TrinoDialect, PostgresDialect, SnowflakeDialect, MySQLDialect, DatabricksDialect, registerDialect, arg, qtz, overload, minScalar, anyExprType, minAggregate, maxScalar, sql, makeParam, param, variadicParam, literal, spread, Dialect, decodeDottedTablePath, validateDottedTablePath, } from './dialect';
2
+ export type { DialectFieldList, DialectFunctionOverloadDef, QueryInfo, MalloyStandardFunctionImplementations, DefinitionBlueprint, DefinitionBlueprintMap, OverloadedDefinitionBlueprint, DecodeDottedTablePathResult, DottedTablePathOptions, TablePathEscapeStyle, TablePathSegment, ValidateTablePathResult, } from './dialect';
3
3
  export type { QueryRecord, StructDef, TableSourceDef, SQLSourceDef, SourceDef, JoinFieldDef, NamedSourceDefs, MalloyQueryData, DateUnit, ExtractUnit, TimestampUnit, TemporalFieldType, QueryData, QueryValue, Expr, FilterCondition, Argument, Parameter, FieldDef, PipeSegment, QueryFieldDef, IndexFieldDef, TurtleDef, SearchValueMapResult, SearchIndexResult, ModelDef, Query, QueryResult, QueryResultDef, QueryRunStats, QueryScalar, NamedQueryDef, NamedModelObject, ExpressionType, FunctionDef, FunctionOverloadDef, FunctionParameterDef, ExpressionValueType, TypeDesc, FunctionParamTypeDesc, DocumentLocation, DocumentRange, DocumentPosition, Sampling, Annotation, BasicAtomicTypeDef, BasicAtomicDef, AtomicTypeDef, AtomicFieldDef, ArrayDef, ArrayTypeDef, RecordTypeDef, RepeatedRecordTypeDef, RecordDef, RepeatedRecordDef, RecordLiteralNode, StringLiteralNode, ArrayLiteralNode, SourceComponentInfo, DateLiteralNode, TimestampLiteralNode, TimestamptzLiteralNode, TimeLiteralExpr, TypecastExpr, BuildID, BuildManifest, BuildManifestEntry, GivenValue, VirtualMap, } from './model';
4
4
  export { isSourceDef, isAtomic, isBasicAtomic, isCompoundArrayData, isJoined, isJoinedSource, isSamplingEnable, isSamplingPercent, isSamplingRows, isRepeatedRecord, isBasicArray, mkArrayDef, mkFieldDef, expressionIsAggregate, expressionIsAnalytic, expressionIsCalculation, expressionIsScalar, expressionIsUngroupedAggregate, indent, composeSQLExpr, isTimestampUnit, isDateUnit, constantExprToSQL, } from './model';
5
5
  export { malloyToQuery, MalloyTranslator, } from './lang';
package/dist/index.js CHANGED
@@ -33,8 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Runtime = exports.Malloy = exports.Model = exports.MalloyTranslator = exports.malloyToQuery = exports.constantExprToSQL = exports.isDateUnit = exports.isTimestampUnit = exports.composeSQLExpr = exports.indent = exports.expressionIsUngroupedAggregate = exports.expressionIsScalar = exports.expressionIsCalculation = exports.expressionIsAnalytic = exports.expressionIsAggregate = exports.mkFieldDef = exports.mkArrayDef = exports.isBasicArray = exports.isRepeatedRecord = exports.isSamplingRows = exports.isSamplingPercent = exports.isSamplingEnable = exports.isJoinedSource = exports.isJoined = exports.isCompoundArrayData = exports.isBasicAtomic = exports.isAtomic = exports.isSourceDef = exports.Dialect = exports.spread = exports.literal = exports.variadicParam = exports.param = exports.makeParam = exports.sql = exports.maxScalar = exports.minAggregate = exports.anyExprType = exports.minScalar = exports.overload = exports.qtz = exports.arg = exports.registerDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.SnowflakeDialect = exports.PostgresDialect = exports.TrinoDialect = exports.StandardSQLDialect = exports.DuckDBDialect = void 0;
37
- exports.makeDigest = exports.EMPTY_BUILD_MANIFEST = exports.PersistSource = exports.annotationToTaglines = exports.annotationToTag = exports.sqlKey = exports.API = exports.sourceDefToSourceInfo = exports.modelDefToModelInfo = exports.toAsyncGenerator = exports.createConnectionsFromConfig = exports.getRegisteredConnectionTypes = exports.getConnectionTypeDisplayName = exports.getConnectionProperties = exports.registerConnectionType = exports.discoverConfig = exports.defaultConfigOverlays = exports.contextOverlay = exports.envOverlay = exports.MalloyConfig = exports.Manifest = exports.CacheManager = exports.InMemoryModelCache = exports.Explore = exports.DataWriter = exports.Parse = exports.JSONWriter = exports.CSVWriter = exports.QueryMaterializer = exports.Result = exports.PreparedResult = exports.TimestampTimeframe = exports.DateTimeframe = exports.SourceRelationship = exports.JoinRelationship = exports.MalloyError = exports.FixedConnectionMap = exports.InMemoryURLReader = exports.EmptyURLReader = exports.SingleConnectionRuntime = exports.ConnectionRuntime = exports.AtomicFieldType = void 0;
36
+ exports.Model = exports.MalloyTranslator = exports.malloyToQuery = exports.constantExprToSQL = exports.isDateUnit = exports.isTimestampUnit = exports.composeSQLExpr = exports.indent = exports.expressionIsUngroupedAggregate = exports.expressionIsScalar = exports.expressionIsCalculation = exports.expressionIsAnalytic = exports.expressionIsAggregate = exports.mkFieldDef = exports.mkArrayDef = exports.isBasicArray = exports.isRepeatedRecord = exports.isSamplingRows = exports.isSamplingPercent = exports.isSamplingEnable = exports.isJoinedSource = exports.isJoined = exports.isCompoundArrayData = exports.isBasicAtomic = exports.isAtomic = exports.isSourceDef = exports.validateDottedTablePath = exports.decodeDottedTablePath = exports.Dialect = exports.spread = exports.literal = exports.variadicParam = exports.param = exports.makeParam = exports.sql = exports.maxScalar = exports.minAggregate = exports.anyExprType = exports.minScalar = exports.overload = exports.qtz = exports.arg = exports.registerDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.SnowflakeDialect = exports.PostgresDialect = exports.TrinoDialect = exports.StandardSQLDialect = exports.DuckDBDialect = void 0;
37
+ exports.makeDigest = exports.EMPTY_BUILD_MANIFEST = exports.PersistSource = exports.annotationToTaglines = exports.annotationToTag = exports.sqlKey = exports.API = exports.sourceDefToSourceInfo = exports.modelDefToModelInfo = exports.toAsyncGenerator = exports.createConnectionsFromConfig = exports.getRegisteredConnectionTypes = exports.getConnectionTypeDisplayName = exports.getConnectionProperties = exports.registerConnectionType = exports.discoverConfig = exports.defaultConfigOverlays = exports.contextOverlay = exports.envOverlay = exports.MalloyConfig = exports.Manifest = exports.CacheManager = exports.InMemoryModelCache = exports.Explore = exports.DataWriter = exports.Parse = exports.JSONWriter = exports.CSVWriter = exports.QueryMaterializer = exports.Result = exports.PreparedResult = exports.TimestampTimeframe = exports.DateTimeframe = exports.SourceRelationship = exports.JoinRelationship = exports.MalloyError = exports.FixedConnectionMap = exports.InMemoryURLReader = exports.EmptyURLReader = exports.SingleConnectionRuntime = exports.ConnectionRuntime = exports.AtomicFieldType = exports.Runtime = exports.Malloy = void 0;
38
38
  /*
39
39
  * Copyright 2023 Google LLC
40
40
  *
@@ -80,6 +80,8 @@ Object.defineProperty(exports, "variadicParam", { enumerable: true, get: functio
80
80
  Object.defineProperty(exports, "literal", { enumerable: true, get: function () { return dialect_1.literal; } });
81
81
  Object.defineProperty(exports, "spread", { enumerable: true, get: function () { return dialect_1.spread; } });
82
82
  Object.defineProperty(exports, "Dialect", { enumerable: true, get: function () { return dialect_1.Dialect; } });
83
+ Object.defineProperty(exports, "decodeDottedTablePath", { enumerable: true, get: function () { return dialect_1.decodeDottedTablePath; } });
84
+ Object.defineProperty(exports, "validateDottedTablePath", { enumerable: true, get: function () { return dialect_1.validateDottedTablePath; } });
83
85
  var model_1 = require("./model");
84
86
  Object.defineProperty(exports, "isSourceDef", { enumerable: true, get: function () { return model_1.isSourceDef; } });
85
87
  // Used in Composer Demo
@@ -3,7 +3,7 @@ import { Source } from './source';
3
3
  import type { ModelEntryReference } from '../types/malloy-element';
4
4
  type TableInfo = {
5
5
  tablePath: string;
6
- connectionName?: string | undefined;
6
+ connectionName: string;
7
7
  };
8
8
  export declare abstract class TableSource extends Source {
9
9
  abstract getTableInfo(): TableInfo | undefined;
@@ -16,10 +16,4 @@ export declare class TableMethodSource extends TableSource {
16
16
  constructor(connectionName: ModelEntryReference, tablePath: string);
17
17
  getTableInfo(): TableInfo | undefined;
18
18
  }
19
- export declare class TableFunctionSource extends TableSource {
20
- readonly tableURI: string;
21
- elementType: string;
22
- constructor(tableURI: string);
23
- getTableInfo(): TableInfo | undefined;
24
- }
25
19
  export {};