@malloydata/malloy 0.0.434 → 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 (47) hide show
  1. package/dist/api/foundation/compile.d.ts +0 -1
  2. package/dist/api/foundation/compile.js +0 -3
  3. package/dist/api/foundation/runtime.d.ts +0 -1
  4. package/dist/api/foundation/runtime.js +1 -23
  5. package/dist/api/foundation/types.d.ts +0 -1
  6. package/dist/dialect/dialect.d.ts +14 -1
  7. package/dist/dialect/dialect.js +34 -2
  8. package/dist/dialect/dialect_map.js +2 -0
  9. package/dist/dialect/index.d.ts +2 -1
  10. package/dist/dialect/index.js +3 -1
  11. package/dist/dialect/sqlserver/dialect_functions.d.ts +2 -0
  12. package/dist/dialect/sqlserver/dialect_functions.js +42 -0
  13. package/dist/dialect/sqlserver/function_overrides.d.ts +2 -0
  14. package/dist/dialect/sqlserver/function_overrides.js +55 -0
  15. package/dist/dialect/sqlserver/index.d.ts +1 -0
  16. package/dist/dialect/sqlserver/index.js +22 -0
  17. package/dist/dialect/sqlserver/sqlserver.d.ts +102 -0
  18. package/dist/dialect/sqlserver/sqlserver.js +538 -0
  19. package/dist/dialect/sqlserver/windows_zones.d.ts +1 -0
  20. package/dist/dialect/sqlserver/windows_zones.js +613 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +5 -3
  23. package/dist/lang/ast/source-elements/named-source.js +0 -2
  24. package/dist/lang/ast/source-elements/sql-source.js +0 -3
  25. package/dist/lang/ast/source-elements/table-source.js +1 -2
  26. package/dist/lang/ast/statements/import-statement.js +4 -0
  27. package/dist/lang/ast/types/malloy-element.d.ts +0 -7
  28. package/dist/lang/ast/types/malloy-element.js +0 -18
  29. package/dist/lang/parse-malloy.d.ts +17 -2
  30. package/dist/lang/parse-malloy.js +48 -14
  31. package/dist/lang/parse-tree-walkers/find-external-references.d.ts +3 -3
  32. package/dist/lang/parse-tree-walkers/find-external-references.js +17 -16
  33. package/dist/lang/test/test-translator.d.ts +10 -1
  34. package/dist/lang/test/test-translator.js +15 -1
  35. package/dist/model/expression_compiler.js +1 -1
  36. package/dist/model/query_query.d.ts +3 -1
  37. package/dist/model/query_query.js +157 -139
  38. package/dist/model/source_def_utils.d.ts +7 -1
  39. package/dist/model/source_def_utils.js +20 -0
  40. package/dist/test/index.js +16 -8
  41. package/dist/test/matchers.d.ts +1 -1
  42. package/dist/test/matchers.js +1 -1
  43. package/dist/test/test-models.js +6 -13
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/femto-config.motly +13 -1
  47. package/package.json +7 -5
@@ -34,9 +34,52 @@ 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
85
  const { sqlExpression, rawName } = f;
