@malloydata/malloy 0.0.413 → 0.0.415

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 (36) hide show
  1. package/dist/api/foundation/core.d.ts +21 -0
  2. package/dist/api/foundation/core.js +30 -0
  3. package/dist/dialect/databricks/databricks.d.ts +1 -1
  4. package/dist/dialect/databricks/databricks.js +3 -2
  5. package/dist/dialect/dialect.d.ts +11 -1
  6. package/dist/dialect/dialect.js +18 -0
  7. package/dist/dialect/duckdb/duckdb.d.ts +1 -1
  8. package/dist/dialect/duckdb/duckdb.js +3 -2
  9. package/dist/dialect/mysql/mysql.d.ts +1 -1
  10. package/dist/dialect/mysql/mysql.js +9 -5
  11. package/dist/dialect/postgres/postgres.d.ts +1 -1
  12. package/dist/dialect/postgres/postgres.js +3 -2
  13. package/dist/dialect/snowflake/snowflake.d.ts +1 -1
  14. package/dist/dialect/snowflake/snowflake.js +4 -2
  15. package/dist/dialect/standardsql/standardsql.d.ts +1 -1
  16. package/dist/dialect/standardsql/standardsql.js +4 -2
  17. package/dist/dialect/trino/trino.d.ts +1 -1
  18. package/dist/dialect/trino/trino.js +4 -2
  19. package/dist/lang/ast/field-space/dynamic-space.js +5 -0
  20. package/dist/lang/ast/source-elements/named-source.js +4 -1
  21. package/dist/lang/ast/statements/define-source.js +9 -2
  22. package/dist/lang/ast/types/malloy-element.js +3 -4
  23. package/dist/model/expression_compiler.js +2 -2
  24. package/dist/model/field_instance.d.ts +1 -0
  25. package/dist/model/field_instance.js +5 -0
  26. package/dist/model/index.d.ts +2 -1
  27. package/dist/model/index.js +3 -1
  28. package/dist/model/malloy_types.d.ts +25 -6
  29. package/dist/model/persist_utils.js +1 -1
  30. package/dist/model/query_query.d.ts +19 -1
  31. package/dist/model/query_query.js +81 -10
  32. package/dist/model/source_def_utils.d.ts +31 -7
  33. package/dist/model/source_def_utils.js +39 -9
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +4 -4
@@ -119,6 +119,27 @@ export declare class Explore extends Entity implements Taggable {
119
119
  * model annotations. */
120
120
  get _modelDef(): ModelDef;
121
121
  get source(): Explore | undefined;
122
+ /**
123
+ * THIS IS A HIGHLY EXPERIMENTAL API AND MAY VANISH OR CHANGE WITHOUT NOTICE
124
+ *
125
+ * If this source was created as an unmodified reference to another source, a
126
+ * stable identifier of the source it refers to; undefined when this source
127
+ * defines its own shape. Two sources that refer to the same thing share this
128
+ * id, so it can be compared to tell whether two otherwise un-nameable sources
129
+ * are the same — even when the referenced source can't be named here.
130
+ */
131
+ get referenceSourceID(): string | undefined;
132
+ /**
133
+ * THIS IS A HIGHLY EXPERIMENTAL API AND MAY VANISH OR CHANGE WITHOUT NOTICE
134
+ *
135
+ * If this source was created as an unmodified reference to a source that is in
136
+ * this model's namespace (`source: a is b`, or a plain join), return that
137
+ * source as it appears in the namespace — read `.name` for the name it goes by
138
+ * here. Returns undefined when this source defines its own shape (a table,
139
+ * SQL, query, or modified/extended source), or when the referenced source is
140
+ * not in this model's namespace.
141
+ */
142
+ referencedSource(): Explore | undefined;
122
143
  isIntrinsic(): boolean;
123
144
  isExploreField(): this is ExploreField;
124
145
  /** @deprecated Use `.annotations.parseAsTag(route)`. */
@@ -172,6 +172,36 @@ class Explore extends Entity {
172
172
  get source() {
173
173
  return this.sourceExplore;
174
174
  }
175
+ /**
176
+ * THIS IS A HIGHLY EXPERIMENTAL API AND MAY VANISH OR CHANGE WITHOUT NOTICE
177
+ *
178
+ * If this source was created as an unmodified reference to another source, a
179
+ * stable identifier of the source it refers to; undefined when this source
180
+ * defines its own shape. Two sources that refer to the same thing share this
181
+ * id, so it can be compared to tell whether two otherwise un-nameable sources
182
+ * are the same — even when the referenced source can't be named here.
183
+ */
184
+ get referenceSourceID() {
185
+ return (0, model_1.isSourceDef)(this._structDef)
186
+ ? this._structDef.referenceID
187
+ : undefined;
188
+ }
189
+ /**
190
+ * THIS IS A HIGHLY EXPERIMENTAL API AND MAY VANISH OR CHANGE WITHOUT NOTICE
191
+ *
192
+ * If this source was created as an unmodified reference to a source that is in
193
+ * this model's namespace (`source: a is b`, or a plain join), return that
194
+ * source as it appears in the namespace — read `.name` for the name it goes by
195
+ * here. Returns undefined when this source defines its own shape (a table,
196
+ * SQL, query, or modified/extended source), or when the referenced source is
197
+ * not in this model's namespace.
198
+ */
199
+ referencedSource() {
200
+ if (!(0, model_1.isSourceDef)(this._structDef))
201
+ return undefined;
202
+ const ref = (0, source_def_utils_1.sourceNamespaceReference)(this._ownerModelDef, this._structDef);
203
+ return ref ? new Explore(this._ownerModelDef, ref.source) : undefined;
204
+ }
175
205
  isIntrinsic() {
176
206
  if ((0, model_1.isAtomicFieldType)(this._structDef.type)) {
177
207
  return !('e' in this._structDef);
@@ -49,7 +49,7 @@ export declare class DatabricksDialect extends Dialect {
49
49
  sqlOrderBy(orderTerms: string[], obr?: OrderByRequest): string;
50
50
  sqlAnyValue(groupSet: number, fieldName: string): string;
51
51
  private buildNamedStructExpression;
52
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
52
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
53
53
  private buildArraySortComparator;
54
54
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
55
55
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
@@ -172,8 +172,9 @@ class DatabricksDialect extends dialect_1.Dialect {
172
172
  }
173
173
  sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
174
174
  const namedStruct = this.buildNamedStructExpression(fieldList);
175
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
176
- const collectExpr = `COLLECT_LIST(${namedStruct}) FILTER (WHERE group_set=${groupSet}${filter})`;
175
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
176
+ const filterClause = cond ? ` FILTER (WHERE ${cond})` : '';
177
+ const collectExpr = `COLLECT_LIST(${namedStruct})${filterClause}`;
177
178
  // COLLECT_LIST is unordered, so ordering is done post-aggregation via
178
179
  // ARRAY_SORT — the limit must slice the *ordered* array.
179
180
  const ordered = !orderBy || orderBy.length === 0
@@ -63,6 +63,16 @@ export type FieldReferenceType = 'table' | 'nest source' | 'array[scalar]' | 'ar
63
63
  export declare const dayIndex: number;
64
64
  export declare function inDays(units: string): boolean;
65
65
  export declare function qtz(qi: QueryInfo): string | undefined;
66
+ /**
67
+ * The per-element condition for an aggregate turtle (sqlAggregateTurtle). On
68
+ * the normal path it gates each row by its group_set (and a projection's own
69
+ * `where:`). On the single-group-set fast path `groupSet` is `undefined` --
70
+ * there is no group_set column, every row is in the one group -- so the
71
+ * condition is just the `where:`, or `undefined` to include unconditionally.
72
+ * Callers wrap the result in whatever form the dialect uses (`FILTER (WHERE
73
+ * ...)`, `CASE WHEN ... THEN`, `IF(...)`).
74
+ */
75
+ export declare function turtleGroupSetCondition(groupSet: number | undefined, filterSQL: string | undefined): string | undefined;
66
76
  export type OrderByClauseType = 'output_name' | 'ordinal' | 'expression';
67
77
  export type OrderByRequest = 'query' | 'turtle' | 'analytical';
68
78
  export type BooleanTypeSupport = 'supported' | 'simulated' | 'none';
@@ -176,7 +186,7 @@ export declare abstract class Dialect {
176
186
  sqlValidateTableName(input: string): ValidateTablePathResult;
177
187
  abstract sqlGroupSetTable(groupSetCount: number): string;
178
188
  abstract sqlAnyValue(groupSet: number, fieldName: string): string;
179
- abstract sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
189
+ abstract sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
180
190
  sqlTurtleOrderByClause(orderBy: CompiledOrderBy[]): string;
181
191
  abstract sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
182
192
  abstract sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.Dialect = exports.dayIndex = exports.EscapeStyle = exports.MAX_DECIMAL38 = exports.MIN_DECIMAL38 = exports.MAX_INT128 = exports.MIN_INT128 = exports.MAX_INT64 = exports.MIN_INT64 = exports.MAX_INT32 = exports.MIN_INT32 = void 0;
8
8
  exports.inDays = inDays;
9
9
  exports.qtz = qtz;
10
+ exports.turtleGroupSetCondition = turtleGroupSetCondition;
10
11
  const malloy_types_1 = require("../model/malloy_types");
11
12
  const table_path_1 = require("./table-path");
12
13
  /*
@@ -61,6 +62,23 @@ function qtz(qi) {
61
62
  }
62
63
  return tz;
63
64
  }
65
+ /**
66
+ * The per-element condition for an aggregate turtle (sqlAggregateTurtle). On
67
+ * the normal path it gates each row by its group_set (and a projection's own
68
+ * `where:`). On the single-group-set fast path `groupSet` is `undefined` --
69
+ * there is no group_set column, every row is in the one group -- so the
70
+ * condition is just the `where:`, or `undefined` to include unconditionally.
71
+ * Callers wrap the result in whatever form the dialect uses (`FILTER (WHERE
72
+ * ...)`, `CASE WHEN ... THEN`, `IF(...)`).
73
+ */
74
+ function turtleGroupSetCondition(groupSet, filterSQL) {
75
+ if (groupSet === undefined) {
76
+ return filterSQL;
77
+ }
78
+ return filterSQL
79
+ ? `group_set=${groupSet} AND ${filterSQL}`
80
+ : `group_set=${groupSet}`;
81
+ }
64
82
  class Dialect {
65
83
  constructor() {
66
84
  // -- we should add flags with default values from now on so as to not break
@@ -32,7 +32,7 @@ export declare class DuckDBDialect extends PostgresBase {
32
32
  sqlAnyValue(groupSet: number, fieldName: string): string;
33
33
  sqlLiteralNumber(literal: string): string;
34
34
  mapFields(fieldList: DialectFieldList): string;
35
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
35
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
36
36
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
37
37
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
38
38
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -97,8 +97,9 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
97
97
  .map(f => `\n ${f.sqlOutputName}: ${f.sqlExpression}`)
98
98
  .join(', ');
99
99
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
100
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
101
- const list = `LIST({${fields}} ${orderByClause}) FILTER (WHERE group_set=${groupSet}${filter})`;
100
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
101
+ const list = `LIST({${fields}} ${orderByClause})` +
102
+ (cond ? ` FILTER (WHERE ${cond})` : '');
102
103
  // A projection nest's limit is applied by slicing the aggregated array
103
104
  // (duckdb lists are 1-based, inclusive).
104
105
  const limited = limit !== undefined ? `(${list})[1:${limit}]` : list;
@@ -42,7 +42,7 @@ export declare class MySQLDialect extends Dialect {
42
42
  sqlGroupSetTable(groupSetCount: number): string;
43
43
  sqlAnyValue(_groupSet: number, fieldName: string): string;
44
44
  private mapFields;
45
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, _limit?: number, filterSQL?: string): string;
45
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, _limit?: number, filterSQL?: string): string;
46
46
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
47
47
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
48
48
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -164,12 +164,16 @@ class MySQLDialect extends dialect_1.Dialect {
164
164
  sqlAggregateTurtle(groupSet, fieldList, orderBy, _limit, filterSQL) {
165
165
  const separator = ',';
166
166
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
167
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
168
- let gc = `GROUP_CONCAT(
169
- IF(group_set=${groupSet}${filter},
170
- JSON_OBJECT(${this.mapFields(fieldList)})
167
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
168
+ const json = `JSON_OBJECT(${this.mapFields(fieldList)})`;
169
+ const element = cond
170
+ ? `IF(${cond},
171
+ ${json}
171
172
  , null
172
- )
173
+ )`
174
+ : json;
175
+ let gc = `GROUP_CONCAT(
176
+ ${element}
173
177
  ${orderByClause}
174
178
  SEPARATOR '${separator}'
175
179
  )`;
@@ -32,7 +32,7 @@ export declare class PostgresDialect extends PostgresBase {
32
32
  sqlGroupSetTable(groupSetCount: number): string;
33
33
  sqlAnyValue(groupSet: number, fieldName: string): string;
34
34
  mapFields(fieldList: DialectFieldList): string;
35
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
35
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
36
36
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
37
37
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
38
38
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -100,8 +100,9 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
100
100
  sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
101
101
  const fields = this.mapFields(fieldList);
102
102
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
103
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
104
- const arrayAgg = `(ARRAY_AGG((SELECT TO_JSONB(__x) FROM (SELECT ${fields}\n ) as __x) ${orderByClause} ) FILTER (WHERE group_set=${groupSet}${filter}))`;
103
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
104
+ const filterClause = cond ? ` FILTER (WHERE ${cond})` : '';
105
+ const arrayAgg = `(ARRAY_AGG((SELECT TO_JSONB(__x) FROM (SELECT ${fields}\n ) as __x) ${orderByClause} )${filterClause})`;
105
106
  // Slice the native ARRAY (1-based, inclusive) before TO_JSONB turns it into
106
107
  // a single jsonb value — a jsonb scalar can't be range-sliced.
107
108
  const limited = limit !== undefined ? `${arrayAgg}[1:${limit}]` : arrayAgg;
@@ -36,7 +36,7 @@ export declare class SnowflakeDialect extends Dialect {
36
36
  sqlAnyValue(groupSet: number, fieldName: string): string;
37
37
  mapFields(fieldList: DialectFieldList): string;
38
38
  mapFieldsForObjectConstruct(fieldList: DialectFieldList): string;
39
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
39
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
40
40
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
41
41
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
42
42
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -113,8 +113,10 @@ class SnowflakeDialect extends dialect_1.Dialect {
113
113
  const orderByClause = orderBy
114
114
  ? ` WITHIN GROUP (${this.sqlTurtleOrderByClause(orderBy)})`
115
115
  : '';
116
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
117
- const aggClause = `ARRAY_AGG(CASE WHEN group_set=${groupSet}${filter} THEN OBJECT_CONSTRUCT_KEEP_NULL(${fields}) END)${orderByClause}`;
116
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
117
+ const struct = `OBJECT_CONSTRUCT_KEEP_NULL(${fields})`;
118
+ const element = cond ? `CASE WHEN ${cond} THEN ${struct} END` : struct;
119
+ const aggClause = `ARRAY_AGG(${element})${orderByClause}`;
118
120
  // ARRAY_SLICE is 0-based, end-exclusive, so (0, n) keeps the first n.
119
121
  const limited = limit !== undefined
120
122
  ? `ARRAY_SLICE(${aggClause}, 0, ${limit})`
@@ -43,7 +43,7 @@ export declare class StandardSQLDialect extends Dialect {
43
43
  sqlGroupSetTable(groupSetCount: number): string;
44
44
  sqlAnyValue(groupSet: number, fieldName: string): string;
45
45
  sqlOrderBy(orderTerms: string[], obr?: OrderByRequest): string;
46
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
46
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
47
47
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
48
48
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
49
49
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -155,8 +155,10 @@ class StandardSQLDialect extends dialect_1.Dialect {
155
155
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
156
156
  // BigQuery ARRAY_AGG takes LIMIT as its final sub-clause, after ORDER BY.
157
157
  const limitClause = limit !== undefined ? ` LIMIT ${limit}` : '';
158
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
159
- return `ARRAY_AGG(CASE WHEN group_set=${groupSet}${filter} THEN STRUCT(${fields}\n ) END IGNORE NULLS ${orderByClause}${limitClause})`;
158
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
159
+ const struct = `STRUCT(${fields}\n )`;
160
+ const element = cond ? `CASE WHEN ${cond} THEN ${struct} END` : struct;
161
+ return `ARRAY_AGG(${element} IGNORE NULLS ${orderByClause}${limitClause})`;
160
162
  }
161
163
  sqlAnyValueTurtle(groupSet, fieldList) {
162
164
  const fields = fieldList
@@ -36,7 +36,7 @@ export declare class TrinoDialect extends PostgresBase {
36
36
  exprToSQL(qi: QueryInfo, df: Expr): string | undefined;
37
37
  sqlAnyValue(groupSet: number, fieldName: string): string;
38
38
  buildTypeExpression(fieldList: DialectFieldList): string;
39
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
39
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
40
40
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
41
41
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
42
42
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -8,6 +8,7 @@ exports.PrestoDialect = exports.TrinoDialect = void 0;
8
8
  const utils_1 = require("../../model/utils");
9
9
  const malloy_types_1 = require("../../model/malloy_types");
10
10
  const functions_1 = require("../functions");
11
+ const dialect_1 = require("../dialect");
11
12
  const pg_impl_1 = require("../pg_impl");
12
13
  const dialect_functions_1 = require("./dialect_functions");
13
14
  const function_overrides_1 = require("./function_overrides");
@@ -230,8 +231,9 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
230
231
  const expressions = fieldList.map(f => f.sqlExpression).join(',\n ');
231
232
  const definitions = this.buildTypeExpression(fieldList);
232
233
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
233
- const filter = filterSQL ? ` AND ${filterSQL}` : '';
234
- const arrayAgg = `ARRAY_AGG(CAST(ROW(${expressions}) AS ROW(${definitions})) ${orderByClause}) FILTER (WHERE group_set=${groupSet}${filter})`;
234
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
235
+ const filterClause = cond ? ` FILTER (WHERE ${cond})` : '';
236
+ const arrayAgg = `ARRAY_AGG(CAST(ROW(${expressions}) AS ROW(${definitions})) ${orderByClause})${filterClause}`;
235
237
  // SLICE(array, start, length) is 1-based; length n keeps the first n.
236
238
  return limit !== undefined ? `SLICE(${arrayAgg}, 1, ${limit})` : arrayAgg;
237
239
  }
@@ -114,6 +114,10 @@ class DynamicSpace extends static_space_1.StaticSpace {
114
114
  }
115
115
  }
116
116
  this.sourceDef = { ...this.fromSource, fields: [] };
117
+ // This is a freshly built (modified) source: it presents a new exported
118
+ // shape, so it is no longer a reference to the source it was built from.
119
+ // DefineSource resets referenceID to this source's own sourceID.
120
+ delete this.sourceDef.referenceID;
117
121
  this.sourceDef.parameters = parameters;
118
122
  const fieldIndices = new Map();
119
123
  // Need to process the entities in specific order
@@ -196,6 +200,7 @@ class DynamicSpace extends static_space_1.StaticSpace {
196
200
  emptyStructDef() {
197
201
  const ret = { ...this.fromSource };
198
202
  ret.fields = [];
203
+ delete ret.referenceID;
199
204
  return ret;
200
205
  }
201
206
  }
@@ -91,7 +91,10 @@ class NamedSource extends source_1.Source {
91
91
  else {
92
92
  (_a = this.document()) === null || _a === void 0 ? void 0 : _a.checkExperimentalDialect(this, entry.dialect);
93
93
  if ((0, malloy_types_1.isSourceDef)(entry)) {
94
- return { ...entry };
94
+ // This struct is created as an unmodified reference to `entry`. Mark it
95
+ // with entry's own identity; the modification path (DynamicSpace) clears
96
+ // this so only true references carry a referenceID.
97
+ return { ...entry, referenceID: entry.sourceID };
95
98
  }
96
99
  }
97
100
  // I think this is now a never
@@ -58,9 +58,16 @@ class DefineSource extends malloy_element_1.MalloyElement {
58
58
  }
59
59
  : { ...this.note };
60
60
  }
61
- if ((0, malloy_types_1.isPersistableSourceDef)(entry)) {
61
+ if ((0, malloy_types_1.isSourceDef)(entry)) {
62
+ // Every source gets a stable identity for its own definition. referenceID
63
+ // is left as it arrived: set to the referenced source's sourceID for an
64
+ // unmodified rename (`source: a is b`), and absent for a source that
65
+ // defines its own shape (table/sql/query, or a modified/extended source,
66
+ // which DynamicSpace cleared).
62
67
  entry.sourceID = (0, source_def_utils_1.mkSourceID)(this.name, (_b = this.location) === null || _b === void 0 ? void 0 : _b.url);
63
- entry.persistent = (0, persist_utils_1.checkPersistAnnotation)(entry).persist;
68
+ if ((0, malloy_types_1.isPersistableSourceDef)(entry)) {
69
+ entry.persistent = (0, persist_utils_1.checkPersistAnnotation)(entry).persist;
70
+ }
64
71
  }
65
72
  entry.partitionComposite =
66
73
  (_d = (0, composite_source_utils_1.getPartitionCompositeDesc)(this.note, structDef, (_c = this.sourceExpr) !== null && _c !== void 0 ? _c : this)) !== null && _d !== void 0 ? _d : structDef.partitionComposite;
@@ -665,10 +665,9 @@ class Document extends MalloyElement {
665
665
  this.modelWasModified = true;
666
666
  }
667
667
  this.documentModel.set(str, ent);
668
- // Maintain sourceRegistry for persistable sources with sourceID
669
- if ((0, malloy_types_1.isSourceDef)(ent.entry) &&
670
- (0, malloy_types_1.isPersistableSourceDef)(ent.entry) &&
671
- ent.entry.sourceID) {
668
+ // Maintain the source-id table so sourceID/referenceID values resolve to
669
+ // their SourceDef. Every named source is registered by its sourceID.
670
+ if ((0, malloy_types_1.isSourceDef)(ent.entry) && ent.entry.sourceID) {
672
671
  this.documentSrcRegistry[ent.entry.sourceID] = {
673
672
  entry: {
674
673
  type: 'source_registry_reference',
@@ -143,7 +143,7 @@ function compileExpr(resultSet, context, expr, state = new utils_1.GenerateState
143
143
  else {
144
144
  throw new Error(`Internal Error: Unknown aggregate function ${expr.function}`);
145
145
  }
146
- if (resultSet.root().isComplexQuery) {
146
+ if (resultSet.root().isComplexQuery && resultSet.root().emitsGroupSet) {
147
147
  let groupSet = resultSet.groupSet;
148
148
  if (state.totalGroupSet !== -1) {
149
149
  groupSet = state.totalGroupSet;
@@ -411,7 +411,7 @@ function generateAsymmetricStringAggExpression(resultSet, context, value, separa
411
411
  return context.dialect.sqlStringAggDistinct(distinctKey, valueSQL, separatorSQL);
412
412
  }
413
413
  function generateAnalyticFragment(dialect, resultStruct, context, expr, overload, state, args, partitionByFields, funcOrdering) {
414
- const isComplex = resultStruct.root().isComplexQuery;
414
+ const isComplex = resultStruct.root().isComplexQuery && resultStruct.root().emitsGroupSet;
415
415
  const partitionFields = getAnalyticPartitions(resultStruct, partitionByFields);
416
416
  const allPartitions = [
417
417
  ...(isComplex ? ['group_set'] : []),
@@ -96,6 +96,7 @@ export declare class FieldInstanceResultRoot extends FieldInstanceResult {
96
96
  joins: Map<string, JoinInstance>;
97
97
  havings: AndChain;
98
98
  isComplexQuery: boolean;
99
+ emitsGroupSet: boolean;
99
100
  queryUsesPartitioning: boolean;
100
101
  computeOnlyGroups: number[];
101
102
  elimatedComputeGroups: boolean;
@@ -410,6 +410,11 @@ class FieldInstanceResultRoot extends FieldInstanceResult {
410
410
  this.joins = new Map();
411
411
  this.havings = new utils_1.AndChain();
412
412
  this.isComplexQuery = false;
413
+ // True when the query emits a `group_set` column to demux fanned-out rows
414
+ // (the general path). The single-group-set fast path (generateSingleGroupSetSQL)
415
+ // sets this false: there is no group_set column, so aggregate and window
416
+ // expressions must not wrap themselves in `CASE WHEN group_set=N`.
417
+ this.emitsGroupSet = true;
413
418
  this.queryUsesPartitioning = false;
414
419
  this.computeOnlyGroups = [];
415
420
  this.elimatedComputeGroups = false;
@@ -9,4 +9,5 @@ export { getModelAnnotations } from './annotation_utils';
9
9
  export { constantExprToSQL } from './constant_expression_compiler';
10
10
  export { getCompiledSQL } from './sql_compiled';
11
11
  export { MalloyCompileError } from './malloy_compile_error';
12
- export { mkSourceID, mkBuildID, mkQuerySourceDef, mkSQLSourceDef, mkTableSourceDef, resolveSourceID, registerSource, hasSourceRegistryEntry, } from './source_def_utils';
12
+ export { mkSourceID, mkBuildID, mkQuerySourceDef, mkSQLSourceDef, mkTableSourceDef, resolveSourceID, resolveSourceRef, sourceNamespaceReference, registerSource, hasSourceRegistryEntry, } from './source_def_utils';
13
+ export type { NamespaceReference } from './source_def_utils';
@@ -18,7 +18,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.hasSourceRegistryEntry = exports.registerSource = exports.resolveSourceID = exports.mkTableSourceDef = exports.mkSQLSourceDef = exports.mkQuerySourceDef = exports.mkBuildID = exports.mkSourceID = exports.MalloyCompileError = exports.getCompiledSQL = exports.constantExprToSQL = exports.getModelAnnotations = exports.typeDefToString = exports.pathToKey = exports.mkModelID = exports.mkModelDef = exports.makeDigest = exports.composeSQLExpr = exports.indent = exports.getResultStructDefForView = exports.getResultStructDefForQuery = exports.QueryModel = exports.QueryQuery = exports.QueryStruct = exports.QueryField = void 0;
21
+ exports.hasSourceRegistryEntry = exports.registerSource = exports.sourceNamespaceReference = exports.resolveSourceRef = exports.resolveSourceID = exports.mkTableSourceDef = exports.mkSQLSourceDef = exports.mkQuerySourceDef = exports.mkBuildID = exports.mkSourceID = exports.MalloyCompileError = exports.getCompiledSQL = exports.constantExprToSQL = exports.getModelAnnotations = exports.typeDefToString = exports.pathToKey = exports.mkModelID = exports.mkModelDef = exports.makeDigest = exports.composeSQLExpr = exports.indent = exports.getResultStructDefForView = exports.getResultStructDefForQuery = exports.QueryModel = exports.QueryQuery = exports.QueryStruct = exports.QueryField = void 0;
22
22
  __exportStar(require("./malloy_types"), exports);
23
23
  const query_node_1 = require("./query_node");
24
24
  Object.defineProperty(exports, "QueryField", { enumerable: true, get: function () { return query_node_1.QueryField; } });
@@ -60,6 +60,8 @@ Object.defineProperty(exports, "mkQuerySourceDef", { enumerable: true, get: func
60
60
  Object.defineProperty(exports, "mkSQLSourceDef", { enumerable: true, get: function () { return source_def_utils_1.mkSQLSourceDef; } });
61
61
  Object.defineProperty(exports, "mkTableSourceDef", { enumerable: true, get: function () { return source_def_utils_1.mkTableSourceDef; } });
62
62
  Object.defineProperty(exports, "resolveSourceID", { enumerable: true, get: function () { return source_def_utils_1.resolveSourceID; } });
63
+ Object.defineProperty(exports, "resolveSourceRef", { enumerable: true, get: function () { return source_def_utils_1.resolveSourceRef; } });
64
+ Object.defineProperty(exports, "sourceNamespaceReference", { enumerable: true, get: function () { return source_def_utils_1.sourceNamespaceReference; } });
63
65
  Object.defineProperty(exports, "registerSource", { enumerable: true, get: function () { return source_def_utils_1.registerSource; } });
64
66
  Object.defineProperty(exports, "hasSourceRegistryEntry", { enumerable: true, get: function () { return source_def_utils_1.hasSourceRegistryEntry; } });
65
67
  //# sourceMappingURL=index.js.map
@@ -858,6 +858,22 @@ interface SourceDefBase extends StructDefBase, Filtered, ResultStructMetadata {
858
858
  primaryKey?: PrimaryKeyRef;
859
859
  dialect: string;
860
860
  partitionComposite?: PartitionCompositeDesc;
861
+ /**
862
+ * Identity of this source's own definition (`name@modelURL`). Set for every
863
+ * source when it is defined. The persistence machinery reads this (gated by
864
+ * `isPersistableSourceDef`).
865
+ */
866
+ sourceID?: SourceID;
867
+ /**
868
+ * Set only when this source was created as an unmodified reference to another
869
+ * source (`source: a is b`, or a plain join) — then it holds the `sourceID`
870
+ * of the *immediately* referenced source. Absent when the source defines its
871
+ * own shape (table/sql/query, or a modified/extended source). So
872
+ * `referenceID !== undefined` means "created as a reference", and the value
873
+ * resolves through `ModelDef.sourceRegistry` to the referenced source and its
874
+ * namespace name (see `sourceNamespaceReference`).
875
+ */
876
+ referenceID?: SourceID;
861
877
  }
862
878
  /** which field is the primary key in this struct */
863
879
  export type PrimaryKeyRef = string;
@@ -878,7 +894,7 @@ export interface SQLStringSegment {
878
894
  export type SQLPhraseSegment = Query | PersistableSourceDef | SQLStringSegment;
879
895
  export declare function isSegmentSQL(f: SQLPhraseSegment): f is SQLStringSegment;
880
896
  export declare function isSegmentSource(f: SQLPhraseSegment): f is PersistableSourceDef;
881
- /** Format: "name@modelUrl" - uniquely identifies a source for persistence */
897
+ /** Format: "name@modelUrl" - uniquely identifies a defined source */
882
898
  export type SourceID = string;
883
899
  /** Created with `mkGivenID`. */
884
900
  export type GivenID = string;
@@ -915,7 +931,6 @@ export interface SourceRegistryValue {
915
931
  }
916
932
  export declare function isSourceRegistryReference(entry: SourceRegistryEntry): entry is SourceRegistryReference;
917
933
  export interface PersistableSourceProperties {
918
- sourceID?: SourceID;
919
934
  extends?: SourceID;
920
935
  persistent?: boolean;
921
936
  }
@@ -943,13 +958,17 @@ export declare function isSourceDef(sd: NamedModelObject | FieldDef): sd is Sour
943
958
  /**
944
959
  * Union of all source definition types.
945
960
  *
946
- * IMPORTANT: Never use object spread to copy a SourceDef. Use the factory
947
- * methods in source_def_utils.ts to merge changes into a source def:
961
+ * When building a *persisted/derived* source in the compiler, use the factory
962
+ * methods in source_def_utils.ts rather than object spread:
948
963
  * - mkSQLSourceDef(base, ...) - create SQLSourceDef from base
949
964
  * - mkQuerySourceDef(base, ...) - create QuerySourceDef from base
950
965
  *
951
- * These factories explicitly copy only safe fields, preventing accidental
952
- * propagation of sourceID/extends which must only be set in DefineSource.
966
+ * These factories explicitly copy only safe fields, dropping the identity
967
+ * fields (sourceID/referenceID/extends/persistent) so they are not propagated
968
+ * onto a freshly built source. In the translator, by contrast, object spread is
969
+ * used deliberately to *carry* sourceID/referenceID through an unmodified
970
+ * reference; DefineSource then sets them, and DynamicSpace clears referenceID
971
+ * on the modification path.
953
972
  */
954
973
  export type SourceDef = TableSourceDef | SQLSourceDef | QuerySourceDef | QueryResultDef | FinalizeSourceDef | NestSourceDef | VirtualSourceDef | CompositeSourceDef;
955
974
  /** Sources that can be persisted (materialized to tables) */
@@ -145,7 +145,7 @@ function findPersistentDependencies(root, modelDef, tagParseLog = []) {
145
145
  return results;
146
146
  }
147
147
  function processJoinedSource(source) {
148
- // If it has a sourceID, go through the registry
148
+ // If it has an sourceID, go through the registry
149
149
  if ((0, malloy_types_1.isPersistableSourceDef)(source) && source.sourceID) {
150
150
  return processSourceID(source.sourceID);
151
151
  }
@@ -91,7 +91,25 @@ export declare class QueryQuery extends QueryField {
91
91
  */
92
92
  collectArrayJoins(ji: JoinInstance): JoinInstance[];
93
93
  genereateSQLOrderBy(queryDef: QuerySegment, resultStruct: FieldInstanceResult): string;
94
+ /**
95
+ * The single-scan form, with no `group_set` fan-out: one `SELECT` over the
96
+ * scan, grouped by the dimensions. Used both for a query with no nests and --
97
+ * via canUseSingleGroupSetSQL -- for one whose only nests are single-stage
98
+ * grain-preserving projections, which become an array-agg in the same SELECT.
99
+ * That array-agg needs no `FILTER (WHERE group_set=N)` (every row is in the
100
+ * one group), and aggregate/window expressions must not gate themselves by
101
+ * group_set either, so emitsGroupSet is cleared (see FieldInstanceResultRoot).
102
+ */
94
103
  generateSimpleSQL(stageWriter: StageWriter): string;
104
+ /**
105
+ * Can this query take the single-group-set fast path? True when the query
106
+ * needs no group-set fan-out: it has nests (so it's "complex") but every one
107
+ * is a single-stage grain-preserving projection riding group_set 0, and there
108
+ * are no reduce nests, ungroupings, or totals (all of which would mint a
109
+ * second group set and push maxGroupSet above 0). Anything outside that shape
110
+ * falls back to the general demux path, which is correct for every case.
111
+ */
112
+ canUseSingleGroupSetSQL(): boolean;
95
113
  generatePipelinedStages(outputPipelinedSQL: OutputPipelinedSQL[], lastStageName: string, stageWriter: StageWriter, priorStageColumns: StageOutputColumn[]): string;
96
114
  generateStage0Fields(resultSet: FieldInstanceResult, output: StageOutputContext, stageWriter: StageWriter): void;
97
115
  generateSQLWhereChildren(resultStruct: FieldInstanceResult): AndChain;
@@ -102,7 +120,7 @@ export declare class QueryQuery extends QueryField {
102
120
  generateSQLDepthN(depth: number, stageWriter: StageWriter, stageName: string): string;
103
121
  genereateSQLCombineTurtles(stageWriter: StageWriter, stage0Name: string): string;
104
122
  buildDialectFieldList(resultStruct: FieldInstanceResult): DialectFieldList;
105
- generateTurtleSQL(resultStruct: FieldInstanceResult, stageWriter: StageWriter, sqlFieldName: string, outputPipelinedSQL: OutputPipelinedSQL[]): string;
123
+ generateTurtleSQL(resultStruct: FieldInstanceResult, stageWriter: StageWriter, sqlFieldName: string, outputPipelinedSQL: OutputPipelinedSQL[], omitGroupSetFilter?: boolean): string;
106
124
  generateTurtlePipelineSQL(fi: FieldInstanceResult, stageWriter: StageWriter, sourceSQLExpression: string): {
107
125
  structDef: QueryResultDef;
108
126
  pipeOut: any;
@@ -30,6 +30,11 @@ function usesShaveLimit(result) {
30
30
  const isNestedProjection = result.parent !== undefined && result.firstSegment.type === 'project';
31
31
  return !isNestedProjection;
32
32
  }
33
+ /** Is this an analytic (window) field -- a `calculate:`? */
34
+ function fieldIsAnalytic(fi) {
35
+ return ((0, malloy_types_1.hasExpression)(fi.f.fieldDef) &&
36
+ (0, malloy_types_1.expressionIsAnalytic)(fi.f.fieldDef.expressionType));
37
+ }
33
38
  // 1-based SELECT positions of the dimension columns, for `GROUP BY 1,2,...`.
34
39
  function groupByPositions(columns) {
35
40
  return columns.flatMap((c, i) => (c.isDimension ? [i + 1] : []));
@@ -903,15 +908,31 @@ class QueryQuery extends query_node_1.QueryField {
903
908
  }
904
909
  return s;
905
910
  }
911
+ /**
912
+ * The single-scan form, with no `group_set` fan-out: one `SELECT` over the
913
+ * scan, grouped by the dimensions. Used both for a query with no nests and --
914
+ * via canUseSingleGroupSetSQL -- for one whose only nests are single-stage
915
+ * grain-preserving projections, which become an array-agg in the same SELECT.
916
+ * That array-agg needs no `FILTER (WHERE group_set=N)` (every row is in the
917
+ * one group), and aggregate/window expressions must not gate themselves by
918
+ * group_set either, so emitsGroupSet is cleared (see FieldInstanceResultRoot).
919
+ */
906
920
  generateSimpleSQL(stageWriter) {
921
+ this.rootResult.emitsGroupSet = false;
907
922
  let s = '';
908
923
  s += 'SELECT \n';
909
924
  const fields = [];
910
- for (const [name, field] of this.rootResult.allFields) {
911
- const fi = field;
925
+ const outputPipelinedSQL = [];
926
+ for (const [name, fi] of this.rootResult.allFields) {
912
927
  const sqlName = this.parent.dialect.sqlQuoteIdentifier(name);
913
- if (fi.fieldUsage.type === 'result') {
914
- fields.push(` ${fi.generateExpression()} as ${sqlName}`);
928
+ if (fi instanceof field_instance_1.FieldInstanceField) {
929
+ if (fi.fieldUsage.type === 'result') {
930
+ fields.push(` ${fi.generateExpression()} as ${sqlName}`);
931
+ }
932
+ }
933
+ else if (fi instanceof field_instance_1.FieldInstanceResult) {
934
+ const turtle = this.generateTurtleSQL(fi, stageWriter, sqlName, outputPipelinedSQL, true);
935
+ fields.push(` ${turtle} as ${sqlName}`);
915
936
  }
916
937
  }
917
938
  s += (0, utils_1.indent)(fields.join(',\n')) + '\n';
@@ -938,8 +959,52 @@ class QueryQuery extends query_node_1.QueryField {
938
959
  s += `LIMIT ${this.firstSegment.limit}\n`;
939
960
  }
940
961
  this.resultStage = stageWriter.addStage(s);
962
+ // Consumes any pipelined turtle output. The nests that reach here are
963
+ // single-stage (canUseSingleGroupSetSQL), so outputPipelinedSQL is empty and
964
+ // this is a no-op; kept so the function stays correct if that guard widens.
965
+ this.resultStage = this.generatePipelinedStages(outputPipelinedSQL, this.resultStage, stageWriter, []);
941
966
  return this.resultStage;
942
967
  }
968
+ /**
969
+ * Can this query take the single-group-set fast path? True when the query
970
+ * needs no group-set fan-out: it has nests (so it's "complex") but every one
971
+ * is a single-stage grain-preserving projection riding group_set 0, and there
972
+ * are no reduce nests, ungroupings, or totals (all of which would mint a
973
+ * second group set and push maxGroupSet above 0). Anything outside that shape
974
+ * falls back to the general demux path, which is correct for every case.
975
+ */
976
+ canUseSingleGroupSetSQL() {
977
+ if (this.maxGroupSet !== 0)
978
+ return false;
979
+ if (this.firstSegment.type !== 'reduce')
980
+ return false;
981
+ for (const [, fi] of this.rootResult.allFields) {
982
+ if (fi instanceof field_instance_1.FieldInstanceField) {
983
+ // A root-level `calculate:` (window) needs the general path's analytic
984
+ // handling: on dialects that can't partition a window on an expression
985
+ // (BigQuery) it rides a lateral-join bag this path doesn't build.
986
+ if (fieldIsAnalytic(fi))
987
+ return false;
988
+ }
989
+ else if (fi instanceof field_instance_1.FieldInstanceResult) {
990
+ if (fi.firstSegment.type !== 'project')
991
+ return false;
992
+ if (fi.turtleDef.pipeline.length !== 1)
993
+ return false;
994
+ if (fi.getRepeatedResultType() !== 'nested')
995
+ return false;
996
+ // A `calculate:` inside a `select:` would put a window function inside
997
+ // the array-agg, which no dialect allows; leave it on the general path.
998
+ for (const [, nestField] of fi.allFields) {
999
+ if (nestField instanceof field_instance_1.FieldInstanceField &&
1000
+ fieldIsAnalytic(nestField)) {
1001
+ return false;
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ return true;
1007
+ }
943
1008
  // This probably should be generated in a dialect independat way.
944
1009
  // but for now, it is just googleSQL.
945
1010
  generatePipelinedStages(outputPipelinedSQL, lastStageName, stageWriter,
@@ -1509,7 +1574,12 @@ class QueryQuery extends query_node_1.QueryField {
1509
1574
  }
1510
1575
  return dialectFieldList;
1511
1576
  }
1512
- generateTurtleSQL(resultStruct, stageWriter, sqlFieldName, outputPipelinedSQL) {
1577
+ generateTurtleSQL(resultStruct, stageWriter, sqlFieldName, outputPipelinedSQL,
1578
+ // The single-group-set fast path (generateSingleGroupSetSQL) has no
1579
+ // group_set column to filter on: every row is in the one group. The
1580
+ // array-agg drops its `FILTER (WHERE group_set=N)` and keeps only a
1581
+ // projection's own `where:`.
1582
+ omitGroupSetFilter = false) {
1513
1583
  // How to emit this nest's first stage — and the last line of defense before
1514
1584
  // broken SQL for IR that didn't pass through the translator (cached,
1515
1585
  // deserialized, or query-builder IR). Same `nestStrategy` the translator
@@ -1580,7 +1650,7 @@ class QueryQuery extends query_node_1.QueryField {
1580
1650
  }
1581
1651
  }
1582
1652
  // (capability is guarded by nestStrategy at the top of this method)
1583
- ret = this.parent.dialect.sqlAggregateTurtle(resultStruct.groupSet, dialectFieldList, orderBy, limit, filterSQL);
1653
+ ret = this.parent.dialect.sqlAggregateTurtle(omitGroupSetFilter ? undefined : resultStruct.groupSet, dialectFieldList, orderBy, limit, filterSQL);
1584
1654
  }
1585
1655
  // If the turtle is a pipeline, generate a UDF to compute it.
1586
1656
  const newStageWriter = new stage_writer_1.StageWriter(this.parent.dialect.supportsCTEinCoorelatedSubQueries, stageWriter);
@@ -1658,12 +1728,13 @@ class QueryQuery extends query_node_1.QueryField {
1658
1728
  this.maxGroupSet = r.nextGroupSetNumber - 1;
1659
1729
  this.rootResult.assignFieldsToGroups();
1660
1730
  (_a = this.rootResult).isComplexQuery || (_a.isComplexQuery = this.maxDepth > 0 || r.isComplex);
1661
- if (this.rootResult.isComplexQuery) {
1731
+ // A complex query needs the group-set fan-out unless it has just one group
1732
+ // set (only single-stage projection nests), in which case generateSimpleSQL
1733
+ // emits the same no-fan-out form a non-nested query gets.
1734
+ if (this.rootResult.isComplexQuery && !this.canUseSingleGroupSetSQL()) {
1662
1735
  return this.generateComplexSQL(stageWriter);
1663
1736
  }
1664
- else {
1665
- return this.generateSimpleSQL(stageWriter);
1666
- }
1737
+ return this.generateSimpleSQL(stageWriter);
1667
1738
  }
1668
1739
  generateSQLFromPipeline(stageWriter) {
1669
1740
  this.parent.maybeEmitParameterizedSourceUsage();
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * SourceDef Utilities for Persistence
3
3
  *
4
- * Key invariant: sourceID is ONLY assigned in DefineSource when a source
5
- * gets a name. Factory functions explicitly copy only the fields they need,
6
- * never using spread, to prevent accidental propagation of sourceID/extends.
7
- *
8
- * The `extends` property is set by callers when processing extend blocks.
4
+ * Key invariant: a source's identity fields (sourceID, referenceID, extends)
5
+ * are assigned in the translator (DefineSource / the reference path), never by
6
+ * these compiler factories. The factory functions explicitly copy only the
7
+ * fields they need, never using spread, so identity is not propagated onto a
8
+ * freshly built source.
9
9
  */
10
10
  import type { BuildID, FieldDef, GivenID, ModelDef, PersistableSourceDef, Query, QuerySourceDef, SourceDef, SourceID, SourceRegistryEntry, SourceRegistryValue, SQLPhraseSegment, SQLSourceDef, TableSourceDef } from './malloy_types';
11
11
  export declare function mkSourceID(name: string, url: string | undefined): SourceID;
@@ -30,13 +30,37 @@ export declare function mkSQLSourceDef(base: SourceDef, selectStr: string, selec
30
30
  */
31
31
  export declare function mkTableSourceDef(name: string, connection: string, tablePath: string, dialect: string, fields: FieldDef[]): TableSourceDef;
32
32
  /**
33
- * Resolve a sourceID to a SourceDef using the sourceRegistry.
33
+ * Resolve a SourceID (a source's own `sourceID`, or a `referenceID` pointing at
34
+ * one) to its SourceDef using the sourceRegistry, returning any kind of source.
34
35
  *
35
36
  * @param modelDef The model definition containing the registry
36
- * @param sourceID The sourceID to resolve
37
+ * @param sourceID The SourceID to resolve
37
38
  * @returns The SourceDef if found, undefined otherwise
38
39
  */
40
+ export declare function resolveSourceRef(modelDef: ModelDef, sourceID: SourceID): SourceDef | undefined;
41
+ /**
42
+ * Resolve a sourceID to a persistable SourceDef using the sourceRegistry.
43
+ *
44
+ * @param modelDef The model definition containing the registry
45
+ * @param sourceID The sourceID to resolve
46
+ * @returns The PersistableSourceDef if found, undefined otherwise
47
+ */
39
48
  export declare function resolveSourceID(modelDef: ModelDef, sourceID: SourceID): PersistableSourceDef | undefined;
49
+ /** The namespace entry a source refers to (see `sourceNamespaceReference`). */
50
+ export interface NamespaceReference {
51
+ /** The name this source is bound to in `modelDef.contents`. */
52
+ name: string;
53
+ /** The referenced source. */
54
+ source: SourceDef;
55
+ }
56
+ /**
57
+ * If `sd` was created as an unmodified reference to another source
58
+ * (`sd.referenceID` is set) and that source is present in this model's
59
+ * namespace, return it together with the name it goes by. Returns undefined
60
+ * when `sd` defines its own shape, or when the referenced source is not in this
61
+ * model's namespace (e.g. an imported source whose own target wasn't imported).
62
+ */
63
+ export declare function sourceNamespaceReference(modelDef: ModelDef, sd: SourceDef): NamespaceReference | undefined;
40
64
  /**
41
65
  * Add an entry to the sourceRegistry.
42
66
  *
@@ -10,7 +10,9 @@ exports.mkBuildID = mkBuildID;
10
10
  exports.mkQuerySourceDef = mkQuerySourceDef;
11
11
  exports.mkSQLSourceDef = mkSQLSourceDef;
12
12
  exports.mkTableSourceDef = mkTableSourceDef;
13
+ exports.resolveSourceRef = resolveSourceRef;
13
14
  exports.resolveSourceID = resolveSourceID;
15
+ exports.sourceNamespaceReference = sourceNamespaceReference;
14
16
  exports.registerSource = registerSource;
15
17
  exports.hasSourceRegistryEntry = hasSourceRegistryEntry;
16
18
  const utils_1 = require("./utils");
@@ -59,8 +61,9 @@ function mkQuerySourceDef(base, query, name) {
59
61
  dialect: base.dialect,
60
62
  partitionComposite: base.partitionComposite,
61
63
  errorFactory: base.errorFactory,
62
- // PersistableSourceProperties - explicitly NOT copied
64
+ // Identity fields - explicitly NOT copied
63
65
  // sourceID: undefined,
66
+ // referenceID: undefined,
64
67
  // extends: undefined,
65
68
  // persistent: undefined,
66
69
  };
@@ -97,8 +100,9 @@ function mkSQLSourceDef(base, selectStr, selectSegments) {
97
100
  dialect: base.dialect,
98
101
  partitionComposite: base.partitionComposite,
99
102
  errorFactory: base.errorFactory,
100
- // PersistableSourceProperties - explicitly NOT copied
103
+ // Identity fields - explicitly NOT copied
101
104
  // sourceID: undefined,
105
+ // referenceID: undefined,
102
106
  // extends: undefined,
103
107
  // persistent: undefined,
104
108
  };
@@ -120,25 +124,51 @@ function mkTableSourceDef(name, connection, tablePath, dialect, fields) {
120
124
  // Source Registry Utilities
121
125
  // =============================================================================
122
126
  /**
123
- * Resolve a sourceID to a SourceDef using the sourceRegistry.
127
+ * Resolve a SourceID (a source's own `sourceID`, or a `referenceID` pointing at
128
+ * one) to its SourceDef using the sourceRegistry, returning any kind of source.
124
129
  *
125
130
  * @param modelDef The model definition containing the registry
126
- * @param sourceID The sourceID to resolve
131
+ * @param sourceID The SourceID to resolve
127
132
  * @returns The SourceDef if found, undefined otherwise
128
133
  */
129
- function resolveSourceID(modelDef, sourceID) {
134
+ function resolveSourceRef(modelDef, sourceID) {
130
135
  const value = modelDef.sourceRegistry[sourceID];
131
136
  if (!value)
132
137
  return undefined;
133
138
  if ((0, malloy_types_1.isSourceRegistryReference)(value.entry)) {
134
139
  const obj = (0, malloy_types_1.safeRecordGet)(modelDef.contents, value.entry.name);
135
- return obj && (0, malloy_types_1.isSourceDef)(obj) && (0, malloy_types_1.isPersistableSourceDef)(obj)
136
- ? obj
137
- : undefined;
140
+ return obj && (0, malloy_types_1.isSourceDef)(obj) ? obj : undefined;
138
141
  }
139
- // It's a PersistableSourceDef
140
142
  return value.entry;
141
143
  }
144
+ /**
145
+ * Resolve a sourceID to a persistable SourceDef using the sourceRegistry.
146
+ *
147
+ * @param modelDef The model definition containing the registry
148
+ * @param sourceID The sourceID to resolve
149
+ * @returns The PersistableSourceDef if found, undefined otherwise
150
+ */
151
+ function resolveSourceID(modelDef, sourceID) {
152
+ const sd = resolveSourceRef(modelDef, sourceID);
153
+ return sd && (0, malloy_types_1.isPersistableSourceDef)(sd) ? sd : undefined;
154
+ }
155
+ /**
156
+ * If `sd` was created as an unmodified reference to another source
157
+ * (`sd.referenceID` is set) and that source is present in this model's
158
+ * namespace, return it together with the name it goes by. Returns undefined
159
+ * when `sd` defines its own shape, or when the referenced source is not in this
160
+ * model's namespace (e.g. an imported source whose own target wasn't imported).
161
+ */
162
+ function sourceNamespaceReference(modelDef, sd) {
163
+ if (sd.referenceID === undefined)
164
+ return undefined;
165
+ const value = modelDef.sourceRegistry[sd.referenceID];
166
+ if (!value || !(0, malloy_types_1.isSourceRegistryReference)(value.entry))
167
+ return undefined;
168
+ const name = value.entry.name;
169
+ const source = (0, malloy_types_1.safeRecordGet)(modelDef.contents, name);
170
+ return source && (0, malloy_types_1.isSourceDef)(source) ? { name, source } : undefined;
171
+ }
142
172
  /**
143
173
  * Add an entry to the sourceRegistry.
144
174
  *
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.413";
1
+ export declare const MALLOY_VERSION = "0.0.415";
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.413';
5
+ exports.MALLOY_VERSION = '0.0.415';
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.413",
3
+ "version": "0.0.415",
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.413",
55
- "@malloydata/malloy-interfaces": "0.0.413",
56
- "@malloydata/malloy-tag": "0.0.413",
54
+ "@malloydata/malloy-filter": "0.0.415",
55
+ "@malloydata/malloy-interfaces": "0.0.415",
56
+ "@malloydata/malloy-tag": "0.0.415",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",