@malloydata/malloy 0.0.412 → 0.0.413
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/dialect/databricks/databricks.d.ts +2 -1
- package/dist/dialect/databricks/databricks.js +11 -6
- package/dist/dialect/dialect.d.ts +2 -1
- package/dist/dialect/dialect.js +7 -0
- package/dist/dialect/duckdb/duckdb.d.ts +2 -1
- package/dist/dialect/duckdb/duckdb.js +8 -2
- package/dist/dialect/mysql/mysql.d.ts +2 -1
- package/dist/dialect/mysql/mysql.js +7 -2
- package/dist/dialect/postgres/postgres.d.ts +2 -1
- package/dist/dialect/postgres/postgres.js +8 -2
- package/dist/dialect/snowflake/snowflake.d.ts +2 -1
- package/dist/dialect/snowflake/snowflake.js +9 -3
- package/dist/dialect/standardsql/standardsql.d.ts +2 -1
- package/dist/dialect/standardsql/standardsql.js +6 -2
- package/dist/dialect/trino/trino.d.ts +2 -1
- package/dist/dialect/trino/trino.js +6 -2
- package/dist/lang/ast/field-space/dynamic-space.js +9 -1
- package/dist/lang/ast/field-space/query-spaces.d.ts +1 -0
- package/dist/lang/ast/field-space/query-spaces.js +5 -0
- package/dist/lang/ast/query-items/field-references.d.ts +10 -1
- package/dist/lang/ast/query-items/field-references.js +67 -4
- package/dist/lang/ast/query-properties/nest.js +15 -0
- package/dist/lang/parse-log.d.ts +10 -0
- package/dist/lang/parse-log.js +3 -0
- package/dist/model/field_instance.js +9 -0
- package/dist/model/nest-capability.d.ts +37 -0
- package/dist/model/nest-capability.js +31 -0
- package/dist/model/query_query.d.ts +7 -4
- package/dist/model/query_query.js +176 -79
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -222,6 +222,15 @@ class FieldInstanceResult {
|
|
|
222
222
|
maxDepth = r.maxDepth;
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
+
else if (fir.firstSegment.type === 'project') {
|
|
226
|
+
// A projection nest is grain-preserving (one array element per
|
|
227
|
+
// in-scope row), so it rides on the enclosing scope's group_set
|
|
228
|
+
// rather than getting its own. Without this it pins to group_set 0,
|
|
229
|
+
// and inside a deeper nest its array-agg FILTER never lines up with
|
|
230
|
+
// the enclosing group_set, producing empty arrays. (A first-stage
|
|
231
|
+
// projection; any following stages recurse in their own scope.)
|
|
232
|
+
fir.groupSet = this.groupSet;
|
|
233
|
+
}
|
|
225
234
|
}
|
|
226
235
|
}
|
|
227
236
|
this.childGroups = children;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Dialect } from '../dialect/dialect';
|
|
2
|
+
import type { PipeSegment } from './malloy_types';
|
|
3
|
+
/**
|
|
4
|
+
* Why the compiler can't emit a segment for a dialect. The translator maps each
|
|
5
|
+
* reason to a located, user-facing message (these values are also parse-log
|
|
6
|
+
* codes); the compiler throws a `MalloyCompileError` with the reason.
|
|
7
|
+
*/
|
|
8
|
+
export type NestUnsupported = 'nesting-unsupported' | 'nested-multi-stage-unsupported' | 'nested-projection-limit-unsupported';
|
|
9
|
+
/**
|
|
10
|
+
* How the compiler should emit one stage of a nest's pipeline — or, if the
|
|
11
|
+
* dialect can't, why not. This is THE single decision shared by the two callers
|
|
12
|
+
* that hold a dialect: the compiler (`generateTurtleSQL`) dispatches on `kind`
|
|
13
|
+
* and reads `limit`; the translator (`nest.ts`) turns `unsupported` into a
|
|
14
|
+
* located error. "How do I emit this" and "can I emit this" are one answer, so
|
|
15
|
+
* neither side can drift from the other.
|
|
16
|
+
*
|
|
17
|
+
* It is a *segment* question, not a pipeline one. The compiler compiles the
|
|
18
|
+
* first stage of a pipeline and recurses on the rest, so every stage is asked
|
|
19
|
+
* in turn as "the first stage" — pipeline length is irrelevant. `hasSuccessor`
|
|
20
|
+
* (is a stage materialized after this one?) is the only positional input, and
|
|
21
|
+
* it is what the multi-stage gate keys on.
|
|
22
|
+
*
|
|
23
|
+
* The dialect-free half of this — "is this stage an inline projection?" — is a
|
|
24
|
+
* plain `segment.type === 'project'`, used directly by the structural callers
|
|
25
|
+
* (group_set assignment, where-clause placement) that legitimately have no
|
|
26
|
+
* dialect; group_set numbering is not dialect-dependent.
|
|
27
|
+
*/
|
|
28
|
+
export type NestStrategy = {
|
|
29
|
+
kind: 'reduce';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'project';
|
|
32
|
+
limit: number | undefined;
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'unsupported';
|
|
35
|
+
reason: NestUnsupported;
|
|
36
|
+
};
|
|
37
|
+
export declare function nestStrategy(segment: PipeSegment, dialect: Dialect, hasSuccessor: boolean): NestStrategy;
|
|
@@ -0,0 +1,31 @@
|
|
|
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.nestStrategy = nestStrategy;
|
|
8
|
+
const malloy_types_1 = require("./malloy_types");
|
|
9
|
+
function nestStrategy(segment, dialect, hasSuccessor) {
|
|
10
|
+
if (!dialect.supportsNesting) {
|
|
11
|
+
return { kind: 'unsupported', reason: 'nesting-unsupported' };
|
|
12
|
+
}
|
|
13
|
+
// A stage with another stage after it must be materialized as a nested
|
|
14
|
+
// pipeline stage, which not every dialect can do inside a view.
|
|
15
|
+
if (hasSuccessor && !dialect.supportsPipelinesInViews) {
|
|
16
|
+
return { kind: 'unsupported', reason: 'nested-multi-stage-unsupported' };
|
|
17
|
+
}
|
|
18
|
+
if ((0, malloy_types_1.isProjectSegment)(segment)) {
|
|
19
|
+
// A projection applies `limit:` by slicing its aggregated array; some
|
|
20
|
+
// dialects can't slice an aggregate (see sqlAggregateTurtle).
|
|
21
|
+
if (segment.limit !== undefined && !dialect.supportsNestedProjectionLimit) {
|
|
22
|
+
return {
|
|
23
|
+
kind: 'unsupported',
|
|
24
|
+
reason: 'nested-projection-limit-unsupported',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { kind: 'project', limit: segment.limit };
|
|
28
|
+
}
|
|
29
|
+
return { kind: 'reduce' };
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=nest-capability.js.map
|
|
@@ -14,11 +14,14 @@ type StageGroupMaping = {
|
|
|
14
14
|
fromGroup: number;
|
|
15
15
|
toGroup: number;
|
|
16
16
|
};
|
|
17
|
+
interface StageOutputColumn {
|
|
18
|
+
sql: string;
|
|
19
|
+
name: string;
|
|
20
|
+
isDimension: boolean;
|
|
21
|
+
}
|
|
17
22
|
type StageOutputContext = {
|
|
18
|
-
|
|
23
|
+
columns: StageOutputColumn[];
|
|
19
24
|
lateralJoinSQLExpressions: LateralJoinExpression[];
|
|
20
|
-
dimensionIndexes: number[];
|
|
21
|
-
fieldIndex: number;
|
|
22
25
|
groupsAggregated: StageGroupMaping[];
|
|
23
26
|
outputPipelinedSQL: OutputPipelinedSQL[];
|
|
24
27
|
};
|
|
@@ -89,7 +92,7 @@ export declare class QueryQuery extends QueryField {
|
|
|
89
92
|
collectArrayJoins(ji: JoinInstance): JoinInstance[];
|
|
90
93
|
genereateSQLOrderBy(queryDef: QuerySegment, resultStruct: FieldInstanceResult): string;
|
|
91
94
|
generateSimpleSQL(stageWriter: StageWriter): string;
|
|
92
|
-
generatePipelinedStages(outputPipelinedSQL: OutputPipelinedSQL[], lastStageName: string, stageWriter: StageWriter): string;
|
|
95
|
+
generatePipelinedStages(outputPipelinedSQL: OutputPipelinedSQL[], lastStageName: string, stageWriter: StageWriter, priorStageColumns: StageOutputColumn[]): string;
|
|
93
96
|
generateStage0Fields(resultSet: FieldInstanceResult, output: StageOutputContext, stageWriter: StageWriter): void;
|
|
94
97
|
generateSQLWhereChildren(resultStruct: FieldInstanceResult): AndChain;
|
|
95
98
|
generateSQLWhereTurtled(): string;
|
|
@@ -14,9 +14,26 @@ const field_instance_1 = require("./field_instance");
|
|
|
14
14
|
const sql_compiled_1 = require("./sql_compiled");
|
|
15
15
|
const source_def_utils_1 = require("./source_def_utils");
|
|
16
16
|
const malloy_compile_error_1 = require("./malloy_compile_error");
|
|
17
|
+
const nest_capability_1 = require("./nest-capability");
|
|
17
18
|
function pathToCol(path) {
|
|
18
19
|
return path.map(el => encodeURIComponent(el)).join('/');
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Is this result's row limit applied by the ROW_NUMBER "shave" in
|
|
23
|
+
* generateSQLHavingLimit? A nested projection's is not: its limit slices its
|
|
24
|
+
* array-agg (see sqlAggregateTurtle), because a projection has no group-by
|
|
25
|
+
* columns for the shave to partition and order on.
|
|
26
|
+
*/
|
|
27
|
+
function usesShaveLimit(result) {
|
|
28
|
+
if (result.getLimit() === undefined)
|
|
29
|
+
return false;
|
|
30
|
+
const isNestedProjection = result.parent !== undefined && result.firstSegment.type === 'project';
|
|
31
|
+
return !isNestedProjection;
|
|
32
|
+
}
|
|
33
|
+
// 1-based SELECT positions of the dimension columns, for `GROUP BY 1,2,...`.
|
|
34
|
+
function groupByPositions(columns) {
|
|
35
|
+
return columns.flatMap((c, i) => (c.isDimension ? [i + 1] : []));
|
|
36
|
+
}
|
|
20
37
|
function pushDialectField(dl, f) {
|
|
21
38
|
const { sqlExpression, sqlOutputName, rawName } = f;
|
|
22
39
|
if ((0, malloy_types_1.isAtomic)(f.fieldDef)) {
|
|
@@ -925,25 +942,29 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
925
942
|
}
|
|
926
943
|
// This probably should be generated in a dialect independat way.
|
|
927
944
|
// but for now, it is just googleSQL.
|
|
928
|
-
generatePipelinedStages(outputPipelinedSQL, lastStageName, stageWriter
|
|
945
|
+
generatePipelinedStages(outputPipelinedSQL, lastStageName, stageWriter,
|
|
946
|
+
// Every column `lastStageName` produces. The replaced turtle columns (in
|
|
947
|
+
// outputPipelinedSQL) are dropped and re-added by the pipeline expressions;
|
|
948
|
+
// everything else is carried through unchanged by its emitted name -- for a
|
|
949
|
+
// grouped stage that is the group-set-suffixed form (e.g. `f1__0`), not the
|
|
950
|
+
// final output name.
|
|
951
|
+
priorStageColumns) {
|
|
929
952
|
if (outputPipelinedSQL.length === 0) {
|
|
930
953
|
return lastStageName;
|
|
931
954
|
}
|
|
955
|
+
const pipelinesSQL = outputPipelinedSQL
|
|
956
|
+
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
957
|
+
.join(',\n');
|
|
932
958
|
let retSQL;
|
|
933
959
|
if (this.parent.dialect.supportsSelectReplace) {
|
|
934
|
-
const pipelinesSQL = outputPipelinedSQL
|
|
935
|
-
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
936
|
-
.join(',\n');
|
|
937
960
|
retSQL = `SELECT * replace (${pipelinesSQL}) FROM ${lastStageName}
|
|
938
961
|
`;
|
|
939
962
|
}
|
|
940
963
|
else {
|
|
941
|
-
const pipelinesSQL = outputPipelinedSQL
|
|
942
|
-
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
943
|
-
.join(',\n');
|
|
944
964
|
const outputFields = outputPipelinedSQL.map(f => f.sqlFieldName);
|
|
945
|
-
const
|
|
946
|
-
|
|
965
|
+
const fields = priorStageColumns
|
|
966
|
+
.map(c => c.name)
|
|
967
|
+
.filter(name => outputFields.indexOf(name) === -1);
|
|
947
968
|
retSQL = `SELECT ${fields.length > 0 ? fields.join(', ') + ',' : ''} ${pipelinesSQL} FROM ${lastStageName}`;
|
|
948
969
|
}
|
|
949
970
|
return stageWriter.addStage(retSQL);
|
|
@@ -978,12 +999,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
978
999
|
sql: exp,
|
|
979
1000
|
name: outputName,
|
|
980
1001
|
});
|
|
981
|
-
output.
|
|
1002
|
+
output.columns.push({
|
|
1003
|
+
sql: outputFieldName,
|
|
1004
|
+
name: outputName,
|
|
1005
|
+
isDimension: true,
|
|
1006
|
+
});
|
|
982
1007
|
if (fi.f.fieldDef.type === 'number') {
|
|
983
1008
|
const outputNameString = this.parent.dialect.sqlQuoteIdentifier(`${name}__${resultSet.groupSet}_string`);
|
|
984
1009
|
const outputFieldNameString = `__lateral_join_bag.${outputNameString}`;
|
|
985
|
-
output.
|
|
986
|
-
|
|
1010
|
+
output.columns.push({
|
|
1011
|
+
sql: outputFieldNameString,
|
|
1012
|
+
name: outputNameString,
|
|
1013
|
+
isDimension: true,
|
|
1014
|
+
});
|
|
987
1015
|
output.lateralJoinSQLExpressions.push({
|
|
988
1016
|
sql: `CAST(${exp} as STRING)`,
|
|
989
1017
|
name: outputNameString,
|
|
@@ -993,13 +1021,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
993
1021
|
}
|
|
994
1022
|
else {
|
|
995
1023
|
// just treat it like a regular field.
|
|
996
|
-
output.
|
|
1024
|
+
output.columns.push({
|
|
1025
|
+
sql: `${exp} as ${outputName}`,
|
|
1026
|
+
name: outputName,
|
|
1027
|
+
isDimension: true,
|
|
1028
|
+
});
|
|
997
1029
|
}
|
|
998
|
-
output.dimensionIndexes.push(output.fieldIndex++);
|
|
999
1030
|
}
|
|
1000
1031
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1001
|
-
output.
|
|
1002
|
-
|
|
1032
|
+
output.columns.push({
|
|
1033
|
+
sql: `${exp} as ${outputName}`,
|
|
1034
|
+
name: outputName,
|
|
1035
|
+
isDimension: false,
|
|
1036
|
+
});
|
|
1003
1037
|
}
|
|
1004
1038
|
}
|
|
1005
1039
|
}
|
|
@@ -1009,8 +1043,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1009
1043
|
}
|
|
1010
1044
|
else if (fi.firstSegment.type === 'project') {
|
|
1011
1045
|
const s = this.generateTurtleSQL(fi, stageWriter, outputName, output.outputPipelinedSQL);
|
|
1012
|
-
output.
|
|
1013
|
-
|
|
1046
|
+
output.columns.push({
|
|
1047
|
+
sql: `${s} as ${outputName}`,
|
|
1048
|
+
name: outputName,
|
|
1049
|
+
isDimension: false,
|
|
1050
|
+
});
|
|
1014
1051
|
}
|
|
1015
1052
|
}
|
|
1016
1053
|
}
|
|
@@ -1028,8 +1065,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1028
1065
|
}
|
|
1029
1066
|
else {
|
|
1030
1067
|
resultSet.hasHaving = true;
|
|
1031
|
-
output.
|
|
1032
|
-
|
|
1068
|
+
output.columns.push({
|
|
1069
|
+
sql: `CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END as __delete__${resultSet.groupSet}`,
|
|
1070
|
+
name: `__delete__${resultSet.groupSet}`,
|
|
1071
|
+
isDimension: false,
|
|
1072
|
+
});
|
|
1033
1073
|
}
|
|
1034
1074
|
}
|
|
1035
1075
|
}
|
|
@@ -1038,11 +1078,18 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1038
1078
|
for (const [, field] of resultStruct.allFields) {
|
|
1039
1079
|
if (field.type === 'query') {
|
|
1040
1080
|
const fir = field;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1081
|
+
// A projection first stage's `where:` folds into its array-agg
|
|
1082
|
+
// condition (see generateTurtleSQL / sqlAggregateTurtle), not a
|
|
1083
|
+
// scan-level WHERE: it rides its enclosing group_set, so a WHERE here
|
|
1084
|
+
// can't isolate it from the parent, and its childGroups is empty (no
|
|
1085
|
+
// group_set of its own) which would emit a broken `group_set IN ()`.
|
|
1086
|
+
if (fir.firstSegment.type !== 'project') {
|
|
1087
|
+
const turtleWhere = this.generateSQLFilters(fir, 'where');
|
|
1088
|
+
if (turtleWhere.present()) {
|
|
1089
|
+
const groupSets = fir.childGroups.join(',');
|
|
1090
|
+
wheres.add(`(group_set NOT IN (${groupSets})` +
|
|
1091
|
+
` OR (group_set IN (${groupSets}) AND ${turtleWhere.sql()}))`);
|
|
1092
|
+
}
|
|
1046
1093
|
}
|
|
1047
1094
|
wheres.addChain(this.generateSQLWhereChildren(fir));
|
|
1048
1095
|
}
|
|
@@ -1079,11 +1126,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1079
1126
|
const partitionSQL = [];
|
|
1080
1127
|
let hasAnyLimits = false;
|
|
1081
1128
|
let hasResultsWithChildren = false;
|
|
1082
|
-
const resultsWithHavingOrLimit = this.rootResult.selectStructs([], (result) => result.hasHaving || result
|
|
1129
|
+
const resultsWithHavingOrLimit = this.rootResult.selectStructs([], (result) => result.hasHaving || usesShaveLimit(result));
|
|
1083
1130
|
if (resultsWithHavingOrLimit.length > 0) {
|
|
1084
1131
|
// loop through an generate the partitions
|
|
1085
1132
|
for (const result of this.rootResult.selectStructs([], (_result) => true)) {
|
|
1086
|
-
const hasLimit = result
|
|
1133
|
+
const hasLimit = usesShaveLimit(result);
|
|
1087
1134
|
hasResultsWithChildren || (hasResultsWithChildren = result.childGroups.length > 1 && (hasLimit || result.hasHaving));
|
|
1088
1135
|
hasAnyLimits || (hasAnyLimits = hasLimit);
|
|
1089
1136
|
// find all the parent dimension names.
|
|
@@ -1201,9 +1248,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1201
1248
|
let from = this.generateSQLJoins(stageWriter);
|
|
1202
1249
|
const wheres = this.generateSQLWhereTurtled();
|
|
1203
1250
|
const f = {
|
|
1204
|
-
|
|
1205
|
-
fieldIndex: 2,
|
|
1206
|
-
sql: ['group_set'],
|
|
1251
|
+
columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
|
|
1207
1252
|
lateralJoinSQLExpressions: [],
|
|
1208
1253
|
groupsAggregated: [],
|
|
1209
1254
|
outputPipelinedSQL: [],
|
|
@@ -1213,9 +1258,9 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1213
1258
|
throw new malloy_compile_error_1.MalloyCompileError("Cannot use 'select:' in a stage that contains nested views. " +
|
|
1214
1259
|
'Use `group_by:` or restructure the pipeline.', 'compiler-project-with-turtles', this.fieldDef.location);
|
|
1215
1260
|
}
|
|
1216
|
-
const groupBy = 'GROUP BY ' + f.
|
|
1261
|
+
const groupBy = 'GROUP BY ' + groupByPositions(f.columns).join(',') + '\n';
|
|
1217
1262
|
from += this.parent.dialect.sqlGroupSetTable(this.maxGroupSet) + '\n';
|
|
1218
|
-
s += (0, utils_1.indent)(f.sql.join(',\n')) + '\n';
|
|
1263
|
+
s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
|
|
1219
1264
|
if (f.lateralJoinSQLExpressions.length > 0) {
|
|
1220
1265
|
from += this.parent.dialect.sqlLateralJoinBag(f.lateralJoinSQLExpressions);
|
|
1221
1266
|
}
|
|
@@ -1224,7 +1269,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1224
1269
|
const resultStage = stageWriter.addStage(s);
|
|
1225
1270
|
// generate stages for havings and limits
|
|
1226
1271
|
this.resultStage = this.generateSQLHavingLimit(stageWriter, resultStage);
|
|
1227
|
-
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1272
|
+
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter, f.columns);
|
|
1228
1273
|
return this.resultStage;
|
|
1229
1274
|
}
|
|
1230
1275
|
generateDepthNFields(depth, resultSet, output, stageWriter) {
|
|
@@ -1235,13 +1280,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1235
1280
|
if (fi.fieldUsage.type === 'result') {
|
|
1236
1281
|
if ((0, query_node_1.isScalarField)(fi.f)) {
|
|
1237
1282
|
const exp = (0, utils_1.caseGroup)(resultSet.groupSet > 0 ? resultSet.childGroups : [], sqlFieldName);
|
|
1238
|
-
output.
|
|
1239
|
-
|
|
1283
|
+
output.columns.push({
|
|
1284
|
+
sql: `${exp} as ${sqlFieldName}`,
|
|
1285
|
+
name: sqlFieldName,
|
|
1286
|
+
isDimension: true,
|
|
1287
|
+
});
|
|
1240
1288
|
}
|
|
1241
1289
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1242
1290
|
const exp = this.parent.dialect.sqlAnyValue(resultSet.groupSet, sqlFieldName);
|
|
1243
|
-
output.
|
|
1244
|
-
|
|
1291
|
+
output.columns.push({
|
|
1292
|
+
sql: `${exp} as ${sqlFieldName}`,
|
|
1293
|
+
name: sqlFieldName,
|
|
1294
|
+
isDimension: false,
|
|
1295
|
+
});
|
|
1245
1296
|
}
|
|
1246
1297
|
}
|
|
1247
1298
|
}
|
|
@@ -1256,8 +1307,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1256
1307
|
toGroup: resultSet.groupSet,
|
|
1257
1308
|
});
|
|
1258
1309
|
groupsToMap.push(fi.groupSet);
|
|
1259
|
-
output.
|
|
1260
|
-
|
|
1310
|
+
output.columns.push({
|
|
1311
|
+
sql: `${s} as ${sqlFieldName}`,
|
|
1312
|
+
name: sqlFieldName,
|
|
1313
|
+
isDimension: false,
|
|
1314
|
+
});
|
|
1261
1315
|
}
|
|
1262
1316
|
else {
|
|
1263
1317
|
this.generateDepthNFields(depth, fi, output, stageWriter);
|
|
@@ -1267,14 +1321,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1267
1321
|
if (output.groupsAggregated.length > 0) {
|
|
1268
1322
|
if (!this.parent.dialect.hasLateralColumnAliasInSelect) {
|
|
1269
1323
|
// Safe to remap in-place; aggregate expressions won't see the alias.
|
|
1270
|
-
// This runs on every recursive return but
|
|
1324
|
+
// This runs on every recursive return but column[0] replacement is
|
|
1271
1325
|
// idempotent — last call builds the full CASE from all accumulated
|
|
1272
|
-
// groupsAggregated entries.
|
|
1273
|
-
|
|
1326
|
+
// groupsAggregated entries. The output is still named group_set.
|
|
1327
|
+
let groupSetSQL = 'CASE ';
|
|
1274
1328
|
for (const m of output.groupsAggregated) {
|
|
1275
|
-
|
|
1329
|
+
groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
|
|
1276
1330
|
}
|
|
1277
|
-
|
|
1331
|
+
groupSetSQL += 'ELSE group_set END as group_set';
|
|
1332
|
+
output.columns[0] = {
|
|
1333
|
+
sql: groupSetSQL,
|
|
1334
|
+
name: 'group_set',
|
|
1335
|
+
isDimension: true,
|
|
1336
|
+
};
|
|
1278
1337
|
}
|
|
1279
1338
|
// For hasLateralColumnAliasInSelect, the remap is handled in
|
|
1280
1339
|
// generateSQLDepthN to avoid adding it multiple times from recursion.
|
|
@@ -1283,88 +1342,100 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1283
1342
|
generateSQLDepthN(depth, stageWriter, stageName) {
|
|
1284
1343
|
let s = 'SELECT \n';
|
|
1285
1344
|
const f = {
|
|
1286
|
-
|
|
1287
|
-
fieldIndex: 2,
|
|
1288
|
-
sql: ['group_set'],
|
|
1345
|
+
columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
|
|
1289
1346
|
lateralJoinSQLExpressions: [],
|
|
1290
1347
|
groupsAggregated: [],
|
|
1291
1348
|
outputPipelinedSQL: [],
|
|
1292
1349
|
};
|
|
1293
1350
|
this.generateDepthNFields(depth, this.rootResult, f, stageWriter);
|
|
1294
1351
|
// When column aliases are visible in the same SELECT (e.g. Databricks),
|
|
1295
|
-
// replace
|
|
1296
|
-
// (not group_set). This avoids shadowing the input
|
|
1297
|
-
// that aggregate expressions reference. GROUP BY still
|
|
1298
|
-
// (the remapped value), so rows with different original
|
|
1299
|
-
// collapse correctly. A follow-up CTE renames it
|
|
1352
|
+
// replace column[0]'s SQL with the remap CASE aliased as
|
|
1353
|
+
// __remapped_group_set (not group_set). This avoids shadowing the input
|
|
1354
|
+
// group_set column that aggregate expressions reference. GROUP BY still
|
|
1355
|
+
// uses position 1 (the remapped value), so rows with different original
|
|
1356
|
+
// group_sets collapse correctly. A follow-up CTE renames it back, so the
|
|
1357
|
+
// column's carried-forward name stays group_set.
|
|
1300
1358
|
const needsRemapStage = f.groupsAggregated.length > 0 &&
|
|
1301
1359
|
this.parent.dialect.hasLateralColumnAliasInSelect;
|
|
1302
1360
|
if (needsRemapStage) {
|
|
1303
|
-
|
|
1361
|
+
let groupSetSQL = 'CASE ';
|
|
1304
1362
|
for (const m of f.groupsAggregated) {
|
|
1305
|
-
|
|
1363
|
+
groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
|
|
1306
1364
|
}
|
|
1307
|
-
|
|
1365
|
+
groupSetSQL += 'ELSE group_set END as __remapped_group_set';
|
|
1366
|
+
f.columns[0] = { sql: groupSetSQL, name: 'group_set', isDimension: true };
|
|
1308
1367
|
}
|
|
1309
|
-
s += (0, utils_1.indent)(f.sql.join(',\n')) + '\n';
|
|
1368
|
+
s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
|
|
1310
1369
|
s += `FROM ${stageName}\n`;
|
|
1311
1370
|
const where = this.rootResult.eliminateComputeGroupsSQL();
|
|
1312
1371
|
if (where.length > 0) {
|
|
1313
1372
|
s += `WHERE ${where}\n`;
|
|
1314
1373
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1374
|
+
const dimensionPositions = groupByPositions(f.columns);
|
|
1375
|
+
if (dimensionPositions.length > 0) {
|
|
1376
|
+
s += `GROUP BY ${dimensionPositions.join(',')}\n`;
|
|
1317
1377
|
}
|
|
1318
1378
|
this.resultStage = stageWriter.addStage(s);
|
|
1319
1379
|
if (needsRemapStage) {
|
|
1320
|
-
const cols = f.
|
|
1321
|
-
const asMatch = col.match(/as\s+(\S+)\s*$/i);
|
|
1322
|
-
return asMatch ? asMatch[1] : '*';
|
|
1323
|
-
});
|
|
1380
|
+
const cols = f.columns.slice(1).map(c => c.name);
|
|
1324
1381
|
const remapSQL = `SELECT \n${(0, utils_1.indent)(['__remapped_group_set as group_set', ...cols].join(',\n'))}\nFROM ${this.resultStage}\n`;
|
|
1325
1382
|
this.resultStage = stageWriter.addStage(remapSQL);
|
|
1326
1383
|
}
|
|
1327
|
-
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1384
|
+
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter, f.columns);
|
|
1328
1385
|
return this.resultStage;
|
|
1329
1386
|
}
|
|
1330
1387
|
genereateSQLCombineTurtles(stageWriter, stage0Name) {
|
|
1331
1388
|
let s = 'SELECT\n';
|
|
1332
|
-
|
|
1333
|
-
|
|
1389
|
+
// Columns this combine stage emits. Unlike the grouped stages above, these
|
|
1390
|
+
// are the final output names (unsuffixed), not the group-set-suffixed form.
|
|
1391
|
+
// A following pipelined stage carries them forward by name.
|
|
1392
|
+
const columns = [];
|
|
1334
1393
|
const outputPipelinedSQL = [];
|
|
1335
|
-
const dimensionIndexes = [];
|
|
1336
1394
|
for (const [name, fi] of this.rootResult.allFields) {
|
|
1337
1395
|
const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
|
|
1338
1396
|
if (fi instanceof field_instance_1.FieldInstanceField) {
|
|
1339
1397
|
if (fi.fieldUsage.type === 'result') {
|
|
1340
1398
|
if ((0, query_node_1.isScalarField)(fi.f)) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1399
|
+
columns.push({
|
|
1400
|
+
sql: this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`) + ` as ${sqlName}`,
|
|
1401
|
+
name: sqlName,
|
|
1402
|
+
isDimension: true,
|
|
1403
|
+
});
|
|
1343
1404
|
}
|
|
1344
1405
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1345
|
-
|
|
1346
|
-
|
|
1406
|
+
columns.push({
|
|
1407
|
+
sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
|
|
1408
|
+
name: sqlName,
|
|
1409
|
+
isDimension: false,
|
|
1410
|
+
});
|
|
1347
1411
|
}
|
|
1348
1412
|
}
|
|
1349
1413
|
}
|
|
1350
1414
|
else if (fi instanceof field_instance_1.FieldInstanceResult) {
|
|
1351
1415
|
if (fi.firstSegment.type === 'reduce') {
|
|
1352
|
-
|
|
1353
|
-
|
|
1416
|
+
columns.push({
|
|
1417
|
+
sql: `${this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL)} as ${sqlName}`,
|
|
1418
|
+
name: sqlName,
|
|
1419
|
+
isDimension: false,
|
|
1420
|
+
});
|
|
1354
1421
|
}
|
|
1355
1422
|
else if (fi.firstSegment.type === 'project') {
|
|
1356
|
-
|
|
1357
|
-
|
|
1423
|
+
columns.push({
|
|
1424
|
+
sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
|
|
1425
|
+
name: sqlName,
|
|
1426
|
+
isDimension: false,
|
|
1427
|
+
});
|
|
1358
1428
|
}
|
|
1359
1429
|
}
|
|
1360
1430
|
}
|
|
1361
|
-
s += (0, utils_1.indent)(
|
|
1431
|
+
s += (0, utils_1.indent)(columns.map(c => c.sql).join(',\n')) + `\nFROM ${stage0Name}\n`;
|
|
1362
1432
|
const where = this.rootResult.eliminateComputeGroupsSQL();
|
|
1363
1433
|
if (where.length > 0) {
|
|
1364
1434
|
s += `WHERE ${where}\n`;
|
|
1365
1435
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1436
|
+
const dimensionPositions = groupByPositions(columns);
|
|
1437
|
+
if (dimensionPositions.length > 0) {
|
|
1438
|
+
s += `GROUP BY ${dimensionPositions.join(',')}\n`;
|
|
1368
1439
|
}
|
|
1369
1440
|
// order by
|
|
1370
1441
|
s += this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
|
|
@@ -1373,7 +1444,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1373
1444
|
s += `LIMIT ${this.firstSegment.limit}\n`;
|
|
1374
1445
|
}
|
|
1375
1446
|
this.resultStage = stageWriter.addStage(s);
|
|
1376
|
-
this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1447
|
+
this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, columns);
|
|
1377
1448
|
return this.resultStage;
|
|
1378
1449
|
}
|
|
1379
1450
|
// create a simplified version of the StructDef for dialects.
|
|
@@ -1439,6 +1510,15 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1439
1510
|
return dialectFieldList;
|
|
1440
1511
|
}
|
|
1441
1512
|
generateTurtleSQL(resultStruct, stageWriter, sqlFieldName, outputPipelinedSQL) {
|
|
1513
|
+
// How to emit this nest's first stage — and the last line of defense before
|
|
1514
|
+
// broken SQL for IR that didn't pass through the translator (cached,
|
|
1515
|
+
// deserialized, or query-builder IR). Same `nestStrategy` the translator
|
|
1516
|
+
// uses, so the two can't disagree.
|
|
1517
|
+
const pipeline = resultStruct.turtleDef.pipeline;
|
|
1518
|
+
const strategy = (0, nest_capability_1.nestStrategy)(pipeline[0], this.parent.dialect, pipeline.length > 1);
|
|
1519
|
+
if (strategy.kind === 'unsupported') {
|
|
1520
|
+
throw new malloy_compile_error_1.MalloyCompileError(`dialect '${this.parent.dialect.name}' cannot emit this nested query (${strategy.reason})`, strategy.reason, resultStruct.turtleDef.location);
|
|
1521
|
+
}
|
|
1442
1522
|
// calculate the ordering.
|
|
1443
1523
|
const compiledOrderBy = [];
|
|
1444
1524
|
let orderingField;
|
|
@@ -1483,7 +1563,24 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1483
1563
|
}
|
|
1484
1564
|
}
|
|
1485
1565
|
else {
|
|
1486
|
-
|
|
1566
|
+
// A projection first stage applies its own `limit:` by slicing the
|
|
1567
|
+
// array-agg (it has no group-by keys to ROW_NUMBER-shave on) and its
|
|
1568
|
+
// `where:` by folding into the array-agg's group_set condition (it rides
|
|
1569
|
+
// the enclosing group_set, so a scan-level WHERE can't isolate it). This
|
|
1570
|
+
// is a first-stage question: if more stages follow, they consume this
|
|
1571
|
+
// (limited/filtered) array via the recursion in generateTurtlePipelineSQL.
|
|
1572
|
+
// A reduce first stage keeps the shave and the scan-level WHERE.
|
|
1573
|
+
let limit;
|
|
1574
|
+
let filterSQL;
|
|
1575
|
+
if (strategy.kind === 'project') {
|
|
1576
|
+
limit = strategy.limit;
|
|
1577
|
+
const where = this.generateSQLFilters(resultStruct, 'where');
|
|
1578
|
+
if (where.present()) {
|
|
1579
|
+
filterSQL = where.sql();
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// (capability is guarded by nestStrategy at the top of this method)
|
|
1583
|
+
ret = this.parent.dialect.sqlAggregateTurtle(resultStruct.groupSet, dialectFieldList, orderBy, limit, filterSQL);
|
|
1487
1584
|
}
|
|
1488
1585
|
// If the turtle is a pipeline, generate a UDF to compute it.
|
|
1489
1586
|
const newStageWriter = new stage_writer_1.StageWriter(this.parent.dialect.supportsCTEinCoorelatedSubQueries, stageWriter);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MALLOY_VERSION = "0.0.
|
|
1
|
+
export declare const MALLOY_VERSION = "0.0.413";
|
package/dist/version.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MALLOY_VERSION = void 0;
|
|
4
4
|
// generated with 'generate-version-file' script; do not edit manually
|
|
5
|
-
exports.MALLOY_VERSION = '0.0.
|
|
5
|
+
exports.MALLOY_VERSION = '0.0.413';
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/malloy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.413",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@malloydata/malloy-filter": "0.0.
|
|
55
|
-
"@malloydata/malloy-interfaces": "0.0.
|
|
56
|
-
"@malloydata/malloy-tag": "0.0.
|
|
54
|
+
"@malloydata/malloy-filter": "0.0.413",
|
|
55
|
+
"@malloydata/malloy-interfaces": "0.0.413",
|
|
56
|
+
"@malloydata/malloy-tag": "0.0.413",
|
|
57
57
|
"@noble/hashes": "^1.8.0",
|
|
58
58
|
"antlr4ts": "^0.5.0-alpha.4",
|
|
59
59
|
"assert": "^2.0.0",
|