@@ -904,9 +947,10 @@ class QueryQuery extends query_node_1.QueryField {
904
947
  // Joins
905
948
  let structSQL = this.getStructSourceSQL(qs, stageWriter);
906
949
  if ((0, malloy_types_1.isIndexSegment)(this.firstSegment)) {
907
- structSQL = this.parent.dialect.sqlSampleTable(structSQL, this.firstSegment.sample);
950
+ structSQL = this.parent.dialect.sqlSampleTable(structSQL, this.firstSegment.sample, qs.structDef.type === 'table');
908
951
  if (this.firstSegment.sample) {
909
- 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)}`);
910
954
  }
911
955
  }
912
956
  if ((0, malloy_types_1.isBaseTable)(qs.structDef)) {
@@ -950,7 +994,6 @@ class QueryQuery extends query_node_1.QueryField {
950
994
  return result;
951
995
  }
952
996
  genereateSQLOrderBy(queryDef, resultStruct) {
953
- let s = '';
954
997
  // Collect array joins for dialects that need explicit ordering.
955
998
  // Only for project queries - reduce queries aggregate rows so individual
956
999
  // row order doesn't matter, and we can't ORDER BY columns not in GROUP BY.
@@ -964,24 +1007,24 @@ class QueryQuery extends query_node_1.QueryField {
964
1007
  // For project without explicit ordering, we still need array ordinality
965
1008
  // ordering if the dialect requires it
966
1009
  if (arrayJoins.length === 0) {
967
- return ''; // No default ordering for project.
1010
+ return []; // No default ordering for project.
968
1011
  }
969
1012
  }
970
1013
  // Intermediate results (in a pipeline or join) that have no limit, don't need an orderby
971
1014
  // Some database don't have this optimization.
972
1015
  if (this.fieldDef.pipeline.length > 1 && queryDef.limit === undefined) {
973
- return '';
1016
+ return [];
974
1017
  }
975
1018
  // ignore orderby if all aggregates.
976
1019
  if (resultStruct.getRepeatedResultType() === 'inline_all_numbers') {
977
- return '';
1020
+ return [];
978
1021
  }
979
1022
  // if we are in the last stage of a query and the query is a subquery
980
1023
  // and has no limit, ORDER BY is superfluous
981
1024
  if (this.isJoinedSubquery &&
982
1025
  this.fieldDef.pipeline.length === 1 &&
983
1026
  queryDef.limit === undefined) {
984
- return '';
1027
+ return [];
985
1028
  }
986
1029
  const orderBy = queryDef.orderBy || resultStruct.calculateDefaultOrderBy();
987
1030
  const o = [];
@@ -1024,10 +1067,7 @@ class QueryQuery extends query_node_1.QueryField {
1024
1067
  for (const aj of arrayJoins) {
1025
1068
  o.push(`${aj.alias}_outer.__row_id ASC`);
1026
1069
  }
1027
- if (o.length > 0) {
1028
- s = this.parent.dialect.sqlOrderBy(o, 'query') + '\n';
1029
- }
1030
- return s;
1070
+ return o;
1031
1071
  }
1032
1072
  /**
1033
1073
  * The single-scan form, with no `group_set` fan-out: one `SELECT` over the
@@ -1040,45 +1080,36 @@ class QueryQuery extends query_node_1.QueryField {
1040
1080
  */
1041
1081
  generateSimpleSQL(stageWriter) {
1042
1082
  this.rootResult.emitsGroupSet = false;
1043
- let s = '';
1044
- s += 'SELECT \n';
1045
- 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 = [];
1046
1090
  const outputPipelinedSQL = [];
1047
1091
  for (const [name, fi] of this.rootResult.allFields) {
1048
1092
  const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
1049
1093
  if (fi instanceof field_instance_1.FieldInstanceField) {
1050
1094
  if (fi.fieldUsage.type === 'result') {
1051
- 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
+ }));
1052
1099
  }
1053
1100
  }
1054
1101
  else if (fi instanceof field_instance_1.FieldInstanceResult) {
1055
1102
  const turtle = this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL, true);
1056
- fields.push(` ${turtle} as ${sqlName}`);
1103
+ columns.push(aliasedColumn(turtle, sqlName, { isDimension: false }));
1057
1104
  }
1058
1105
  }
1059
- s += (0, utils_1.indent)(fields.join(',\n')) + '\n';
1106
+ s += (0, utils_1.indent)(columns.map(c => ` ${c.sql}`).join(',\n')) + '\n';
1060
1107
  s += this.generateSQLJoins(stageWriter);
1061
1108
  s += this.generateSQLFilters(this.rootResult, 'where').sql('where');
1062
- // group by
1063
- if (this.firstSegment.type === 'reduce') {
1064
- const n = [];
1065
- for (const field of this.rootResult.fields()) {
1066
- const fi = field;
1067
- if (fi.fieldUsage.type === 'result' && (0, query_node_1.isScalarField)(fi.f)) {
1068
- n.push(fi.fieldUsage.resultIndex.toString());
1069
- }
1070
- }
1071
- if (n.length > 0) {
1072
- s += `GROUP BY ${n.join(',')}\n`;
1073
- }
1074
- }
1109
+ s += sqlGroupBy(this.parent.dialect, columns);
1075
1110
  s += this.generateSQLFilters(this.rootResult, 'having').sql('having');
1076
- // order by
1077
- s += this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
1078
- // limit
1079
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
1080
- s += `LIMIT ${this.firstSegment.limit}\n`;
1081
- }
1111
+ // order by, limit
1112
+ s += this.parent.dialect.sqlOrderByLimit(this.genereateSQLOrderBy(this.firstSegment, this.rootResult), limit);
1082
1113
  this.resultStage = stageWriter.addStage(s);
1083
1114
  // Consumes any pipelined turtle output. The nests that reach here are
1084
1115
  // single-stage (canUseSingleGroupSetSQL), so outputPipelinedSQL is empty and
@@ -1190,6 +1221,7 @@ class QueryQuery extends query_node_1.QueryField {
1190
1221
  });
1191
1222
  output.columns.push({
1192
1223
  sql: outputFieldName,
1224
+ expr: outputFieldName,
1193
1225
  name: outputName,
1194
1226
  isDimension: true,
1195
1227
  });
@@ -1198,6 +1230,7 @@ class QueryQuery extends query_node_1.QueryField {
1198
1230
  const outputFieldNameString = `__lateral_join_bag.${outputNameString}`;
1199
1231
  output.columns.push({
1200
1232
  sql: outputFieldNameString,
1233
+ expr: outputFieldNameString,
1201
1234
  name: outputNameString,
1202
1235
  isDimension: true,
1203
1236
  });
@@ -1210,19 +1243,14 @@ class QueryQuery extends query_node_1.QueryField {
1210
1243
  }
1211
1244
  else {
1212
1245
  // just treat it like a regular field.
1213
- output.columns.push({
1214
- sql: `${exp} as ${outputName}`,
1215
- name: outputName,
1246
+ output.columns.push(aliasedColumn(exp, outputName, {
1216
1247
  isDimension: true,
1217
- });
1248
+ constant: !readsColumn(fi.f.fieldDef),
1249
+ }));
1218
1250
  }
1219
1251
  }
1220
1252
  else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
1221
- output.columns.push({
1222
- sql: `${exp} as ${outputName}`,
1223
- name: outputName,
1224
- isDimension: false,
1225
- });
1253
+ output.columns.push(aliasedColumn(exp, outputName, { isDimension: false }));
1226
1254
  }
1227
1255
  }
1228
1256
  }
@@ -1232,11 +1260,7 @@ class QueryQuery extends query_node_1.QueryField {
1232
1260
  }
1233
1261
  else if (fi.firstSegment.type === 'project') {
1234
1262
  const s = this.generateTurtleSQL(fi, stageWriter, outputName, output.outputPipelinedSQL);
1235
- output.columns.push({
1236
- sql: `${s} as ${outputName}`,
1237
- name: outputName,
1238
- isDimension: false,
1239
- });
1263
+ output.columns.push(aliasedColumn(s, outputName, { isDimension: false }));
1240
1264
  }
1241
1265
  }
1242
1266
  }
@@ -1254,11 +1278,7 @@ class QueryQuery extends query_node_1.QueryField {
1254
1278
  }
1255
1279
  else {
1256
1280
  resultSet.hasHaving = true;
1257
- output.columns.push({
1258
- sql: `CASE WHEN group_set=${resultSet.groupSet} THEN CASE WHEN ${having.sql()} THEN 0 ELSE 1 END END as __delete__${resultSet.groupSet}`,
1259
- name: `__delete__${resultSet.groupSet}`,
1260
- isDimension: false,
1261
- });
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 }));
1262
1282
  }
1263
1283
  }
1264
1284
  }
@@ -1437,7 +1457,14 @@ class QueryQuery extends query_node_1.QueryField {
1437
1457
  let from = this.generateSQLJoins(stageWriter);
1438
1458
  const wheres = this.generateSQLWhereTurtled();
1439
1459
  const f = {
1440
- 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
+ ],
1441
1468
  lateralJoinSQLExpressions: [],
1442
1469
  groupsAggregated: [],
1443
1470
  outputPipelinedSQL: [],
@@ -1447,7 +1474,7 @@ class QueryQuery extends query_node_1.QueryField {
1447
1474
  throw new malloy_compile_error_1.MalloyCompileError("Cannot use 'select:' in a stage that contains nested views. " +
1448
1475
  'Use `group_by:` or restructure the pipeline.', 'compiler-project-with-turtles', this.fieldDef.location);
1449
1476
  }
1450
- const groupBy = 'GROUP BY ' + groupByPositions(f.columns).join(',') + '\n';
1477
+ const groupBy = sqlGroupBy(this.parent.dialect, f.columns);
1451
1478
  from += this.parent.dialect.sqlGroupSetTable(this.maxGroupSet) + '\n';
1452
1479
  s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
1453
1480
  if (f.lateralJoinSQLExpressions.length > 0) {
@@ -1469,19 +1496,11 @@ class QueryQuery extends query_node_1.QueryField {
1469
1496
  if (fi.fieldUsage.type === 'result') {
1470
1497
  if ((0, query_node_1.isScalarField)(fi.f)) {
1471
1498
  const exp = (0, utils_1.caseGroup)(resultSet.groupSet > 0 ? resultSet.childGroups : [], sqlFieldName);
1472
- output.columns.push({
1473
- sql: `${exp} as ${sqlFieldName}`,
1474
- name: sqlFieldName,
1475
- isDimension: true,
1476
- });
1499
+ output.columns.push(aliasedColumn(exp, sqlFieldName, { isDimension: true }));
1477
1500
  }
1478
1501
  else if ((0, query_node_1.isBasicCalculation)(fi.f)) {
1479
1502
  const exp = this.parent.dialect.sqlAnyValue(resultSet.groupSet, sqlFieldName);
1480
- output.columns.push({
1481
- sql: `${exp} as ${sqlFieldName}`,
1482
- name: sqlFieldName,
1483
- isDimension: false,
1484
- });
1503
+ output.columns.push(aliasedColumn(exp, sqlFieldName, { isDimension: false }));
1485
1504
  }
1486
1505
  }
1487
1506
  }
@@ -1496,11 +1515,7 @@ class QueryQuery extends query_node_1.QueryField {
1496
1515
  toGroup: resultSet.groupSet,
1497
1516
  });
1498
1517
  groupsToMap.push(fi.groupSet);
1499
- output.columns.push({
1500
- sql: `${s} as ${sqlFieldName}`,
1501
- name: sqlFieldName,
1502
- isDimension: false,
1503
- });
1518
+ output.columns.push(aliasedColumn(s, sqlFieldName, { isDimension: false }));
1504
1519
  }
1505
1520
  else {
1506
1521
  this.generateDepthNFields(depth, fi, output, stageWriter);
@@ -1517,12 +1532,10 @@ class QueryQuery extends query_node_1.QueryField {
1517
1532
  for (const m of output.groupsAggregated) {
1518
1533
  groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1519
1534
  }
1520
- groupSetSQL += 'ELSE group_set END as group_set';
1521
- output.columns[0] = {
1522
- sql: groupSetSQL,
1523
- name: 'group_set',
1535
+ groupSetSQL += 'ELSE group_set END';
1536
+ output.columns[0] = aliasedColumn(groupSetSQL, 'group_set', {
1524
1537
  isDimension: true,
1525
- };
1538
+ });
1526
1539
  }
1527
1540
  // For hasLateralColumnAliasInSelect, the remap is handled in
1528
1541
  // generateSQLDepthN to avoid adding it multiple times from recursion.
@@ -1531,7 +1544,14 @@ class QueryQuery extends query_node_1.QueryField {
1531
1544
  generateSQLDepthN(depth, stageWriter, stageName) {
1532
1545
  let s = 'SELECT \n';
1533
1546
  const f = {
1534
- 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
+ ],
1535
1555
  lateralJoinSQLExpressions: [],
1536
1556
  groupsAggregated: [],
1537
1557
  outputPipelinedSQL: [],
@@ -1551,8 +1571,13 @@ class QueryQuery extends query_node_1.QueryField {
1551
1571
  for (const m of f.groupsAggregated) {
1552
1572
  groupSetSQL += `WHEN group_set=${m.fromGroup} THEN ${m.toGroup} `;
1553
1573
  }
1554
- groupSetSQL += 'ELSE group_set END as __remapped_group_set';
1555
- 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
+ };
1556
1581
  }
1557
1582
  s += (0, utils_1.indent)(f.columns.map(c => c.sql).join(',\n')) + '\n';
1558
1583
  s += `FROM ${stageName}\n`;
@@ -1560,10 +1585,7 @@ class QueryQuery extends query_node_1.QueryField {
1560
1585
  if (where.length > 0) {
1561
1586
  s += `WHERE ${where}\n`;
1562
1587
  }
1563
- const dimensionPositions = groupByPositions(f.columns);
1564
- if (dimensionPositions.length > 0) {
1565
- s += `GROUP BY ${dimensionPositions.join(',')}\n`;
1566
- }
1588
+ s += sqlGroupBy(this.parent.dialect, f.columns);
1567
1589
  this.resultStage = stageWriter.addStage(s);
1568
1590
  if (needsRemapStage) {
1569
1591
  const cols = f.columns.slice(1).map(c => c.name);
@@ -1574,6 +1596,9 @@ class QueryQuery extends query_node_1.QueryField {
1574
1596
  return this.resultStage;
1575
1597
  }
1576
1598
  genereateSQLCombineTurtles(stageWriter, stage0Name) {
1599
+ const limit = !(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit
1600
+ ? this.firstSegment.limit
1601
+ : undefined;
1577
1602
  let s = 'SELECT\n';
1578
1603
  // Columns this combine stage emits. Unlike the grouped stages above, these
1579
1604
  // are the final output names (unsuffixed), not the group-set-suffixed form.
@@ -1585,15 +1610,13 @@ class QueryQuery extends query_node_1.QueryField {
1585
1610
  if (fi instanceof field_instance_1.FieldInstanceField) {
1586
1611
  if (fi.fieldUsage.type === 'result') {
1587
1612
  if ((0, query_node_1.isScalarField)(fi.f)) {
1588
- columns.push({
1589
- sql: this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`) + ` as ${sqlName}`,
1590
- name: sqlName,
1591
- isDimension: true,
1592
- });
1613
+ columns.push(aliasedColumn(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), sqlName, { isDimension: true }));
1593
1614
  }
