pytrilogy 0.0.3.94__py3-none-any.whl → 0.0.3.96__py3-none-any.whl

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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (35) hide show
  1. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/METADATA +184 -136
  2. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/RECORD +35 -30
  3. trilogy/__init__.py +1 -1
  4. trilogy/authoring/__init__.py +61 -43
  5. trilogy/core/enums.py +13 -0
  6. trilogy/core/env_processor.py +19 -10
  7. trilogy/core/environment_helpers.py +111 -0
  8. trilogy/core/exceptions.py +21 -1
  9. trilogy/core/functions.py +6 -1
  10. trilogy/core/graph_models.py +11 -37
  11. trilogy/core/internal.py +18 -0
  12. trilogy/core/models/core.py +3 -0
  13. trilogy/core/models/environment.py +28 -0
  14. trilogy/core/models/execute.py +7 -0
  15. trilogy/core/processing/node_generators/select_merge_node.py +2 -2
  16. trilogy/core/query_processor.py +2 -1
  17. trilogy/core/statements/author.py +18 -3
  18. trilogy/core/statements/common.py +0 -10
  19. trilogy/core/statements/execute.py +73 -16
  20. trilogy/core/validation/common.py +110 -0
  21. trilogy/core/validation/concept.py +125 -0
  22. trilogy/core/validation/datasource.py +194 -0
  23. trilogy/core/validation/environment.py +71 -0
  24. trilogy/dialect/base.py +48 -21
  25. trilogy/dialect/metadata.py +233 -0
  26. trilogy/dialect/sql_server.py +3 -1
  27. trilogy/engine.py +25 -7
  28. trilogy/executor.py +94 -162
  29. trilogy/parsing/parse_engine.py +34 -3
  30. trilogy/parsing/trilogy.lark +11 -5
  31. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/WHEEL +0 -0
  32. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/entry_points.txt +0 -0
  33. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/licenses/LICENSE.md +0 -0
  34. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/top_level.txt +0 -0
  35. /trilogy/{compiler.py → core/validation/__init__.py} +0 -0
trilogy/executor.py CHANGED
@@ -1,14 +1,12 @@
1
- from dataclasses import dataclass
2
1
  from functools import singledispatchmethod
3
2
  from pathlib import Path
4
- from typing import Any, Generator, List, Optional, Protocol
3
+ from typing import Any, Generator, List, Optional
5
4
 
6
5
  from sqlalchemy import text
7
- from sqlalchemy.engine import CursorResult
8
6
 
9
7
  from trilogy.constants import MagicConstants, Rendering, logger
10
- from trilogy.core.enums import FunctionType, Granularity, IOType
11
- from trilogy.core.models.author import Concept, ConceptRef, Function
8
+ from trilogy.core.enums import FunctionType, Granularity, IOType, ValidationScope
9
+ from trilogy.core.models.author import Concept, Function
12
10
  from trilogy.core.models.build import BuildFunction
13
11
  from trilogy.core.models.core import ListWrapper, MapWrapper
14
12
  from trilogy.core.models.datasource import Datasource
@@ -23,53 +21,38 @@ from trilogy.core.statements.author import (
23
21
  RawSQLStatement,
24
22
  SelectStatement,
25
23
  ShowStatement,
24
+ ValidateStatement,
26
25
  )
27
26
  from trilogy.core.statements.execute import (
27
+ PROCESSED_STATEMENT_TYPES,
28
28
  ProcessedCopyStatement,
29
29
  ProcessedQuery,
30
30
  ProcessedQueryPersist,
31
31
  ProcessedRawSQLStatement,
32
32
  ProcessedShowStatement,
33
- ProcessedStaticValueOutput,
33
+ ProcessedValidateStatement,
34
+ )
35
+ from trilogy.core.validation.common import (
36
+ ValidationTest,
34
37
  )
35
38
  from trilogy.dialect.base import BaseDialect
36
39
  from trilogy.dialect.enums import Dialects
37
- from trilogy.engine import ExecutionEngine
40
+ from trilogy.dialect.metadata import (
41
+ generate_result_set,
42
+ handle_concept_declaration,
43
+ handle_datasource,
44
+ handle_import_statement,
45
+ handle_merge_statement,
46
+ handle_processed_show_statement,
47
+ handle_processed_validate_statement,
48
+ handle_show_statement_outputs,
49
+ )
50
+ from trilogy.engine import ExecutionEngine, ResultProtocol
38
51
  from trilogy.hooks.base_hook import BaseHook
