@malloydata/malloy 0.0.123-dev240209030627 → 0.0.123-dev240209210150

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.
@@ -226,11 +226,12 @@ class ExprFunc extends expression_def_1.ExpressionDef {
226
226
  if (e.found === undefined) {
227
227
  partitionField.log(`${partitionField.refString} is not defined`);
228
228
  }
229
- else if ((0, malloy_types_1.expressionIsScalar)(e.found.typeDesc().expressionType)) {
230
- partitionByFields.push(partitionField.nameString);
229
+ else if ((0, malloy_types_1.expressionIsAnalytic)(e.found.typeDesc().expressionType) ||
230
+ (0, malloy_types_1.expressionIsUngroupedAggregate)(e.found.typeDesc().expressionType)) {
231
+ partitionField.log('Partition expression must be scalar or aggregate');
231
232
  }
232
233
  else {
233
- partitionField.log('Partition expression must be scalar');
234
+ partitionByFields.push(partitionField.nameString);
234
235
  }
235
236
  }
236
237
  }
@@ -215,6 +215,25 @@ describe('expressions', () => {
215
215
  calculate: x is lag(ai) { partition_by: ai; order_by: ai }
216
216
  }
217
217
  `).toTranslate();
218
+ });
219
+ test('partition by works with scalar and aggregate', () => {
220
+ expect((0, test_translator_1.markSource) `
221
+ run: a -> {
222
+ group_by: ai
223
+ aggregate: c is count()
224
+ calculate: x is lag(ai) { partition_by: ai, c }
225
+ }
226
+ `).toTranslate();
227
+ });
228
+ test('partition by fails with analytic and ungrouped aggregate', () => {
229
+ expect((0, test_translator_1.markSource) `
230
+ run: a -> {
231
+ group_by: ai
232
+ aggregate: ac is all(count())
233
+ calculate: x is lag(ai) { partition_by: ac }
234
+ calculate: y is lag(ai) { partition_by: x }
235
+ }
236
+ `).translationToFailWith('Partition expression must be scalar or aggregate', 'Partition expression must be scalar or aggregate');
218
237
  });
219
238
  test('analytics order_by requires expression', () => {
220
239
  expect((0, test_translator_1.markSource) `
package/dist/malloy.d.ts CHANGED
@@ -1296,13 +1296,33 @@ export declare abstract class DataWriter {
1296
1296
  export declare class JSONWriter extends DataWriter {
1297
1297
  process(data: AsyncIterableIterator<DataRecord>): Promise<void>;
1298
1298
  }
1299
+ /**
1300
+ * CSV writer class that handles nested data.
1301
+ * This writer creates CSV using a DFS traversal of the result dataset.
1302
+ * Each trivial column value is converted to a CSV of 1x1 matrix and all the
1303
+ * columns are merged together to create a CSV that represents 1 QueryDataRow.
1304
+ * Since this follows DFS, each non trivial data is rendered into a NxM matrix
1305
+ * where N is the number of rows in the nested data and M is the number of
1306
+ * columns it has.
1307
+ * For any row with X number of columns, we end up with X number of NxM matrices
1308
+ * where the value of N,M pair may be different for each column.
1309
+ * We then merge the matrices so that we end up with a larger matrix of size
1310
+ * Max(N)xSum(M) by taking one row of csv from each matric at a time. For any
1311
+ * matrix with N<Max(N), we add a row of empty CSV cells of size N.
1312
+ */
1299
1313
  export declare class CSVWriter extends DataWriter {
1300
1314
  private readonly columnSeparator;
1301
1315
  private readonly rowSeparator;
1302
1316
  private readonly quoteCharacter;
1303
1317
  private readonly includeHeader;
1318
+ private readonly emptyCell;
1304
1319
  private escape;
1305
1320
  private stringify;
1321
+ private getColWeight;
1322
+ private getHeaderRow;
1323
+ private mergeMatrices;
1324
+ private getChildMatrix;
1325
+ private getRowMatrix;
1306
1326
  process(data: AsyncIterableIterator<DataRecord>): Promise<void>;
1307
1327
  }
1308
1328
  export {};
package/dist/malloy.js CHANGED
@@ -2831,6 +2831,20 @@ class JSONWriter extends DataWriter {
2831
2831
  }
2832
2832
  }
2833
2833
  exports.JSONWriter = JSONWriter;
2834
+ /**
2835
+ * CSV writer class that handles nested data.
2836
+ * This writer creates CSV using a DFS traversal of the result dataset.
2837
+ * Each trivial column value is converted to a CSV of 1x1 matrix and all the
2838
+ * columns are merged together to create a CSV that represents 1 QueryDataRow.
2839
+ * Since this follows DFS, each non trivial data is rendered into a NxM matrix
2840
+ * where N is the number of rows in the nested data and M is the number of
2841
+ * columns it has.
2842
+ * For any row with X number of columns, we end up with X number of NxM matrices
2843
+ * where the value of N,M pair may be different for each column.
2844
+ * We then merge the matrices so that we end up with a larger matrix of size
2845
+ * Max(N)xSum(M) by taking one row of csv from each matric at a time. For any
2846
+ * matrix with N<Max(N), we add a row of empty CSV cells of size N.
2847
+ */
2834
2848
  class CSVWriter extends DataWriter {
2835
2849
  constructor() {
2836
2850
  super(...arguments);
@@ -2838,6 +2852,7 @@ class CSVWriter extends DataWriter {
2838
2852
  this.rowSeparator = '\n';
2839
2853
  this.quoteCharacter = '"';
2840
2854
  this.includeHeader = true;
2855
+ this.emptyCell = '';
2841
2856
  }
2842
2857
  escape(value) {
2843
2858
  const hasInnerQuote = value.includes(this.quoteCharacter);
@@ -2852,50 +2867,145 @@ class CSVWriter extends DataWriter {
2852
2867
  }
2853
2868
  return value;
2854
2869
  }
2855
- stringify(cell) {
2856
- if (cell.isNull()) {
2857
- return '';
2858
- }
2859
- else if (cell.isArray() ||
2860
- cell.isRecord() ||
2861
- cell.isBoolean() ||
2862
- cell.isNumber()) {
2863
- return JSON.stringify(cell.value);
2870
+ // Re-using the old stringify method for sanity.
2871
+ stringify(value) {
2872
+ if (value === null) {
2873
+ return this.emptyCell;
2864
2874
  }
2865
- else if (cell.isDate() || cell.isTimestamp()) {
2866
- return cell.value.toISOString();
2875
+ else if (value instanceof Date) {
2876
+ return value.toISOString();
2867
2877
  }
2868
- else if (cell.isString()) {
2869
- return cell.value;
2878
+ else if (typeof value === 'boolean' || typeof value === 'number') {
2879
+ return JSON.stringify(value);
2870
2880
  }
2871
2881
  else {
2872
- return `${cell.value}`;
2882
+ return `${value}`;
2883
+ }
2884
+ }
2885
+ // Extra weight to be added becase of nested tables inside the cells.
2886
+ getColWeight(jsonVal) {
2887
+ let firstVal = jsonVal;
2888
+ if (Array.isArray(jsonVal)) {
2889
+ firstVal = jsonVal[0];
2890
+ }
2891
+ let numKeys = 0;
2892
+ for (const key in firstVal) {
2893
+ numKeys = numKeys + 1;
2894
+ const val = firstVal[key];
2895
+ if (Array.isArray(val)) {
2896
+ const weight = this.getColWeight(val) - 1;
2897
+ numKeys = numKeys + weight;
2898
+ }
2899
+ }
2900
+ return numKeys;
2901
+ }
2902
+ // Get header row along with extra empty spaces for nested children.
2903
+ getHeaderRow(row) {
2904
+ const csv = [];
2905
+ let width = 0;
2906
+ for (const key in row) {
2907
+ csv.push(this.escape(key));
2908
+ const val = row[key];
2909
+ width++;
2910
+ if (Array.isArray(val)) {
2911
+ const numKeys = this.getColWeight(val) - 1;
2912
+ width = width + numKeys;
2913
+ for (let i = 0; i < numKeys; i++) {
2914
+ csv.push(this.emptyCell);
2915
+ }
2916
+ }
2917
+ }
2918
+ return { rows: [csv.join(this.columnSeparator)], length: 1, width: width };
2919
+ }
2920
+ // Merge the child matrices i.e. merge the columns into one bigger matrix i.e. CSV.
2921
+ mergeMatrices(matrices) {
2922
+ const maxLength = Math.max(...matrices.map(matrix => matrix.length));
2923
+ const matrixWidth = matrices.reduce((sum, matrix) => sum + matrix.width, 0);
2924
+ const csvMatrix = [];
2925
+ for (let i = 0; i < maxLength; i++) {
2926
+ const csvRow = [];
2927
+ for (const matrix of matrices) {
2928
+ if (i < matrix.length) {
2929
+ csvRow.push(matrix.rows[i]);
2930
+ }
2931
+ else {
2932
+ // Add empty cells.
2933
+ const emptyCells = Array(matrix.width).fill(this.emptyCell);
2934
+ csvRow.push(...emptyCells);
2935
+ }
2936
+ }
2937
+ csvMatrix.push(csvRow.join(this.columnSeparator));
2873
2938
  }
2939
+ return {
2940
+ rows: csvMatrix,
2941
+ length: maxLength,
2942
+ width: matrixWidth,
2943
+ };
2944
+ }
2945
+ // Gets CSV for a data cell that has nested data.
2946
+ getChildMatrix(jsonVal) {
2947
+ // This is not expected to happen.
2948
+ if (!Array.isArray(jsonVal)) {
2949
+ return {
2950
+ rows: ['Invalid data found, value is not an array'],
2951
+ length: 1,
2952
+ width: 1,
2953
+ };
2954
+ }
2955
+ else if (jsonVal.length === 0) {
2956
+ return {
2957
+ rows: [''],
2958
+ length: 1,
2959
+ width: 1,
2960
+ };
2961
+ }
2962
+ const csvMatrix = [];
2963
+ const header = this.getHeaderRow(jsonVal[0]);
2964
+ // Header has 1 row.
2965
+ csvMatrix.push(...header.rows);
2966
+ const width = header.width;
2967
+ let rowCount = 1;
2968
+ for (const row of jsonVal) {
2969
+ const rowMatrix = this.getRowMatrix(row);
2970
+ rowCount = rowCount + rowMatrix.length;
2971
+ csvMatrix.push(...rowMatrix.rows);
2972
+ }
2973
+ return { rows: csvMatrix, length: rowCount, width: width };
2974
+ }
2975
+ // Creates CSV content for one row of data.
2976
+ getRowMatrix(row) {
2977
+ const matrices = [];
2978
+ for (const key in row) {
2979
+ const val = row[key];
2980
+ if (!Array.isArray(val)) {
2981
+ const cell = {
2982
+ rows: [this.stringify(val)],
2983
+ length: 1,
2984
+ width: 1,
2985
+ };
2986
+ matrices.push(cell);
2987
+ }
2988
+ else {
2989
+ const cell = this.getChildMatrix(val);
2990
+ matrices.push(cell);
2991
+ }
2992
+ }
2993
+ return this.mergeMatrices(matrices);
2874
2994
  }
2875
2995
  async process(data) {
2876
- let fields;
2996
+ let headerDefined = false;
2877
2997
  for await (const row of data) {
2878
- if (fields === undefined) {
2879
- fields = row.field.allFields;
2880
- if (this.includeHeader) {
2881
- for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
2882
- const field = fields[fieldIndex];
2883
- this.stream.write(this.escape(field.name));
2884
- if (fieldIndex !== fields.length - 1) {
2885
- this.stream.write(this.columnSeparator);
2886
- }
2887
- }
2888
- this.stream.write(this.rowSeparator);
2889
- }
2998
+ if (!headerDefined && this.includeHeader) {
2999
+ const header = this.getHeaderRow(row.toObject());
3000
+ this.stream.write(header.rows[0]);
3001
+ this.stream.write(this.rowSeparator);
3002
+ headerDefined = true;
2890
3003
  }
2891
- for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
2892
- const field = fields[fieldIndex];
2893
- this.stream.write(this.escape(this.stringify(row.cell(field))));
2894
- if (fieldIndex !== fields.length - 1) {
2895
- this.stream.write(this.columnSeparator);
2896
- }
3004
+ const rowCsv = this.getRowMatrix(row.toObject());
3005
+ for (const line of rowCsv.rows) {
3006
+ this.stream.write(line);
3007
+ this.stream.write(this.rowSeparator);
2897
3008
  }
2898
- this.stream.write(this.rowSeparator);
2899
3009
  }
2900
3010
  this.stream.close();
2901
3011
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.123-dev240209030627",
3
+ "version": "0.0.123-dev240209210150",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",