@malloydata/malloy-filter 0.0.237-dev250224215546 → 0.0.237-dev250225015031

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 (58) hide show
  1. package/SAMPLES.md +336 -114
  2. package/SERIALIZE_SAMPLES.md +268 -64
  3. package/dist/a_simple_parser.js +6 -0
  4. package/dist/a_simple_parser.js.map +1 -1
  5. package/dist/base_parser.js +6 -0
  6. package/dist/base_parser.js.map +1 -1
  7. package/dist/boolean_parser.js +28 -13
  8. package/dist/boolean_parser.js.map +1 -1
  9. package/dist/boolean_serializer.js +12 -6
  10. package/dist/boolean_serializer.js.map +1 -1
  11. package/dist/clause_types.d.ts +20 -15
  12. package/dist/clause_types.js +6 -0
  13. package/dist/clause_types.js.map +1 -1
  14. package/dist/date_parser.js +135 -116
  15. package/dist/date_parser.js.map +1 -1
  16. package/dist/date_serializer.js +26 -20
  17. package/dist/date_serializer.js.map +1 -1
  18. package/dist/date_types.d.ts +21 -21
  19. package/dist/date_types.js +6 -0
  20. package/dist/date_types.js.map +1 -1
  21. package/dist/generate_samples.js +32 -25
  22. package/dist/generate_samples.js.map +1 -1
  23. package/dist/index.js +6 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/number_parser.js +43 -25
  26. package/dist/number_parser.js.map +1 -1
  27. package/dist/number_serializer.js +10 -4
  28. package/dist/number_serializer.js.map +1 -1
  29. package/dist/string_parser.d.ts +0 -1
  30. package/dist/string_parser.js +47 -79
  31. package/dist/string_parser.js.map +1 -1
  32. package/dist/string_serializer.d.ts +1 -0
  33. package/dist/string_serializer.js +49 -33
  34. package/dist/string_serializer.js.map +1 -1
  35. package/dist/token_types.js +6 -0
  36. package/dist/token_types.js.map +1 -1
  37. package/dist/tokenizer.js +9 -3
  38. package/dist/tokenizer.js.map +1 -1
  39. package/dist/tokenizer.spec.js +13 -7
  40. package/dist/tokenizer.spec.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/a_simple_parser.ts +7 -0
  43. package/src/base_parser.ts +7 -0
  44. package/src/boolean_parser.ts +30 -18
  45. package/src/boolean_serializer.ts +13 -6
  46. package/src/clause_types.ts +36 -31
  47. package/src/date_parser.ts +136 -118
  48. package/src/date_serializer.ts +27 -20
  49. package/src/date_types.ts +42 -34
  50. package/src/generate_samples.ts +33 -25
  51. package/src/index.ts +7 -0
  52. package/src/number_parser.ts +45 -26
  53. package/src/number_serializer.ts +11 -4
  54. package/src/string_parser.ts +51 -79
  55. package/src/string_serializer.ts +65 -39
  56. package/src/token_types.ts +7 -0
  57. package/src/tokenizer.spec.ts +14 -7
  58. package/src/tokenizer.ts +10 -3
@@ -1,10 +1,17 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
1
8
  import {SpecialToken, Tokenizer, TokenizerParams} from './tokenizer';
2
9
  import {
3
10
  StringClause,
4
11
  StringCondition,
12
+ StringMatchOperator,
5
13
  StringConditionOperator,
6
- QuoteType,
7
- FilterError,
14
+ FilterLog,
8
15
  StringParserResponse,
9
16
  } from './clause_types';
10
17
  import {BaseParser} from './base_parser';
