@malloydata/malloy 0.0.412 → 0.0.414
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/core.d.ts +21 -0
- package/dist/api/foundation/core.js +30 -0
- package/dist/dialect/databricks/databricks.d.ts +2 -1
- package/dist/dialect/databricks/databricks.js +12 -6
- package/dist/dialect/dialect.d.ts +12 -1
- package/dist/dialect/dialect.js +25 -0
- package/dist/dialect/duckdb/duckdb.d.ts +2 -1
- package/dist/dialect/duckdb/duckdb.js +9 -2
- package/dist/dialect/mysql/mysql.d.ts +2 -1
- package/dist/dialect/mysql/mysql.js +14 -5
- package/dist/dialect/postgres/postgres.d.ts +2 -1
- package/dist/dialect/postgres/postgres.js +9 -2
- package/dist/dialect/snowflake/snowflake.d.ts +2 -1
- package/dist/dialect/snowflake/snowflake.js +11 -3
- package/dist/dialect/standardsql/standardsql.d.ts +2 -1
- package/dist/dialect/standardsql/standardsql.js +8 -2
- package/dist/dialect/trino/trino.d.ts +2 -1
- package/dist/dialect/trino/trino.js +8 -2
- package/dist/lang/ast/field-space/dynamic-space.js +14 -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/ast/source-elements/named-source.js +4 -1
- package/dist/lang/ast/statements/define-source.js +9 -2
- package/dist/lang/ast/types/malloy-element.js +3 -4
- package/dist/lang/parse-log.d.ts +10 -0
- package/dist/lang/parse-log.js +3 -0
- package/dist/model/expression_compiler.js +2 -2
- package/dist/model/field_instance.d.ts +1 -0
- package/dist/model/field_instance.js +14 -0
- package/dist/model/index.d.ts +2 -1
- package/dist/model/index.js +3 -1
- package/dist/model/malloy_types.d.ts +25 -6
- package/dist/model/nest-capability.d.ts +37 -0
- package/dist/model/nest-capability.js +31 -0
- package/dist/model/persist_utils.js +1 -1
- package/dist/model/query_query.d.ts +26 -5
- package/dist/model/query_query.js +256 -88
- package/dist/model/source_def_utils.d.ts +31 -7
- package/dist/model/source_def_utils.js +39 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -14,9 +14,31 @@ 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
|
+
/** Is this an analytic (window) field -- a `calculate:`? */
|
|
34
|
+
function fieldIsAnalytic(fi) {
|
|
35
|
+
return ((0, malloy_types_1.hasExpression)(fi.f.fieldDef) &&
|
|
36
|
+
(0, malloy_types_1.expressionIsAnalytic)(fi.f.fieldDef.expressionType));
|
|
37
|
+
}
|
|
38
|
+
// 1-based SELECT positions of the dimension columns, for `GROUP BY 1,2,...`.
|
|
39
|
+
function groupByPositions(columns) {
|
|
40
|
+
return columns.flatMap((c, i) => (c.isDimension ? [i + 1] : []));
|
|
41
|
+
}
|
|
20
42
|
function pushDialectField(dl, f) {
|
|
21
43
|
const { sqlExpression, sqlOutputName, rawName } = f;
|
|
22
44
|
if ((0, malloy_types_1.isAtomic)(f.fieldDef)) {
|
|
@@ -886,15 +908,31 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
886
908
|
}
|
|
887
909
|
return s;
|
|
888
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* The single-scan form, with no `group_set` fan-out: one `SELECT` over the
|
|
913
|
+
* scan, grouped by the dimensions. Used both for a query with no nests and --
|
|
914
|
+
* via canUseSingleGroupSetSQL -- for one whose only nests are single-stage
|
|
915
|
+
* grain-preserving projections, which become an array-agg in the same SELECT.
|
|
916
|
+
* That array-agg needs no `FILTER (WHERE group_set=N)` (every row is in the
|
|
917
|
+
* one group), and aggregate/window expressions must not gate themselves by
|
|
918
|
+
* group_set either, so emitsGroupSet is cleared (see FieldInstanceResultRoot).
|
|
919
|
+
*/
|
|
889
920
|
generateSimpleSQL(stageWriter) {
|
|
921
|
+
this.rootResult.emitsGroupSet = false;
|
|
890
922
|
let s = '';
|
|
891
923
|
s += 'SELECT \n';
|
|
892
924
|
const fields = [];
|
|
893
|
-
|
|
894
|
-
|
|
925
|
+
const outputPipelinedSQL = [];
|
|
926
|
+
for (const [name, fi] of this.rootResult.allFields) {
|
|
895
927
|
const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
|
|
896
|
-
if (fi
|
|
897
|
-
|
|
928
|
+
if (fi instanceof field_instance_1.FieldInstanceField) {
|
|
929
|
+
if (fi.fieldUsage.type === 'result') {
|
|
930
|
+
fields.push(` ${fi.generateExpression()} as ${sqlName}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else if (fi instanceof field_instance_1.FieldInstanceResult) {
|
|
934
|
+
const turtle = this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL, true);
|
|
935
|
+
fields.push(` ${turtle} as ${sqlName}`);
|
|
898
936
|
}
|
|
899
937
|
}
|
|
900
938
|
s += (0, utils_1.indent)(fields.join(',\n')) + '\n';
|
|
@@ -921,29 +959,77 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
921
959
|
s += `LIMIT ${this.firstSegment.limit}\n`;
|
|
922
960
|
}
|
|
923
961
|
this.resultStage = stageWriter.addStage(s);
|
|
962
|
+
// Consumes any pipelined turtle output. The nests that reach here are
|
|
963
|
+
// single-stage (canUseSingleGroupSetSQL), so outputPipelinedSQL is empty and
|
|
964
|
+
// this is a no-op; kept so the function stays correct if that guard widens.
|
|
965
|
+
this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, []);
|
|
924
966
|
return this.resultStage;
|
|
925
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* Can this query take the single-group-set fast path? True when the query
|
|
970
|
+
* needs no group-set fan-out: it has nests (so it's "complex") but every one
|
|
971
|
+
* is a single-stage grain-preserving projection riding group_set 0, and there
|
|
972
|
+
* are no reduce nests, ungroupings, or totals (all of which would mint a
|
|
973
|
+
* second group set and push maxGroupSet above 0). Anything outside that shape
|
|
974
|
+
* falls back to the general demux path, which is correct for every case.
|
|
975
|
+
*/
|
|
976
|
+
canUseSingleGroupSetSQL() {
|
|
977
|
+
if (this.maxGroupSet !== 0)
|
|
978
|
+
return false;
|
|
979
|
+
if (this.firstSegment.type !== 'reduce')
|
|
980
|
+
return false;
|
|
981
|
+
for (const [, fi] of this.rootResult.allFields) {
|
|
982
|
+
if (fi instanceof field_instance_1.FieldInstanceField) {
|
|
983
|
+
// A root-level `calculate:` (window) needs the general path's analytic
|
|
984
|
+
// handling: on dialects that can't partition a window on an expression
|
|
985
|
+
// (BigQuery) it rides a lateral-join bag this path doesn't build.
|
|
986
|
+
if (fieldIsAnalytic(fi))
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
else if (fi instanceof field_instance_1.FieldInstanceResult) {
|
|
990
|
+
if (fi.firstSegment.type !== 'project')
|
|
991
|
+
return false;
|
|
992
|
+
if (fi.turtleDef.pipeline.length !== 1)
|
|
993
|
+
return false;
|
|
994
|
+
if (fi.getRepeatedResultType() !== 'nested')
|
|
995
|
+
return false;
|
|
996
|
+
// A `calculate:` inside a `select:` would put a window function inside
|
|
997
|
+
// the array-agg, which no dialect allows; leave it on the general path.
|
|
998
|
+
for (const [, nestField] of fi.allFields) {
|
|
999
|
+
if (nestField instanceof field_instance_1.FieldInstanceField &&
|
|
1000
|
+
fieldIsAnalytic(nestField)) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
926
1008
|
// This probably should be generated in a dialect independat way.
|
|
927
1009
|
// but for now, it is just googleSQL.
|
|
928
|
-
generatePipelinedStages(outputPipelinedSQL, lastStageName, stageWriter
|
|
1010
|
+
generatePipelinedStages(outputPipelinedSQL, lastStageName, stageWriter,
|
|
1011
|
+
// Every column `lastStageName` produces. The replaced turtle columns (in
|
|
1012
|
+
// outputPipelinedSQL) are dropped and re-added by the pipeline expressions;
|
|
1013
|
+
// everything else is carried through unchanged by its emitted name -- for a
|
|
1014
|
+
// grouped stage that is the group-set-suffixed form (e.g. `f1__0`), not the
|
|
1015
|
+
// final output name.
|
|
1016
|
+
priorStageColumns) {
|
|
929
1017
|
if (outputPipelinedSQL.length === 0) {
|
|
930
1018
|
return lastStageName;
|
|
931
1019
|
}
|
|
1020
|
+
const pipelinesSQL = outputPipelinedSQL
|
|
1021
|
+
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
1022
|
+
.join(',\n');
|
|
932
1023
|
let retSQL;
|
|
933
1024
|
if (this.parent.dialect.supportsSelectReplace) {
|
|
934
|
-
const pipelinesSQL = outputPipelinedSQL
|
|
935
|
-
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
936
|
-
.join(',\n');
|
|
937
1025
|
retSQL = `SELECT * replace (${pipelinesSQL}) FROM ${lastStageName}
|
|
938
1026
|
`;
|
|
939
1027
|
}
|
|
940
1028
|
else {
|
|
941
|
-
const pipelinesSQL = outputPipelinedSQL
|
|
942
|
-
.map(o => `${o.pipelineSQL} as ${o.sqlFieldName}`)
|
|
943
|
-
.join(',\n');
|
|
944
1029
|
const outputFields = outputPipelinedSQL.map(f => f.sqlFieldName);
|
|
945
|
-
const
|
|
946
|
-
|
|
1030
|
+
const fields = priorStageColumns
|
|
1031
|
+
.map(c => c.name)
|
|
1032
|
+
.filter(name => outputFields.indexOf(name) === -1);
|
|
947
1033
|
retSQL = `SELECT ${fields.length > 0 ? fields.join(', ') + ',' : ''} ${pipelinesSQL} FROM ${lastStageName}`;
|
|
948
1034
|
}
|
|
949
1035
|
return stageWriter.addStage(retSQL);
|
|
@@ -978,12 +1064,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
978
1064
|
sql: exp,
|
|
979
1065
|
name: outputName,
|
|
980
1066
|
});
|
|
981
|
-
output.
|
|
1067
|
+
output.columns.push({
|
|
1068
|
+
sql: outputFieldName,
|
|
1069
|
+
name: outputName,
|
|
1070
|
+
isDimension: true,
|
|
1071
|
+
});
|
|
982
1072
|
if (fi.f.fieldDef.type === 'number') {
|
|
983
1073
|
const outputNameString = this.parent.dialect.sqlQuoteIdentifier(`${name}__${resultSet.groupSet}_string`);
|
|
984
1074
|
const outputFieldNameString = `__lateral_join_bag.${outputNameString}`;
|
|
985
|
-
output.
|
|
986
|
-
|
|
1075
|
+
output.columns.push({
|
|
1076
|
+
sql: outputFieldNameString,
|
|
1077
|
+
name: outputNameString,
|
|
1078
|
+
isDimension: true,
|
|
1079
|
+
});
|
|
987
1080
|
output.lateralJoinSQLExpressions.push({
|
|
988
1081
|
sql: `CAST(${exp} as STRING)`,
|
|
989
1082
|
name: outputNameString,
|
|
@@ -993,13 +1086,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
993
1086
|
}
|
|
994
1087
|
else {
|
|
995
1088
|
// just treat it like a regular field.
|
|
996
|
-
output.
|
|
1089
|
+
output.columns.push({
|
|
1090
|
+
sql: `${exp} as ${outputName}`,
|
|
1091
|
+
name: outputName,
|
|
1092
|
+
isDimension: true,
|
|
1093
|
+
});
|
|
997
1094
|
}
|
|
998
|
-
output.dimensionIndexes.push(output.fieldIndex++);
|
|
999
1095
|
}
|
|
1000
1096
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1001
|
-
output.
|
|
1002
|
-
|
|
1097
|
+
output.columns.push({
|
|
1098
|
+
sql: `${exp} as ${outputName}`,
|
|
1099
|
+
name: outputName,
|
|
1100
|
+
isDimension: false,
|
|
1101
|
+
});
|
|
1003
1102
|
}
|
|
1004
1103
|
}
|
|
1005
1104
|
}
|
|
@@ -1009,8 +1108,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1009
1108
|
}
|
|
1010
1109
|
else if (fi.firstSegment.type === 'project') {
|
|
1011
1110
|
const s = this.generateTurtleSQL(fi, stageWriter, outputName, output.outputPipelinedSQL);
|
|
1012
|
-
output.
|
|
1013
|
-
|
|
1111
|
+
output.columns.push({
|
|
1112
|
+
sql: `${s} as ${outputName}`,
|
|
1113
|
+
name: outputName,
|
|
1114
|
+
isDimension: false,
|
|
1115
|
+
});
|
|
1014
1116
|
}
|
|
1015
1117
|
}
|
|
1016
1118
|
}
|
|
@@ -1028,8 +1130,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1028
1130
|
}
|
|
1029
1131
|
else {
|
|
1030
1132
|
resultSet.hasHaving = true;
|
|
1031
|
-
output.
|
|
1032
|
-
|
|
1133
|
+
output.columns.push({
|
|
1134
|
+
sql: `CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END as __delete__${resultSet.groupSet}`,
|
|
1135
|
+
name: `__delete__${resultSet.groupSet}`,
|
|
1136
|
+
isDimension: false,
|
|
1137
|
+
});
|
|
1033
1138
|
}
|
|
1034
1139
|
}
|
|
1035
1140
|
}
|
|
@@ -1038,11 +1143,18 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1038
1143
|
for (const [, field] of resultStruct.allFields) {
|
|
1039
1144
|
if (field.type === 'query') {
|
|
1040
1145
|
const fir = field;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1146
|
+
// A projection first stage's `where:` folds into its array-agg
|
|
1147
|
+
// condition (see generateTurtleSQL / sqlAggregateTurtle), not a
|
|
1148
|
+
// scan-level WHERE: it rides its enclosing group_set, so a WHERE here
|
|
1149
|
+
// can't isolate it from the parent, and its childGroups is empty (no
|
|
1150
|
+
// group_set of its own) which would emit a broken `group_set IN ()`.
|
|
1151
|
+
if (fir.firstSegment.type !== 'project') {
|
|
1152
|
+
const turtleWhere = this.generateSQLFilters(fir, 'where');
|
|
1153
|
+
if (turtleWhere.present()) {
|
|
1154
|
+
const groupSets = fir.childGroups.join(',');
|
|
1155
|
+
wheres.add(`(group_set NOT IN (${groupSets})` +
|
|
1156
|
+
` OR (group_set IN (${groupSets}) AND ${turtleWhere.sql()}))`);
|
|
1157
|
+
}
|
|
1046
1158
|
}
|
|
1047
1159
|
wheres.addChain(this.generateSQLWhereChildren(fir));
|
|
1048
1160
|
}
|
|
@@ -1079,11 +1191,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1079
1191
|
const partitionSQL = [];
|
|
1080
1192
|
let hasAnyLimits = false;
|
|
1081
1193
|
let hasResultsWithChildren = false;
|
|
1082
|
-
const resultsWithHavingOrLimit = this.rootResult.selectStructs([], (result) => result.hasHaving || result
|
|
1194
|
+
const resultsWithHavingOrLimit = this.rootResult.selectStructs([], (result) => result.hasHaving || usesShaveLimit(result));
|
|
1083
1195
|
if (resultsWithHavingOrLimit.length > 0) {
|
|
1084
1196
|
// loop through an generate the partitions
|
|
1085
1197
|
for (const result of this.rootResult.selectStructs([], (_result) => true)) {
|
|
1086
|
-
const hasLimit = result
|
|
1198
|
+
const hasLimit = usesShaveLimit(result);
|
|
1087
1199
|
hasResultsWithChildren || (hasResultsWithChildren = result.childGroups.length > 1 && (hasLimit || result.hasHaving));
|
|
1088
1200
|
hasAnyLimits || (hasAnyLimits = hasLimit);
|
|
1089
1201
|
// find all the parent dimension names.
|
|
@@ -1201,9 +1313,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1201
1313
|
let from = this.generateSQLJoins(stageWriter);
|
|
1202
1314
|
const wheres = this.generateSQLWhereTurtled();
|
|
1203
1315
|
const f = {
|
|
1204
|
-
|
|
1205
|
-
fieldIndex: 2,
|
|
1206
|
-
sql: ['group_set'],
|
|
1316
|
+
columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
|
|
1207
1317
|
lateralJoinSQLExpressions: [],
|
|
1208
1318
|
groupsAggregated: [],
|
|
1209
1319
|
outputPipelinedSQL: [],
|
|
@@ -1213,9 +1323,9 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1213
1323
|
throw new malloy_compile_error_1.MalloyCompileError("Cannot use 'select:' in a stage that contains nested views. " +
|
|
1214
1324
|
'Use `group_by:` or restructure the pipeline.', 'compiler-project-with-turtles', this.fieldDef.location);
|
|
1215
1325
|
}
|
|
1216
|
-
const groupBy = 'GROUP BY ' + f.
|
|
1326
|
+
const groupBy = 'GROUP BY ' + groupByPositions(f.columns).join(',') + '\n';
|
|
1217
1327
|
from += this.parent.dialect.sqlGroupSetTable(this.maxGroupSet) + '\n';
|
|
1218
|
-
s += (0, utils_1.indent)(f.sql.join(',\n')) + '\n';
|
|
1328
|
+
s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
|
|
1219
1329
|
if (f.lateralJoinSQLExpressions.length > 0) {
|
|
1220
1330
|
from += this.parent.dialect.sqlLateralJoinBag(f.lateralJoinSQLExpressions);
|
|
1221
1331
|
}
|
|
@@ -1224,7 +1334,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1224
1334
|
const resultStage = stageWriter.addStage(s);
|
|
1225
1335
|
// generate stages for havings and limits
|
|
1226
1336
|
this.resultStage = this.generateSQLHavingLimit(stageWriter, resultStage);
|
|
1227
|
-
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1337
|
+
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter, f.columns);
|
|
1228
1338
|
return this.resultStage;
|
|
1229
1339
|
}
|
|
1230
1340
|
generateDepthNFields(depth, resultSet, output, stageWriter) {
|
|
@@ -1235,13 +1345,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1235
1345
|
if (fi.fieldUsage.type === 'result') {
|
|
1236
1346
|
if ((0, query_node_1.isScalarField)(fi.f)) {
|
|
1237
1347
|
const exp = (0, utils_1.caseGroup)(resultSet.groupSet > 0 ? resultSet.childGroups : [], sqlFieldName);
|
|
1238
|
-
output.
|
|
1239
|
-
|
|
1348
|
+
output.columns.push({
|
|
1349
|
+
sql: `${exp} as ${sqlFieldName}`,
|
|
1350
|
+
name: sqlFieldName,
|
|
1351
|
+
isDimension: true,
|
|
1352
|
+
});
|
|
1240
1353
|
}
|
|
1241
1354
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1242
1355
|
const exp = this.parent.dialect.sqlAnyValue(resultSet.groupSet, sqlFieldName);
|
|
1243
|
-
output.
|
|
1244
|
-
|
|
1356
|
+
output.columns.push({
|
|
1357
|
+
sql: `${exp} as ${sqlFieldName}`,
|
|
1358
|
+
name: sqlFieldName,
|
|
1359
|
+
isDimension: false,
|
|
1360
|
+
});
|
|
1245
1361
|
}
|
|
1246
1362
|
}
|
|
1247
1363
|
}
|
|
@@ -1256,8 +1372,11 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1256
1372
|
toGroup: resultSet.groupSet,
|
|
1257
1373
|
});
|
|
1258
1374
|
groupsToMap.push(fi.groupSet);
|
|
1259
|
-
output.
|
|
1260
|
-
|
|
1375
|
+
output.columns.push({
|
|
1376
|
+
sql: `${s} as ${sqlFieldName}`,
|
|
1377
|
+
name: sqlFieldName,
|
|
1378
|
+
isDimension: false,
|
|
1379
|
+
});
|
|
1261
1380
|
}
|
|
1262
1381
|
else {
|
|
1263
1382
|
this.generateDepthNFields(depth, fi, output, stageWriter);
|
|
@@ -1267,14 +1386,19 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1267
1386
|
if (output.groupsAggregated.length > 0) {
|
|
1268
1387
|
if (!this.parent.dialect.hasLateralColumnAliasInSelect) {
|
|
1269
1388
|
// Safe to remap in-place; aggregate expressions won't see the alias.
|
|
1270
|
-
// This runs on every recursive return but
|
|
1389
|
+
// This runs on every recursive return but column[0] replacement is
|
|
1271
1390
|
// idempotent — last call builds the full CASE from all accumulated
|
|
1272
|
-
// groupsAggregated entries.
|
|
1273
|
-
|
|
1391
|
+
// groupsAggregated entries. The output is still named group_set.
|
|
1392
|
+
let groupSetSQL = 'CASE ';
|
|
1274
1393
|
for (const m of output.groupsAggregated) {
|
|
1275
|
-
|
|
1394
|
+
groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
|
|
1276
1395
|
}
|
|
1277
|
-
|
|
1396
|
+
groupSetSQL += 'ELSE group_set END as group_set';
|
|
1397
|
+
output.columns[0] = {
|
|
1398
|
+
sql: groupSetSQL,
|
|
1399
|
+
name: 'group_set',
|
|
1400
|
+
isDimension: true,
|
|
1401
|
+
};
|
|
1278
1402
|
}
|
|
1279
1403
|
// For hasLateralColumnAliasInSelect, the remap is handled in
|
|
1280
1404
|
// generateSQLDepthN to avoid adding it multiple times from recursion.
|
|
@@ -1283,88 +1407,100 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1283
1407
|
generateSQLDepthN(depth, stageWriter, stageName) {
|
|
1284
1408
|
let s = 'SELECT \n';
|
|
1285
1409
|
const f = {
|
|
1286
|
-
|
|
1287
|
-
fieldIndex: 2,
|
|
1288
|
-
sql: ['group_set'],
|
|
1410
|
+
columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
|
|
1289
1411
|
lateralJoinSQLExpressions: [],
|
|
1290
1412
|
groupsAggregated: [],
|
|
1291
1413
|
outputPipelinedSQL: [],
|
|
1292
1414
|
};
|
|
1293
1415
|
this.generateDepthNFields(depth, this.rootResult, f, stageWriter);
|
|
1294
1416
|
// 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
|
|
1417
|
+
// replace column[0]'s SQL with the remap CASE aliased as
|
|
1418
|
+
// __remapped_group_set (not group_set). This avoids shadowing the input
|
|
1419
|
+
// group_set column that aggregate expressions reference. GROUP BY still
|
|
1420
|
+
// uses position 1 (the remapped value), so rows with different original
|
|
1421
|
+
// group_sets collapse correctly. A follow-up CTE renames it back, so the
|
|
1422
|
+
// column's carried-forward name stays group_set.
|
|
1300
1423
|
const needsRemapStage = f.groupsAggregated.length > 0 &&
|
|
1301
1424
|
this.parent.dialect.hasLateralColumnAliasInSelect;
|
|
1302
1425
|
if (needsRemapStage) {
|
|
1303
|
-
|
|
1426
|
+
let groupSetSQL = 'CASE ';
|
|
1304
1427
|
for (const m of f.groupsAggregated) {
|
|
1305
|
-
|
|
1428
|
+
groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
|
|
1306
1429
|
}
|
|
1307
|
-
|
|
1430
|
+
groupSetSQL += 'ELSE group_set END as __remapped_group_set';
|
|
1431
|
+
f.columns[0] = { sql: groupSetSQL, name: 'group_set', isDimension: true };
|
|
1308
1432
|
}
|
|
1309
|
-
s += (0, utils_1.indent)(f.sql.join(',\n')) + '\n';
|
|
1433
|
+
s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
|
|
1310
1434
|
s += `FROM ${stageName}\n`;
|
|
1311
1435
|
const where = this.rootResult.eliminateComputeGroupsSQL();
|
|
1312
1436
|
if (where.length > 0) {
|
|
1313
1437
|
s += `WHERE ${where}\n`;
|
|
1314
1438
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1439
|
+
const dimensionPositions = groupByPositions(f.columns);
|
|
1440
|
+
if (dimensionPositions.length > 0) {
|
|
1441
|
+
s += `GROUP BY ${dimensionPositions.join(',')}\n`;
|
|
1317
1442
|
}
|
|
1318
1443
|
this.resultStage = stageWriter.addStage(s);
|
|
1319
1444
|
if (needsRemapStage) {
|
|
1320
|
-
const cols = f.
|
|
1321
|
-
const asMatch = col.match(/as\s+(\S+)\s*$/i);
|
|
1322
|
-
return asMatch ? asMatch[1] : '*';
|
|
1323
|
-
});
|
|
1445
|
+
const cols = f.columns.slice(1).map(c => c.name);
|
|
1324
1446
|
const remapSQL = `SELECT \n${(0, utils_1.indent)(['__remapped_group_set as group_set', ...cols].join(',\n'))}\nFROM ${this.resultStage}\n`;
|
|
1325
1447
|
this.resultStage = stageWriter.addStage(remapSQL);
|
|
1326
1448
|
}
|
|
1327
|
-
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1449
|
+
this.resultStage = this.generatePipelinedStages(f.outputPipelinedSQL, this.resultStage, stageWriter, f.columns);
|
|
1328
1450
|
return this.resultStage;
|
|
1329
1451
|
}
|
|
1330
1452
|
genereateSQLCombineTurtles(stageWriter, stage0Name) {
|
|
1331
1453
|
let s = 'SELECT\n';
|
|
1332
|
-
|
|
1333
|
-
|
|
1454
|
+
// Columns this combine stage emits. Unlike the grouped stages above, these
|
|
1455
|
+
// are the final output names (unsuffixed), not the group-set-suffixed form.
|
|
1456
|
+
// A following pipelined stage carries them forward by name.
|
|
1457
|
+
const columns = [];
|
|
1334
1458
|
const outputPipelinedSQL = [];
|
|
1335
|
-
const dimensionIndexes = [];
|
|
1336
1459
|
for (const [name, fi] of this.rootResult.allFields) {
|
|
1337
1460
|
const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
|
|
1338
1461
|
if (fi instanceof field_instance_1.FieldInstanceField) {
|
|
1339
1462
|
if (fi.fieldUsage.type === 'result') {
|
|
1340
1463
|
if ((0, query_node_1.isScalarField)(fi.f)) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1464
|
+
columns.push({
|
|
1465
|
+
sql: this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`) + ` as ${sqlName}`,
|
|
1466
|
+
name: sqlName,
|
|
1467
|
+
isDimension: true,
|
|
1468
|
+
});
|
|
1343
1469
|
}
|
|
1344
1470
|
else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
|
|
1345
|
-
|
|
1346
|
-
|
|
1471
|
+
columns.push({
|
|
1472
|
+
sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
|
|
1473
|
+
name: sqlName,
|
|
1474
|
+
isDimension: false,
|
|
1475
|
+
});
|
|
1347
1476
|
}
|
|
1348
1477
|
}
|
|
1349
1478
|
}
|
|
1350
1479
|
else if (fi instanceof field_instance_1.FieldInstanceResult) {
|
|
1351
1480
|
if (fi.firstSegment.type === 'reduce') {
|
|
1352
|
-
|
|
1353
|
-
|
|
1481
|
+
columns.push({
|
|
1482
|
+
sql: `${this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL)} as ${sqlName}`,
|
|
1483
|
+
name: sqlName,
|
|
1484
|
+
isDimension: false,
|
|
1485
|
+
});
|
|
1354
1486
|
}
|
|
1355
1487
|
else if (fi.firstSegment.type === 'project') {
|
|
1356
|
-
|
|
1357
|
-
|
|
1488
|
+
columns.push({
|
|
1489
|
+
sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
|
|
1490
|
+
name: sqlName,
|
|
1491
|
+
isDimension: false,
|
|
1492
|
+
});
|
|
1358
1493
|
}
|
|
1359
1494
|
}
|
|
1360
1495
|
}
|
|
1361
|
-
s += (0, utils_1.indent)(
|
|
1496
|
+
s += (0, utils_1.indent)(columns.map(c => c.sql).join(',\n')) + `\nFROM ${stage0Name}\n`;
|
|
1362
1497
|
const where = this.rootResult.eliminateComputeGroupsSQL();
|
|
1363
1498
|
if (where.length > 0) {
|
|
1364
1499
|
s += `WHERE ${where}\n`;
|
|
1365
1500
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1501
|
+
const dimensionPositions = groupByPositions(columns);
|
|
1502
|
+
if (dimensionPositions.length > 0) {
|
|
1503
|
+
s += `GROUP BY ${dimensionPositions.join(',')}\n`;
|
|
1368
1504
|
}
|
|
1369
1505
|
// order by
|
|
1370
1506
|
s += this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
|
|
@@ -1373,7 +1509,7 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1373
1509
|
s += `LIMIT ${this.firstSegment.limit}\n`;
|
|
1374
1510
|
}
|
|
1375
1511
|
this.resultStage = stageWriter.addStage(s);
|
|
1376
|
-
this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter);
|
|
1512
|
+
this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, columns);
|
|
1377
1513
|
return this.resultStage;
|
|
1378
1514
|
}
|
|
1379
1515
|
// create a simplified version of the StructDef for dialects.
|
|
@@ -1438,7 +1574,21 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1438
1574
|
}
|
|
1439
1575
|
return dialectFieldList;
|
|
1440
1576
|
}
|
|
1441
|
-
generateTurtleSQL(resultStruct, stageWriter, sqlFieldName, outputPipelinedSQL
|
|
1577
|
+
generateTurtleSQL(resultStruct, stageWriter, sqlFieldName, outputPipelinedSQL,
|
|
1578
|
+
// The single-group-set fast path (generateSingleGroupSetSQL) has no
|
|
1579
|
+
// group_set column to filter on: every row is in the one group. The
|
|
1580
|
+
// array-agg drops its `FILTER (WHERE group_set=N)` and keeps only a
|
|
1581
|
+
// projection's own `where:`.
|
|
1582
|
+
omitGroupSetFilter = false) {
|
|
1583
|
+
// How to emit this nest's first stage — and the last line of defense before
|
|
1584
|
+
// broken SQL for IR that didn't pass through the translator (cached,
|
|
1585
|
+
// deserialized, or query-builder IR). Same `nestStrategy` the translator
|
|
1586
|
+
// uses, so the two can't disagree.
|
|
1587
|
+
const pipeline = resultStruct.turtleDef.pipeline;
|
|
1588
|
+
const strategy = (0, nest_capability_1.nestStrategy)(pipeline[0], this.parent.dialect, pipeline.length > 1);
|
|
1589
|
+
if (strategy.kind === 'unsupported') {
|
|
1590
|
+
throw new malloy_compile_error_1.MalloyCompileError(`dialect '${this.parent.dialect.name}' cannot emit this nested query (${strategy.reason})`, strategy.reason, resultStruct.turtleDef.location);
|
|
1591
|
+
}
|
|
1442
1592
|
// calculate the ordering.
|
|
1443
1593
|
const compiledOrderBy = [];
|
|
1444
1594
|
let orderingField;
|
|
@@ -1483,7 +1633,24 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1483
1633
|
}
|
|
1484
1634
|
}
|
|
1485
1635
|
else {
|
|
1486
|
-
|
|
1636
|
+
// A projection first stage applies its own `limit:` by slicing the
|
|
1637
|
+
// array-agg (it has no group-by keys to ROW_NUMBER-shave on) and its
|
|
1638
|
+
// `where:` by folding into the array-agg's group_set condition (it rides
|
|
1639
|
+
// the enclosing group_set, so a scan-level WHERE can't isolate it). This
|
|
1640
|
+
// is a first-stage question: if more stages follow, they consume this
|
|
1641
|
+
// (limited/filtered) array via the recursion in generateTurtlePipelineSQL.
|
|
1642
|
+
// A reduce first stage keeps the shave and the scan-level WHERE.
|
|
1643
|
+
let limit;
|
|
1644
|
+
let filterSQL;
|
|
1645
|
+
if (strategy.kind === 'project') {
|
|
1646
|
+
limit = strategy.limit;
|
|
1647
|
+
const where = this.generateSQLFilters(resultStruct, 'where');
|
|
1648
|
+
if (where.present()) {
|
|
1649
|
+
filterSQL = where.sql();
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
// (capability is guarded by nestStrategy at the top of this method)
|
|
1653
|
+
ret = this.parent.dialect.sqlAggregateTurtle(omitGroupSetFilter ? undefined : resultStruct.groupSet, dialectFieldList, orderBy, limit, filterSQL);
|
|
1487
1654
|
}
|
|
1488
1655
|
// If the turtle is a pipeline, generate a UDF to compute it.
|
|
1489
1656
|
const newStageWriter = new stage_writer_1.StageWriter(this.parent.dialect.supportsCTEinCoorelatedSubQueries, stageWriter);
|
|
@@ -1561,12 +1728,13 @@ class QueryQuery extends query_node_1.QueryField {
|
|
|
1561
1728
|
this.maxGroupSet = r.nextGroupSetNumber - 1;
|
|
1562
1729
|
this.rootResult.assignFieldsToGroups();
|
|
1563
1730
|
(_a = this.rootResult).isComplexQuery || (_a.isComplexQuery = this.maxDepth > 0 || r.isComplex);
|
|
1564
|
-
|
|
1731
|
+
// A complex query needs the group-set fan-out unless it has just one group
|
|
1732
|
+
// set (only single-stage projection nests), in which case generateSimpleSQL
|
|
1733
|
+
// emits the same no-fan-out form a non-nested query gets.
|
|
1734
|
+
if (this.rootResult.isComplexQuery && !this.canUseSingleGroupSetSQL()) {
|
|
1565
1735
|
return this.generateComplexSQL(stageWriter);
|
|
1566
1736
|
}
|
|
1567
|
-
|
|
1568
|
-
return this.generateSimpleSQL(stageWriter);
|
|
1569
|
-
}
|
|
1737
|
+
return this.generateSimpleSQL(stageWriter);
|
|
1570
1738
|
}
|
|
1571
1739
|
generateSQLFromPipeline(stageWriter) {
|
|
1572
1740
|
this.parent.maybeEmitParameterizedSourceUsage();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SourceDef Utilities for Persistence
|
|
3
3
|
*
|
|
4
|
-
* Key invariant:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Key invariant: a source's identity fields (sourceID, referenceID, extends)
|
|
5
|
+
* are assigned in the translator (DefineSource / the reference path), never by
|
|
6
|
+
* these compiler factories. The factory functions explicitly copy only the
|
|
7
|
+
* fields they need, never using spread, so identity is not propagated onto a
|
|
8
|
+
* freshly built source.
|
|
9
9
|
*/
|
|
10
10
|
import type { BuildID, FieldDef, GivenID, ModelDef, PersistableSourceDef, Query, QuerySourceDef, SourceDef, SourceID, SourceRegistryEntry, SourceRegistryValue, SQLPhraseSegment, SQLSourceDef, TableSourceDef } from './malloy_types';
|
|
11
11
|
export declare function mkSourceID(name: string, url: string | undefined): SourceID;
|
|
@@ -30,13 +30,37 @@ export declare function mkSQLSourceDef(base: SourceDef, selectStr: string, selec
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function mkTableSourceDef(name: string, connection: string, tablePath: string, dialect: string, fields: FieldDef[]): TableSourceDef;
|
|
32
32
|
/**
|
|
33
|
-
* Resolve a sourceID
|
|
33
|
+
* Resolve a SourceID (a source's own `sourceID`, or a `referenceID` pointing at
|
|
34
|
+
* one) to its SourceDef using the sourceRegistry, returning any kind of source.
|
|
34
35
|
*
|
|
35
36
|
* @param modelDef The model definition containing the registry
|
|
36
|
-
* @param sourceID The
|
|
37
|
+
* @param sourceID The SourceID to resolve
|
|
37
38
|
* @returns The SourceDef if found, undefined otherwise
|
|
38
39
|
*/
|
|
40
|
+
export declare function resolveSourceRef(modelDef: ModelDef, sourceID: SourceID): SourceDef | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a sourceID to a persistable SourceDef using the sourceRegistry.
|
|
43
|
+
*
|
|
44
|
+
* @param modelDef The model definition containing the registry
|
|
45
|
+
* @param sourceID The sourceID to resolve
|
|
46
|
+
* @returns The PersistableSourceDef if found, undefined otherwise
|
|
47
|
+
*/
|
|
39
48
|
export declare function resolveSourceID(modelDef: ModelDef, sourceID: SourceID): PersistableSourceDef | undefined;
|
|
49
|
+
/** The namespace entry a source refers to (see `sourceNamespaceReference`). */
|
|
50
|
+
export interface NamespaceReference {
|
|
51
|
+
/** The name this source is bound to in `modelDef.contents`. */
|
|
52
|
+
name: string;
|
|
53
|
+
/** The referenced source. */
|
|
54
|
+
source: SourceDef;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* If `sd` was created as an unmodified reference to another source
|
|
58
|
+
* (`sd.referenceID` is set) and that source is present in this model's
|
|
59
|
+
* namespace, return it together with the name it goes by. Returns undefined
|
|
60
|
+
* when `sd` defines its own shape, or when the referenced source is not in this
|
|
61
|
+
* model's namespace (e.g. an imported source whose own target wasn't imported).
|
|
62
|
+
*/
|
|
63
|
+
export declare function sourceNamespaceReference(modelDef: ModelDef, sd: SourceDef): NamespaceReference | undefined;
|
|
40
64
|
/**
|
|
41
65
|
* Add an entry to the sourceRegistry.
|
|
42
66
|
*
|