@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
@@ -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
  }
@@ -0,0 +1,2 @@
1
+ import type { ValidateTablePathResult } from '../table-path';
2
+ export declare function validateDuckDBTablePath(input: string): ValidateTablePathResult;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright Contributors to the Malloy project
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateDuckDBTablePath = validateDuckDBTablePath;
8
+ const table_path_1 = require("../table-path");
9
+ const DUCKDB_FILE_PATH_RE = /^[A-Za-z0-9._\-/:?*]+$/;
10
+ const DUCKDB_SINGLE_QUOTED_RE = /^'(?:[^']|'')*'$/;
11
+ function validateDuckDBTablePath(input) {
12
+ if (input.length === 0) {
13
+ return { ok: false, error: 'DuckDB table path is empty' };
14
+ }
15
+ // Branch 1: explicit single-quoted literal.
16
+ if (input[0] === "'") {
17
+ if (DUCKDB_SINGLE_QUOTED_RE.test(input)) {
18
+ const body = input.slice(1, -1).replace(/''/g, "'");
19
+ if (body.includes(';') || body.includes('--')) {
20
+ return {
21
+ ok: false,
22
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — ` +
23
+ 'forbidden character `;` or `--` in single-quoted body.',
24
+ };
25
+ }
26
+ return { ok: true, canonical: input };
27
+ }
28
+ return {
29
+ ok: false,
30
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — ` +
31
+ 'unterminated or trailing-junk single-quoted literal.',
32
+ };
33
+ }
34
+ // Branch 2: identifier path (same grammar as ANSI dialects).
35
+ const id = (0, table_path_1.validateDottedTablePath)(input, {
36
+ quoteChar: '"',
37
+ escapeStyle: 'doubled',
38
+ bareIdentRegex: /^[A-Za-z_][A-Za-z0-9_]*/,
39
+ dialectName: 'DuckDB',
40
+ });
41
+ if (id.ok)
42
+ return id;
43
+ // Branch 3: file-path convenience.
44
+ if (DUCKDB_FILE_PATH_RE.test(input)) {
45
+ return { ok: true, canonical: `'${input}'` };
46
+ }
47
+ return {
48
+ ok: false,
49
+ error: `Invalid DuckDB table path: ${JSON.stringify(input)} — expected an ` +
50
+ 'identifier path, a quoted identifier path, a single-quoted ' +
51
+ "literal ('foo.csv'), or a file-path-shaped string of letters, " +
52
+ 'digits, and `._-/:?*`. For table-valued function calls (e.g. ' +
53
+ "read_parquet('foo.parquet')) or other table expressions, use a " +
54
+ 'SQL block instead: connection.sql("""SELECT * FROM …""").',
55
+ };
56
+ }
57
+ //# sourceMappingURL=table-path-parser.js.map
@@ -14,3 +14,5 @@ export { getMalloyStandardFunctions } from './functions';
14
14
  export type { MalloyStandardFunctionImplementations } from './functions';
15
15
  export type { TinyToken } from './tiny_parser';
16
16
  export { TinyParser } from './tiny_parser';
17
+ export type { DecodeDottedTablePathResult, DottedTablePathOptions, TablePathEscapeStyle, TablePathSegment, ValidateTablePathResult, } from './table-path';
18
+ export { decodeDottedTablePath, validateDottedTablePath } from './table-path';
@@ -22,7 +22,7 @@
22
22
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.TinyParser = exports.getMalloyStandardFunctions = exports.registerDialect = exports.getDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.TrinoDialect = exports.SnowflakeDialect = exports.DuckDBDialect = exports.PostgresDialect = exports.StandardSQLDialect = exports.qtz = exports.Dialect = exports.sql = exports.literal = exports.variadicParam = exports.param = exports.spread = exports.maxScalar = exports.minAggregate = exports.minScalar = exports.overload = exports.makeParam = exports.anyExprType = exports.arg = void 0;
25
+ exports.validateDottedTablePath = exports.decodeDottedTablePath = exports.TinyParser = exports.getMalloyStandardFunctions = exports.registerDialect = exports.getDialect = exports.DatabricksDialect = exports.MySQLDialect = exports.TrinoDialect = exports.SnowflakeDialect = exports.DuckDBDialect = exports.PostgresDialect = exports.StandardSQLDialect = exports.qtz = exports.Dialect = exports.sql = exports.literal = exports.variadicParam = exports.param = exports.spread = exports.maxScalar = exports.minAggregate = exports.minScalar = exports.overload = exports.makeParam = exports.anyExprType = exports.arg = void 0;
26
26
  var util_1 = require("./functions/util");
27
27
  Object.defineProperty(exports, "arg", { enumerable: true, get: function () { return util_1.arg; } });
28
28
  Object.defineProperty(exports, "anyExprType", { enumerable: true, get: function () { return util_1.anyExprType; } });
@@ -60,4 +60,7 @@ var functions_1 = require("./functions");
60
60
  Object.defineProperty(exports, "getMalloyStandardFunctions", { enumerable: true, get: function () { return functions_1.getMalloyStandardFunctions; } });
61
61
  var tiny_parser_1 = require("./tiny_parser");
62
62
  Object.defineProperty(exports, "TinyParser", { enumerable: true, get: function () { return tiny_parser_1.TinyParser; } });
63
+ var table_path_1 = require("./table-path");
64
+ Object.defineProperty(exports, "decodeDottedTablePath", { enumerable: true, get: function () { return table_path_1.decodeDottedTablePath; } });
65
+ Object.defineProperty(exports, "validateDottedTablePath", { enumerable: true, get: function () { return table_path_1.validateDottedTablePath; } });
63
66
  //# sourceMappingURL=index.js.map
@@ -4,6 +4,9 @@ import { Dialect } from '../dialect';
4
4
  import type { DialectFunctionOverloadDef } from '../functions';
5
5
  export declare class MySQLDialect extends Dialect {
6
6
  name: string;
7
+ stringLiteralStyle: "backslash";
8
+ identifierEscapeStyle: "doubled";
9
+ identifierQuoteChar: string;
7
10
  defaultNumberType: string;
8
11
  defaultDecimalType: string;
9
12
  udfPrefix: string;
@@ -32,9 +35,9 @@ export declare class MySQLDialect extends Dialect {
32
35
  booleanType: BooleanTypeSupport;
33
36
  orderByClause: OrderByClauseType;
34
37
  maxIdentifierLength: number;
38
+ tablePathBareIdentRegex: RegExp;
35
39
  malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
36
40
  sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
37
- quoteTablePath(tablePath: string): string;
38
41
  sqlGroupSetTable(groupSetCount: number): string;
39
42
  sqlAnyValue(_groupSet: number, fieldName: string): string;
40
43
  private mapFields;
@@ -54,7 +57,6 @@ export declare class MySQLDialect extends Dialect {
54
57
  sqlCreateFunction(id: string, funcText: string): string;
55
58
  sqlCreateFunctionCombineLastStage(lastStageName: string): string;
56
59
  sqlSelectAliasAsStruct(_alias: string, _fieldList: DialectFieldList): string;
57
- sqlMaybeQuoteIdentifier(identifier: string): string;
58
60
  sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
59
61
  sqlNowExpr(): string;
60
62
  sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
@@ -73,8 +75,6 @@ export declare class MySQLDialect extends Dialect {
73
75
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
74
76
  sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
75
77
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
76
- sqlLiteralString(literal: string): string;
77
- sqlLiteralRegexp(literal: string): string;
78
78
  getDialectFunctionOverrides(): {
79
79
  [name: string]: DialectFunctionOverloadDef[];
80
80
  };
@@ -94,6 +94,9 @@ class MySQLDialect extends dialect_1.Dialect {
94
94
  constructor() {
95
95
  super(...arguments);
96
96
  this.name = 'mysql';
97
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Backslash;
98
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
99
+ this.identifierQuoteChar = '`';
97
100
  this.defaultNumberType = 'DOUBLE PRECISION';
98
101
  this.defaultDecimalType = 'DECIMAL';
99
102
  this.udfPrefix = 'ms_temp.__udf';
@@ -121,6 +124,11 @@ class MySQLDialect extends dialect_1.Dialect {
121
124
  this.booleanType = 'simulated';
122
125
  this.orderByClause = 'ordinal';
123
126
  this.maxIdentifierLength = 64;
127
+ // MySQL bare identifiers allow `$` and may start with a digit, but
128
+ // cannot be entirely digits (or they lex as number literals). The
129
+ // regex requires at least one non-digit char somewhere in the run.
130
+ // Verified against the live engine.
131
+ this.tablePathBareIdentRegex = /^[A-Za-z0-9_$]*[A-Za-z_$][A-Za-z0-9_$]*/;
124
132
  }
125
133
  malloyTypeToSQLType(malloyType) {
126
134
  switch (malloyType.type) {
@@ -154,12 +162,6 @@ class MySQLDialect extends dialect_1.Dialect {
154
162
  rawType: baseSqlType,
155
163
  });
156
164
  }
157
- quoteTablePath(tablePath) {
158
- return tablePath
159
- .split('.')
160
- .map(part => `\`${part}\``)
161
- .join('.');
162
- }
163
165
  sqlGroupSetTable(groupSetCount) {
164
166
  return `CROSS JOIN (select number - 1 as group_set from JSON_TABLE(cast(concat("[1", repeat(",1", ${groupSetCount}), "]") as JSON),"$[*]" COLUMNS(number FOR ORDINALITY)) group_set) as group_set`;
165
167
  }
@@ -167,7 +169,11 @@ class MySQLDialect extends dialect_1.Dialect {
167
169
  return `MAX(${fieldName})`;
168
170
  }
169
171
  mapFields(fieldList) {
170
- return fieldList.map(f => `"${f.rawName}", ${f.sqlExpression}`).join(', ');
172
+ // JSON_OBJECT key is a string value. Routing rawName through
173
+ // sqlLiteralString also keeps the SQL valid under ANSI_QUOTES mode.
174
+ return fieldList
175
+ .map(f => `${this.sqlLiteralString(f.rawName)}, ${f.sqlExpression}`)
176
+ .join(', ');
171
177
  }
172
178
  sqlAggregateTurtle(groupSet, fieldList, orderBy) {
173
179
  const separator = ',';
@@ -218,7 +224,12 @@ class MySQLDialect extends dialect_1.Dialect {
218
224
  ((_a = f.typeDef.rawType) === null || _a === void 0 ? void 0 : _a.match(/json/))) {
219
225
  fType = f.typeDef.rawType.toUpperCase();
220
226
  }
221
- fields.push(`${this.sqlMaybeQuoteIdentifier(f.sqlOutputName)} ${fType} PATH "$.${f.rawName}"`);
227
+ // JSON_TABLE PATH argument is a string literal containing a
228
+ // JSONPath. Render rawName through sqlLiteralString so a single
229
+ // quote in the user-controlled field name cannot close the SQL
230
+ // string literal.
231
+ const jsonPathLit = this.sqlLiteralString('$.' + f.rawName);
232
+ fields.push(`${this.sqlQuoteIdentifier(f.sqlOutputName)} ${fType} PATH ${jsonPathLit}`);
222
233
  }
223
234
  return fields.join(',\n');
224
235
  }
@@ -266,7 +277,11 @@ class MySQLDialect extends dialect_1.Dialect {
266
277
  }
267
278
  sqlFieldReference(parentAlias, parentType, childName, childType) {
268
279
  if (parentType === 'array[scalar]' || parentType === 'record') {
269
- let ret = `JSON_UNQUOTE(JSON_EXTRACT(${parentAlias},'$.${childName}'))`;
280
+ // childName comes from user-controlled record field names; render
281
+ // the JSON path through sqlLiteralString so a single quote in the
282
+ // name cannot close the SQL string literal.
283
+ const jsonPathLit = this.sqlLiteralString('$.' + childName);
284
+ let ret = `JSON_UNQUOTE(JSON_EXTRACT(${parentAlias},${jsonPathLit}))`;
270
285
  if (parentType === 'array[scalar]') {
271
286
  ret = `JSON_UNQUOTE(${parentAlias}.\`value\`)`;
272
287
  }
@@ -280,7 +295,7 @@ class MySQLDialect extends dialect_1.Dialect {
280
295
  return `CAST(${ret} as JSON)`;
281
296
  }
282
297
  }
283
- const child = this.sqlMaybeQuoteIdentifier(childName);
298
+ const child = this.sqlQuoteIdentifier(childName);
284
299
  return `${parentAlias}.${child}`;
285
300
  }
286
301
  sqlCreateFunction(id, funcText) {
@@ -297,9 +312,6 @@ class MySQLDialect extends dialect_1.Dialect {
297
312
  // .map(name => `'${name.replace(/`/g, '')}', \`${alias}\`.${name}`)
298
313
  // .join(',')})`;
299
314
  }
300
- sqlMaybeQuoteIdentifier(identifier) {
301
- return '`' + identifier.replace(/`/g, '``') + '`';
302
- }
303
315
  // TODO: Check what this is.
304
316
  sqlCreateTableAsSelect(_tableName, _sql) {
305
317
  throw new Error('Not implemented Yet');
@@ -441,13 +453,6 @@ class MySQLDialect extends dialect_1.Dialect {
441
453
  }
442
454
  return tableSQL;
443
455
  }
444
- sqlLiteralString(literal) {
445
- const noVirgule = literal.replace(/\\/g, '\\\\');
446
- return "'" + noVirgule.replace(/'/g, "\\'") + "'";
447
- }
448
- sqlLiteralRegexp(literal) {
449
- return "'" + literal.replace(/'/g, "''") + "'";
450
- }
451
456
  getDialectFunctionOverrides() {
452
457
  return (0, functions_1.expandOverrideMap)(function_overrides_1.MYSQL_MALLOY_STANDARD_OVERLOADS);
453
458
  }
@@ -9,6 +9,9 @@ export declare const timeExtractMap: Record<string, string>;
9
9
  export declare abstract class PostgresBase extends Dialect {
10
10
  hasTimestamptz: boolean;
11
11
  supportsBigIntPrecision: boolean;
12
+ stringLiteralStyle: "doubled";
13
+ identifierEscapeStyle: "doubled";
14
+ identifierQuoteChar: string;
12
15
  sqlNowExpr(): string;
13
16
  sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
14
17
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
@@ -18,7 +21,6 @@ export declare abstract class PostgresBase extends Dialect {
18
21
  sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
19
22
  sqlLiteralRecord(_lit: RecordLiteralNode): string;
20
23
  sqlLiteralArray(lit: ArrayLiteralNode): string;
21
- sqlMaybeQuoteIdentifier(identifier: string): string;
22
24
  sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
23
25
  sql: string;
24
26
  typeDef: AtomicTypeDef;
@@ -23,6 +23,12 @@ class PostgresBase extends dialect_1.Dialect {
23
23
  this.hasTimestamptz = true;
24
24
  // Postgres-family dialects use JSON serialization which loses bigint precision
25
25
  this.supportsBigIntPrecision = false;
26
+ // All current Postgres-family dialects (Postgres, DuckDB, Trino, Presto)
27
+ // use ANSI doubled-quote literal escaping and ANSI double-quote identifiers.
28
+ // Subclasses may override.
29
+ this.stringLiteralStyle = dialect_1.EscapeStyle.Doubled;
30
+ this.identifierEscapeStyle = dialect_1.EscapeStyle.Doubled;
31
+ this.identifierQuoteChar = '"';
26
32
  }
27
33
  sqlNowExpr() {
28
34
  return 'LOCALTIMESTAMP';
@@ -96,9 +102,6 @@ class PostgresBase extends dialect_1.Dialect {
96
102
  const array = lit.kids.values.map(val => val.sql);
97
103
  return 'ARRAY[' + array.join(',') + ']';
98
104
  }
99
- sqlMaybeQuoteIdentifier(identifier) {
100
- return '"' + identifier.replace(/"/g, '""') + '"';
101
- }
102
105
  sqlConvertToCivilTime(expr, timezone, typeDef) {
103
106
  // PostgreSQL/DuckDB: AT TIME ZONE is polymorphic
104
107
  // For timestamptz (TIMESTAMPTZ): AT TIME ZONE converts to plain TIMESTAMP (civil in timezone)
@@ -27,7 +27,7 @@ export declare class PostgresDialect extends PostgresBase {
27
27
  compoundObjectInSchema: boolean;
28
28
  likeEscape: boolean;
29
29
  maxIdentifierLength: number;
30
- quoteTablePath(tablePath: string): string;
30
+ tablePathBareIdentRegex: RegExp;
31
31
  sqlGroupSetTable(groupSetCount: number): string;
32
32
  sqlAnyValue(groupSet: number, fieldName: string): string;
33
33
  mapFields(fieldList: DialectFieldList): string;
@@ -52,8 +52,6 @@ export declare class PostgresDialect extends PostgresBase {
52
52
  sqlAggDistinct(key: string, values: string[], func: (valNames: string[]) => string): string;
53
53
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
54
54
  sqlOrderBy(orderTerms: string[]): string;
55
- sqlLiteralString(literal: string): string;
56
- sqlLiteralRegexp(literal: string): string;
57
55
  getDialectFunctionOverrides(): {
58
56
  [name: string]: DialectFunctionOverloadDef[];
59
57
  };
@@ -99,12 +99,9 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
99
99
  this.compoundObjectInSchema = false;
100
100
  this.likeEscape = false;
101
101
  this.maxIdentifierLength = 63;
102
- }
103
- quoteTablePath(tablePath) {
104
- return tablePath
105
- .split('.')
106
- .map(part => `"${part}"`)
107
- .join('.');
102
+ // Postgres bare-identifier continuation allows `$` (verified against
103
+ // the live engine: `postgres.table('foo$bar')` resolves successfully).
104
+ this.tablePathBareIdentRegex = /^[A-Za-z_][A-Za-z0-9_$]*/;
108
105
  }
109
106
  sqlGroupSetTable(groupSetCount) {
110
107
  return `CROSS JOIN GENERATE_SERIES(0,${groupSetCount},1) as group_set`;
@@ -164,7 +161,8 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
164
161
  return `(${parentAlias}->>'__row_id')`;
165
162
  }
166
163
  if (parentType !== 'table') {
167
- let ret = `JSONB_EXTRACT_PATH_TEXT(${parentAlias},'${childName}')`;
164
+ const nameLit = this.sqlLiteralString(childName);
165
+ let ret = `JSONB_EXTRACT_PATH_TEXT(${parentAlias},${nameLit})`;
168
166
  switch (childType) {
169
167
  case 'string':
170
168
  break;
@@ -176,13 +174,13 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
176
174
  case 'record':
177
175
  case 'array[record]':
178
176
  case 'sql native':
179
- ret = `JSONB_EXTRACT_PATH(${parentAlias},'${childName}')`;
177
+ ret = `JSONB_EXTRACT_PATH(${parentAlias},${nameLit})`;
180
178
  break;
181
179
  }
182
180
  return ret;
183
181
  }
184
182
  else {
185
- const child = this.sqlMaybeQuoteIdentifier(childName);
183
+ const child = this.sqlQuoteIdentifier(childName);
186
184
  return `${parentAlias}.${child}`;
187
185
  }
188
186
  }
@@ -307,12 +305,6 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
307
305
  sqlOrderBy(orderTerms) {
308
306
  return `ORDER BY ${orderTerms.map(t => `${t} NULLS LAST`).join(',')}`;
309
307
  }
310
- sqlLiteralString(literal) {
311
- return "'" + literal.replace(/'/g, "''") + "'";
312
- }
313
- sqlLiteralRegexp(literal) {
314
- return "'" + literal.replace(/'/g, "''") + "'";
315
- }
316
308
  getDialectFunctionOverrides() {
317
309
  return (0, functions_1.expandOverrideMap)(function_overrides_1.POSTGRES_MALLOY_STANDARD_OVERLOADS);
318
310
  }
@@ -372,7 +364,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
372
364
  sqlLiteralRecord(lit) {
373
365
  const props = [];
374
366
  for (const [kName, kVal] of Object.entries(lit.kids)) {
375
- props.push(`'${kName}',${kVal.sql}`);
367
+ props.push(`${this.sqlLiteralString(kName)},${kVal.sql}`);
376
368
  }
377
369
  return `JSONB_BUILD_OBJECT(${props.join(', ')})`;
378
370
  }