@malloydata/malloy 0.0.412 → 0.0.414

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) 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 +2 -1
  4. package/dist/dialect/databricks/databricks.js +12 -6
  5. package/dist/dialect/dialect.d.ts +12 -1
  6. package/dist/dialect/dialect.js +25 -0
  7. package/dist/dialect/duckdb/duckdb.d.ts +2 -1
  8. package/dist/dialect/duckdb/duckdb.js +9 -2
  9. package/dist/dialect/mysql/mysql.d.ts +2 -1
  10. package/dist/dialect/mysql/mysql.js +14 -5
  11. package/dist/dialect/postgres/postgres.d.ts +2 -1
  12. package/dist/dialect/postgres/postgres.js +9 -2
  13. package/dist/dialect/snowflake/snowflake.d.ts +2 -1
  14. package/dist/dialect/snowflake/snowflake.js +11 -3
  15. package/dist/dialect/standardsql/standardsql.d.ts +2 -1
  16. package/dist/dialect/standardsql/standardsql.js +8 -2
  17. package/dist/dialect/trino/trino.d.ts +2 -1
  18. package/dist/dialect/trino/trino.js +8 -2
  19. package/dist/lang/ast/field-space/dynamic-space.js +14 -1
  20. package/dist/lang/ast/field-space/query-spaces.d.ts +1 -0
  21. package/dist/lang/ast/field-space/query-spaces.js +5 -0
  22. package/dist/lang/ast/query-items/field-references.d.ts +10 -1
  23. package/dist/lang/ast/query-items/field-references.js +67 -4
  24. package/dist/lang/ast/query-properties/nest.js +15 -0
  25. package/dist/lang/ast/source-elements/named-source.js +4 -1
  26. package/dist/lang/ast/statements/define-source.js +9 -2
  27. package/dist/lang/ast/types/malloy-element.js +3 -4
  28. package/dist/lang/parse-log.d.ts +10 -0
  29. package/dist/lang/parse-log.js +3 -0
  30. package/dist/model/expression_compiler.js +2 -2
  31. package/dist/model/field_instance.d.ts +1 -0
  32. package/dist/model/field_instance.js +14 -0
  33. package/dist/model/index.d.ts +2 -1
  34. package/dist/model/index.js +3 -1
  35. package/dist/model/malloy_types.d.ts +25 -6
  36. package/dist/model/nest-capability.d.ts +37 -0
  37. package/dist/model/nest-capability.js +31 -0
  38. package/dist/model/persist_utils.js +1 -1
  39. package/dist/model/query_query.d.ts +26 -5
  40. package/dist/model/query_query.js +256 -88
  41. package/dist/model/source_def_utils.d.ts +31 -7
  42. package/dist/model/source_def_utils.js +39 -9
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. 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);
@@ -25,6 +25,7 @@ export declare class DatabricksDialect extends Dialect {
25
25
  dontUnionIndex: boolean;
26
26
  supportsQualify: boolean;
27
27
  supportsNesting: boolean;
28
+ supportsNestedProjectionLimit: boolean;
28
29
  hasLateralColumnAliasInSelect: boolean;
29
30
  cantPartitionWindowFunctionsOnExpressions: boolean;
30
31
  experimental: boolean;
@@ -48,7 +49,7 @@ export declare class DatabricksDialect extends Dialect {
48
49
  sqlOrderBy(orderTerms: string[], obr?: OrderByRequest): string;
49
50
  sqlAnyValue(groupSet: number, fieldName: string): string;
50
51
  private buildNamedStructExpression;
51
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
52
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
52
53
  private buildArraySortComparator;
53
54
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
54
55
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
@@ -62,6 +62,7 @@ class DatabricksDialect extends dialect_1.Dialect {
62
62
  this.dontUnionIndex = false;
63
63
  this.supportsQualify = false;
64
64
  this.supportsNesting = true;
65
+ this.supportsNestedProjectionLimit = true;
65
66
  this.hasLateralColumnAliasInSelect = true;
66
67
  this.cantPartitionWindowFunctionsOnExpressions = true;
67
68
  this.experimental = false;
@@ -169,13 +170,18 @@ class DatabricksDialect extends dialect_1.Dialect {
169
170
  .join(', ') +
170
171
  ')');
171
172
  }
172
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
173
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
173
174
  const namedStruct = this.buildNamedStructExpression(fieldList);
174
- const collectExpr = `COLLECT_LIST(${namedStruct}) FILTER (WHERE group_set=${groupSet})`;
175
- if (!orderBy || orderBy.length === 0) {
176
- return collectExpr;
177
- }
178
- return `ARRAY_SORT(${collectExpr}, (l, r) -> ${this.buildArraySortComparator(orderBy)})`;
175
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
176
+ const filterClause = cond ? ` FILTER (WHERE ${cond})` : '';
177
+ const collectExpr = `COLLECT_LIST(${namedStruct})${filterClause}`;
178
+ // COLLECT_LIST is unordered, so ordering is done post-aggregation via
179
+ // ARRAY_SORT the limit must slice the *ordered* array.
180
+ const ordered = !orderBy || orderBy.length === 0
181
+ ? collectExpr
182
+ : `ARRAY_SORT(${collectExpr}, (l, r) -> ${this.buildArraySortComparator(orderBy)})`;
183
+ // SLICE(array, start, length) is 1-based; length n keeps the first n.
184
+ return limit !== undefined ? `SLICE(${ordered}, 1, ${limit})` : ordered;
179
185
  }
180
186
  // Build a lambda comparator for ARRAY_SORT that handles multi-field
181
187
  // mixed-direction ordering. Each field comparison returns -1/0/1;
@@ -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';
@@ -99,6 +109,7 @@ export declare abstract class Dialect {
99
109
  cantPartitionWindowFunctionsOnExpressions: boolean;
100
110
  sqlLateralJoinBag(_expressions: LateralJoinExpression[]): string;
101
111
  supportsPipelinesInViews: boolean;
112
+ supportsNestedProjectionLimit: boolean;
102
113
  supportsArraysInData: boolean;
103
114
  hasTimestamptz: boolean;
104
115
  readsNestedData: boolean;
@@ -175,7 +186,7 @@ export declare abstract class Dialect {
175
186
  sqlValidateTableName(input: string): ValidateTablePathResult;
176
187
  abstract sqlGroupSetTable(groupSetCount: number): string;
177
188
  abstract sqlAnyValue(groupSet: number, fieldName: string): string;
178
- abstract sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
189
+ abstract sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
179
190
  sqlTurtleOrderByClause(orderBy: CompiledOrderBy[]): string;
180
191
  abstract sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
181
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
@@ -73,6 +91,13 @@ class Dialect {
73
91
  this.cantPartitionWindowFunctionsOnExpressions = false;
74
92
  // Snowflake can't yet support pipelines in nested views.
75
93
  this.supportsPipelinesInViews = true;
94
+ // Can the dialect apply a `limit:` to a nested `select:` (a projection
95
+ // nest)? A projection has no group-by keys to ROW_NUMBER-shave on, so the
96
+ // limit is applied by limiting/slicing the aggregated array inside
97
+ // `sqlAggregateTurtle`. Defaults false so a new dialect must consciously
98
+ // opt in (and implement the slice) rather than silently dropping the limit;
99
+ // the compiler rejects projection `limit:` when this is false.
100
+ this.supportsNestedProjectionLimit = false;
76
101
  // Some dialects don't supporrt arrays (mysql)
77
102
  this.supportsArraysInData = true;
78
103
  // Does the dialect support timestamptz (TIMESTAMP WITH TIME ZONE)?
@@ -22,6 +22,7 @@ export declare class DuckDBDialect extends PostgresBase {
22
22
  supportsQualify: boolean;
23
23
  supportsSafeCast: boolean;
24
24
  supportsNesting: boolean;
25
+ supportsNestedProjectionLimit: boolean;
25
26
  supportsCountApprox: boolean;
26
27
  requiresExplicitUnnestOrdering: boolean;
27
28
  integerTypeMappings: IntegerTypeMapping[];
@@ -31,7 +32,7 @@ export declare class DuckDBDialect extends PostgresBase {
31
32
  sqlAnyValue(groupSet: number, fieldName: string): string;
32
33
  sqlLiteralNumber(literal: string): string;
33
34
  mapFields(fieldList: DialectFieldList): string;
34
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
35
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
35
36
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
36
37
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
37
38
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -55,6 +55,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
55
55
  this.supportsQualify = true;
56
56
  this.supportsSafeCast = true;
57
57
  this.supportsNesting = true;
58
+ this.supportsNestedProjectionLimit = true;
58
59
  this.supportsCountApprox = true;
59
60
  // DuckDB UNNEST in LATERAL JOINs doesn't preserve array element order
60
61
  this.requiresExplicitUnnestOrdering = true;
@@ -91,12 +92,18 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
91
92
  mapFields(fieldList) {
92
93
  return fieldList.join(', ');
93
94
  }
94
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
95
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
95
96
  const fields = fieldList
96
97
  .map(f => `\n ${f.sqlOutputName}: ${f.sqlExpression}`)
97
98
  .join(', ');
98
99
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
99
- return `COALESCE(LIST({${fields}} ${orderByClause}) FILTER (WHERE group_set=${groupSet}),[])`;
100
+ const cond = (0, dialect_1.turtleGroupSetCondition)(groupSet, filterSQL);
101
+ const list = `LIST({${fields}} ${orderByClause})` +
102
+ (cond ? ` FILTER (WHERE ${cond})` : '');
103
+ // A projection nest's limit is applied by slicing the aggregated array
104
+ // (duckdb lists are 1-based, inclusive).
105
+ const limited = limit !== undefined ? `(${list})[1:${limit}]` : list;
106
+ return `COALESCE(${limited},[])`;
100
107
  }
101
108
  sqlAnyValueTurtle(groupSet, fieldList) {
102
109
  const fields = fieldList
@@ -31,6 +31,7 @@ export declare class MySQLDialect extends Dialect {
31
31
  readsNestedData: boolean;
32
32
  supportsComplexFilteredSources: boolean;
33
33
  supportsArraysInData: boolean;
34
+ supportsNestedProjectionLimit: boolean;
34
35
  compoundObjectInSchema: boolean;
35
36
  booleanType: BooleanTypeSupport;
36
37
  orderByClause: OrderByClauseType;
@@ -41,7 +42,7 @@ export declare class MySQLDialect extends Dialect {
41
42
  sqlGroupSetTable(groupSetCount: number): string;
42
43
  sqlAnyValue(_groupSet: number, fieldName: string): string;
43
44
  private mapFields;
44
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
45
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, _limit?: number, filterSQL?: string): string;
45
46
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
46
47
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
47
48
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -102,6 +102,10 @@ class MySQLDialect extends dialect_1.Dialect {
102
102
  this.readsNestedData = false;
103
103
  this.supportsComplexFilteredSources = false;
104
104
  this.supportsArraysInData = false;
105
+ // MySQL builds nested arrays with GROUP_CONCAT (not an array type), which has
106
+ // no in-expression slice; limiting would need query-level ROW_NUMBER plumbing
107
+ // the sqlAggregateTurtle seam doesn't expose. Left false (default) explicitly.
108
+ this.supportsNestedProjectionLimit = false;
105
109
  this.compoundObjectInSchema = false;
106
110
  this.booleanType = 'simulated';
107
111
  this.orderByClause = 'ordinal';
@@ -157,14 +161,19 @@ class MySQLDialect extends dialect_1.Dialect {
157
161
  .map(f => `${this.sqlLiteralString(f.rawName)}, ${f.sqlExpression}`)
158
162
  .join(', ');
159
163
  }
160
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
164
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, _limit, filterSQL) {
161
165
  const separator = ',';
162
166
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
163
- let gc = `GROUP_CONCAT(
164
- IF(group_set=${groupSet},
165
- 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}
166
172
  , null
167
- )
173
+ )`
174
+ : json;
175
+ let gc = `GROUP_CONCAT(
176
+ ${element}
168
177
  ${orderByClause}
169
178
  SEPARATOR '${separator}'
170
179
  )`;
@@ -21,6 +21,7 @@ export declare class PostgresDialect extends PostgresBase {
21
21
  dontUnionIndex: boolean;
22
22
  supportsQualify: boolean;
23
23
  supportsNesting: boolean;
24
+ supportsNestedProjectionLimit: boolean;
24
25
  experimental: boolean;
25
26
  readsNestedData: boolean;
26
27
  supportsComplexFilteredSources: boolean;
@@ -31,7 +32,7 @@ export declare class PostgresDialect extends PostgresBase {
31
32
  sqlGroupSetTable(groupSetCount: number): string;
32
33
  sqlAnyValue(groupSet: number, fieldName: string): string;
33
34
  mapFields(fieldList: DialectFieldList): string;
34
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
35
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
35
36
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
36
37
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
37
38
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -75,6 +75,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
75
75
  this.dontUnionIndex = false;
76
76
  this.supportsQualify = false;
77
77
  this.supportsNesting = true;
78
+ this.supportsNestedProjectionLimit = true;
78
79
  this.experimental = false;
79
80
  this.readsNestedData = false;
80
81
  this.supportsComplexFilteredSources = false;
@@ -96,10 +97,16 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
96
97
  .map(f => `\n ${f.sqlExpression}${f.typeDef.type === 'number' ? `::${this.defaultNumberType}` : ''} as ${f.sqlOutputName}`)
97
98
  .join(', ');
98
99
  }
99
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
100
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
100
101
  const fields = this.mapFields(fieldList);
101
102
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
102
- return `COALESCE(TO_JSONB((ARRAY_AGG((SELECT TO_JSONB(__x) FROM (SELECT ${fields}\n ) as __x) ${orderByClause} ) FILTER (WHERE group_set=${groupSet}))),'[]'::JSONB)`;
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})`;
106
+ // Slice the native ARRAY (1-based, inclusive) before TO_JSONB turns it into
107
+ // a single jsonb value — a jsonb scalar can't be range-sliced.
108
+ const limited = limit !== undefined ? `${arrayAgg}[1:${limit}]` : arrayAgg;
109
+ return `COALESCE(TO_JSONB(${limited}),'[]'::JSONB)`;
103
110
  }
104
111
  sqlAnyValueTurtle(groupSet, fieldList) {
105
112
  const fields = fieldList
@@ -17,6 +17,7 @@ export declare class SnowflakeDialect extends Dialect {
17
17
  supportsSumDistinctFunction: boolean;
18
18
  supportsSafeCast: boolean;
19
19
  supportsNesting: boolean;
20
+ supportsNestedProjectionLimit: boolean;
20
21
  defaultSampling: {
21
22
  rows: number;
22
23
  };
@@ -35,7 +36,7 @@ export declare class SnowflakeDialect extends Dialect {
35
36
  sqlAnyValue(groupSet: number, fieldName: string): string;
36
37
  mapFields(fieldList: DialectFieldList): string;
37
38
  mapFieldsForObjectConstruct(fieldList: DialectFieldList): string;
38
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
39
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
39
40
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
40
41
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
41
42
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -71,6 +71,7 @@ class SnowflakeDialect extends dialect_1.Dialect {
71
71
  this.supportsSumDistinctFunction = true;
72
72
  this.supportsSafeCast = true;
73
73
  this.supportsNesting = true;
74
+ this.supportsNestedProjectionLimit = true;
74
75
  this.defaultSampling = { rows: 50000 };
75
76
  this.supportsHyperLogLog = true;
76
77
  // NOTE: safely setting all these to false for now
@@ -107,13 +108,20 @@ class SnowflakeDialect extends dialect_1.Dialect {
107
108
  .map(f => `${this.sqlLiteralString(f.rawName)}, (${f.sqlExpression})`)
108
109
  .join(', ');
109
110
  }
110
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
111
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
111
112
  const fields = this.mapFieldsForObjectConstruct(fieldList);
112
113
  const orderByClause = orderBy
113
114
  ? ` WITHIN GROUP (${this.sqlTurtleOrderByClause(orderBy)})`
114
115
  : '';
115
- const aggClause = `ARRAY_AGG(CASE WHEN group_set=${groupSet} THEN OBJECT_CONSTRUCT_KEEP_NULL(${fields}) END)${orderByClause}`;
116
- return `COALESCE(${aggClause}, [])`;
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}`;
120
+ // ARRAY_SLICE is 0-based, end-exclusive, so (0, n) keeps the first n.
121
+ const limited = limit !== undefined
122
+ ? `ARRAY_SLICE(${aggClause}, 0, ${limit})`
123
+ : aggClause;
124
+ return `COALESCE(${limited}, [])`;
117
125
  }
118
126
  sqlAnyValueTurtle(groupSet, fieldList) {
119
127
  const fields = this.mapFieldsForObjectConstruct(fieldList);
@@ -25,6 +25,7 @@ export declare class StandardSQLDialect extends Dialect {
25
25
  supportsQualify: boolean;
26
26
  supportsSafeCast: boolean;
27
27
  supportsNesting: boolean;
28
+ supportsNestedProjectionLimit: boolean;
28
29
  cantPartitionWindowFunctionsOnExpressions: boolean;
29
30
  hasModOperator: boolean;
30
31
  sqlLateralJoinBag(expressions: LateralJoinExpression[]): string;
@@ -42,7 +43,7 @@ export declare class StandardSQLDialect extends Dialect {
42
43
  sqlGroupSetTable(groupSetCount: number): string;
43
44
  sqlAnyValue(groupSet: number, fieldName: string): string;
44
45
  sqlOrderBy(orderTerms: string[], obr?: OrderByRequest): string;
45
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
46
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
46
47
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
47
48
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
48
49
  sqlCoaleseMeasuresInline(groupSet: number, fieldList: DialectFieldList): string;
@@ -77,6 +77,7 @@ class StandardSQLDialect extends dialect_1.Dialect {
77
77
  this.supportsQualify = true;
78
78
  this.supportsSafeCast = true;
79
79
  this.supportsNesting = true;
80
+ this.supportsNestedProjectionLimit = true;
80
81
  this.cantPartitionWindowFunctionsOnExpressions = true;
81
82
  this.hasModOperator = false;
82
83
  this.nestedArrays = false; // Can't have an array of arrays for some reason
@@ -147,12 +148,17 @@ class StandardSQLDialect extends dialect_1.Dialect {
147
148
  return `ORDER BY ${orderTerms.map(t => `${t} NULLS LAST`).join(',')}`;
148
149
  }
149
150
  // can array agg or any_value a struct...
150
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
151
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
151
152
  const fields = fieldList
152
153
  .map(f => `\n ${f.sqlExpression} as ${f.sqlOutputName}`)
153
154
  .join(', ');
154
155
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
155
- return `ARRAY_AGG(CASE WHEN group_set=${groupSet} THEN STRUCT(${fields}\n ) END IGNORE NULLS ${orderByClause})`;
156
+ // BigQuery ARRAY_AGG takes LIMIT as its final sub-clause, after ORDER BY.
157
+ const limitClause = limit !== undefined ? ` LIMIT ${limit}` : '';
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})`;
156
162
  }
157
163
  sqlAnyValueTurtle(groupSet, fieldList) {
158
164
  const fields = fieldList
@@ -22,6 +22,7 @@ export declare class TrinoDialect extends PostgresBase {
22
22
  supportsQualify: boolean;
23
23
  supportsSafeCast: boolean;
24
24
  supportsNesting: boolean;
25
+ supportsNestedProjectionLimit: boolean;
25
26
  cantPartitionWindowFunctionsOnExpressions: boolean;
26
27
  orderByClause: OrderByClauseType;
27
28
  nullMatchesFunctionSignature: boolean;
@@ -35,7 +36,7 @@ export declare class TrinoDialect extends PostgresBase {
35
36
  exprToSQL(qi: QueryInfo, df: Expr): string | undefined;
36
37
  sqlAnyValue(groupSet: number, fieldName: string): string;
37
38
  buildTypeExpression(fieldList: DialectFieldList): string;
38
- sqlAggregateTurtle(groupSet: number, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined): string;
39
+ sqlAggregateTurtle(groupSet: number | undefined, fieldList: DialectFieldList, orderBy: CompiledOrderBy[] | undefined, limit?: number, filterSQL?: string): string;
39
40
  sqlAnyValueTurtle(groupSet: number, fieldList: DialectFieldList): string;
40
41
  sqlAnyValueLastTurtle(name: string, groupSet: number, sqlName: string): string;
41
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");
@@ -76,6 +77,7 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
76
77
  this.supportsQualify = true;
77
78
  this.supportsSafeCast = true;
78
79
  this.supportsNesting = true;
80
+ this.supportsNestedProjectionLimit = true;
79
81
  this.cantPartitionWindowFunctionsOnExpressions = false;
80
82
  this.orderByClause = 'output_name';
81
83
  this.nullMatchesFunctionSignature = false;
@@ -225,11 +227,15 @@ class TrinoDialect extends pg_impl_1.PostgresBase {
225
227
  .join(', \n');
226
228
  }
227
229
  // can array agg or any_value a struct...
228
- sqlAggregateTurtle(groupSet, fieldList, orderBy) {
230
+ sqlAggregateTurtle(groupSet, fieldList, orderBy, limit, filterSQL) {
229
231
  const expressions = fieldList.map(f => f.sqlExpression).join(',\n ');
230
232
  const definitions = this.buildTypeExpression(fieldList);
231
233
  const orderByClause = orderBy ? this.sqlTurtleOrderByClause(orderBy) : '';
232
- return `ARRAY_AGG(CAST(ROW(${expressions}) AS ROW(${definitions})) ${orderByClause}) FILTER (WHERE group_set=${groupSet})`;
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}`;
237
+ // SLICE(array, start, length) is 1-based; length n keeps the first n.
238
+ return limit !== undefined ? `SLICE(${arrayAgg}, 1, ${limit})` : arrayAgg;
233
239
  }
234
240
  sqlAnyValueTurtle(groupSet, fieldList) {
235
241
  const expressions = fieldList.map(f => f.sqlExpression).join(',\n ');
@@ -79,7 +79,15 @@ class DynamicSpace extends static_space_1.StaticSpace {
79
79
  }
80
80
  newEntry(name, logTo, entry) {
81
81
  if (this.entry(name)) {
82
- logTo.logError('definition-name-conflict', `Cannot redefine '${name}'`);
82
+ if (this.isQueryFieldSpace() &&
83
+ this.outputSpace().synthesizedNames.has(name)) {
84
+ logTo.logError('definition-name-conflict', `Cannot redefine '${name}', which was auto-generated by renaming an ` +
85
+ 'ambiguous reference; name one of the conflicting references ' +
86
+ "explicitly with 'is' to resolve this");
87
+ }
88
+ else {
89
+ logTo.logError('definition-name-conflict', `Cannot redefine '${name}'`);
90
+ }
83
91
  return;
84
92
  }
85
93
  this.setEntry(name, entry);
@@ -106,6 +114,10 @@ class DynamicSpace extends static_space_1.StaticSpace {
106
114
  }
107
115
  }
108
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;
109
121
  this.sourceDef.parameters = parameters;
110
122
  const fieldIndices = new Map();
111
123
  // Need to process the entities in specific order
@@ -188,6 +200,7 @@ class DynamicSpace extends static_space_1.StaticSpace {
188
200
  emptyStructDef() {
189
201
  const ret = { ...this.fromSource };
190
202
  ret.fields = [];
203
+ delete ret.referenceID;
191
204
  return ret;
192
205
  }
193
206
  }
@@ -43,6 +43,7 @@ export declare abstract class QueryOperationSpace extends RefinedSpace implement
43
43
  name: string;
44
44
  field: SpaceField;
45
45
  })[];
46
+ synthesizedNames: Set<string>;
46
47
  private _refSummary;
47
48
  private _refSummaryComputed;
48
49
  get refSummary(): model.RefSummary | undefined;
@@ -80,6 +80,11 @@ class QueryOperationSpace extends refined_space_1.RefinedSpace {
80
80
  this.expandedWild = new Map();
81
81
  this.drillDimensions = [];
82
82
  this.compositeFieldUsers = [];
83
+ // Output names this space generated by auto-renaming an ambiguous reference
84
+ // (e.g. `user.name` -> `user_name`). Tracked here, on the namespace that
85
+ // minted them, rather than on the entries: a later explicit definition that
86
+ // lands on a synthesized name is a hard error, not a silent shadow.
87
+ this.synthesizedNames = new Set();
83
88
  // Reference summary is not computed until `queryFieldDefs` is called
84
89
  // (or `getPipeSegment` for index segments). Reading it before that throws;
85
90
  // assigning to it (via the setter) flips the computed flag.
@@ -1,19 +1,24 @@
1
1
  import type { AnnotationsDef, FieldDef, RefToField, TypeDesc } from '../../../model/malloy_types';
2
2
  import type { DynamicSpace } from '../field-space/dynamic-space';
3
3
  import { DefinitionList } from '../types/definition-list';
4
- import type { FieldName, FieldSpace } from '../types/field-space';
4
+ import type { FieldSpace } from '../types/field-space';
5
+ import { FieldName } from '../types/field-space';
5
6
  import type { LookupResult } from '../types/lookup-result';
6
7
  import { ListOf, MalloyElement } from '../types/malloy-element';
7
8
  import type { Noteable } from '../types/noteable';
8
9
  import { extendNoteMethod } from '../types/noteable';
9
10
  import type { MakeEntry } from '../types/space-entry';
11
+ import type { FieldDeclarationConstructor } from './field-declaration';
12
+ import { AggregateFieldDeclaration, CalculateFieldDeclaration, GroupByFieldDeclaration, ProjectFieldDeclaration } from './field-declaration';
10
13
  export type FieldReferenceConstructor = new (names: FieldName[]) => FieldReference;
11
14
  export declare abstract class FieldReference extends ListOf<FieldName> implements Noteable, MakeEntry {
12
15
  readonly isNoteableObj = true;
13
16
  note?: AnnotationsDef;
14
17
  extendNote: typeof extendNoteMethod;
15
18
  constructor(names: FieldName[]);
19
+ protected autoRenameDeclarationCtor(): FieldDeclarationConstructor | undefined;
16
20
  makeEntry(fs: DynamicSpace): void;
21
+ private autoRename;
17
22
  getName(): string;
18
23
  get refToField(): RefToField;
19
24
  get refString(): string;
@@ -58,6 +63,7 @@ export declare class DrillFieldReference extends FieldReference {
58
63
  }
59
64
  export declare class CalculateFieldReference extends FieldReference {
60
65
  elementType: string;
66
+ protected autoRenameDeclarationCtor(): typeof CalculateFieldDeclaration;
61
67
  typecheck(type: TypeDesc): void;
62
68
  }
63
69
  export declare class IndexFieldReference extends FieldReference {
@@ -68,14 +74,17 @@ export declare class IndexFieldReference extends FieldReference {
68
74
  }
69
75
  export declare class AggregateFieldReference extends FieldReference {
70
76
  elementType: string;
77
+ protected autoRenameDeclarationCtor(): typeof AggregateFieldDeclaration;
71
78
  typecheck(type: TypeDesc): void;
72
79
  }
73
80
  export declare class GroupByFieldReference extends FieldReference {
74
81
  elementType: string;
82
+ protected autoRenameDeclarationCtor(): typeof GroupByFieldDeclaration;
75
83
  typecheck(type: TypeDesc): void;
76
84
  }
77
85
  export declare class ProjectFieldReference extends FieldReference {
78
86
  elementType: string;
87
+ protected autoRenameDeclarationCtor(): typeof ProjectFieldDeclaration;
79
88
  typecheck(type: TypeDesc): void;
80
89
  }
81
90
  export declare class DeclareFieldReference extends FieldReference {