@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.
Files changed (32) hide show
  1. package/dist/dialect/databricks/databricks.d.ts +2 -1
  2. package/dist/dialect/databricks/databricks.js +11 -6
  3. package/dist/dialect/dialect.d.ts +2 -1
  4. package/dist/dialect/dialect.js +7 -0
  5. package/dist/dialect/duckdb/duckdb.d.ts +2 -1
  6. package/dist/dialect/duckdb/duckdb.js +8 -2
  7. package/dist/dialect/mysql/mysql.d.ts +2 -1
  8. package/dist/dialect/mysql/mysql.js +7 -2
  9. package/dist/dialect/postgres/postgres.d.ts +2 -1
  10. package/dist/dialect/postgres/postgres.js +8 -2
  11. package/dist/dialect/snowflake/snowflake.d.ts +2 -1
  12. package/dist/dialect/snowflake/snowflake.js +9 -3
  13. package/dist/dialect/standardsql/standardsql.d.ts +2 -1
  14. package/dist/dialect/standardsql/standardsql.js +6 -2
  15. package/dist/dialect/trino/trino.d.ts +2 -1
  16. package/dist/dialect/trino/trino.js +6 -2
  17. package/dist/lang/ast/field-space/dynamic-space.js +9 -1
  18. package/dist/lang/ast/field-space/query-spaces.d.ts +1 -0
  19. package/dist/lang/ast/field-space/query-spaces.js +5 -0
  20. package/dist/lang/ast/query-items/field-references.d.ts +10 -1
  21. package/dist/lang/ast/query-items/field-references.js +67 -4
  22. package/dist/lang/ast/query-properties/nest.js +15 -0
  23. package/dist/lang/parse-log.d.ts +10 -0
  24. package/dist/lang/parse-log.js +3 -0
  25. package/dist/model/field_instance.js +9 -0
  26. package/dist/model/nest-capability.d.ts +37 -0
  27. package/dist/model/nest-capability.js +31 -0
  28. package/dist/model/query_query.d.ts +7 -4
  29. package/dist/model/query_query.js +176 -79
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. 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
- sql: string[];
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 allFields = Array.from(this.rootResult.allFields.keys()).map(f => this.parent.dialect.sqlQuoteIdentifier(f));
946
- const fields = allFields.filter(f => outputFields.indexOf(f) === -1);
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.sql.push(outputFieldName);
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.sql.push(outputFieldNameString);
986
- output.dimensionIndexes.push(output.fieldIndex++);
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.sql.push(`${exp} as ${outputName}`);
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.sql.push(`${exp} as ${outputName}`);
1002
- output.fieldIndex++;
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.sql.push(`${s} as ${outputName}`);
1013
- output.fieldIndex++;
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.sql.push(`CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END as __delete__${resultSet.groupSet}`);
1032
- output.fieldIndex++;
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
- const turtleWhere = this.generateSQLFilters(fir, 'where');
1042
- if (turtleWhere.present()) {
1043
- const groupSets = fir.childGroups.join(',');
1044
- wheres.add(`(group_set NOT IN (${groupSets})` +
1045
- ` OR (group_set IN (${groupSets}) AND ${turtleWhere.sql()}))`);
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.getLimit() !== undefined);
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.getLimit() !== undefined;
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
- dimensionIndexes: [1],
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.dimensionIndexes.join(',') + '\n';
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.sql.push(`${exp} as ${sqlFieldName}`);
1239
- output.dimensionIndexes.push(output.fieldIndex++);
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.sql.push(`${exp} as ${sqlFieldName}`);
1244
- output.fieldIndex++;
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.sql.push(`${s} as ${sqlFieldName}`);
1260
- output.fieldIndex++;
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 sql[0] replacement is
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
- output.sql[0] = 'CASE ';
1326
+ // groupsAggregated entries. The output is still named group_set.
1327
+ let groupSetSQL = 'CASE ';
1274
1328
  for (const m of output.groupsAggregated) {
1275
- output.sql[0] += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1329
+ groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1276
1330
  }
1277
- output.sql[0] += 'ELSE group_set END as group_set';
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
- dimensionIndexes: [1],
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 sql[0] with the remap CASE aliased as __remapped_group_set
1296
- // (not group_set). This avoids shadowing the input group_set column
1297
- // that aggregate expressions reference. GROUP BY still uses position 1
1298
- // (the remapped value), so rows with different original group_sets
1299
- // collapse correctly. A follow-up CTE renames it to group_set.
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
- f.sql[0] = 'CASE ';
1361
+ let groupSetSQL = 'CASE ';
1304
1362
  for (const m of f.groupsAggregated) {
1305
- f.sql[0] += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1363
+ groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1306
1364
  }
1307
- f.sql[0] += 'ELSE group_set END as __remapped_group_set';
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
- if (f.dimensionIndexes.length > 0) {
1316
- s += `GROUP BY ${f.dimensionIndexes.join(',')}\n`;
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.sql.slice(1).map(col => {
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
- const fieldsSQL = [];
1333
- let fieldIndex = 1;
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
- fieldsSQL.push(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`) + ` as ${sqlName}`);
1342
- dimensionIndexes.push(fieldIndex++);
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
- fieldsSQL.push(this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName));
1346
- fieldIndex++;
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
- fieldsSQL.push(`${this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL)} as ${sqlName}`);
1353
- fieldIndex++;
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
- fieldsSQL.push(this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName));
1357
- fieldIndex++;
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)(fieldsSQL.join(',\n')) + `\nFROM ${stage0Name}\n`;
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
- if (dimensionIndexes.length > 0) {
1367
- s += `GROUP BY ${dimensionIndexes.join(',')}\n`;
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
- ret = this.parent.dialect.sqlAggregateTurtle(resultStruct.groupSet, dialectFieldList, orderBy);
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.412";
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.412';
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.412",
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.412",
55
- "@malloydata/malloy-interfaces": "0.0.412",
56
- "@malloydata/malloy-tag": "0.0.412",
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",