39
52
  from trilogy.parser import parse_text
40
53
  from trilogy.render import get_dialect_generator
41
54
 
42
55
 
43
- class ResultProtocol(Protocol):
44
- values: List[Any]
45
- columns: List[str]
46
-
47
- def fetchall(self) -> List[Any]: ...
48
-
49
- def keys(self) -> List[str]: ...
50
-
51
-
52
- @dataclass
53
- class MockResult:
54
- values: list[Any]
55
- columns: list[str]
56
-
57
- def fetchall(self):
58
- return self.values
59
-
60
- def keys(self):
61
- return self.columns
62
-
63
-
64
- def generate_result_set(
65
- columns: List[ConceptRef], output_data: list[Any]
66
- ) -> MockResult:
67
- names = [x.address.replace(".", "_") for x in columns]
68
- return MockResult(
69
- values=[dict(zip(names, [row])) for row in output_data], columns=names
70
- )
71
-
72
-
73
56
  class Executor(object):
74
57
  def __init__(
75
58
  self,
@@ -93,141 +76,93 @@ class Executor(object):
93
76
 
94
77
  def execute_statement(
95
78
  self,
96
- statement: (
97
- ProcessedQuery
98
- | ProcessedCopyStatement
99
- | ProcessedRawSQLStatement
100
- | ProcessedQueryPersist
101
- | ProcessedShowStatement
102
- ),
103
- ) -> Optional[CursorResult]:
104
- if not isinstance(
105
- statement,
106
- (
107
- ProcessedQuery,
108
- ProcessedShowStatement,
109
- ProcessedQueryPersist,
110
- ProcessedCopyStatement,
111
- ProcessedRawSQLStatement,
112
- ),
113
- ):
79
+ statement: PROCESSED_STATEMENT_TYPES,
80
+ ) -> Optional[ResultProtocol]:
81
+ if not isinstance(statement, PROCESSED_STATEMENT_TYPES):
114
82
  return None
115
83
  return self.execute_query(statement)
116
84
 
117
85
  @singledispatchmethod
118
- def execute_query(self, query) -> CursorResult:
86
+ def execute_query(self, query) -> ResultProtocol | None:
119
87
  raise NotImplementedError("Cannot execute type {}".format(type(query)))
120
88
 
121
89
  @execute_query.register
122
- def _(self, query: ConceptDeclarationStatement) -> CursorResult:
123
- concept = query.concept
124
- return MockResult(
125
- [
126
- {
127
- "address": concept.address,
128
- "type": concept.datatype.value,
129
- "purpose": concept.purpose.value,
130
- "derivation": concept.derivation.value,
131
- }
132
- ],
133
- ["address", "type", "purpose", "derivation"],
134
- )
90
+ def _(self, query: ConceptDeclarationStatement) -> ResultProtocol | None:
91
+ return handle_concept_declaration(query)
135
92
 
136
93
  @execute_query.register
137
- def _(self, query: Datasource) -> CursorResult:
138
- return MockResult(
139
- [
140
- {
141
- "name": query.name,
142
- }
143
- ],
144
- ["name"],
145
- )
94
+ def _(self, query: Datasource) -> ResultProtocol | None:
95
+ return handle_datasource(query)
146
96
 
147
97
  @execute_query.register
148
- def _(self, query: str) -> CursorResult | None:
98
+ def _(self, query: str) -> ResultProtocol | None:
149
99
  results = self.execute_text(query)
150
100
  if results:
151
101
  return results[-1]
152
102
  return None
153
103
 
154
104
  @execute_query.register
155
- def _(self, query: SelectStatement) -> CursorResult:
105
+ def _(self, query: SelectStatement) -> ResultProtocol | None:
156
106
  sql = self.generator.generate_queries(
157
107
  self.environment, [query], hooks=self.hooks
158
108
  )
159
109
  return self.execute_query(sql[0])
160
110
 
161
111
  @execute_query.register
162
- def _(self, query: PersistStatement) -> CursorResult:
112
+ def _(self, query: PersistStatement) -> ResultProtocol | None:
163
113
  sql = self.generator.generate_queries(
164
114
  self.environment, [query], hooks=self.hooks
165
115
  )
166
116
  return self.execute_query(sql[0])
167
117
 
