@malloydata/malloy 0.0.433 → 0.0.435

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 (74) hide show
  1. package/dist/api/foundation/compile.d.ts +2 -2
  2. package/dist/api/foundation/compile.js +8 -9
  3. package/dist/api/foundation/config.js +6 -15
  4. package/dist/api/foundation/runtime.d.ts +2 -8
  5. package/dist/api/foundation/runtime.js +11 -39
  6. package/dist/api/foundation/types.d.ts +1 -1
  7. package/dist/connection/validate_table_path.d.ts +1 -2
  8. package/dist/connection/validate_table_path.js +1 -2
  9. package/dist/dialect/databricks/databricks.js +6 -6
  10. package/dist/dialect/dialect.d.ts +35 -3
  11. package/dist/dialect/dialect.js +67 -5
  12. package/dist/dialect/dialect_map.js +2 -0
  13. package/dist/dialect/duckdb/duckdb.js +7 -21
  14. package/dist/dialect/index.d.ts +2 -1
  15. package/dist/dialect/index.js +3 -1
  16. package/dist/dialect/mysql/mysql.js +8 -8
  17. package/dist/dialect/pg_impl.js +12 -12
  18. package/dist/dialect/postgres/postgres.js +3 -3
  19. package/dist/dialect/snowflake/snowflake.js +12 -12
  20. package/dist/dialect/sqlserver/dialect_functions.d.ts +2 -0
  21. package/dist/dialect/sqlserver/dialect_functions.js +42 -0
  22. package/dist/dialect/sqlserver/function_overrides.d.ts +2 -0
  23. package/dist/dialect/sqlserver/function_overrides.js +55 -0
  24. package/dist/dialect/sqlserver/index.d.ts +1 -0
  25. package/dist/dialect/sqlserver/index.js +22 -0
  26. package/dist/dialect/sqlserver/sqlserver.d.ts +102 -0
  27. package/dist/dialect/sqlserver/sqlserver.js +538 -0
  28. package/dist/dialect/sqlserver/windows_zones.d.ts +1 -0
  29. package/dist/dialect/sqlserver/windows_zones.js +613 -0
  30. package/dist/dialect/standardsql/standardsql.d.ts +1 -1
  31. package/dist/dialect/standardsql/standardsql.js +16 -13
  32. package/dist/dialect/trino/trino.d.ts +3 -3
  33. package/dist/dialect/trino/trino.js +32 -28
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +5 -3
  36. package/dist/lang/ast/source-elements/named-source.js +0 -2
  37. package/dist/lang/ast/source-elements/refined-source.js +1 -1
  38. package/dist/lang/ast/source-elements/sql-source.js +2 -4
  39. package/dist/lang/ast/source-elements/table-source.js +1 -2
  40. package/dist/lang/ast/source-properties/timezone-statement.d.ts +4 -3
  41. package/dist/lang/ast/source-properties/timezone-statement.js +14 -12
  42. package/dist/lang/ast/statements/import-statement.js +4 -0
  43. package/dist/lang/ast/types/malloy-element.d.ts +0 -7
  44. package/dist/lang/ast/types/malloy-element.js +0 -18
  45. package/dist/lang/composite-source-utils.js +10 -3
  46. package/dist/lang/malloy-to-ast.js +2 -8
  47. package/dist/lang/parse-malloy.d.ts +20 -4
  48. package/dist/lang/parse-malloy.js +50 -15
  49. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +3 -3
  50. package/dist/lang/parse-tree-walkers/find-external-references.js +17 -16
  51. package/dist/lang/test/test-translator.d.ts +12 -2
  52. package/dist/lang/test/test-translator.js +16 -2
  53. package/dist/model/expression_compiler.js +1 -1
  54. package/dist/model/join_instance.d.ts +1 -0
  55. package/dist/model/join_instance.js +9 -1
  56. package/dist/model/query_query.d.ts +28 -1
  57. package/dist/model/query_query.js +299 -156
  58. package/dist/model/source_def_utils.d.ts +7 -1
  59. package/dist/model/source_def_utils.js +20 -0
  60. package/dist/model/sql_block.d.ts +2 -2
  61. package/dist/model/sql_block.js +11 -47
  62. package/dist/model/sql_compiled.d.ts +3 -1
  63. package/dist/model/sql_compiled.js +8 -6
  64. package/dist/model/stage_writer.js +3 -3
  65. package/dist/model/utils.d.ts +2 -2
  66. package/dist/model/utils.js +2 -3
  67. package/dist/test/index.js +16 -8
  68. package/dist/test/matchers.d.ts +1 -1
  69. package/dist/test/matchers.js +1 -1
  70. package/dist/test/test-models.js +6 -13
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/femto-config.motly +15 -1
  74. package/package.json +7 -5
@@ -34,14 +34,57 @@ function fieldIsAnalytic(fi) {
34
34
  return ((0, malloy_types_1.hasExpression)(fi.f.fieldDef) &&
35
35
  (0, malloy_types_1.expressionIsAnalytic)(fi.f.fieldDef.expressionType));
36
36
  }
