@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.
- package/dist/api/foundation/config.d.ts +2 -3
- package/dist/api/foundation/config.js +23 -11
- package/dist/api/foundation/core.js +1 -1
- package/dist/api/foundation/runtime.js +21 -1
- package/dist/api/util.js +4 -0
- package/dist/connection/base_connection.js +6 -0
- package/dist/connection/validate_table_path.d.ts +10 -0
- package/dist/connection/validate_table_path.js +56 -0
- package/dist/dialect/databricks/databricks.d.ts +4 -4
- package/dist/dialect/databricks/databricks.js +17 -22
- package/dist/dialect/dialect.d.ts +100 -4
- package/dist/dialect/dialect.js +145 -1
- package/dist/dialect/duckdb/duckdb.d.ts +2 -3
- package/dist/dialect/duckdb/duckdb.js +12 -14
- package/dist/dialect/duckdb/table-path-parser.d.ts +2 -0
- package/dist/dialect/duckdb/table-path-parser.js +57 -0
- package/dist/dialect/index.d.ts +2 -0
- package/dist/dialect/index.js +4 -1
- package/dist/dialect/mysql/mysql.d.ts +4 -4
- package/dist/dialect/mysql/mysql.js +25 -20
- package/dist/dialect/pg_impl.d.ts +3 -1
- package/dist/dialect/pg_impl.js +6 -3
- package/dist/dialect/postgres/postgres.d.ts +1 -3
- package/dist/dialect/postgres/postgres.js +8 -16
- package/dist/dialect/snowflake/snowflake.d.ts +4 -4
- package/dist/dialect/snowflake/snowflake.js +11 -27
- package/dist/dialect/standardsql/standardsql.d.ts +6 -4
- package/dist/dialect/standardsql/standardsql.js +36 -15
- package/dist/dialect/table-path.d.ts +54 -0
- package/dist/dialect/table-path.js +144 -0
- package/dist/dialect/trino/trino.d.ts +0 -3
- package/dist/dialect/trino/trino.js +7 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -2
- package/dist/lang/ast/source-elements/table-source.d.ts +1 -7
- package/dist/lang/ast/source-elements/table-source.js +20 -19
- package/dist/lang/parse-log.d.ts +1 -0
- package/dist/lang/parse-malloy.js +37 -7
- package/dist/lang/parse-tree-walkers/find-external-references.d.ts +2 -15
- package/dist/lang/parse-tree-walkers/find-external-references.js +6 -23
- package/dist/lang/translate-response.d.ts +1 -1
- package/dist/model/filter_compilers.js +1 -1
- package/dist/model/query_model_impl.js +7 -7
- package/dist/model/query_query.js +37 -33
- package/dist/model/sql_compiled.d.ts +2 -4
- package/dist/model/sql_compiled.js +14 -15
- package/dist/test/test-models.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
106
|
-
|
|
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
|
|
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
|
|
456
|
-
|
|
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);
|
|
@@ -1101,7 +1101,7 @@ class PersistSource {
|
|
|
1101
1101
|
const sd = this.persistableDef;
|
|
1102
1102
|
const queryModel = this.model.queryModel;
|
|
1103
1103
|
if (sd.type === 'sql_select') {
|
|
1104
|
-
return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {},
|
|
1104
|
+
return (0, model_1.getCompiledSQL)(sd, options !== null && options !== void 0 ? options : {}, (query, opts) => queryModel.compileQuery(query, opts).sql);
|
|
1105
1105
|
}
|
|
1106
1106
|
else {
|
|
1107
1107
|
const compiled = queryModel.compileQuery(sd.query, options);
|
|
@@ -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).
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
/**
|