@malloydata/malloy 0.0.122 → 0.0.123-dev240205152128

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.
@@ -695,7 +695,8 @@ class MalloyToAST extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
695
695
  }
696
696
  visitAggregateOrderBySpec(pcx) {
697
697
  const dir = pcx.ASC() ? 'asc' : pcx.DESC() ? 'desc' : undefined;
698
- const f = this.getFieldExpr(pcx.fieldExpr());
698
+ const fCx = pcx.fieldExpr();
699
+ const f = fCx ? this.getFieldExpr(fCx) : undefined;
699
700
  return this.astAt(new ast.FunctionOrderBy(f, dir), pcx);
700
701
  }
701
702
  visitAggregateOrderByStatement(pcx) {
@@ -202,6 +202,42 @@ describe('expressions', () => {
202
202
  group_by: ai
203
203
  calculate: x is lag(ai) { partition_by: ai; order_by: ai }
204
204
  }
205
+ `).toTranslate();
206
+ });
207
+ test('analytics order_by requires expression', () => {
208
+ expect((0, test_translator_1.markSource) `
209
+ ##! experimental { function_order_by }
210
+ run: a -> {
211
+ group_by: ai
212
+ calculate: x is lag(ai) { order_by: asc }
213
+ }
214
+ `).translationToFailWith('analytic `order_by` must specify an aggregate expression or output field reference');
215
+ });
216
+ test('string_agg_distinct order by cannot specify expression', () => {
217
+ expect((0, test_translator_1.markSource) `
218
+ ##! experimental { function_order_by }
219
+ run: a -> {
220
+ group_by: ai
221
+ aggregate: x is string_agg_distinct(astr) { order_by: ai }
222
+ }
223
+ `).translationToFailWith('`order_by` must be only `asc` or `desc` with no expression');
224
+ });
225
+ test('string_agg_distinct order by can be just direction', () => {
226
+ expect((0, test_translator_1.markSource) `
227
+ ##! experimental { function_order_by }
228
+ run: a -> {
229
+ group_by: ai
230
+ aggregate: x is string_agg_distinct(astr) { order_by: asc }
231
+ }
232
+ `).toTranslate();
233
+ });
234
+ test('string_agg order by can be just direction', () => {
235
+ expect((0, test_translator_1.markSource) `
236
+ ##! experimental { function_order_by }
237
+ run: a -> {
238
+ group_by: ai
239
+ aggregate: x is string_agg(astr) { order_by: asc }
240
+ }
205
241
  `).toTranslate();
206
242
  });
207
243
  test('can specify multiple partition_bys', () => {
@@ -1,5 +1,5 @@
1
1
  import { Dialect, DialectFieldList } from '../dialect';
2
- import { AggregateFragment, AggregateFunctionType, Annotation, CompiledQuery, DialectFragment, Expr, FieldDef, FieldFragment, Filtered, FilterExpression, FilterFragment, FunctionCallFragment, FunctionOverloadDef, JoinRelationship, ModelDef, OrderBy, OutputFieldFragment, Parameter, ParameterFragment, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SourceReferenceFragment, SegmentFieldDef, SpreadFragment, SQLExpressionFragment, SqlStringFragment, StructDef, StructRef, TurtleDef, UngroupFragment } from './malloy_types';
2
+ import { AggregateFragment, AggregateFunctionType, Annotation, CompiledQuery, DialectFragment, Expr, FieldDef, FieldFragment, Filtered, FilterExpression, FilterFragment, FunctionCallFragment, FunctionOverloadDef, FunctionParameterDef, JoinRelationship, ModelDef, OrderBy, OutputFieldFragment, Parameter, ParameterFragment, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SourceReferenceFragment, SegmentFieldDef, SpreadFragment, SQLExpressionFragment, SqlStringFragment, StructDef, StructRef, TurtleDef, UngroupFragment, FunctionOrderBy } from './malloy_types';
3
3
  import { Connection } from '../runtime_types';
4
4
  import { AndChain } from './utils';
5
5
  import { QueryInfo } from '../dialect/dialect';
@@ -17,7 +17,7 @@ interface OutputPipelinedSQL {
17
17
  sqlFieldName: string;
18
18
  pipelineSQL: string;
19
19
  }
20
- type UniqueKeyPossibleUse = AggregateFunctionType | 'generic_aggregate';
20
+ type UniqueKeyPossibleUse = AggregateFunctionType | 'generic_asymmetric_aggregate';
21
21
  declare class UniqueKeyUse extends Set<UniqueKeyPossibleUse> {
22
22
  add_use(k: UniqueKeyPossibleUse | undefined): this | undefined;
23
23
  hasAsymetricFunctions(): boolean;
@@ -71,6 +71,9 @@ declare class QueryField extends QueryNode {
71
71
  generateSQLExpression(resultSet: FieldInstanceResult, context: QueryStruct, frag: SQLExpressionFragment, state: GenerateState): string;
72
72
  private getParameterMap;
73
73
  private expandFunctionCall;
74
+ getFunctionOrderBy(resultSet: FieldInstanceResult, context: QueryStruct, state: GenerateState, orderBy: FunctionOrderBy[], args: Expr[], overload: FunctionOverloadDef): string | undefined;
75
+ generateAsymmetricStringAggExpression(resultSet: FieldInstanceResult, context: QueryStruct, value: Expr, separator: Expr | undefined, distinctKey: string, orderBy: FunctionOrderBy[] | undefined, dialectName: string, state: GenerateState): string;
76
+ getParamForArgIndex(params: FunctionParameterDef[], argIndex: number): FunctionParameterDef;
74
77
  generateFunctionCallExpression(resultSet: FieldInstanceResult, context: QueryStruct, frag: FunctionCallFragment, state: GenerateState): string;
75
78
  generateSpread(_resultSet: FieldInstanceResult, _context: QueryStruct, _frag: SpreadFragment, _state: GenerateState): string;
76
79
  generateParameterFragment(resultSet: FieldInstanceResult, context: QueryStruct, expr: ParameterFragment, _state: GenerateState): string;
@@ -44,7 +44,10 @@ class UniqueKeyUse extends Set {
44
44
  }
45
45
  }
46
46
  hasAsymetricFunctions() {
47
- return this.has('sum') || this.has('avg') || this.has('count');
47
+ return (this.has('sum') ||
48
+ this.has('avg') ||
49
+ this.has('count') ||
50
+ this.has('generic_asymmetric_aggregate'));
48
51
  }
49
52
  }
50
53
  class StageWriter {
@@ -295,34 +298,91 @@ class QueryField extends QueryNode {
295
298
  return [fragment];
296
299
  });
297
300
  }
301
+ getFunctionOrderBy(resultSet, context, state, orderBy, args, overload) {
302
+ if (orderBy.length === 0)
303
+ return undefined;
304
+ return ('ORDER BY ' +
305
+ orderBy
306
+ .map(ob => {
307
+ var _a, _b;
308
+ const defaultOrderByArgIndex = (_a = overload.dialect[context.dialect.name].defaultOrderByArgIndex) !== null && _a !== void 0 ? _a : 0;
309
+ const expr = (_b = ob.e) !== null && _b !== void 0 ? _b : args[defaultOrderByArgIndex];
310
+ const osql = this.generateDimFragment(resultSet, context, expr, state);
311
+ const dirsql = ob.dir === 'asc' ? ' ASC' : ob.dir === 'desc' ? ' DESC' : '';
312
+ return `${osql}${dirsql}`;
313
+ })
314
+ .join(', '));
315
+ }
316
+ generateAsymmetricStringAggExpression(resultSet, context, value, separator, distinctKey, orderBy, dialectName, state) {
317
+ if (orderBy) {
318
+ throw new Error(`Function \`string_agg\` does not support fanning out with an order by in ${dialectName}`);
319
+ }
320
+ const valueSQL = this.generateDimFragment(resultSet, context, value, state);
321
+ const separatorSQL = separator
322
+ ? ' ,' + this.generateDimFragment(resultSet, context, separator, state)
323
+ : '';
324
+ const keyStart = '__STRING_AGG_KS__';
325
+ const keyEnd = '__STRING_AGG_KE__';
326
+ const distinctValueSQL = `concat('${keyStart}', ${distinctKey}, '${keyEnd}', ${valueSQL})`;
327
+ return `REGEXP_REPLACE(
328
+ STRING_AGG(DISTINCT ${distinctValueSQL}${separatorSQL}),
329
+ '${keyStart}.*?${keyEnd}',
330
+ ''
331
+ )`;
332
+ }
333
+ getParamForArgIndex(params, argIndex) {
334
+ const prevVariadic = params.slice(0, argIndex).find(p => p.isVariadic);
335
+ return prevVariadic !== null && prevVariadic !== void 0 ? prevVariadic : params[argIndex];
336
+ }
298
337
  generateFunctionCallExpression(resultSet, context, frag, state) {
299
- var _a, _b;
338
+ var _a, _b, _c;
300
339
  const overload = frag.overload;
301
340
  const args = frag.args;
302
341
  const isSymmetric = (_a = frag.overload.isSymmetric) !== null && _a !== void 0 ? _a : false;
303
342
  const distinctKey = (0, malloy_types_1.expressionIsAggregate)(overload.returnType.expressionType) &&
304
343
  !isSymmetric &&
305
344
  this.generateDistinctKeyIfNecessary(resultSet, context, frag.structPath);
306
- const aggregateOrderBy = frag.orderBy
307
- ? 'ORDER BY ' +
308
- frag.orderBy
309
- .map(ob => {
310
- const osql = this.generateDimFragment(resultSet, context, ob.e, state);
311
- const dirsql = ob.dir === 'asc' ? ' ASC' : ob.dir === 'desc' ? ' DESC' : '';
312
- return `${osql}${dirsql}`;
313
- })
314
- .join(', ')
315
- : undefined;
316
345
  const aggregateLimit = frag.limit ? `LIMIT ${frag.limit}` : undefined;
346
+ if (frag.name === 'string_agg' &&
347
+ distinctKey &&
348
+ !context.dialect.supportsAggDistinct) {
349
+ return this.generateAsymmetricStringAggExpression(resultSet, context, args[0], args[1], distinctKey, frag.orderBy, context.dialect.name, state);
350
+ }
317
351
  if (distinctKey) {
318
352
  if (!context.dialect.supportsAggDistinct) {
319
- throw new Error(`Asymmetric aggregates are not supported for custom functions in ${context.dialect.name}.`);
353
+ throw new Error(`Function \`${frag.name}\` does not support fanning out in ${context.dialect.name}`);
320
354
  }
321
355
  const argsExpressions = args.map(arg => {
322
356
  return this.generateDimFragment(resultSet, context, arg, state);
323
357
  });
324
- return context.dialect.sqlAggDistinct(distinctKey, argsExpressions, valNames => {
325
- const funcCall = this.expandFunctionCall(context.dialect.name, overload, valNames.map(v => [v]), aggregateOrderBy, aggregateLimit);
358
+ const orderBys = (_b = frag.orderBy) !== null && _b !== void 0 ? _b : [];
359
+ const orderByExpressions = orderBys.map(ob => {
360
+ var _a, _b;
361
+ const defaultOrderByArgIndex = (_a = overload.dialect[context.dialect.name].defaultOrderByArgIndex) !== null && _a !== void 0 ? _a : 0;
362
+ const expr = (_b = ob.e) !== null && _b !== void 0 ? _b : args[defaultOrderByArgIndex];
363
+ return this.generateDimFragment(resultSet, context, expr, state);
364
+ });
365
+ return context.dialect.sqlAggDistinct(distinctKey, [...argsExpressions, ...orderByExpressions], valNames => {
366
+ const vals = valNames.map((v, i) => {
367
+ // Special case: the argument is required to be literal, so we use the actual argument
368
+ // rather than the packed value
369
+ // TODO don't even pack the value in the first place
370
+ if (i < args.length) {
371
+ const param = this.getParamForArgIndex(overload.params, i);
372
+ if (param.allowedTypes.every(t => (0, malloy_types_1.isLiteral)(t.evalSpace))) {
373
+ return args[i];
374
+ }
375
+ }
376
+ return [v];
377
+ });
378
+ const newArgs = vals.slice(0, argsExpressions.length);
379
+ const orderBy = vals
380
+ .slice(argsExpressions.length)
381
+ .map((e, i) => {
382
+ return { e, dir: orderBys[i].dir };
383
+ });
384
+ const orderBySQL = this.getFunctionOrderBy(resultSet, context, state, orderBy, newArgs, overload);
385
+ const funcCall = this.expandFunctionCall(context.dialect.name, overload, newArgs, orderBySQL, aggregateLimit);
326
386
  return this.generateExpressionFromExpr(resultSet, context, funcCall, state);
327
387
  });
328
388
  }
@@ -337,7 +397,7 @@ class QueryField extends QueryNode {
337
397
  // Update: Now we apply this only to arguments whose parameter is not constant-requiring.
338
398
  // So in `string_agg(val, sep)`, `sep` does not get filters applied to it because
339
399
  // it must be constant
340
- const param = overload.params[index];
400
+ const param = this.getParamForArgIndex(overload.params, index);
341
401
  // TODO technically this should probably look at _which_ allowed param type was matched
342
402
  // for this argument and see if that type is at most constant... but we lose type information
343
403
  // by this point in the compilation, so that info would have to be passed into the func call
@@ -347,14 +407,15 @@ class QueryField extends QueryNode {
347
407
  : [this.generateDimFragment(resultSet, context, arg, state)];
348
408
  })
349
409
  : args;
350
- const funcCall = this.expandFunctionCall(context.dialect.name, overload, mappedArgs, aggregateOrderBy, aggregateLimit);
410
+ const orderBySql = frag.orderBy
411
+ ? this.getFunctionOrderBy(resultSet, context, state, frag.orderBy, args, overload)
412
+ : '';
413
+ const funcCall = this.expandFunctionCall(context.dialect.name, overload, mappedArgs, orderBySql, aggregateLimit);
351
414
  if ((0, malloy_types_1.expressionIsAnalytic)(overload.returnType.expressionType)) {
352
- const extraPartitions = ((_b = frag.partitionBy) !== null && _b !== void 0 ? _b : []).map(outputName => {
415
+ const extraPartitions = ((_c = frag.partitionBy) !== null && _c !== void 0 ? _c : []).map(outputName => {
353
416
  return `(${resultSet.getField(outputName).getAnalyticalSQL(false)})`;
354
417
  });
355
- // TODO probably need to pass in the function and arguments separately
356
- // in order to generate parameter SQL correctly in BQ re: partition
357
- return this.generateAnalyticFragment(resultSet, context, funcCall, overload, state, args, extraPartitions, aggregateOrderBy);
418
+ return this.generateAnalyticFragment(resultSet, context, funcCall, overload, state, args, extraPartitions, orderBySql);
358
419
  }
359
420
  return this.generateExpressionFromExpr(resultSet, context, funcCall, state);
360
421
  }
@@ -1473,6 +1534,7 @@ class QueryQuery extends QueryField {
1473
1534
  .addStructToJoin(struct.getJoinableParent(), this, uniqueKeyPossibleUse, joinStack);
1474
1535
  }
1475
1536
  addDependantExpr(resultStruct, context, e, joinStack) {
1537
+ var _a;
1476
1538
  for (const expr of e) {
1477
1539
  if ((0, malloy_types_1.isFunctionCallFragment)(expr) &&
1478
1540
  (0, malloy_types_1.expressionIsAnalytic)(expr.overload.returnType.expressionType) &&
@@ -1564,11 +1626,18 @@ class QueryQuery extends QueryField {
1564
1626
  this.addDependantExpr(resultStruct, context, expr.e, joinStack);
1565
1627
  }
1566
1628
  else if ((0, malloy_types_1.isFunctionCallFragment)(expr)) {
1629
+ const isSymmetric = (_a = expr.overload.isSymmetric) !== null && _a !== void 0 ? _a : false;
1630
+ const isAggregate = (0, malloy_types_1.expressionIsAggregate)(expr.overload.returnType.expressionType);
1631
+ const isAsymmetricAggregate = isAggregate && !isSymmetric;
1632
+ const uniqueKeyPossibleUse = isAsymmetricAggregate
1633
+ ? 'generic_asymmetric_aggregate'
1634
+ : undefined;
1567
1635
  if (expr.structPath) {
1568
- this.addDependantPath(resultStruct, context, expr.structPath, 'generic_aggregate', joinStack);
1636
+ this.addDependantPath(resultStruct, context, expr.structPath, uniqueKeyPossibleUse, joinStack);
1637
+ }
1638
+ else if (isAsymmetricAggregate) {
1639
+ resultStruct.addStructToJoin(context, this, uniqueKeyPossibleUse, joinStack);
1569
1640
  }
1570
- // TODO Do we need to call `addStructToJoin` here in the case when there is no `structPath`
1571
- // and the function is an aggregate function?
1572
1641
  for (const e of expr.args) {
1573
1642
  this.addDependantExpr(resultStruct, context, e, joinStack);
1574
1643
  }
@@ -1577,7 +1646,9 @@ class QueryQuery extends QueryField {
1577
1646
  }
1578
1647
  if (expr.orderBy) {
1579
1648
  for (const ob of expr.orderBy) {
1580
- this.addDependantExpr(resultStruct, context, ob.e, joinStack);
1649
+ if (ob.e) {
1650
+ this.addDependantExpr(resultStruct, context, ob.e, joinStack);
1651
+ }
1581
1652
  }
1582
1653
  }
1583
1654
  }
@@ -132,6 +132,7 @@ export interface AggregateLimitFragment {
132
132
  }
133
133
  export interface FunctionCallFragment {
134
134
  type: 'function_call';
135
+ name: string;
135
136
  overload: FunctionOverloadDef;
136
137
  expressionType: ExpressionType;
137
138
  args: Expr[];
@@ -386,10 +387,15 @@ export interface OrderBy {
386
387
  field: string | number;
387
388
  dir?: 'asc' | 'desc';
388
389
  }
389
- export interface FunctionOrderBy {
390
+ export interface FunctionOrderByExpression {
390
391
  e: Expr;
391
392
  dir?: 'asc' | 'desc';
392
393
  }
394
+ export interface FunctionOrderByDefaultExpression {
395
+ e: undefined;
396
+ dir: 'asc' | 'desc';
397
+ }
398
+ export type FunctionOrderBy = FunctionOrderByExpression | FunctionOrderByDefaultExpression;
393
399
  export interface ByName {
394
400
  by: 'name';
395
401
  name: string;
@@ -599,7 +605,8 @@ export interface FunctionOverloadDef {
599
605
  dialect: {
600
606
  [dialect: string]: {
601
607
  e: Expr;
602
- supportsOrderBy?: boolean;
608
+ supportsOrderBy?: boolean | 'only_default';
609
+ defaultOrderByArgIndex?: number;
603
610
  supportsLimit?: boolean;
604
611
  };
605
612
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.122",
3
+ "version": "0.0.123-dev240205152128",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",