@@ -24,10 +31,10 @@ export class StringParser extends BaseParser {
24
31
  private tokenize(): void {
25
32
  const specialSubstrings: SpecialToken[] = [{type: ',', value: ','}];
26
33
  const specialWords: SpecialToken[] = [
27
- {type: 'NULL', value: 'null', ignoreCase: true},
28
- {type: 'EMPTY', value: 'empty', ignoreCase: true},
29
- {type: 'NOTNULL', value: '-null', ignoreCase: true},
30
- {type: 'NOTEMPTY', value: '-empty', ignoreCase: true},
34
+ {type: 'null', value: 'null', ignoreCase: true},
35
+ {type: 'empty', value: 'empty', ignoreCase: true},
36
+ {type: 'not_null', value: '-null', ignoreCase: true},
37
+ {type: 'not_empty', value: '-empty', ignoreCase: true},
31
38
  ];
32
39
  const params: TokenizerParams = {
33
40
  trimWordWhitespace: true,
@@ -44,89 +51,45 @@ export class StringParser extends BaseParser {
44
51
  public parse(): StringParserResponse {
45
52
  this.index = 0;
46
53
  this.tokenize();
47
- const clauses: StringClause[] = [];
48
- const errors: FilterError[] = [];
54
+ let clauses: StringClause[] = [];
55
+ const logs: FilterLog[] = [];
49
56
  while (this.index < this.tokens.length) {
50
57
  const token = this.getNext();
51
58
  if (token.type === ',') {
59
+ if (this.index > 0 && this.tokens[this.index - 1].type === ',') {
60
+ logs.push({
61
+ severity: 'warn',
62
+ message: 'Empty clause',
63
+ startIndex: token.startIndex,
64
+ endIndex: token.endIndex,
65
+ });
66
+ }
52
67
  this.index++;
53
68
  } else if (
54
- token.type === 'NULL' ||
55
- token.type === 'NOTNULL' ||
56
- token.type === 'EMPTY' ||
57
- token.type === 'NOTEMPTY'
69
+ token.type === 'null' ||
70
+ token.type === 'not_null' ||
71
+ token.type === 'empty' ||
72
+ token.type === 'not_empty'
58
73
  ) {
59
74
  clauses.push({operator: token.type});
60
75
  this.index++;
61
76
  } else if (this.checkSimpleWord(clauses)) {
62
77
  this.index++;
63
78
  } else {
64
- errors.push({
65
- message: 'Invalid expression',
79
+ logs.push({
80
+ severity: 'warn',
81
+ message: 'Empty clause',
66
82
  startIndex: token.startIndex,
67
83
  endIndex: token.endIndex,
68
84
  });
69
85
  this.index++;
70
86
  }
71
87
  }
72
- const response: StringParserResponse = {
73
- clauses: StringParser.groupClauses(clauses),
74
- errors,
88
+ clauses = StringParser.groupClauses(clauses);
89
+ return {
90
+ clauses,
91
+ logs,
75
92
  };
76
- // const quotes: QuoteType[] = StringParser.findQuotes(this.inputString);
77
- // if (quotes.length > 0) {
78
- // response.quotes = quotes;
79
- // }
80
- return response;
81
- }
82
-
83
- private static findQuotes(str: string): QuoteType[] {
84
- const quotes: Set<QuoteType> = new Set();
85
- let i = 0;
86
-
87
- while (i < str.length) {
88
- // Check for triple quotes first to avoid false positives
89
- if (str.slice(i, i + 3) === "'''") {
90
- quotes.add('TRIPLESINGLE');
91
- i += 3;
92
- } else if (str.slice(i, i + 3) === '"""') {
93
- quotes.add('TRIPLEDOUBLE');
94
- i += 3;
95
- } else if (str[i] === '\\') {
96
- // Check for escaped quotes
97
- if (i + 1 < str.length) {
98
- switch (str[i + 1]) {
99
- case "'":
100
- quotes.add('ESCAPEDSINGLE');
101
- break;
102
- case '"':
103
- quotes.add('ESCAPEDDOUBLE');
104
- break;
105
- case '`':
106
- quotes.add('ESCAPEDBACKTICK');
107
- break;
108
- }
109
- i += 2;
110
- } else {
111
- i++;
112
- }
113
- } else {
114
- // Check for single quotes
115
- switch (str[i]) {
116
- case "'":
117
- quotes.add('SINGLE');
118
- break;
119
- case '"':
120
- quotes.add('DOUBLE');
121
- break;
122
- case '`':
123
- quotes.add('BACKTICK');
124
- break;
125
- }
126
- i++;
127
- }
128
- }
129
- return Array.from(quotes);
130
93
  }
131
94
 
132
95
  private static groupClauses(clauses: StringClause[]): StringClause[] {
@@ -143,6 +106,12 @@ export class StringParser extends BaseParser {
143
106
  'values' in current
144
107
  ) {
145
108
  previous.values.push(...current.values);
109
+ } else if (
110
+ previous.operator === current.operator &&
111
+ 'escaped_values' in previous &&
112
+ 'escaped_values' in current
113
+ ) {
114
+ previous.escaped_values.push(...current.escaped_values);
146
115
  } else {
147
116
  previous = current;
148
117
  outputs.push(current);
@@ -178,23 +147,26 @@ export class StringParser extends BaseParser {
178
147
 
179
148
  let operator: StringConditionOperator = negatedMatch ? '!=' : '=';
180
149
  if (isUnderscore || isPercentMiddle || (isPercentBoth && word.length < 3)) {
181
- operator = negatedMatch ? '!~' : '~';
150
+ // Special handling for string match
151
+ const matchOperator: StringMatchOperator = negatedMatch ? '!~' : '~';
152
+ if (word.length === 0) {
153
+ return false;
154
+ }
155
+ clauses.push({operator: matchOperator, escaped_values: [word]});
156
+ return true;
182
157
  } else if (isPercentBoth && word.length > 2) {
183
- operator = negatedMatch ? 'notContains' : 'contains';
158
+ operator = negatedMatch ? 'not_contains' : 'contains';
184
159
  word = word.substring(1, word.length - 1);
185
- word = StringParser.removeBackslashes(word);
186
160
  } else if (isPercentStart) {
187
- operator = negatedMatch ? 'notEnds' : 'ends';
161
+ operator = negatedMatch ? 'not_ends' : 'ends';
188
162
  word = word.substring(1, word.length);
189
- word = StringParser.removeBackslashes(word);
190
163
  } else if (isPercentEnd) {
191
- operator = negatedMatch ? 'notStarts' : 'starts';
164
+ operator = negatedMatch ? 'not_starts' : 'starts';
192
165
  word = word.substring(0, word.length - 1);
193
- word = StringParser.removeBackslashes(word);
194
166
  } else {
195
167
  // = or !=
196
- word = StringParser.removeBackslashes(word);
197
168
  }
169
+ word = StringParser.removeBackslashes(word);
198
170
  if (word.length === 0) {
199
171
  return false;
200
172
  }
@@ -1,7 +1,16 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
1
8
  import {
2
9
  StringClause,
3
10
  StringCondition,
4
11
  StringConditionOperator,
12
+ StringMatch,
13
+ StringMatchOperator,
5
14
  } from './clause_types';
6
15
 
7
16
  export class StringSerializer {
@@ -16,11 +25,10 @@ export class StringSerializer {
16
25
 
17
26
  private static isNegated(operator: StringConditionOperator): boolean {
18
27
  return (
19
- operator === '!~' ||
20
28
  operator === '!=' ||
21
- operator === 'notStarts' ||
22
- operator === 'notEnds' ||
23
- operator === 'notContains'
29
+ operator === 'not_starts' ||
30
+ operator === 'not_ends' ||
31
+ operator === 'not_contains'
24
32
  );
25
33
  }
26
34
 
@@ -32,66 +40,84 @@ export class StringSerializer {
32
40
  return input.replace(/[_%]/g, match => `\\${match}`);
33
41
  }
34
42
 
35
- // export type StringOperator =
43
+ // export type StringConditionOperator =
36
44
  // | 'starts' | 'ends' | 'contains' | 'notStarts' | 'notEnds' | 'notContains'
37
- // | '~' | '=' | '!~' | '!=';
45
+ // | '='| '!=';
38
46
  private static StringConditionWordToString(
39
47
  operator: StringConditionOperator,
40
48
  value: string
41
49
  ): string {
42
50
  const negated: boolean = StringSerializer.isNegated(operator);
43
- if (value === 'NULL' || value === '-NULL') {
51
+ if (value === 'null' || value === '-null') {
44
52
  return (negated ? '-' : '') + '\\' + value;
45
53
  }
46
54
 
47
55
  value = StringSerializer.escapeSpecialCharacters(value);
48
- if (operator === 'starts' || operator === 'notStarts') {
49
- value = StringSerializer.escapeWildcardCharacters(value);
56
+ value = StringSerializer.escapeWildcardCharacters(value);
57
+ if (operator === 'starts' || operator === 'not_starts') {
50
58
  return (negated ? '-' : '') + value + '%';
51
- } else if (operator === 'ends' || operator === 'notEnds') {
52
- value = StringSerializer.escapeWildcardCharacters(value);
59
+ } else if (operator === 'ends' || operator === 'not_ends') {
53
60
  return (negated ? '-' : '') + '%' + value;
54
- } else if (operator === 'contains' || operator === 'notContains') {
55
- value = StringSerializer.escapeWildcardCharacters(value);
61
+ } else if (operator === 'contains' || operator === 'not_contains') {
56
62
  return (negated ? '-' : '') + '%' + value + '%';
57
- } else if (operator === '=' || operator === '!=') {
58
- value = StringSerializer.escapeWildcardCharacters(value);
59
- return (negated ? '-' : '') + value;
60
63
  }
64
+ return (negated ? '-' : '') + value;
65
+ }
61
66
 
67
+ // export type StringMatchOperator = '~' | '!~';
68
+ private static StringMatchWordToString(
69
+ operator: StringMatchOperator,
70
+ value: string
71
+ ): string {
72
+ const negated: boolean = operator === '!~' ? true : false;
73
+ if (value === 'null' || value === '-null') {
74
+ return (negated ? '-' : '') + '\\' + value;
75
+ }
76
+ value = StringSerializer.escapeSpecialCharacters(value);
62
77
  return (negated ? '-' : '') + value;
63
78
  }
64
79
 
65
80
  private static StringClauseToString(
66
81
  operator:
67
82
  | StringConditionOperator
68
- | 'EMPTY'
69
- | 'NOTEMPTY'
70
- | 'NULL'
71
- | 'NOTNULL',
83
+ | StringMatchOperator
84
+ | 'empty'
85
+ | 'not_empty'
86
+ | 'null'
87
+ | 'not_null',
72
88
  clause: StringClause
73
89
  ): string {
74
- if (operator === 'EMPTY') {
75
- return 'EMPTY';
76
- } else if (operator === 'NOTEMPTY') {
77
- return '-EMPTY';
78
- } else if (operator === 'NULL') {
79
- return 'NULL';
80
- } else if (operator === 'NOTNULL') {
81
- return '-NULL';
82
- }
83
- if (!('values' in clause) || clause.values.length === 0) {
84
- return '';
90
+ if (operator === 'empty') {
91
+ return 'empty';
92
+ } else if (operator === 'not_empty') {
93
+ return '-empty';
94
+ } else if (operator === 'null') {
95
+ return 'null';
96
+ } else if (operator === 'not_null') {
97
+ return '-null';
85
98
  }
86
99
  let result = '';
87
- const condition: StringCondition = clause;
88
- for (const value of condition.values) {
89
- const word = StringSerializer.StringConditionWordToString(
90
- condition.operator,
91
- value
92
- );
93
- if (word) {
94
- result += word + ', ';
100
+ if ('values' in clause && clause.values.length > 0) {
101
+ const condition: StringCondition = clause;
102
+ for (const value of condition.values) {
103
+ const word = StringSerializer.StringConditionWordToString(
104
+ condition.operator,
105
+ value
106
+ );
107
+ if (word) {
108
+ result += word + ', ';
109
+ }
110
+ }
111
+ } else if ('escaped_values' in clause && clause.escaped_values.length > 0) {
112
+ const condition: StringMatch = clause;
113
+ for (const value of condition.escaped_values) {
114
+ const word = StringSerializer.StringMatchWordToString(
115
+ condition.operator,
116
+ value
117
+ );
118
+ if (word) {
119
+ result += word + ', ';
120
+ }
95
121
  }
96
122
  }
97
123
  return result.trim().replace(/,$/, '');
@@ -1,3 +1,10 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
1
8
  export interface Token {
2
9
  type: string;
3
10
  value: string;
@@ -1,3 +1,10 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
1
8
  import {Tokenizer, SpecialToken, TokenizerParams} from './tokenizer';
2
9
  import {Token} from './token_types';
3
10
 
@@ -68,16 +75,16 @@ describe('Tokenizer', () => {
68
75
  const input = 'hello NULL world,-Null,-\'NULL" ,NULL, NULL , ';
69
76
  const expectedTokens = [
70
77
  makeToken('word', 'hello', 0, 5),
71
- makeToken('NULL', 'NULL', 6, 10),
78
+ makeToken('NULL', 'null', 6, 10),
72
79
  makeToken('word', 'world', 11, 16),
73
80
  makeToken(',', ',', 16, 17),
74
- makeToken('NOTNULL', '-NULL', 17, 22),
81
+ makeToken('NOTNULL', '-null', 17, 22),
75
82
  makeToken(',', ',', 22, 23),
76
83
  makeToken('word', '-\'NULL"', 23, 30),
77
84
  makeToken(',', ',', 31, 32),
78
- makeToken('NULL', 'NULL', 32, 36),
85
+ makeToken('NULL', 'null', 32, 36),
79
86
  makeToken(',', ',', 36, 37),
80
- makeToken('NULL', 'NULL', 38, 42),
87
+ makeToken('NULL', 'null', 38, 42),
81
88
  makeToken(',', ',', 43, 44),
82
89
  ];
83
90
  const params = makeParams();
@@ -121,18 +128,18 @@ describe('Tokenizer', () => {
121
128
  ];
122
129
  expect(new Tokenizer(input, makeParams()).parse()).toEqual(expectedTokens);
123
130
  });
124
- it('should match regexp and capitalize special matches', () => {
131
+ it('should match regexp and lowercase special matches', () => {
125
132
  const input =
126
133
  "hello tuesDAY,ttuesday, tuesdayy ,Tuesday , ttuesday, 'TUESday' ";
127
134
  const expectedTokens = [
128
135
  makeToken('word', 'hello', 0, 5),
129
- makeToken('DAYOFWEEK', 'TUESDAY', 6, 13),
136
+ makeToken('DAYOFWEEK', 'tuesday', 6, 13),
130
137
  makeToken(',', ',', 13, 14),
131
138
  makeToken('word', 'ttuesday', 14, 22),
132
139
  makeToken(',', ',', 22, 23),
133
140
  makeToken('word', 'tuesdayy', 24, 32),
134
141
  makeToken(',', ',', 33, 34),
135
- makeToken('DAYOFWEEK', 'TUESDAY', 34, 41),
142
+ makeToken('DAYOFWEEK', 'tuesday', 34, 41),
136
143
  makeToken(',', ',', 42, 43),
137
144
  makeToken('word', 'ttuesday', 44, 52),
138
145
  makeToken(',', ',', 52, 53),
package/src/tokenizer.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /*
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
1
8
  import {Token} from './token_types';
2
9
 
3
10
  export interface SpecialToken {
@@ -138,7 +145,7 @@ export class Tokenizer {
138
145
  : subString === special.value;
139
146
  if (matches) {
140
147
  const value = special.ignoreCase
141
- ? subString.toUpperCase()
148
+ ? subString.toLowerCase()
142
149
  : subString;
143
150
  return {
144
151
  type: special.type,
@@ -205,7 +212,7 @@ export class Tokenizer {
205
212
  regexp.lastIndex = 0; // Set the starting index for the search
206
213
  if (regexp.test(token.value)) {
207
214
  const value = special.ignoreCase
208
- ? token.value.toUpperCase()
215
+ ? token.value.toLowerCase()
209
216
  : token.value;
210
217
  return {
211
218
  type: special.type,
@@ -220,7 +227,7 @@ export class Tokenizer {
220
227
  : token.value === special.value;
221
228
  if (matches) {
222
229
  const value = special.ignoreCase
223
- ? token.value.toUpperCase()
230
+ ? token.value.toLowerCase()
224
231
  : token.value;
225
232
  return {
226
233
  type: special.type,