@malloydata/malloy 0.0.119-dev240116200529 → 0.0.119-dev240118215411

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.
@@ -60,6 +60,7 @@ const simple_numeric_functions_1 = require("./simple_numeric_functions");
60
60
  const avg_moving_1 = require("./avg_moving");
61
61
  const function_map_1 = require("./function_map");
62
62
  const coalesce_1 = require("./coalesce");
63
+ const sql_1 = require("./sql");
63
64
  /**
64
65
  * This is a function map containing default implementations of all Malloy
65
66
  * built-in functions. These don't work in all dialects, but are a good starting
@@ -131,5 +132,10 @@ exports.FUNCTIONS.add('min_window', sum_min_max_window_1.fnMinWindow);
131
132
  exports.FUNCTIONS.add('max_window', sum_min_max_window_1.fnMaxWindow);
132
133
  exports.FUNCTIONS.add('sum_window', sum_min_max_window_1.fnSumWindow);
133
134
  exports.FUNCTIONS.add('avg_moving', avg_moving_1.fnAvgRolling);
135
+ exports.FUNCTIONS.add('sql_number', sql_1.fnSqlNumber);
136
+ exports.FUNCTIONS.add('sql_string', sql_1.fnSqlString);
137
+ exports.FUNCTIONS.add('sql_date', sql_1.fnSqlDate);
138
+ exports.FUNCTIONS.add('sql_timestamp', sql_1.fnSqlTimestamp);
139
+ exports.FUNCTIONS.add('sql_boolean', sql_1.fnSqlBoolean);
134
140
  exports.FUNCTIONS.seal();
135
141
  //# sourceMappingURL=all_functions.js.map
@@ -0,0 +1,6 @@
1
+ import { DialectFunctionOverloadDef } from './util';
2
+ export declare function fnSqlNumber(): DialectFunctionOverloadDef[];
3
+ export declare function fnSqlString(): DialectFunctionOverloadDef[];
4
+ export declare function fnSqlDate(): DialectFunctionOverloadDef[];
5
+ export declare function fnSqlTimestamp(): DialectFunctionOverloadDef[];
6
+ export declare function fnSqlBoolean(): DialectFunctionOverloadDef[];
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright 2023 Google LLC
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining
6
+ * a copy of this software and associated documentation files
7
+ * (the "Software"), to deal in the Software without restriction,
8
+ * including without limitation the rights to use, copy, modify, merge,
9
+ * publish, distribute, sublicense, and/or sell copies of the Software,
10
+ * and to permit persons to whom the Software is furnished to do so,
11
+ * subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be
14
+ * included in all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.fnSqlBoolean = exports.fnSqlTimestamp = exports.fnSqlDate = exports.fnSqlString = exports.fnSqlNumber = void 0;
26
+ const util_1 = require("./util");
27
+ function fnSqlNumber() {
28
+ const value = (0, util_1.makeParam)('value', (0, util_1.literal)((0, util_1.maxScalar)('string')));
29
+ return [
30
+ (0, util_1.overload)((0, util_1.minScalar)('number'), [value.param], [{ type: 'sql-string', e: [value.arg] }]),
31
+ ];
32
+ }
33
+ exports.fnSqlNumber = fnSqlNumber;
34
+ function fnSqlString() {
35
+ const value = (0, util_1.makeParam)('value', (0, util_1.literal)((0, util_1.maxScalar)('string')));
36
+ return [
37
+ (0, util_1.overload)((0, util_1.minScalar)('string'), [value.param], [{ type: 'sql-string', e: [value.arg] }]),
38
+ ];
39
+ }
40
+ exports.fnSqlString = fnSqlString;
41
+ function fnSqlDate() {
42
+ const value = (0, util_1.makeParam)('value', (0, util_1.literal)((0, util_1.maxScalar)('string')));
43
+ return [
44
+ (0, util_1.overload)((0, util_1.minScalar)('date'), [value.param], [{ type: 'sql-string', e: [value.arg] }]),
45
+ ];
46
+ }
47
+ exports.fnSqlDate = fnSqlDate;
48
+ function fnSqlTimestamp() {
49
+ const value = (0, util_1.makeParam)('value', (0, util_1.literal)((0, util_1.maxScalar)('string')));
50
+ return [
51
+ (0, util_1.overload)((0, util_1.minScalar)('timestamp'), [value.param], [{ type: 'sql-string', e: [value.arg] }]),
52
+ ];
53
+ }
54
+ exports.fnSqlTimestamp = fnSqlTimestamp;
55
+ function fnSqlBoolean() {
56
+ const value = (0, util_1.makeParam)('value', (0, util_1.literal)((0, util_1.maxScalar)('string')));
57
+ return [
58
+ (0, util_1.overload)((0, util_1.minScalar)('boolean'), [value.param], [{ type: 'sql-string', e: [value.arg] }]),
59
+ ];
60
+ }
61
+ exports.fnSqlBoolean = fnSqlBoolean;
62
+ //# sourceMappingURL=sql.js.map
@@ -193,16 +193,16 @@ class PostgresDialect extends dialect_1.Dialect {
193
193
  return 'GEN_RANDOM_UUID()';
194
194
  }
195
195
  sqlFieldReference(alias, fieldName, fieldType, isNested, _isArray) {
196
- let ret = `${alias}->>'${fieldName}'`;
196
+ let ret = `(${alias}->>'${fieldName}')`;
197
197
  if (isNested) {
198
198
  switch (fieldType) {
199
199
  case 'string':
200
200
  break;
201
201
  case 'number':
202
- ret = `(${ret})::double precision`;
202
+ ret = `${ret}::double precision`;
203
203
  break;
204
204
  case 'struct':
205
- ret = `(${ret})::jsonb`;
205
+ ret = `${ret}::jsonb`;
206
206
  break;
207
207
  }
208
208
  return ret;
@@ -1,16 +1,16 @@
1
- import { FieldValueType, StructRelationship } from '../../../model/malloy_types';
1
+ import { AggregateFunctionType, FieldValueType, StructRelationship } from '../../../model/malloy_types';
2
2
  import { FieldReference } from '../query-items/field-references';
3
3
  import { ExprValue } from '../types/expr-value';
4
4
  import { ExpressionDef } from '../types/expression-def';
5
5
  import { FieldSpace } from '../types/field-space';
6
6
  export declare abstract class ExprAggregateFunction extends ExpressionDef {
7
- readonly func: string;
7
+ readonly func: AggregateFunctionType;
8
8
  elementType: string;
9
9
  source?: FieldReference;
10
10
  expr?: ExpressionDef;
11
11
  explicitSource?: boolean;
12
12
  legalChildTypes: import("../../../model/malloy_types").TypeDesc[];
13
- constructor(func: string, expr?: ExpressionDef, explicitSource?: boolean);
13
+ constructor(func: AggregateFunctionType, expr?: ExpressionDef, explicitSource?: boolean);
14
14
  returns(_forExpression: ExprValue): FieldValueType;
15
15
  getExpression(fs: FieldSpace): ExprValue;
16
16
  isSymmetricFunction(): boolean;
@@ -216,6 +216,7 @@ function getJoinUsage(fs, expr) {
216
216
  };
217
217
  (0, utils_1.exprWalk)(expr, frag => {
218
218
  if (typeof frag !== 'string') {
219
+ // TODO make this work for field references inside sql_* functions
219
220
  if (frag.type === 'field') {
220
221
  const def = lookup(fs, frag.path);
221
222
  if (def.def.type !== 'struct' && def.def.type !== 'turtle') {
@@ -161,7 +161,7 @@ class ExprFunc extends expression_def_1.ExpressionDef {
161
161
  .join(', ')}) with source`);
162
162
  return (0, ast_utils_1.errorFor)('cannot call with source');
163
163
  }
164
- const funcCall = [
164
+ let funcCall = [
165
165
  {
166
166
  type: 'function_call',
167
167
  overload,
@@ -170,6 +170,50 @@ class ExprFunc extends expression_def_1.ExpressionDef {
170
170
  structPath,
171
171
  },
172
172
  ];
173
+ if ([
174
+ 'sql_number',
175
+ 'sql_string',
176
+ 'sql_date',
177
+ 'sql_timestamp',
178
+ 'sql_boolean',
179
+ ].includes(func.name)) {
180
+ if (!this.inExperiment('sql_functions', true)) {
181
+ return (0, ast_utils_1.errorFor)(`Cannot use sql_function \`${func.name}\`; use \`sql_functions\` experiment to enable this behavior`);
182
+ }
183
+ const str = argExprs[0].value;
184
+ if (str.length !== 1 ||
185
+ typeof str[0] === 'string' ||
186
+ str[0].type !== 'dialect' ||
187
+ str[0].function !== 'stringLiteral') {
188
+ this.log(`Invalid string literal for \`${func.name}\``);
189
+ }
190
+ else {
191
+ const literal = str[0].literal;
192
+ const parts = parseSQLInterpolation(literal);
193
+ const unsupportedInterpolations = parts
194
+ .filter(part => part.type === 'interpolation' && part.name.includes('.'))
195
+ .map(unsupportedPart => unsupportedPart.type === 'interpolation'
196
+ ? `\${${unsupportedPart.name}}`
197
+ : `\${${unsupportedPart.value}}`);
198
+ if (unsupportedInterpolations.length > 0) {
199
+ const unsupportedInterpolationMsg = unsupportedInterpolations.length === 1
200
+ ? `'.' paths are not yet supported in sql interpolations, found ${unsupportedInterpolations.at(0)}`
201
+ : `'.' paths are not yet supported in sql interpolations, found [${unsupportedInterpolations.join(', ')}]`;
202
+ this.log(unsupportedInterpolationMsg);
203
+ return (0, ast_utils_1.errorFor)(`${unsupportedInterpolationMsg}. See LookML \${...} documentation at https://cloud.google.com/looker/docs/reference/param-field-sql#sql_for_dimensions`);
204
+ }
205
+ funcCall = [
206
+ {
207
+ type: 'sql-string',
208
+ e: parts.map(part => part.type === 'string'
209
+ ? part.value
210
+ : part.name === 'TABLE'
211
+ ? { type: 'source-reference' }
212
+ : { type: 'field-reference', path: part.name }),
213
+ },
214
+ ];
215
+ }
216
+ }
173
217
  if (type.dataType === 'any') {
174
218
  this.log(`Invalid return type ${type.dataType} for function '${this.name}'`);
175
219
  return (0, ast_utils_1.errorFor)('invalid return type');
@@ -285,4 +329,31 @@ function findOverload(func, args) {
285
329
  }
286
330
  }
287
331
  }
332
+ function parseSQLInterpolation(template) {
333
+ const parts = [];
334
+ let remaining = template;
335
+ while (remaining.length) {
336
+ const nextInterp = remaining.indexOf('${');
337
+ if (nextInterp === -1) {
338
+ parts.push({ type: 'string', value: remaining });
339
+ break;
340
+ }
341
+ else {
342
+ const interpEnd = remaining.slice(nextInterp).indexOf('}');
343
+ if (interpEnd === -1) {
344
+ parts.push({ type: 'string', value: remaining });
345
+ break;
346
+ }
347
+ if (nextInterp > 0) {
348
+ parts.push({ type: 'string', value: remaining.slice(0, nextInterp) });
349
+ }
350
+ parts.push({
351
+ type: 'interpolation',
352
+ name: remaining.slice(nextInterp + 2, interpEnd + nextInterp),
353
+ });
354
+ remaining = remaining.slice(interpEnd + nextInterp + 1);
355
+ }
356
+ }
357
+ return parts;
358
+ }
288
359
  //# sourceMappingURL=expr-func.js.map
@@ -1,5 +1,5 @@
1
1
  import { Dialect, DialectFieldList } from '../dialect';
2
- import { AggregateFragment, CompiledQuery, DialectFragment, Expr, FieldDef, FieldFragment, FieldRef, Filtered, FilterExpression, FilterFragment, FunctionCallFragment, FunctionOverloadDef, JoinRelationship, ModelDef, OrderBy, OutputFieldFragment, Parameter, ParameterFragment, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SpreadFragment, SQLExpressionFragment, StructDef, StructRef, TurtleDef, UngroupFragment } from './malloy_types';
2
+ import { AggregateFragment, AggregateFunctionType, CompiledQuery, DialectFragment, Expr, FieldDef, FieldFragment, FieldRef, FieldReferenceFragment, Filtered, FilterExpression, FilterFragment, FunctionCallFragment, FunctionOverloadDef, JoinRelationship, ModelDef, OrderBy, OutputFieldFragment, Parameter, ParameterFragment, PipeSegment, Query, QueryFieldDef, QuerySegment, ResultMetadataDef, ResultStructMetadataDef, SearchIndexResult, SourceReferenceFragment, SpreadFragment, SQLExpressionFragment, SqlStringFragment, StructDef, StructRef, TurtleDef, UngroupFragment } from './malloy_types';
3
3
  import { Connection } from '../runtime_types';
4
4
  import { AndChain } from './utils';
5
5
  import { QueryInfo } from '../dialect/dialect';
@@ -17,6 +17,11 @@ interface OutputPipelinedSQL {
17
17
  sqlFieldName: string;
18
18
  pipelineSQL: string;
19
19
  }
20
+ type UniqueKeyPossibleUse = AggregateFunctionType | 'generic_aggregate';
21
+ declare class UniqueKeyUse extends Set<UniqueKeyPossibleUse> {
22
+ add_use(k: UniqueKeyPossibleUse | undefined): this | undefined;
23
+ hasAsymetricFunctions(): boolean;
24
+ }
20
25
  declare class StageWriter {
21
26
  parent: StageWriter | undefined;
22
27
  withs: string[];
@@ -57,7 +62,7 @@ declare class QueryField extends QueryNode {
57
62
  fieldDef: FieldDef;
58
63
  parent: QueryStruct;
59
64
  constructor(fieldDef: FieldDef, parent: QueryStruct);
60
- mayNeedUniqueKey(): boolean;
65
+ uniqueKeyPossibleUse(): UniqueKeyPossibleUse | undefined;
61
66
  getJoinableParent(): QueryStruct;
62
67
  caseGroup(groupSets: number[], s: string): string;
63
68
  getFullOutputName(): string;
@@ -78,6 +83,9 @@ declare class QueryField extends QueryNode {
78
83
  generateAvgFragment(resultSet: FieldInstanceResult, context: QueryStruct, expr: AggregateFragment, state: GenerateState): string;
79
84
  generateCountFragment(resultSet: FieldInstanceResult, context: QueryStruct, expr: AggregateFragment, state: GenerateState): string;
80
85
  generateDialect(resultSet: FieldInstanceResult, context: QueryStruct, expr: DialectFragment, state: GenerateState): string;
86
+ generateFieldReference(resultSet: FieldInstanceResult, context: QueryStruct, expr: FieldReferenceFragment, state: GenerateState): string;
87
+ generateSqlString(resultSet: FieldInstanceResult, context: QueryStruct, expr: SqlStringFragment, state: GenerateState): string;
88
+ generateSourceReference(resultSet: FieldInstanceResult, context: QueryStruct, expr: SourceReferenceFragment): string;
81
89
  getAnalyticPartitions(resultStruct: FieldInstanceResult): string;
82
90
  generateAnalyticFragment(resultStruct: FieldInstanceResult, context: QueryStruct, expr: Expr, overload: FunctionOverloadDef, state: GenerateState, args: Expr[]): string;
83
91
  generateExpressionFromExpr(resultSet: FieldInstanceResult, context: QueryStruct, e: Expr, state?: GenerateState): string;
@@ -166,7 +174,7 @@ declare class FieldInstanceResult implements FieldInstance {
166
174
  structs(): FieldInstanceResult[];
167
175
  selectStructs(result: FieldInstanceResult[], fn: (result: FieldInstanceResult) => boolean): FieldInstanceResult[];
168
176
  calculateDefaultOrderBy(): OrderBy[];
169
- addStructToJoin(qs: QueryStruct, query: QueryQuery, mayNeedUniqueKey: boolean, joinStack: string[]): void;
177
+ addStructToJoin(qs: QueryStruct, query: QueryQuery, uniqueKeyPossibleUse: UniqueKeyPossibleUse | undefined, joinStack: string[]): void;
170
178
  findJoins(query: QueryQuery): void;
171
179
  root(): FieldInstanceResultRoot;
172
180
  getUngroupPartitions(ungroupSet: UngroupSet | undefined): FieldInstanceField[];
@@ -188,7 +196,7 @@ declare class JoinInstance {
188
196
  queryStruct: QueryStruct;
189
197
  alias: string;
190
198
  parent: JoinInstance | undefined;
191
- mayNeedUniqueKey: boolean;
199
+ uniqueKeyPossibleUses: UniqueKeyUse;
192
200
  makeUniqueKey: boolean;
193
201
  leafiest: boolean;
194
202
  joinFilterConditions?: QueryFieldBoolean[];
@@ -243,7 +251,7 @@ declare class QueryQuery extends QueryField {
243
251
  expandDependantField(resultStruct: FieldInstanceResult, fieldRef: FieldRef): void;
244
252
  expandWildCardStruct(struct: QueryStruct, expandChildren: boolean, filter?: ((qf: QueryNode) => boolean) | undefined): string[];
245
253
  expandWildCards(fields: QueryFieldDef[], filter?: ((qf: QueryNode) => boolean) | undefined): QueryFieldDef[];
246
- addDependantPath(resultStruct: FieldInstanceResult, context: QueryStruct, path: string, mayNeedUniqueKey: boolean, joinStack: string[]): void;
254
+ addDependantPath(resultStruct: FieldInstanceResult, context: QueryStruct, path: string, uniqueKeyPossibleUse: UniqueKeyPossibleUse | undefined, joinStack: string[]): void;
247
255
  addDependantExpr(resultStruct: FieldInstanceResult, context: QueryStruct, e: Expr, joinStack: string[]): void;
248
256
  addDependancies(resultStruct: FieldInstanceResult, field: QueryField): void;
249
257
  expandFields(resultStruct: FieldInstanceResult): void;
@@ -34,6 +34,16 @@ function generateSQLStringLiteral(sourceString) {
34
34
  function identifierNormalize(s) {
35
35
  return s.replace(/[^a-zA-Z0-9_]/g, '_o_');
36
36
  }
37
+ class UniqueKeyUse extends Set {
38
+ add_use(k) {
39
+ if (k !== undefined) {
40
+ return this.add(k);
41
+ }
42
+ }
43
+ hasAsymetricFunctions() {
44
+ return this.has('sum') || this.has('avg') || this.has('count');
45
+ }
46
+ }
37
47
  class StageWriter {
38
48
  constructor(useCTE = true, parent) {
39
49
  this.parent = parent;
@@ -173,8 +183,8 @@ class QueryField extends QueryNode {
173
183
  this.parent = parent;
174
184
  this.fieldDef = fieldDef;
175
185
  }
176
- mayNeedUniqueKey() {
177
- return false;
186
+ uniqueKeyPossibleUse() {
187
+ return undefined;
178
188
  }
179
189
  getJoinableParent() {
180
190
  // if it is inline it should always have a parent
@@ -436,11 +446,28 @@ class QueryField extends QueryNode {
436
446
  generateCountFragment(resultSet, context, expr, state) {
437
447
  let func = 'COUNT(';
438
448
  let thing = '1';
439
- const distinctKeySQL = this.generateDistinctKeyIfNecessary(resultSet, context, expr.structPath);
440
- if (distinctKeySQL) {
441
- func = 'COUNT(DISTINCT';
442
- thing = distinctKeySQL;
449
+ let struct = context;
450
+ if (expr.structPath) {
451
+ struct = this.parent.root().getStructByName(expr.structPath);
452
+ }
453
+ const joinName = struct.getJoinableParent().getIdentifier();
454
+ const join = resultSet.root().joins.get(joinName);
455
+ if (!join) {
456
+ throw new Error(`Join ${joinName} not found in result set`);
443
457
  }
458
+ if (!join.leafiest || join.makeUniqueKey) {
459
+ func = 'COUNT(DISTINCT';
460
+ thing = struct.getDistinctKey().generateExpression(resultSet);
461
+ }
462
+ // const distinctKeySQL = this.generateDistinctKeyIfNecessary(
463
+ // resultSet,
464
+ // context,
465
+ // expr.structPath
466
+ // );
467
+ // if (distinctKeySQL) {
468
+ // func = 'COUNT(DISTINCT';
469
+ // thing = distinctKeySQL;
470
+ // }
444
471
  // find the structDef and return the path to the field...
445
472
  if (state.whereSQL) {
446
473
  return `${func} CASE WHEN ${state.whereSQL} THEN ${thing} END)`;
@@ -452,6 +479,24 @@ class QueryField extends QueryNode {
452
479
  generateDialect(resultSet, context, expr, state) {
453
480
  return this.generateExpressionFromExpr(resultSet, context, context.dialect.dialectExpr(resultSet.getQueryInfo(), expr), state);
454
481
  }
482
+ generateFieldReference(resultSet, context, expr, state) {
483
+ return this.generateFieldFragment(resultSet, context, { type: 'field', path: expr.path }, state);
484
+ }
485
+ generateSqlString(resultSet, context, expr, state) {
486
+ return expr.e
487
+ .map(part => typeof part === 'string'
488
+ ? part
489
+ : this.generateExpressionFromExpr(resultSet, context, [part], state))
490
+ .join('');
491
+ }
492
+ generateSourceReference(resultSet, context, expr) {
493
+ if (expr.path === undefined) {
494
+ return context.getSQLIdentifier();
495
+ }
496
+ else {
497
+ return context.getFieldByName(expr.path).getIdentifier();
498
+ }
499
+ }
455
500
  getAnalyticPartitions(resultStruct) {
456
501
  const ret = [];
457
502
  let p = resultStruct.parent;
@@ -616,6 +661,15 @@ class QueryField extends QueryNode {
616
661
  else if (expr.type === 'dialect') {
617
662
  s += this.generateDialect(resultSet, context, expr, state);
618
663
  }
664
+ else if (expr.type === 'sql-string') {
665
+ s += this.generateSqlString(resultSet, context, expr, state);
666
+ }
667
+ else if (expr.type === 'source-reference') {
668
+ s += this.generateSourceReference(resultSet, context, expr);
669
+ }
670
+ else if (expr.type === 'field-reference') {
671
+ s += this.generateFieldReference(resultSet, context, expr, state);
672
+ }
619
673
  else {
620
674
  throw new Error(`Internal Error: Unknown expression fragment ${JSON.stringify(expr, undefined, 2)}`);
621
675
  }
@@ -746,7 +800,8 @@ class QueryFieldDistinctKey extends QueryAtomicField {
746
800
  }
747
801
  else if (this.parent.fieldDef.structSource.type === 'nested') {
748
802
  const parentKey = (_a = this.parent.parent) === null || _a === void 0 ? void 0 : _a.getDistinctKey().generateExpression(resultSet);
749
- return `CONCAT(${parentKey}, 'x', ${this.parent.dialect.sqlFieldReference(this.parent.getIdentifier(), '__row_id', 'string', true, false)})`;
803
+ return this.parent.dialect.concat(parentKey || '', // shouldn't have to do this...
804
+ "'x'", this.parent.dialect.sqlFieldReference(this.parent.getIdentifier(), '__row_id', 'string', true, false));
750
805
  }
751
806
  else {
752
807
  // return this.parent.getIdentifier() + "." + "__distinct_key";
@@ -1003,7 +1058,7 @@ class FieldInstanceResult {
1003
1058
  }
1004
1059
  return [];
1005
1060
  }
1006
- addStructToJoin(qs, query, mayNeedUniqueKey, joinStack) {
1061
+ addStructToJoin(qs, query, uniqueKeyPossibleUse, joinStack) {
1007
1062
  var _a;
1008
1063
  const name = qs.getIdentifier();
1009
1064
  // we're already chasing the dependency for this join.
@@ -1012,7 +1067,7 @@ class FieldInstanceResult {
1012
1067
  }
1013
1068
  let join;
1014
1069
  if ((join = this.root().joins.get(name))) {
1015
- join.mayNeedUniqueKey || (join.mayNeedUniqueKey = mayNeedUniqueKey);
1070
+ join.uniqueKeyPossibleUses.add_use(uniqueKeyPossibleUse);
1016
1071
  return;
1017
1072
  }
1018
1073
  // if we have a parent, join it first.
@@ -1020,7 +1075,7 @@ class FieldInstanceResult {
1020
1075
  const parentStruct = (_a = qs.parent) === null || _a === void 0 ? void 0 : _a.getJoinableParent();
1021
1076
  if (parentStruct) {
1022
1077
  // add dependant expressions first...
1023
- this.addStructToJoin(parentStruct, query, false, joinStack);
1078
+ this.addStructToJoin(parentStruct, query, undefined, joinStack);
1024
1079
  parent = this.root().joins.get(parentStruct.getIdentifier());
1025
1080
  }
1026
1081
  // add any dependant joins based on the ON
@@ -1038,11 +1093,11 @@ class FieldInstanceResult {
1038
1093
  join = new JoinInstance(qs, name, parent);
1039
1094
  this.root().joins.set(name, join);
1040
1095
  }
1041
- join.mayNeedUniqueKey || (join.mayNeedUniqueKey = mayNeedUniqueKey);
1096
+ join.uniqueKeyPossibleUses.add_use(uniqueKeyPossibleUse);
1042
1097
  }
1043
1098
  findJoins(query) {
1044
1099
  for (const dim of this.fields()) {
1045
- this.addStructToJoin(dim.f.getJoinableParent(), query, dim.f.mayNeedUniqueKey(), []);
1100
+ this.addStructToJoin(dim.f.getJoinableParent(), query, dim.f.uniqueKeyPossibleUse(), []);
1046
1101
  }
1047
1102
  for (const s of this.structs()) {
1048
1103
  s.findJoins(query);
@@ -1167,8 +1222,20 @@ class FieldInstanceResultRoot extends FieldInstanceResult {
1167
1222
  // Nested Unique keys are dependant on the primary key of the parent
1168
1223
  // and the table.
1169
1224
  for (const [_name, join] of this.joins) {
1170
- // don't need keys on leafiest
1171
- if (!join.leafiest && join.mayNeedUniqueKey) {
1225
+ // in a one_to_many join we need a key to count there may be a failed
1226
+ // match in a left join.
1227
+ // users -> {
1228
+ // group_by: user_id
1229
+ // aggregate: order_count is orders.count()
1230
+ if (join.leafiest) {
1231
+ if (join.parent !== null &&
1232
+ join.uniqueKeyPossibleUses.has('count') &&
1233
+ !join.queryStruct.primaryKey()) {
1234
+ join.makeUniqueKey = true;
1235
+ }
1236
+ }
1237
+ else if (!join.leafiest &&
1238
+ join.uniqueKeyPossibleUses.hasAsymetricFunctions()) {
1172
1239
  let j = join;
1173
1240
  while (j) {
1174
1241
  if (!j.queryStruct.primaryKey()) {
@@ -1190,7 +1257,7 @@ class JoinInstance {
1190
1257
  this.queryStruct = queryStruct;
1191
1258
  this.alias = alias;
1192
1259
  this.parent = parent;
1193
- this.mayNeedUniqueKey = false;
1260
+ this.uniqueKeyPossibleUses = new UniqueKeyUse();
1194
1261
  this.makeUniqueKey = false;
1195
1262
  this.leafiest = false;
1196
1263
  this.children = [];
@@ -1474,7 +1541,7 @@ class QueryQuery extends QueryField {
1474
1541
  }
1475
1542
  return ret;
1476
1543
  }
1477
- addDependantPath(resultStruct, context, path, mayNeedUniqueKey, joinStack) {
1544
+ addDependantPath(resultStruct, context, path, uniqueKeyPossibleUse, joinStack) {
1478
1545
  const node = context.getFieldByName(path);
1479
1546
  let struct;
1480
1547
  if (node instanceof QueryField) {
@@ -1488,7 +1555,7 @@ class QueryQuery extends QueryField {
1488
1555
  }
1489
1556
  resultStruct
1490
1557
  .root()
1491
- .addStructToJoin(struct.getJoinableParent(), this, mayNeedUniqueKey, joinStack);
1558
+ .addStructToJoin(struct.getJoinableParent(), this, uniqueKeyPossibleUse, joinStack);
1492
1559
  }
1493
1560
  addDependantExpr(resultStruct, context, e, joinStack) {
1494
1561
  for (const expr of e) {
@@ -1523,7 +1590,7 @@ class QueryQuery extends QueryField {
1523
1590
  else {
1524
1591
  resultStruct
1525
1592
  .root()
1526
- .addStructToJoin(field.parent.getJoinableParent(), this, false, joinStack);
1593
+ .addStructToJoin(field.parent.getJoinableParent(), this, undefined, joinStack);
1527
1594
  // this.addDependantPath(resultStruct, field.parent, expr.path, false);
1528
1595
  }
1529
1596
  }
@@ -1572,18 +1639,18 @@ class QueryQuery extends QueryField {
1572
1639
  else if ((0, malloy_types_1.isAggregateFragment)(expr)) {
1573
1640
  if ((0, malloy_types_1.isAsymmetricFragment)(expr)) {
1574
1641
  if (expr.structPath) {
1575
- this.addDependantPath(resultStruct, context, expr.structPath, true, joinStack);
1642
+ this.addDependantPath(resultStruct, context, expr.structPath, expr.function, joinStack);
1576
1643
  }
1577
1644
  else {
1578
1645
  // we are doing a sum in the root. It may need symetric aggregates
1579
- resultStruct.addStructToJoin(context, this, true, joinStack);
1646
+ resultStruct.addStructToJoin(context, this, expr.function, joinStack);
1580
1647
  }
1581
1648
  }
1582
1649
  this.addDependantExpr(resultStruct, context, expr.e, joinStack);
1583
1650
  }
1584
1651
  else if ((0, malloy_types_1.isFunctionCallFragment)(expr)) {
1585
1652
  if (expr.structPath) {
1586
- this.addDependantPath(resultStruct, context, expr.structPath, true, joinStack);
1653
+ this.addDependantPath(resultStruct, context, expr.structPath, 'generic_aggregate', joinStack);
1587
1654
  }
1588
1655
  // TODO Do we need to call `addStructToJoin` here in the case when there is no `structPath`
1589
1656
  // and the function is an aggregate function?
@@ -1688,7 +1755,7 @@ class QueryQuery extends QueryField {
1688
1755
  prepare(_stageWriter) {
1689
1756
  if (!this.prepared) {
1690
1757
  this.expandFields(this.rootResult);
1691
- this.rootResult.addStructToJoin(this.parent, this, false, []);
1758
+ this.rootResult.addStructToJoin(this.parent, this, undefined, []);
1692
1759
  this.rootResult.findJoins(this);
1693
1760
  this.rootResult.calculateSymmetricAggregates();
1694
1761
  this.prepared = true;
@@ -108,9 +108,10 @@ export interface FilterFragment {
108
108
  }
109
109
  export declare function isFilterFragment(f: Fragment): f is FilterFragment;
110
110
  export declare function isDialectFragment(f: Fragment): f is DialectFragment;
111
+ export type AggregateFunctionType = 'sum' | 'avg' | 'count' | 'count_distinct' | 'max' | 'min';
111
112
  export interface AggregateFragment {
112
113
  type: 'aggregate';
113
- function: string;
114
+ function: AggregateFunctionType;
114
115
  e: Expr;
115
116
  structPath?: string;
116
117
  }
@@ -150,6 +151,21 @@ export interface FieldFragment {
150
151
  path: string;
151
152
  }
152
153
  export declare function isFieldFragment(f: Fragment): f is FieldFragment;
154
+ export interface FieldReferenceFragment {
155
+ type: 'field-reference';
156
+ path: string;
157
+ }
158
+ export declare function isFieldReferenceFragment(f: Fragment): f is FieldReferenceFragment;
159
+ export interface SqlStringFragment {
160
+ type: 'sql-string';
161
+ e: Expr;
162
+ }
163
+ export declare function isSqlStringFragment(f: Fragment): f is SqlStringFragment;
164
+ export interface SourceReferenceFragment {
165
+ type: 'source-reference';
166
+ path?: string;
167
+ }
168
+ export declare function isSourceReferenceFragment(f: Fragment): f is SourceReferenceFragment;
153
169
  export interface ParameterFragment {
154
170
  type: 'parameter';
155
171
  path: string;
@@ -233,7 +249,7 @@ export interface NumberLiteralFragment extends DialectFragmentBase {
233
249
  literal: string;
234
250
  }
235
251
  export type DialectFragment = DivFragment | TimeLiteralFragment | NowFragment | TimeDeltaFragment | TimeDiffFragment | TimeTruncFragment | TypecastFragment | TimeExtractFragment | StringLiteralFragment | RegexpLiteralFragment | NumberLiteralFragment | RegexpMatchFragment;
236
- export type Fragment = string | ApplyFragment | ApplyValueFragment | FieldFragment | ParameterFragment | FilterFragment | OutputFieldFragment | AggregateFragment | UngroupFragment | DialectFragment | FunctionParameterFragment | FunctionCallFragment | SQLExpressionFragment | SpreadFragment;
252
+ export type Fragment = string | ApplyFragment | ApplyValueFragment | FieldFragment | FieldReferenceFragment | SourceReferenceFragment | SqlStringFragment | ParameterFragment | FilterFragment | OutputFieldFragment | AggregateFragment | UngroupFragment | DialectFragment | FunctionParameterFragment | FunctionCallFragment | SQLExpressionFragment | SpreadFragment;
237
253
  export type Expr = Fragment[];
238
254
  export interface TypedValue {
239
255
  value: Expr;
@@ -22,8 +22,8 @@
22
22
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.isMatrixOperation = exports.isIndexSegment = exports.isRawSegment = exports.isSamplingEnable = exports.isSamplingPercent = exports.isSamplingRows = exports.isQuerySegment = exports.isProjectSegment = exports.isPartialSegment = exports.isReduceSegment = exports.refIsStructDef = exports.isByExpression = exports.isByName = exports.ValueType = exports.isExtractUnit = exports.isTimestampUnit = exports.isDateUnit = exports.FieldIsIntrinsic = exports.isCastType = exports.isAtomicFieldType = exports.isTimeFieldType = exports.hasExpression = exports.maxOfExpressionTypes = exports.maxExpressionType = exports.isExpressionTypeLEQ = exports.expressionIsAnalytic = exports.expressionIsCalculation = exports.expressionInvolvesAggregate = exports.expressionIsUngroupedAggregate = exports.expressionIsAggregate = exports.expressionIsScalar = exports.mkExpr = exports.isApplyFragment = exports.isApplyValue = exports.isParameterFragment = exports.isFieldFragment = exports.isSpreadFragment = exports.isSQLExpressionFragment = exports.isFunctionCallFragment = exports.isFunctionParameterFragment = exports.isUngroupFragment = exports.isAsymmetricFragment = exports.isAggregateFragment = exports.isDialectFragment = exports.isFilterFragment = exports.isOutputFieldFragment = exports.isFilteredAliasedName = exports.paramHasValue = exports.isConditionParameter = exports.isValueParameter = void 0;
26
- exports.isValueDate = exports.isValueTimestamp = exports.isValueBoolean = exports.isValueNumber = exports.isValueString = exports.isMeasureLike = exports.getPhysicalFields = exports.getDimensions = exports.isPhysical = exports.isDimensional = exports.isAtomicField = exports.isTurtleDef = exports.getIdentifier = exports.isFieldStructDef = exports.isFieldTimeBased = exports.isFieldTypeDef = exports.isSQLBlockStruct = exports.mergeEvalSpaces = exports.isSQLFragment = exports.isJoinOn = void 0;
25
+ exports.isSamplingEnable = exports.isSamplingPercent = exports.isSamplingRows = exports.isQuerySegment = exports.isProjectSegment = exports.isPartialSegment = exports.isReduceSegment = exports.refIsStructDef = exports.isByExpression = exports.isByName = exports.ValueType = exports.isExtractUnit = exports.isTimestampUnit = exports.isDateUnit = exports.FieldIsIntrinsic = exports.isCastType = exports.isAtomicFieldType = exports.isTimeFieldType = exports.hasExpression = exports.maxOfExpressionTypes = exports.maxExpressionType = exports.isExpressionTypeLEQ = exports.expressionIsAnalytic = exports.expressionIsCalculation = exports.expressionInvolvesAggregate = exports.expressionIsUngroupedAggregate = exports.expressionIsAggregate = exports.expressionIsScalar = exports.mkExpr = exports.isApplyFragment = exports.isApplyValue = exports.isParameterFragment = exports.isSourceReferenceFragment = exports.isSqlStringFragment = exports.isFieldReferenceFragment = exports.isFieldFragment = exports.isSpreadFragment = exports.isSQLExpressionFragment = exports.isFunctionCallFragment = exports.isFunctionParameterFragment = exports.isUngroupFragment = exports.isAsymmetricFragment = exports.isAggregateFragment = exports.isDialectFragment = exports.isFilterFragment = exports.isOutputFieldFragment = exports.isFilteredAliasedName = exports.paramHasValue = exports.isConditionParameter = exports.isValueParameter = void 0;
26
+ exports.isValueDate = exports.isValueTimestamp = exports.isValueBoolean = exports.isValueNumber = exports.isValueString = exports.isMeasureLike = exports.getPhysicalFields = exports.getDimensions = exports.isPhysical = exports.isDimensional = exports.isAtomicField = exports.isTurtleDef = exports.getIdentifier = exports.isFieldStructDef = exports.isFieldTimeBased = exports.isFieldTypeDef = exports.isSQLBlockStruct = exports.mergeEvalSpaces = exports.isSQLFragment = exports.isJoinOn = exports.isMatrixOperation = exports.isIndexSegment = exports.isRawSegment = void 0;
27
27
  function isValueParameter(p) {
28
28
  return p.value !== undefined;
29
29
  }
@@ -90,6 +90,18 @@ function isFieldFragment(f) {
90
90
  return (f === null || f === void 0 ? void 0 : f.type) === 'field';
91
91
  }
92
92
  exports.isFieldFragment = isFieldFragment;
93
+ function isFieldReferenceFragment(f) {
94
+ return (f === null || f === void 0 ? void 0 : f.type) === 'field-reference';
95
+ }
96
+ exports.isFieldReferenceFragment = isFieldReferenceFragment;
97
+ function isSqlStringFragment(f) {
98
+ return (f === null || f === void 0 ? void 0 : f.type) === 'sql-string';
99
+ }
100
+ exports.isSqlStringFragment = isSqlStringFragment;
101
+ function isSourceReferenceFragment(f) {
102
+ return (f === null || f === void 0 ? void 0 : f.type) === 'source-reference';
103
+ }
104
+ exports.isSourceReferenceFragment = isSourceReferenceFragment;
93
105
  function isParameterFragment(f) {
94
106
  return (f === null || f === void 0 ? void 0 : f.type) === 'parameter';
95
107
  }
@@ -126,6 +126,11 @@ function exprMap(expr, func) {
126
126
  case 'parameter':
127
127
  case 'outputField':
128
128
  return fragment;
129
+ case 'sql-string':
130
+ return {
131
+ ...fragment,
132
+ e: exprMap(fragment.e, func),
133
+ };
129
134
  case 'function_call':
130
135
  return {
131
136
  ...fragment,
@@ -232,6 +237,11 @@ function exprWalk(expr, func) {
232
237
  ...fragment,
233
238
  args: fragment.args.map(arg => exprWalk(arg, func)),
234
239
  };
240
+ case 'sql-string':
241
+ return {
242
+ ...fragment,
243
+ e: exprWalk(fragment.e, func),
244
+ };
235
245
  case 'filterExpression':
236
246
  return {
237
247
  ...fragment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.119-dev240116200529",
3
+ "version": "0.0.119-dev240118215411",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",