168
118
  @execute_query.register
169
- def _(self, query: RawSQLStatement) -> CursorResult:
119
+ def _(self, query: RawSQLStatement) -> ResultProtocol | None:
170
120
  return self.execute_raw_sql(query.text)
171
121
 
172
122
  @execute_query.register
173
- def _(self, query: ShowStatement) -> CursorResult:
123
+ def _(self, query: ShowStatement) -> ResultProtocol | None:
174
124
  sql = self.generator.generate_queries(
175
125
  self.environment, [query], hooks=self.hooks
176
126
  )
177
127
  return self.execute_query(sql[0])
178
128
 
179
129
  @execute_query.register
180
- def _(self, query: ProcessedShowStatement) -> CursorResult:
181
- return generate_result_set(
182
- query.output_columns,
130
+ def _(self, query: ProcessedShowStatement) -> ResultProtocol | None:
131
+ return handle_processed_show_statement(
132
+ query,
183
133
  [
184
134
  self.generator.compile_statement(x)
185
135
  for x in query.output_values
186
- if isinstance(x, ProcessedQuery)
136
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
187
137
  ],
188
138
  )
189
139
 
190
140
  @execute_query.register
191
- def _(self, query: ImportStatement) -> CursorResult:
192
- return MockResult(
193
- [
194
- {
195
- "path": query.path,
196
- "alias": query.alias,
197
- }
198
- ],
199
- ["path", "alias"],
141
+ def _(self, query: ProcessedValidateStatement) -> ResultProtocol | None:
142
+ return handle_processed_validate_statement(
143
+ query, self.generator, self.validate_environment
200
144
  )
201
145
 
202
146
  @execute_query.register
203
- def _(self, query: MergeStatementV2) -> CursorResult:
204
- for concept in query.sources:
205
- self.environment.merge_concept(
206
- concept, query.targets[concept.address], modifiers=query.modifiers
207
- )
147
+ def _(self, query: ImportStatement) -> ResultProtocol | None:
148
+ return handle_import_statement(query)
208
149
 
209
- return MockResult(
210
- [
211
- {
212
- "sources": ",".join([x.address for x in query.sources]),
213
- "targets": ",".join([x.address for _, x in query.targets.items()]),
214
- }
215
- ],
216
- ["source", "target"],
217
- )
150
+ @execute_query.register
151
+ def _(self, query: MergeStatementV2) -> ResultProtocol | None:
152
+ return handle_merge_statement(query, self.environment)
218
153
 
219
154
  @execute_query.register
220
- def _(self, query: ProcessedRawSQLStatement) -> CursorResult:
155
+ def _(self, query: ProcessedRawSQLStatement) -> ResultProtocol | None:
221
156
  return self.execute_raw_sql(query.text)
222
157
 
223
158
  @execute_query.register
224
- def _(self, query: ProcessedQuery) -> CursorResult:
159
+ def _(self, query: ProcessedQuery) -> ResultProtocol | None:
225
160
  sql = self.generator.compile_statement(query)
226
161
  output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
227
162
  return output
228
163
 
229
164
  @execute_query.register
230
- def _(self, query: ProcessedQueryPersist) -> CursorResult:
165
+ def _(self, query: ProcessedQueryPersist) -> ResultProtocol | None:
231
166
  sql = self.generator.compile_statement(query)
232
167
 
233
168
  output = self.execute_raw_sql(sql, local_concepts=query.local_concepts)
@@ -235,9 +170,9 @@ class Executor(object):
235
170
  return output
236
171
 
237
172
  @execute_query.register
238
- def _(self, query: ProcessedCopyStatement) -> CursorResult:
173
+ def _(self, query: ProcessedCopyStatement) -> ResultProtocol | None:
239
174
  sql = self.generator.compile_statement(query)
240
- output: CursorResult = self.execute_raw_sql(
175
+ output: ResultProtocol = self.execute_raw_sql(
241
176
  sql, local_concepts=query.local_concepts
242
177
  )
243
178
  if query.target_type == IOType.CSV:
@@ -278,8 +213,6 @@ class Executor(object):
278
213
  for statement in sql:
279
214
  compiled_sql = self.generator.compile_statement(statement)
280
215
  output.append(compiled_sql)
281
-
282
- output.append(compiled_sql)
283
216
  return output
284
217
 
285
218
  @generate_sql.register # type: ignore
@@ -313,23 +246,13 @@ class Executor(object):
313
246
 
314
247
  def parse_file(
315
248
  self, file: str | Path, persist: bool = False
316
- ) -> list[
317
- ProcessedQuery
318
- | ProcessedQueryPersist
319
- | ProcessedShowStatement
320
- | ProcessedRawSQLStatement
321
- | ProcessedCopyStatement,
322
- ]:
249
+ ) -> list[PROCESSED_STATEMENT_TYPES]:
323
250
  return list(self.parse_file_generator(file, persist=persist))
324
251
 
325
252
  def parse_file_generator(
326
253
  self, file: str | Path, persist: bool = False
327
254
  ) -> Generator[
328
- ProcessedQuery
329
- | ProcessedQueryPersist
330
- | ProcessedShowStatement
331
- | ProcessedRawSQLStatement
332
- | ProcessedCopyStatement,
255
+ PROCESSED_STATEMENT_TYPES,
333
256
  None,
334
257
  None,
335
258
  ]:
@@ -340,23 +263,13 @@ class Executor(object):
340
263
 
341
264
  def parse_text(
342
265
  self, command: str, persist: bool = False, root: Path | None = None
343
- ) -> List[
344
- ProcessedQuery
345
- | ProcessedQueryPersist
346
- | ProcessedShowStatement
347
- | ProcessedRawSQLStatement
348
- | ProcessedCopyStatement
349
- ]:
266
+ ) -> List[PROCESSED_STATEMENT_TYPES]:
350
267
  return list(self.parse_text_generator(command, persist=persist, root=root))
351
268
 
352
269
  def parse_text_generator(
353
270
  self, command: str, persist: bool = False, root: Path | None = None
354
271
  ) -> Generator[
355
- ProcessedQuery
356
- | ProcessedQueryPersist
357
- | ProcessedShowStatement
358
- | ProcessedRawSQLStatement
359
- | ProcessedCopyStatement,
272
+ PROCESSED_STATEMENT_TYPES,
360
273
  None,
361
274
  None,
362
275
  ]:
@@ -374,6 +287,7 @@ class Executor(object):
374
287
  ShowStatement,
375
288
  RawSQLStatement,
376
289
  CopyStatement,
290
+ ValidateStatement,
377
291
  ),