37
- // 1-based SELECT positions of the dimension columns, for `GROUP BY 1,2,...`.
38
- function groupByPositions(columns) {
39
- return columns.flatMap((c, i) => (c.isDimension ? [i + 1] : []));
37
+ // `<expr> as <name>`
38
+ function aliasedColumn(expr, name, role) {
39
+ var _a;
40
+ return {
41
+ sql: `${expr} as ${name}`,
42
+ expr,
43
+ name,
44
+ isDimension: role.isDimension,
45
+ constant: (_a = role.constant) !== null && _a !== void 0 ? _a : false,
46
+ };
47
+ }
48
+ // Does a field's expression read any column of its source?
49
+ function readsColumn(fieldDef) {
50
+ if (!(0, malloy_types_1.hasExpression)(fieldDef)) {
51
+ return true;
52
+ }
53
+ for (const node of (0, utils_1.exprWalk)(fieldDef.e)) {
54
+ switch (node.node) {
55
+ case 'field':
56
+ case 'outputField':
57
+ case 'source-reference':
58
+ case 'genericSQLExpr':
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+ // The dimension columns as GROUP BY terms: 1-based SELECT positions, or the
65
+ // expressions for a dialect without ordinals in GROUP BY.
66
+ function groupByTerms(dialect, columns) {
67
+ if (dialect.groupByClause === 'ordinal') {
68
+ return columns.flatMap((c, i) => (c.isDimension ? [`${i + 1}`] : []));
69
+ }
70
+ return columns.flatMap(c => (c.isDimension && !c.constant ? [c.expr] : []));
71
+ }
72
+ function sqlGroupBy(dialect, columns) {
73
+ const terms = groupByTerms(dialect, columns);
74
+ if (terms.length > 0) {
75
+ return `GROUP BY ${terms.join(',')}\n`;
76
+ }
77
+ // Dimensions which are all constants still make one group of the input
78
+ if (dialect.groupByClause === 'expression' &&
79
+ columns.some(c => c.isDimension)) {
80
+ return 'GROUP BY ()\n';
81
+ }
82
+ return '';
40
83
  }
41
84
  function pushDialectField(dl, f) {
42
- const { sqlExpression, sqlOutputName, rawName } = f;
85
+ const { sqlExpression, rawName } = f;
43
86
  if ((0, malloy_types_1.isAtomic)(f.fieldDef)) {
44
- dl.push({ typeDef: f.fieldDef, sqlExpression, sqlOutputName, rawName });
87
+ dl.push({ typeDef: f.fieldDef, sqlExpression, rawName });
45
88
  }
46
89
  }
47
90
  /** Query builder object. */
@@ -539,13 +582,23 @@ class QueryQuery extends query_node_1.QueryField {
539
582
  case 'virtual': {
540
583
  const virtualMap = (_a = qs.prepareResultOptions) === null || _a === void 0 ? void 0 : _a.virtualMap;
541
584
  const tablePath = (_b = virtualMap === null || virtualMap === void 0 ? void 0 : virtualMap.get(qs.structDef.connection)) === null || _b === void 0 ? void 0 : _b.get(qs.structDef.name);
542
- if (!tablePath) {
585
+ if (tablePath === undefined) {
543
586
  throw new malloy_compile_error_1.MalloyCompileError(`No virtual-map entry for virtual source '${qs.structDef.name}' ` +
544
587
  `on connection '${qs.structDef.connection}'. ` +
545
- 'Add a virtual-map entry via the `virtualMap` runtime option.', 'runtime-virtual-map-missing', qs.structDef.location);
588
+ 'Supply the entry in `virtualMap` when compiling or running the model.', 'runtime-virtual-map-missing', qs.structDef.location);
589
+ }
590
+ // Check the binding immediately before SQL substitution, using the
591
+ // executing query's dialect. Other entries in the map may be unused.
592
+ const dialect = this.parent.dialect;
593
+ const checked = dialect.sqlValidateTableName(tablePath);
594
+ if (!checked.ok || checked.canonical !== tablePath) {
595
+ const reason = checked.ok
596
+ ? `use the canonical path ${checked.canonical}`
597
+ : checked.error;
598
+ throw new malloy_compile_error_1.MalloyCompileError(`virtualMap entry '${qs.structDef.connection}.${qs.structDef.name}' ` +
599
+ `has table path ${JSON.stringify(tablePath)}, which is not a ` +
600
+ `canonical table path for ${dialect.name}: ${reason}`, 'runtime-virtual-map-invalid', qs.structDef.location);
546
601
  }
547
- // virtualMap entries are application-supplied — assumed already
548
- // canonical SQL.
549
602
  return tablePath;
550
603
  }
551
604
  case 'composite':
@@ -676,9 +729,7 @@ class QueryQuery extends query_node_1.QueryField {
676
729
  return tempInstance.generateExpression();
677
730
  });
678
731
  }
679
- if (ji.children.length === 0 ||
680
- conditions === undefined ||
681
- !this.parent.dialect.supportsComplexFilteredSources) {
732
+ if (!ji.isSubquery()) {
682
733
  // LTNOTE: need a check here to see the children's where: conditions are local
683
734
  // to the source and not to any of it's joined children.
684
735
  // In Presto, we're going to get a SQL error if in this case
@@ -700,9 +751,27 @@ class QueryQuery extends query_node_1.QueryField {
700
751
  else {
701
752
  let select = `SELECT ${ji.alias}.*`;
702
753
  let joins = '';
754
+ // Every join below this one lives inside the subquery, so every one of
755
+ // them has to be projected out of it, not just the direct children.
756
+ // A child which became a subquery of its own has already flattened its
757
+ // whole subtree into columns, so everything under it is read from that
758
+ // child rather than packed a second time.
759
+ const projectJoin = (j, from) => {
760
+ const fieldList = this.joinProjectionFieldList(j);
761
+ if (fieldList.length > 0) {
762
+ select +=
763
+ from === undefined
764
+ ? `, ${this.parent.dialect.sqlSelectAliasAsStruct(j.alias, fieldList)} AS ${j.alias}`
765
+ : `, ${from}.${j.alias} AS ${j.alias}`;
766
+ }
767
+ const readFrom = from !== null && from !== void 0 ? from : (j.isSubquery() ? j.alias : undefined);
768
+ for (const below of j.children) {
769
+ projectJoin(below, readFrom);
770
+ }
771
+ };
703
772
  for (const childJoin of ji.children) {
704
773
  joins += this.generateSQLJoinBlock(stageWriter, childJoin, depth + 1);
705
- select += `, ${this.parent.dialect.sqlSelectAliasAsStruct(childJoin.alias, (0, utils_1.getDialectFieldList)(childJoin.queryStruct.structDef))} AS ${childJoin.alias}`;
774
+ projectJoin(childJoin, undefined);
706
775
  }
707
776
  select += `\nFROM ${structSQL} AS ${ji.alias}\n${joins}\nWHERE ${conditions === null || conditions === void 0 ? void 0 : conditions.join(' AND ')}\n`;
708
777
  s += `${matrixOperation} JOIN (\n${(0, utils_1.indent)(select)}) AS ${ji.alias}\n ON ${onCondition}\n`;
@@ -746,6 +815,111 @@ class QueryQuery extends query_node_1.QueryField {
746
815
  }
747
816
  return s;
748
817
  }
818
+ /**
819
+ * Which column of which join a reference needs, or undefined when it needs
820
+ * none. Walking `path` from `from`, the answer is the member of the
821
+ * innermost join along it -- a record is kept whole, since a reference into
822
+ * one is a reference to the column holding it.
823
+ *
824
+ * Two kinds of path need no column. One ending at a computed field
825
+ * contributes that field's name, which is no column of the source; the
826
+ * columns the computation reads arrive as references of their own. And a
827
+ * `having:` or `calculate:` names an *output* field, so its path is rooted
828
+ * in the query's output rather than its source and need not resolve here at
829
+ * all. An output name which happens to match a source column resolves as
830
+ * though it were one, which costs nothing: a query's own source is never
831
+ * packed, only the joins beneath a filtered one.
832
+ */
833
+ packedColumnFor(from, path) {
834
+ let struct = from;
835
+ let alias = struct.getIdentifier();
836
+ let column = undefined;
837
+ for (const name of path) {
838
+ const child = struct.getChildByName(name);
839
+ if (child === undefined)
840
+ return undefined;
841
+ if (!(child instanceof query_node_1.QueryFieldStruct)) {
842
+ column !== null && column !== void 0 ? column : (column = name);
843
+ break;
844
+ }
845
+ struct = child.queryStruct;
846
+ if (struct.getJoinableParent() === struct) {
847
+ // A join: what the path named above it says nothing about what this
848
+ // one has to pack.
849
+ alias = struct.getIdentifier();
850
+ column = undefined;
851
+ }
852
+ else {
853
+ column !== null && column !== void 0 ? column : (column = name);
854
+ }
855
+ }
856
+ return column === undefined ? undefined : [alias, column];
857
+ }
858
+ /**
859
+ * Which columns of each join have to survive being packed into a struct:
860
+ * every column the segment's field usage names, plus each join's primary
861
+ * key, which the compiler reaches for as a distinct key without any
862
+ * expression in the query naming it.
863
+ */
864
+ packedColumnsByJoin() {
865
+ var _a;
866
+ if (this.packedColumns === undefined) {
867
+ const packed = new Map();
868
+ const needed = (found) => {
869
+ if (found === undefined)
870
+ return;
871
+ const [alias, column] = found;
872
+ const columns = packed.get(alias);
873
+ if (columns) {
874
+ columns.add(column);
875
+ }
876
+ else {
877
+ packed.set(alias, new Set([column]));
878
+ }
879
+ };
880
+ const usage = (0, malloy_types_1.isRawSegment)(this.firstSegment)
881
+ ? []
882
+ : ((_a = this.firstSegment.expandedFieldUsage) !== null && _a !== void 0 ? _a : []);
883
+ for (const { path } of usage) {
884
+ needed(this.packedColumnFor(this.parent, path));
885
+ }
886
+ // What a primary key needs is what it reads: a computed key's name is
887
+ // not a column, and the columns its expression reads are named nowhere
888
+ // else in the query.
889
+ for (const ji of this.rootResult.joins.values()) {
890
+ const primaryKey = ji.queryStruct.primaryKey();
891
+ if (primaryKey === undefined)
892
+ continue;
893
+ const keyDef = primaryKey.fieldDef;
894
+ if ((0, malloy_types_1.hasExpression)(keyDef)) {
895
+ for (const { path } of (0, malloy_types_1.fieldUsageFrom)(keyDef.refSummary)) {
896
+ needed(this.packedColumnFor(ji.queryStruct, path));
897
+ }
898
+ }
899
+ else {
900
+ needed(this.packedColumnFor(ji.queryStruct, [(0, malloy_types_1.activeName)(keyDef)]));
901
+ }
902
+ }
903
+ this.packedColumns = packed;
904
+ }
905
+ return this.packedColumns;
906
+ }
907
+ // What a join contributes to the SELECT of a filtered join's subquery: every
908
+ // column named outside the subquery, and the generated __distinct_key when
909
+ // the join needed one.
910
+ joinProjectionFieldList(ji) {
911
+ const packed = this.packedColumnsByJoin().get(ji.alias);
912
+ const dialect = ji.queryStruct.dialect;
913
+ const fieldList = (0, utils_1.getDialectFieldList)(ji.queryStruct.structDef, dialect).filter(d => packed === null || packed === void 0 ? void 0 : packed.has(d.rawName));
914
+ if (ji.makeUniqueKey) {
915
+ fieldList.push({
916
+ typeDef: { type: 'string' },
917
+ sqlExpression: dialect.sqlQuoteIdentifier('__distinct_key'),
918
+ rawName: '__distinct_key',
919
+ });
920
+ }
921
+ return fieldList;
922
+ }
749
923
  // BigQuery has wildcard psudo columns that are treated differently
750
924
  // SELECT * FROM xxx doesn't include these psuedo columns but we need them so
751
925
  // filters can get pushed down properly when generating a UNIQUE key.
@@ -773,9 +947,10 @@ class QueryQuery extends query_node_1.QueryField {
773
947
  // Joins
774
948
  let structSQL = this.getStructSourceSQL(qs, stageWriter);
775
949
  if ((0, malloy_types_1.isIndexSegment)(this.firstSegment)) {
776
- structSQL = this.parent.dialect.sqlSampleTable(structSQL, this.firstSegment.sample);
950
+ structSQL = this.parent.dialect.sqlSampleTable(structSQL, this.firstSegment.sample, qs.structDef.type === 'table');
777
951
  if (this.firstSegment.sample) {
778
- structSQL = stageWriter.addStage(`SELECT * from ${structSQL} as x limit 100000 `);
952
+ const d = this.parent.dialect;
953
+ structSQL = stageWriter.addStage(`SELECT * from ${structSQL} as x ${d.sqlOrderByLimit([], 100000)}`);
779
954
  }
780
955
  }
781
956
  if ((0, malloy_types_1.isBaseTable)(qs.structDef)) {
@@ -819,7 +994,6 @@ class QueryQuery extends query_node_1.QueryField {
819
994
  return result;
820
995
  }
821
996
  genereateSQLOrderBy(queryDef, resultStruct) {
822
- let s = '';
823
997
  // Collect array joins for dialects that need explicit ordering.
824
998
  // Only for project queries - reduce queries aggregate rows so individual
825
999
  // row order doesn't matter, and we can't ORDER BY columns not in GROUP BY.
@@ -833,24 +1007,24 @@ class QueryQuery extends query_node_1.QueryField {
833
1007
  // For project without explicit ordering, we still need array ordinality
834
1008
  // ordering if the dialect requires it
835
1009
  if (arrayJoins.length === 0) {
836
- return ''; // No default ordering for project.
1010
+ return []; // No default ordering for project.
837
1011
  }
838
1012
  }
839
1013
  // Intermediate results (in a pipeline or join) that have no limit, don't need an orderby
840
1014
  // Some database don't have this optimization.
841
1015
  if (this.fieldDef.pipeline.length > 1 && queryDef.limit === undefined) {
842
- return '';
1016
+ return [];
843
1017
  }
844
1018
  // ignore orderby if all aggregates.
845
1019
  if (resultStruct.getRepeatedResultType() === 'inline_all_numbers') {
846
- return '';
1020
+ return [];
847
1021
  }
848
1022
  // if we are in the last stage of a query and the query is a subquery
849
1023
  // and has no limit, ORDER BY is superfluous
850
1024
  if (this.isJoinedSubquery &&
851
1025
  this.fieldDef.pipeline.length === 1 &&
852
1026
  queryDef.limit === undefined) {
853
- return '';
1027
+ return [];
854
1028
  }
855
1029
  const orderBy = queryDef.orderBy || resultStruct.calculateDefaultOrderBy();
856
1030
  const o = [];
@@ -893,10 +1067,7 @@ class QueryQuery extends query_node_1.QueryField {
893
1067
  for (const aj of arrayJoins) {
894
1068
  o.push(`${aj.alias}_outer.__row_id ASC`);
895
1069
  }
896
- if (o.length > 0) {
897
- s = this.parent.dialect.sqlOrderBy(o, 'query') + '\n';
898
- }
899
- return s;
1070
+ return o;
900
1071
  }
901
1072
  /**
902
1073
  * The single-scan form, with no `group_set` fan-out: one `SELECT` over the
@@ -909,45 +1080,36 @@ class QueryQuery extends query_node_1.QueryField {
909
1080
  */
910
1081
  generateSimpleSQL(stageWriter) {
911
1082
  this.rootResult.emitsGroupSet = false;
912
- let s = '';
913
- s += 'SELECT \n';
914
- const fields = [];
1083
+ const limit = !(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit
1084
+ ? this.firstSegment.limit
1085
+ : undefined;
1086
+ let s = 'SELECT \n';
1087
+ // A projection groups nothing; a reduce groups its scalar result fields.
1088
+ const grouped = this.firstSegment.type === 'reduce';
1089
+ const columns = [];
915
1090
  const outputPipelinedSQL = [];
916
1091
  for (const [name, fi] of this.rootResult.allFields) {
917
1092
  const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
918
1093
  if (fi instanceof field_instance_1.FieldInstanceField) {
919
1094
  if (fi.fieldUsage.type === 'result') {
920
- fields.push(` ${fi.generateExpression()} as ${sqlName}`);
1095
+ columns.push(aliasedColumn(fi.generateExpression(), sqlName, {
1096
+ isDimension: grouped && (0, query_node_1.isScalarField)(fi.f),
1097
+ constant: !readsColumn(fi.f.fieldDef),
1098
+ }));
921
1099
  }
922
1100
  }
923
1101
  else if (fi instanceof field_instance_1.FieldInstanceResult) {
924
1102
  const turtle = this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL, true);
925
- fields.push(` ${turtle} as ${sqlName}`);
1103
+ columns.push(aliasedColumn(turtle, sqlName, { isDimension: false }));
926
1104
  }
927
1105
  }
928
- s += (0, utils_1.indent)(fields.join(',\n')) + '\n';
1106
+ s += (0, utils_1.indent)(columns.map(c => ` ${c.sql}`).join(',\n')) + '\n';
929
1107
  s += this.generateSQLJoins(stageWriter);
930
1108
  s += this.generateSQLFilters(this.rootResult, 'where').sql('where');
931
- // group by
932
- if (this.firstSegment.type === 'reduce') {
933
- const n = [];
934
- for (const field of this.rootResult.fields()) {
935
- const fi = field;
936
- if (fi.fieldUsage.type === 'result' && (0, query_node_1.isScalarField)(fi.f)) {
937
- n.push(fi.fieldUsage.resultIndex.toString());
938
- }
939
- }
940
- if (n.length > 0) {
941
- s += `GROUP BY ${n.join(',')}\n`;
942
- }
943
- }
1109
+ s += sqlGroupBy(this.parent.dialect, columns);
944
1110
  s += this.generateSQLFilters(this.rootResult, 'having').sql('having');
945
- // order by
946
- s += this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
947
- // limit
948
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
949
- s += `LIMIT ${this.firstSegment.limit}\n`;
950
- }
1111
+ // order by, limit
1112
+ s += this.parent.dialect.sqlOrderByLimit(this.genereateSQLOrderBy(this.firstSegment, this.rootResult), limit);
951
1113
  this.resultStage = stageWriter.addStage(s);
952
1114
  // Consumes any pipelined turtle output. The nests that reach here are
953
1115
  // single-stage (canUseSingleGroupSetSQL), so outputPipelinedSQL is empty and
@@ -1059,6 +1221,7 @@ class QueryQuery extends query_node_1.QueryField {
1059
1221
  });
1060
1222
  output.columns.push({
1061
1223
  sql: outputFieldName,
1224
+ expr: outputFieldName,
1062
1225
  name: outputName,
1063
1226
  isDimension: true,
1064
1227
  });
@@ -1067,6 +1230,7 @@ class QueryQuery extends query_node_1.QueryField {
1067
1230
  const outputFieldNameString = `__lateral_join_bag.${outputNameString}`;
1068
1231
  output.columns.push({
1069
1232
  sql: outputFieldNameString,
1233
+ expr: outputFieldNameString,
1070
1234
  name: outputNameString,
1071
1235
  isDimension: true,
1072
1236
  });
@@ -1079,19 +1243,14 @@ class QueryQuery extends query_node_1.QueryField {
1079
1243
  }
1080
1244
  else {
1081
1245
  // just treat it like a regular field.
1082
- output.columns.push({
1083
- sql: `${exp} as ${outputName}`,
1084
- name: outputName,
1246
+ output.columns.push(aliasedColumn(exp, outputName, {
1085
1247
  isDimension: true,
1086
- });
1248
+ constant: !readsColumn(fi.f.fieldDef),
1249
+ }));
1087
1250
  }
1088
1251
  }
1089
1252
  else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
1090
- output.columns.push({
1091
- sql: `${exp} as ${outputName}`,
1092
- name: outputName,
1093
- isDimension: false,
1094
- });
1253
+ output.columns.push(aliasedColumn(exp, outputName, { isDimension: false }));
1095
1254
  }
1096
1255
  }
1097
1256
  }
@@ -1101,11 +1260,7 @@ class QueryQuery extends query_node_1.QueryField {
1101
1260
  }
1102
1261
  else if (fi.firstSegment.type === 'project') {
1103
1262
  const s = this.generateTurtleSQL(fi, stageWriter, outputName, output.outputPipelinedSQL);
1104
- output.columns.push({
1105
- sql: `${s} as ${outputName}`,
1106
- name: outputName,
1107
- isDimension: false,
1108
- });
1263
+ output.columns.push(aliasedColumn(s, outputName, { isDimension: false }));
1109
1264
  }
1110
1265
  }
1111
1266
  }
@@ -1123,11 +1278,7 @@ class QueryQuery extends query_node_1.QueryField {
1123
1278
  }
1124
1279
  else {
1125
1280
  resultSet.hasHaving = true;
1126
- output.columns.push({
1127
- sql: `CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END as __delete__${resultSet.groupSet}`,
1128
- name: `__delete__${resultSet.groupSet}`,
1129
- isDimension: false,
1130
- });
1281
+ output.columns.push(aliasedColumn(`CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END`, `__delete__${resultSet.groupSet}`, { isDimension: false }));
1131
1282
  }
1132
1283
  }
1133
1284
  }
@@ -1306,7 +1457,14 @@ class QueryQuery extends query_node_1.QueryField {
1306
1457
  let from = this.generateSQLJoins(stageWriter);
1307
1458
  const wheres = this.generateSQLWhereTurtled();
1308
1459
  const f = {
1309
- columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
1460
+ columns: [
1461
+ {
1462
+ sql: 'group_set',
1463
+ expr: 'group_set',
1464
+ name: 'group_set',
1465
+ isDimension: true,
1466
+ },
1467
+ ],
1310
1468
  lateralJoinSQLExpressions: [],
1311
1469
  groupsAggregated: [],
1312
1470
  outputPipelinedSQL: [],
@@ -1316,7 +1474,7 @@ class QueryQuery extends query_node_1.QueryField {
1316
1474
  throw new malloy_compile_error_1.MalloyCompileError("Cannot use 'select:' in a stage that contains nested views. " +
1317
1475
  'Use `group_by:` or restructure the pipeline.', 'compiler-project-with-turtles', this.fieldDef.location);
1318
1476
  }
1319
- const groupBy = 'GROUP BY ' + groupByPositions(f.columns).join(',') + '\n';
1477
+ const groupBy = sqlGroupBy(this.parent.dialect, f.columns);
1320
1478
  from += this.parent.dialect.sqlGroupSetTable(this.maxGroupSet) + '\n';
1321
1479
  s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
1322
1480
  if (f.lateralJoinSQLExpressions.length > 0) {
@@ -1338,19 +1496,11 @@ class QueryQuery extends query_node_1.QueryField {
1338
1496
  if (fi.fieldUsage.type === 'result') {
1339
1497
  if ((0, query_node_1.isScalarField)(fi.f)) {
1340
1498
  const exp = (0, utils_1.caseGroup)(resultSet.groupSet > 0 ? resultSet.childGroups : [], sqlFieldName);
1341
- output.columns.push({
1342
- sql: `${exp} as ${sqlFieldName}`,
1343
- name: sqlFieldName,
1344
- isDimension: true,
1345
- });
1499
+ output.columns.push(aliasedColumn(exp, sqlFieldName, { isDimension: true }));
1346
1500
  }
1347
1501
  else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
1348
1502
  const exp = this.parent.dialect.sqlAnyValue(resultSet.groupSet, sqlFieldName);
1349
- output.columns.push({
1350
- sql: `${exp} as ${sqlFieldName}`,
1351
- name: sqlFieldName,
1352
- isDimension: false,
1353
- });
1503
+ output.columns.push(aliasedColumn(exp, sqlFieldName, { isDimension: false }));
1354
1504
  }
1355
1505
  }
1356
1506
  }
@@ -1365,11 +1515,7 @@ class QueryQuery extends query_node_1.QueryField {
1365
1515
  toGroup: resultSet.groupSet,
1366
1516
  });
1367
1517
  groupsToMap.push(fi.groupSet);
1368
- output.columns.push({
1369
- sql: `${s} as ${sqlFieldName}`,
1370
- name: sqlFieldName,
1371
- isDimension: false,
1372
- });
1518
+ output.columns.push(aliasedColumn(s, sqlFieldName, { isDimension: false }));
1373
1519
  }
1374
1520
  else {
1375
1521
  this.generateDepthNFields(depth, fi, output, stageWriter);
@@ -1386,12 +1532,10 @@ class QueryQuery extends query_node_1.QueryField {
1386
1532
  for (const m of output.groupsAggregated) {
1387
1533
  groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1388
1534
  }
1389
- groupSetSQL += 'ELSE group_set END as group_set';
1390
- output.columns[0] = {
1391
- sql: groupSetSQL,
1392
- name: 'group_set',
1535
+ groupSetSQL += 'ELSE group_set END';
1536
+ output.columns[0] = aliasedColumn(groupSetSQL, 'group_set', {
1393
1537
  isDimension: true,
1394
- };
1538
+ });
1395
1539
  }
1396
1540
  // For hasLateralColumnAliasInSelect, the remap is handled in
1397
1541
  // generateSQLDepthN to avoid adding it multiple times from recursion.
@@ -1400,7 +1544,14 @@ class QueryQuery extends query_node_1.QueryField {
1400
1544
  generateSQLDepthN(depth, stageWriter, stageName) {
1401
1545
  let s = 'SELECT \n';
1402
1546
  const f = {
1403
- columns: [{ sql: 'group_set', name: 'group_set', isDimension: true }],
1547
+ columns: [
1548
+ {
1549
+ sql: 'group_set',
1550
+ expr: 'group_set',
1551
+ name: 'group_set',
1552
+ isDimension: true,
1553
+ },
1554
+ ],
1404
1555
  lateralJoinSQLExpressions: [],
1405
1556
  groupsAggregated: [],
1406
1557
  outputPipelinedSQL: [],
@@ -1420,8 +1571,13 @@ class QueryQuery extends query_node_1.QueryField {
1420
1571
  for (const m of f.groupsAggregated) {
1421
1572
  groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1422
1573
  }
1423
- groupSetSQL += 'ELSE group_set END as __remapped_group_set';
1424
- f.columns[0] = { sql: groupSetSQL, name: 'group_set', isDimension: true };
1574
+ groupSetSQL += 'ELSE group_set END';
1575
+ f.columns[0] = {
1576
+ sql: `${groupSetSQL} as __remapped_group_set`,
1577
+ expr: groupSetSQL,
1578
+ name: 'group_set',
1579
+ isDimension: true,
1580
+ };
1425
1581
  }
1426
1582
  s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
1427
1583
  s += `FROM ${stageName}\n`;
@@ -1429,10 +1585,7 @@ class QueryQuery extends query_node_1.QueryField {
1429
1585
  if (where.length > 0) {
1430
1586
  s += `WHERE ${where}\n`;
1431
1587
  }
1432
- const dimensionPositions = groupByPositions(f.columns);
1433
- if (dimensionPositions.length > 0) {
1434
- s += `GROUP BY ${dimensionPositions.join(',')}\n`;
1435
- }
1588
+ s += sqlGroupBy(this.parent.dialect, f.columns);
1436
1589
  this.resultStage = stageWriter.addStage(s);
1437
1590
  if (needsRemapStage) {
1438
1591
  const cols = f.columns.slice(1).map(c => c.name);
@@ -1443,6 +1596,9 @@ class QueryQuery extends query_node_1.QueryField {
1443
1596
  return this.resultStage;
1444
1597
  }
1445
1598
  genereateSQLCombineTurtles(stageWriter, stage0Name) {
1599
+ const limit = !(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit
1600
+ ? this.firstSegment.limit
1601
+ : undefined;
1446
1602
  let s = 'SELECT\n';
1447
1603
  // Columns this combine stage emits. Unlike the grouped stages above, these
1448
1604
  // are the final output names (unsuffixed), not the group-set-suffixed form.
@@ -1454,15 +1610,13 @@ class QueryQuery extends query_node_1.QueryField {
1454
1610
  if (fi instanceof field_instance_1.FieldInstanceField) {
1455
1611
  if (fi.fieldUsage.type === 'result') {
1456
1612
  if ((0, query_node_1.isScalarField)(fi.f)) {
1457
- columns.push({
1458
- sql: this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`) + ` as ${sqlName}`,
1459
- name: sqlName,
1460
- isDimension: true,
1461
- });
1613
+ columns.push(aliasedColumn(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), sqlName, { isDimension: true }));
1462
1614
  }
1463
1615
  else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
1616
+ const lastTurtle = this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName);
1464
1617
  columns.push({
1465
- sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
1618
+ sql: lastTurtle,
1619
+ expr: lastTurtle,
1466
1620
  name: sqlName,
1467
1621
  isDimension: false,
1468
1622
  });
@@ -1471,15 +1625,13 @@ class QueryQuery extends query_node_1.QueryField {
1471
1625
  }
1472
1626
  else if (fi instanceof field_instance_1.FieldInstanceResult) {
1473
1627
  if (fi.firstSegment.type === 'reduce') {
1474
- columns.push({
1475
- sql: `${this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL)} as ${sqlName}`,
1476
- name: sqlName,
1477
- isDimension: false,
1478
- });
1628
+ columns.push(aliasedColumn(this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL), sqlName, { isDimension: false }));
1479
1629
  }
1480
1630
  else if (fi.firstSegment.type === 'project') {
1631
+ const lastTurtle = this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName);
1481
1632
  columns.push({
1482
- sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
1633
+ sql: lastTurtle,
1634
+ expr: lastTurtle,
1483
1635
  name: sqlName,
1484
1636
  isDimension: false,
1485
1637
  });
@@ -1491,27 +1643,18 @@ class QueryQuery extends query_node_1.QueryField {
1491
1643
  if (where.length > 0) {
1492
1644
  s += `WHERE ${where}\n`;
1493
1645
  }
1494
- const dimensionPositions = groupByPositions(columns);
1495
- if (dimensionPositions.length > 0) {
1496
- s += `GROUP BY ${dimensionPositions.join(',')}\n`;
1497
- }
1498
- // order by
1499
- const orderBySQL = this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
1500
- s += orderBySQL;
1501
- // limit
1502
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
1503
- s += `LIMIT ${this.firstSegment.limit}\n`;
1504
- }
1646
+ s += sqlGroupBy(this.parent.dialect, columns);
1647
+ // order by, limit
1648
+ const orderTerms = this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
1649
+ s += this.parent.dialect.sqlOrderByLimit(orderTerms, limit);
1505
1650
  this.resultStage = stageWriter.addStage(s);
1506
- this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, columns, orderBySQL);
1651
+ this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, columns, this.parent.dialect.sqlOrderByLimit(orderTerms, undefined));
1507
1652
  return this.resultStage;
1508
1653
  }
1509
1654
  // create a simplified version of the StructDef for dialects.
1510
1655
  buildDialectFieldList(resultStruct) {
1511
1656
  const dialectFieldList = [];
1512
1657
  for (const [name, field] of resultStruct.allFields) {
1513
- const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
1514
- //
1515
1658
  if (resultStruct.firstSegment.type === 'reduce' &&
1516
1659
  field instanceof field_instance_1.FieldInstanceResult) {
1517
1660
  const { structDef, repeatedResultType } = this.generateTurtlePipelineSQL(field, new stage_writer_1.StageWriter(true, undefined), '<nosource>');
@@ -1527,7 +1670,6 @@ class QueryQuery extends query_node_1.QueryField {
1527
1670
  typeDef: multiLineNest,
1528
1671
  sqlExpression: this.parent.dialect.sqlQuoteIdentifier(`${name}__${resultStruct.groupSet}`),
1529
1672
  rawName: name,
1530
- sqlOutputName: sqlName,
1531
1673
  });
1532
1674
  }
1533
1675
  else {
@@ -1541,7 +1683,6 @@ class QueryQuery extends query_node_1.QueryField {
1541
1683
  typeDef: oneLineNest,
1542
1684
  sqlExpression: this.parent.dialect.sqlQuoteIdentifier(`${name}__${resultStruct.groupSet}`),
1543
1685
  rawName: name,
1544
- sqlOutputName: sqlName,
1545
1686
  });
1546
1687
  }
1547
1688
  }
@@ -1552,7 +1693,6 @@ class QueryQuery extends query_node_1.QueryField {
1552
1693
  fieldDef: field.f.fieldDef,
1553
1694
  sqlExpression: this.parent.dialect.sqlQuoteIdentifier(`${name}__${resultStruct.groupSet}`),
1554
1695
  rawName: name,
1555
- sqlOutputName: sqlName,
1556
1696
  });
1557
1697
  }
1558
1698
  else if (resultStruct.firstSegment.type === 'project' &&
@@ -1562,7 +1702,6 @@ class QueryQuery extends query_node_1.QueryField {
1562
1702
  fieldDef: field.f.fieldDef,
1563
1703
  sqlExpression: field.generateExpression(),
1564
1704
  rawName: name,
1565
- sqlOutputName: sqlName,
1566
1705
  });
1567
1706
  }
1568
1707
  }
@@ -1684,7 +1823,7 @@ class QueryQuery extends query_node_1.QueryField {
1684
1823
  const inputStruct = {
1685
1824
  type: 'nest_source',
1686
1825
  name: '~pipe~',
1687
- pipeSQL: this.parent.dialect.sqlUnnestPipelineHead(repeatedResultType === 'inline_all_numbers', sourceSQLExpression, (0, utils_1.getDialectFieldList)(structDef)),
1826
+ pipeSQL: this.parent.dialect.sqlUnnestPipelineHead(repeatedResultType === 'inline_all_numbers', sourceSQLExpression, (0, utils_1.getDialectFieldList)(structDef, this.parent.dialect)),
1688
1827
  fields: structDef.fields,
1689
1828
  connection: structDef.connection,
1690
1829
  dialect: structDef.dialect,
@@ -1822,30 +1961,37 @@ class QueryQueryIndexStage extends QueryQuery {
1822
1961
  fields.push({ name, path, type: fi.f.fieldDef.type, expression });
1823
1962
  }
1824
1963
  }
1825
- let s = 'SELECT\n group_set,\n';
1826
- s += ' CASE group_set\n';
1827
- for (let i = 0; i < fields.length; i++) {
1828
- s += ` WHEN ${i} THEN '${fields[i].name}'\n`;
1829
- }
1830
- s += ` END as ${fieldNameColumn},\n`;
1831
- s += ' CASE group_set\n';
1832
- for (let i = 0; i < fields.length; i++) {
1833
- const path = pathToCol(fields[i].path);
1834
- s += ` WHEN ${i} THEN '${path}'\n`;
1835
- }
1836
- s += ` END as ${fieldPathColumn},\n`;
1837
- s += ' CASE group_set\n';
1838
- for (let i = 0; i < fields.length; i++) {
1839
- s += ` WHEN ${i} THEN '${fields[i].type}'\n`;
1840
- }
1841
- s += ` END as ${fieldTypeColumn},`;
1842
- s += ` CASE group_set WHEN 99999 THEN ${dialect.castToString('NULL')}\n`;
1843
- for (let i = 0; i < fields.length; i++) {
1844
- if (fields[i].type === 'string') {
1845
- s += ` WHEN ${i} THEN ${fields[i].expression}\n`;
1964
+ // The grouped columns: group_set and one CASE per index column.
1965
+ const caseOnGroupSet = (when, firstWhen) => {
1966
+ let c = 'CASE group_set\n';
1967
+ if (firstWhen !== undefined) {
1968
+ c += ` ${firstWhen}\n`;
1846
1969
  }
1847
- }
1848
- s += ` END as ${fieldValueColumn},\n`;
1970
+ for (let i = 0; i < fields.length; i++) {
1971
+ const value = when(i);
1972
+ if (value !== undefined) {
1973
+ c += ` WHEN ${i} THEN ${value}\n`;
1974
+ }
1975
+ }
1976
+ return c + ' END';
1977
+ };
1978
+ const grouped = [
1979
+ {
1980
+ sql: 'group_set',
1981
+ expr: 'group_set',
1982
+ name: 'group_set',
1983
+ isDimension: true,
1984
+ },
1985
+ aliasedColumn(caseOnGroupSet(i => `'${fields[i].name}'`), fieldNameColumn, { isDimension: true }),
1986
+ aliasedColumn(caseOnGroupSet(i => `'${pathToCol(fields[i].path)}'`), fieldPathColumn, { isDimension: true }),
1987
+ aliasedColumn(caseOnGroupSet(i => `'${fields[i].type}'`), fieldTypeColumn, { isDimension: true }),
1988
+ aliasedColumn(caseOnGroupSet(i => (fields[i].type === 'string' ? fields[i].expression : undefined), `WHEN 99999 THEN ${dialect.castToString('NULL')}`), fieldValueColumn, { isDimension: true }),
1989
+ ];
1990
+ const limit = !(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit
1991
+ ? this.firstSegment.limit
1992
+ : undefined;
1993
+ let s = 'SELECT\n';
1994
+ s += grouped.map(c => ` ${c.sql},\n`).join('');
1849
1995
  s += ` ${measureSQL} as ${weightColumn},\n`;
1850
1996
  // just in case we don't have any field types, force the case statement to have at least one value.
1851
1997
  s += " CASE group_set\n WHEN 99999 THEN ''";
@@ -1868,11 +2014,8 @@ class QueryQueryIndexStage extends QueryQuery {
1868
2014
  s += this.generateSQLJoins(stageWriter);
1869
2015
  s += dialect.sqlGroupSetTable(fields.length) + '\n';
1870
2016
  s += this.generateSQLFilters(this.rootResult, 'where').sql('where');
1871
- s += 'GROUP BY 1,2,3,4,5\n';
1872
- // limit
1873
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
1874
- s += `LIMIT ${this.firstSegment.limit}\n`;
1875
- }
2017
+ s += sqlGroupBy(dialect, grouped);
2018
+ s += dialect.sqlOrderByLimit([], limit);
1876
2019
  // console.log(s);
1877
2020
  const resultStage = stageWriter.addStage(s);
1878
2021
  this.resultStage = stageWriter.addStage(`SELECT