1594
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);
1595
1617
  columns.push({
1596
- sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
1618
+ sql: lastTurtle,
1619
+ expr: lastTurtle,
1597
1620
  name: sqlName,
1598
1621
  isDimension: false,
1599
1622
  });
@@ -1602,15 +1625,13 @@ class QueryQuery extends query_node_1.QueryField {
1602
1625
  }
1603
1626
  else if (fi instanceof field_instance_1.FieldInstanceResult) {
1604
1627
  if (fi.firstSegment.type === 'reduce') {
1605
- columns.push({
1606
- sql: `${this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL)} as ${sqlName}`,
1607
- name: sqlName,
1608
- isDimension: false,
1609
- });
1628
+ columns.push(aliasedColumn(this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL), sqlName, { isDimension: false }));
1610
1629
  }
1611
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);
1612
1632
  columns.push({
1613
- sql: this.parent.dialect.sqlAnyValueLastTurtle(this.parent.dialect.sqlQuoteIdentifier(`${name}__${this.rootResult.groupSet}`), this.rootResult.groupSet, sqlName),
1633
+ sql: lastTurtle,
1634
+ expr: lastTurtle,
1614
1635
  name: sqlName,
1615
1636
  isDimension: false,
1616
1637
  });
@@ -1622,19 +1643,12 @@ class QueryQuery extends query_node_1.QueryField {
1622
1643
  if (where.length > 0) {
1623
1644
  s += `WHERE ${where}\n`;
1624
1645
  }
1625
- const dimensionPositions = groupByPositions(columns);
1626
- if (dimensionPositions.length > 0) {
1627
- s += `GROUP BY ${dimensionPositions.join(',')}\n`;
1628
- }
1629
- // order by
1630
- const orderBySQL = this.genereateSQLOrderBy(this.firstSegment, this.rootResult);
1631
- s += orderBySQL;
1632
- // limit
1633
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
1634
- s += `LIMIT ${this.firstSegment.limit}\n`;
1635
- }
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);
1636
1650
  this.resultStage = stageWriter.addStage(s);