378
292
  )
379
293
  ]
@@ -420,10 +334,12 @@ class Executor(object):
420
334
  # return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
421
335
  return rval
422
336
  else:
423
- results = self.execute_query(f"select {concept.name} limit 1;").fetchone()
424
- if not results:
337
+ results = self.execute_query(f"select {concept.name} limit 1;")
338
+ if results:
339
+ fetcher = results.fetchone()
340
+ if fetcher:
341
+ return fetcher[0]
425
342
  return None
426
- return results[0]
427
343
 
428
344
  def _hydrate_param(
429
345
  self, param: str, local_concepts: dict[str, Concept] | None = None
@@ -447,13 +363,16 @@ class Executor(object):
447
363
 
448
364
  def execute_raw_sql(
449
365
  self,
450
- command: str,
366
+ command: str | Path,
451
367
  variables: dict | None = None,
452
368
  local_concepts: dict[str, Concept] | None = None,
453
- ) -> CursorResult:
369
+ ) -> ResultProtocol:
454
370
  """Run a command against the raw underlying
455
371
  execution engine."""
456
372
  final_params = None
373
+ if isinstance(command, Path):
374
+ with open(command, "r") as f:
375
+ command = f.read()
457
376
  q = text(command)
458
377
  if variables:
459
378
  final_params = variables
@@ -473,37 +392,50 @@ class Executor(object):
473
392
 
474
393
  def execute_text(
475
394
  self, command: str, non_interactive: bool = False
476
- ) -> List[CursorResult]:
395
+ ) -> List[ResultProtocol]:
477
396
  """Run a trilogy query expressed as text."""
478
- output = []
397
+ output: list[ResultProtocol] = []
479
398
  # connection = self.engine.connect()
480
399
  for statement in self.parse_text_generator(command):
481
400
  if isinstance(statement, ProcessedShowStatement):
482
- for x in statement.output_values:
483
- if isinstance(x, ProcessedStaticValueOutput):
484
- output.append(
485
- generate_result_set(statement.output_columns, x.values)
486
- )
487
- elif isinstance(x, ProcessedQuery):
488
- output.append(
489
- generate_result_set(
490
- statement.output_columns,
491
- [self.generator.compile_statement(x)],
492
- )
493
- )
401
+ results = handle_show_statement_outputs(
402
+ statement,
403
+ [
404
+ self.generator.compile_statement(x)
405
+ for x in statement.output_values
406
+ if isinstance(x, (ProcessedQuery, ProcessedQueryPersist))
407
+ ],
408
+ self.environment,
409
+ self.generator,
410
+ )
411
+ output.extend(results)
494
412
  continue
495
413
  if non_interactive:
496
414
  if not isinstance(
497
415
  statement, (ProcessedCopyStatement, ProcessedQueryPersist)
498
416
  ):
499
417
  continue
500
- output.append(self.execute_query(statement))
418
+ result = self.execute_statement(statement)
419
+ if result:
420
+ output.append(result)
501
421
  return output
502
422
 
503
423
  def execute_file(
504
424
  self, file: str | Path, non_interactive: bool = False
505
- ) -> List[CursorResult]:
425
+ ) -> List[ResultProtocol]:
506
426
  file = Path(file)
507
427
  with open(file, "r") as f:
508
428
  command = f.read()
509
429
  return self.execute_text(command, non_interactive=non_interactive)
430
+
431
+ def validate_environment(
432
+ self,
433
+ scope: ValidationScope = ValidationScope.ALL,
434
+ targets: Optional[List[str]] = None,
435
+ generate_only: bool = False,
436
+ ) -> list[ValidationTest]:
437
+ from trilogy.core.validation.environment import validate_environment
438
+
439
+ return validate_environment(
440
+ self.environment, scope, targets, exec=None if generate_only else self
441
+ )
@@ -38,13 +38,13 @@ from trilogy.core.enums import (
38
38
  Ordering,
39
39
  Purpose,
40
40
  ShowCategory,
41
+ ValidationScope,
41
42
  WindowOrder,
42
43
  WindowType,
43
44
  )
44
45
  from trilogy.core.exceptions import InvalidSyntaxException, UndefinedConceptException
45
46
  from trilogy.core.functions import (
46
47
  CurrentDate,
47
- CurrentDatetime,
48
48
  FunctionFactory,
49
49
  )
50
50
  from trilogy.core.internal import ALL_ROWS_CONCEPT, INTERNAL_NAMESPACE
@@ -129,6 +129,7 @@ from trilogy.core.statements.author import (
129
129
  SelectStatement,
130
130
  ShowStatement,
131
131
  TypeDeclaration,
132
+ ValidateStatement,
132
133
  )
133
134
  from trilogy.parsing.common import (
134
135
  align_item_to_concept,
@@ -819,7 +820,9 @@ class ParseToObjects(Transformer):
819
820
 
820
821
  @v_args(meta=True)
821
822
  def aggregate_by(self, meta: Meta, args):
822
- args = [self.environment.concepts[a] for a in args]
823
+ base = args[0]
824
+ b_concept = base.value.split(" ")[-1]
825
+ args = [self.environment.concepts[a] for a in [b_concept] + args[1:]]
823
826
  return self.function_factory.create_function(args, FunctionType.GROUP, meta)
824
827
 
825
828
  def whole_grain_clause(self, args) -> WholeGrainWrapper:
@@ -990,6 +993,25 @@ class ParseToObjects(Transformer):
990
993
  def over_list(self, args):
991
994
  return [x for x in args]
992
995
 
996
+ def VALIDATION_SCOPE(self, args) -> ValidationScope:
997
+ return ValidationScope(args.lower())
998
+
999
+ @v_args(meta=True)
1000
+ def validate_statement(self, meta: Meta, args) -> ValidateStatement:
1001
+ if len(args) == 2:
1002
+ scope = args[0]
1003
+ targets = args[1]
1004
+ elif len(args) == 0:
1005
+ scope = ValidationScope.ALL
1006
+ targets = None
1007
+ else:
1008
+ scope = args[0]
1009
+ targets = None
1010
+ return ValidateStatement(
1011
+ scope=scope,
1012
+ targets=targets,
1013
+ )
1014
+
993
1015
  @v_args(meta=True)
994
1016
  def merge_statement(self, meta: Meta, args) -> MergeStatementV2 | None:
995
1017
  modifiers = []
@@ -2055,7 +2077,15 @@ class ParseToObjects(Transformer):
2055
2077
 
2056
2078
  @v_args(meta=True)
2057
2079
  def fcurrent_datetime(self, meta, args):
2058
- return CurrentDatetime([])
2080
+ return self.function_factory.create_function(
2081
+ args=[], operator=FunctionType.CURRENT_DATETIME, meta=meta
2082
+ )
2083
+
2084
+ @v_args(meta=True)
2085
+ def fcurrent_timestamp(self, meta, args):
2086
+ return self.function_factory.create_function(
2087
+ args=[], operator=FunctionType.CURRENT_TIMESTAMP, meta=meta
2088
+ )
2059
2089
 
2060
2090
  @v_args(meta=True)
2061
2091
  def fnot(self, meta, args):
@@ -2232,6 +2262,7 @@ def parse_text(
2232
2262
  | PersistStatement
2233
2263
  | ShowStatement
2234
2264
  | RawSQLStatement
2265
+ | ValidateStatement
2235
2266
  | None
2236
2267
  ],
2237
2268
  ]:
@@ -12,6 +12,7 @@
12
12
  | copy_statement
13
13
  | merge_statement
14
14
  | rawsql_statement
15
+ | validate_statement
15
16
 
16
17
  _TERMINATOR: ";"i /\s*/
17
18
 
@@ -81,8 +82,11 @@
81
82
  // raw sql statement
82
83
  rawsql_statement: "raw_sql"i "(" MULTILINE_STRING ")"
83
84
 
85
+ // validate_statement
84
86
 
87
+ VALIDATE_SCOPE: "concepts"i | "datasources"i
85
88
 
89
+ validate_statement: ("validate"i "all"i) | ( "validate"i VALIDATE_SCOPE (IDENTIFIER ("," IDENTIFIER)* ","? )? )
86
90
  // copy statement
87
91
 
88
92
  COPY_TYPE: "csv"i
@@ -262,10 +266,12 @@
262
266
  //constant
263
267
  CURRENT_DATE.1: /current_date\(\)/
264
268
  CURRENT_DATETIME.1: /current_datetime\(\)/
269
+ CURRENT_TIMESTAMP.1: /current_timestamp\(\)/
265
270
  fcurrent_date: CURRENT_DATE
266
271
  fcurrent_datetime: CURRENT_DATETIME
272
+ fcurrent_timestamp: CURRENT_TIMESTAMP
267
273
 
268
- _constant_functions: fcurrent_date | fcurrent_datetime
274
+ _constant_functions: fcurrent_date | fcurrent_datetime | fcurrent_timestamp
269
275
 
270
276
  //string
271
277
  _LIKE.1: "like("i
@@ -325,8 +331,8 @@
325
331
  _GROUP.1: "group("i
326
332
  fgroup: _GROUP expr ")" aggregate_over?
327
333
 
328
- //by:
329
- aggregate_by: "group" IDENTIFIER "BY"i (IDENTIFIER ",")* IDENTIFIER
334
+ //by:
335
+ aggregate_by: /(group)\s+([a-zA-Z\_][a-zA-Z0-9\_\.]*)/i "BY"i (IDENTIFIER ",")* IDENTIFIER
330
336
 
331
337
  //aggregates
332
338
  _COUNT.1: "count("i
@@ -424,7 +430,7 @@
424
430
  map_lit: "{" (literal ":" literal ",")* literal ":" literal ","? "}"
425
431
 
426
432
  _STRUCT.1: "struct("i
427
- struct_lit: _STRUCT (IDENTIFIER "=" literal ",")* IDENTIFIER "=" literal ","? ")"
433
+ struct_lit: _STRUCT (IDENTIFIER "->" expr ",")* IDENTIFIER "->" expr ","? ")"
428
434
 
429
435
  !bool_lit: "True"i | "False"i
430
436
 
@@ -455,7 +461,7 @@
455
461
  DATASOURCES: "DATASOURCES"i
456
462
  show_category: CONCEPTS | DATASOURCES
457
463
 
458
- show_statement: "show"i ( show_category | select_statement | persist_statement) _TERMINATOR
464
+ show_statement: "show"i ( show_category | validate_statement | select_statement | persist_statement) _TERMINATOR
459
465
  COMMENT: /#.*(\n|$)/ | /\/\/.*\n/
460
466
  %import common.WS
461
467
  %ignore WS
File without changes