1637
- 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));
1638
1652
  return this.resultStage;
1639
1653
  }
1640
1654
  // create a simplified version of the StructDef for dialects.
@@ -1947,30 +1961,37 @@ class QueryQueryIndexStage extends QueryQuery {
1947
1961
  fields.push({ name, path, type: fi.f.fieldDef.type, expression });
1948
1962
  }
1949
1963
  }
1950
- let s = 'SELECT\n group_set,\n';
1951
- s += ' CASE group_set\n';
1952
- for (let i = 0; i < fields.length; i++) {
1953
- s += ` WHEN ${i} THEN '${fields[i].name}'\n`;
1954
- }
1955
- s += ` END as ${fieldNameColumn},\n`;
1956
- s += ' CASE group_set\n';
1957
- for (let i = 0; i < fields.length; i++) {
1958
- const path = pathToCol(fields[i].path);
1959
- s += ` WHEN ${i} THEN '${path}'\n`;
1960
- }
1961
- s += ` END as ${fieldPathColumn},\n`;
1962
- s += ' CASE group_set\n';
1963
- for (let i = 0; i < fields.length; i++) {
1964
- s += ` WHEN ${i} THEN '${fields[i].type}'\n`;
1965
- }
1966
- s += ` END as ${fieldTypeColumn},`;
1967
- s += ` CASE group_set WHEN 99999 THEN ${dialect.castToString('NULL')}\n`;
1968
- for (let i = 0; i < fields.length; i++) {
1969
- if (fields[i].type === 'string') {
1970
- 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`;
1971
1969
  }
1972
- }
1973
- 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('');
1974
1995
  s += ` ${measureSQL} as ${weightColumn},\n`;
1975
1996
  // just in case we don't have any field types, force the case statement to have at least one value.
1976
1997
  s += " CASE group_set\n WHEN 99999 THEN ''";
@@ -1993,11 +2014,8 @@ class QueryQueryIndexStage extends QueryQuery {
1993
2014
  s += this.generateSQLJoins(stageWriter);
1994
2015
  s += dialect.sqlGroupSetTable(fields.length) + '\n';
1995
2016
  s += this.generateSQLFilters(this.rootResult, 'where').sql('where');
1996
- s += 'GROUP BY 1,2,3,4,5\n';
1997
- // limit
1998
- if (!(0, malloy_types_1.isRawSegment)(this.firstSegment) && this.firstSegment.limit) {
1999
- s += `LIMIT ${this.firstSegment.limit}\n`;
2000
- }
2017
+ s += sqlGroupBy(dialect, grouped);
2018
+ s += dialect.sqlOrderByLimit([], limit);
2001
2019
  // console.log(s);
2002
2020
  const resultStage = stageWriter.addStage(s);
2003
2021
  this.resultStage = stageWriter.addStage(`SELECT
@@ -7,7 +7,13 @@
7
7
  * fields they need, never using spread, so identity is not propagated onto a
8
8
  * freshly built source.
9
9
  */
10
- import type { BuildID, FieldDef, GivenID, ModelDef, PersistableSourceDef, Query, QuerySourceDef, SourceDef, SourceID, SourceRegistryEntry, SourceRegistryValue, SQLPhraseSegment, SQLSourceDef, TableSourceDef } from './malloy_types';
10
+ import type { BuildID, FieldDef, GivenID, ModelDef, NamedModelObject, PersistableSourceDef, Query, QuerySourceDef, SourceDef, SourceID, SourceRegistryEntry, SourceRegistryValue, SQLPhraseSegment, SQLSourceDef, TableSourceDef } from './malloy_types';
11
+ /**
12
+ * The dialect an entry commits whoever imports it to, if any. A query begins
13
+ * at a source, which is either inline in the query or named in the model the
14
+ * query came from.
15
+ */
16
+ export declare function dialectOfEntry(entry: NamedModelObject, from: ModelDef): string | undefined;
11
17
  export declare function mkSourceID(name: string, url: string | undefined): SourceID;
12
18
  export declare function mkGivenID(name: string, url: string | undefined): GivenID;
13
19
  /**
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.dialectOfEntry = dialectOfEntry;
7
8
  exports.mkSourceID = mkSourceID;
8
9
  exports.mkGivenID = mkGivenID;
9
10
  exports.mkBuildID = mkBuildID;
@@ -17,6 +18,25 @@ exports.registerSource = registerSource;
17
18
  exports.hasSourceRegistryEntry = hasSourceRegistryEntry;
18
19
  const utils_1 = require("./utils");
19
20
  const malloy_types_1 = require("./malloy_types");
21
+ /**
22
+ * The dialect an entry commits whoever imports it to, if any. A query begins
23
+ * at a source, which is either inline in the query or named in the model the
24
+ * query came from.
25
+ */
26
+ function dialectOfEntry(entry, from) {
27
+ if ((0, malloy_types_1.isSourceDef)(entry)) {
28
+ return entry.dialect;
29
+ }
30
+ if (entry.type === 'query') {
31
+ const head = entry.structRef;
32
+ if (typeof head === 'string') {
33
+ const named = (0, malloy_types_1.safeRecordGet)(from.contents, head);
34
+ return named && (0, malloy_types_1.isSourceDef)(named) ? named.dialect : undefined;
35
+ }
36
+ return head.dialect;
37
+ }
38
+ return undefined;
39
+ }
20
40
  function mkSourceID(name, url) {
21
41
  return `${name}@${url !== null && url !== void 0 ? url : 'unknown'}`;
22
42
  }
@@ -113,14 +113,20 @@ function databasesFromEnvironmentOr(defaultDatabases) {
113
113
  : defaultDatabases;
114
114
  }
115
115
  /**
116
- * A replacement for [describe()] that mimics [describe.skip()]
116
+ * A replacement for [describe()] that mimics [describe.skip()].
117
+ *
118
+ * Built on demand, not at module scope: this module is also imported outside
119
+ * jest (scripts that construct a test connection), where the jest globals do
120
+ * not exist.
117
121
  */
118
- const describeSkip = Object.assign((name, fn) => describe.skip(name, fn), {
119
- skip: describe.skip,
120
- // eslint-disable-next-line no-restricted-properties
121
- only: describe.only,
122
- each: (() => () => it.skip('skipped', () => { })),
123
- });
122
+ function makeDescribeSkip() {
123
+ return Object.assign((name, fn) => describe.skip(name, fn), {
124
+ skip: describe.skip,
125
+ // eslint-disable-next-line no-restricted-properties
126
+ only: describe.only,
127
+ each: (() => () => it.skip('skipped', () => { })),
128
+ });
129
+ }
124
130
  /**
125
131
  * Confirms that one or more of the databases being tested overlaps with
126
132
  * the databases a test suite can accept. If there is overlap, return a tuple
@@ -130,7 +136,9 @@ const describeSkip = Object.assign((name, fn) => describe.skip(name, fn), {
130
136
  function describeIfDatabaseAvailable(acceptableDatabases) {
131
137
  const currentDatabases = databasesFromEnvironmentOr(acceptableDatabases);
132
138
  const overlap = acceptableDatabases.filter(d => currentDatabases.includes(d));
133
- return overlap.length > 0 ? [describe, overlap] : [describeSkip, overlap];
139
+ return overlap.length > 0
140
+ ? [describe, overlap]
141
+ : [makeDescribeSkip(), overlap];
134
142
  }
135
143
  function brokenIn(dialectName, connectionName) {
136
144
  return dialectName === connectionName;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Import this module to register Jest matchers (toMatchResult, toEqualResult).
3
3
  *
4
- * Usage in jest.config.js:
4
+ * Usage in jest.config.ts:
5
5
  * setupFilesAfterEnv: ['@malloydata/malloy/test/matchers']
6
6
  *
7
7
  * Or import directly in a test file:
@@ -7,7 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  /**
8
8
  * Import this module to register Jest matchers (toMatchResult, toEqualResult).
9
9
  *
10
- * Usage in jest.config.js:
10
+ * Usage in jest.config.ts:
11
11
  * setupFilesAfterEnv: ['@malloydata/malloy/test/matchers']
12
12
  *
13
13
  * Or import directly in a test file:
@@ -229,19 +229,12 @@ function generateSQL(dialect, rows) {
229
229
  .join(', ');
230
230
  const innerQuery = selects.join('\nUNION ALL ');
231
231
  // Generate ORDER BY based on dialect preference
232
- let orderByClause;
233
- if (dialect.orderByClause === 'ordinal') {
234
- orderByClause = `ORDER BY ${columnList.length + 1}`;
235
- }
236
- else if (dialect.orderByClause === 'output_name') {
237
- orderByClause = `ORDER BY ${rowIdColumn}`;
238
- }
239
- else {
240
- orderByClause = `ORDER BY ${rowIdColumn}`;
241
- }
242
- // Presto/Trino ignores ORDER BY on a subquery without LIMIT
243
- orderByClause += ` LIMIT ${rows.length}`;
244
- const sql = `SELECT ${quotedColumns}\nFROM (\n SELECT *\n FROM (\n${innerQuery}\n ) AS t_sorted\n ${orderByClause}\n) AS t_result\n`;
232
+ const orderTerm = dialect.orderByClause === 'ordinal'
233
+ ? `${columnList.length + 1}`
234
+ : rowIdColumn;
235
+ // Presto/Trino ignores ORDER BY on a subquery without a row limit
236
+ const orderByClause = dialect.sqlOrderByLimit([orderTerm], rows.length);
237
+ const sql = `SELECT ${quotedColumns}\nFROM (\n SELECT *\n FROM (\n${innerQuery}\n ) AS t_sorted\n ${orderByClause}) AS t_result\n`;
245
238
  return sql;
246
239
  }
247
240
  /**
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.434";
1
+ export declare const MALLOY_VERSION = "0.0.435";
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.434';
5
+ exports.MALLOY_VERSION = '0.0.435';
6
6
  //# sourceMappingURL=version.js.map