pytrilogy 0.0.2.35__py3-none-any.whl → 0.0.2.37__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.35
3
+ Version: 0.0.2.37
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,8 +1,8 @@
1
- trilogy/__init__.py,sha256=U5WUYHe0fhxaKEzGbop_KX-WSQsCDldtJFhRs5Ojqys,291
1
+ trilogy/__init__.py,sha256=Keq-D-sl-p9mpGadAtrLKltDPbZTIiu4FdY6d8qJ4kk,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=HQAnGUqJ5uMri7TWtqXHhz8iVWBzi2LCfRG8vKnBIB8,1269
3
+ trilogy/constants.py,sha256=UPymm94T2c6a55XdDaXw0aleTe1pOJ6lf6gOWLKZyKg,1430
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=VcZ2U3RUU2al_VJ75AKVwmCJQLltYouxlgTjq4oxPB0,12577
5
+ trilogy/executor.py,sha256=FXxtBOAFFPY20fsyoDeLHr7DWHHBxxCOnQwpSeyODzI,14757
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
@@ -16,7 +16,7 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
17
17
  trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=9jSrHBh8aE2BD4aoETm_QhRN3KI1mI009xKHTp8fLWE,162668
19
+ trilogy/core/models.py,sha256=GMqdaZ7FgNYsoNDPKoSWt1Xxxk2SJYBNk2yPazhIHzQ,162855
20
20
  trilogy/core/optimization.py,sha256=VFSvJLNoCCOraip-PZUKeE4qrlxtXARjQUzJZiW-yRk,7325
21
21
  trilogy/core/query_processor.py,sha256=mbcZlgjChrRjDHkdmMbKe-T70UpbBkJhS09MyU5a6UY,17785
22
22
  trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
@@ -50,7 +50,7 @@ trilogy/core/processing/nodes/select_node_v2.py,sha256=7WoFxeGEAzhpS4y4Zw2nH2tt7
50
50
  trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
51
51
  trilogy/core/processing/nodes/window_node.py,sha256=ro0QfMFi4ZmIn5Q4D0M_vJWfnHH_C0MN7XkVkx8Gygg,1214
52
52
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- trilogy/dialect/base.py,sha256=V3YSzuyom8PLRS5Qru4iGatlB0huNLKorPKsYoAVWLk,34308
53
+ trilogy/dialect/base.py,sha256=wkK8NESHfko7bUavaMFvJHzaGNoHd3r03j-kfIyD4bY,34626
54
54
  trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
55
55
  trilogy/dialect/common.py,sha256=eqJi_Si1iyb2sV0siTf8g8JOHueWu6RkdtQZtutKazk,3826
56
56
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
@@ -70,14 +70,14 @@ trilogy/parsing/common.py,sha256=_GW9LU6_4RuUgcdcr8EE1ybCRd-7cz3idZtjHZ66pYA,101
70
70
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
71
71
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
72
72
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
- trilogy/parsing/parse_engine.py,sha256=ukdJ5QlWcWGkvb7kd3PnnOXEVpURHm0rap8QCvvjALg,63811
74
- trilogy/parsing/render.py,sha256=VKyo8wEOuiOzUtJ6w9EoGGmkhlqDQyy8wFj_Q_h6EfE,15263
75
- trilogy/parsing/trilogy.lark,sha256=Tuqw5oGMwOYt3TYOEx_hZqGpsAp-PiAKiMW8S3EFRcg,12236
73
+ trilogy/parsing/parse_engine.py,sha256=9qmfqpibuIlTDrI_ywGAe2A_BgiRaU2zrec_BJaOu6k,63945
74
+ trilogy/parsing/render.py,sha256=dhHkwmZ5HNNJr-z81JDpN2TBe4Ktqarus5vtMn2-_B8,15502
75
+ trilogy/parsing/trilogy.lark,sha256=IZ-uSyTkjHtyqqYEH9fomM7yomCvP6AdH8wJSjPAuHc,12338
76
76
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
78
- pytrilogy-0.0.2.35.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.35.dist-info/METADATA,sha256=X18LM4yuJMZ4kh9Q6BUOWlxvEZgmRXMBCp9F3GNmQuA,8424
80
- pytrilogy-0.0.2.35.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
81
- pytrilogy-0.0.2.35.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.35.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.35.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.37.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.37.dist-info/METADATA,sha256=7Metihqd20_keAEEW5_7V3iQDhCG61TCCKNG8hI-d1s,8424
80
+ pytrilogy-0.0.2.37.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
81
+ pytrilogy-0.0.2.37.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.37.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.37.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.4.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.35"
7
+ __version__ = "0.0.2.37"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -39,6 +39,13 @@ class Comments:
39
39
  partial: bool = True
40
40
 
41
41
 
42
+ @dataclass
43
+ class Rendering:
44
+ """Control how the SQL is rendered"""
45
+
46
+ parameters: bool = True
47
+
48
+
42
49
  # TODO: support loading from environments
43
50
  @dataclass
44
51
  class Config:
@@ -48,6 +55,7 @@ class Config:
48
55
  validate_missing: bool = True
49
56
  comments: Comments = field(default_factory=Comments)
50
57
  optimizations: Optimizations = field(default_factory=Optimizations)
58
+ rendering: Rendering = field(default_factory=Rendering)
51
59
 
52
60
  @property
53
61
  def show_comments(self) -> bool:
trilogy/core/models.py CHANGED
@@ -3276,7 +3276,7 @@ class EnvironmentConceptDict(dict):
3276
3276
  return default
3277
3277
 
3278
3278
  def __getitem__(
3279
- self, key, line_no: int | None = None
3279
+ self, key, line_no: int | None = None, file: Path | None = None
3280
3280
  ) -> Concept | UndefinedConcept:
3281
3281
  try:
3282
3282
  return super(EnvironmentConceptDict, self).__getitem__(key)
@@ -3304,6 +3304,10 @@ class EnvironmentConceptDict(dict):
3304
3304
  message += f" Suggestions: {matches}"
3305
3305
 
3306
3306
  if line_no:
3307
+ if file:
3308
+ raise UndefinedConceptException(
3309
+ f"{file}: {line_no}: " + message, matches
3310
+ )
3307
3311
  raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
3308
3312
  raise UndefinedConceptException(message, matches)
3309
3313
 
@@ -3377,6 +3381,21 @@ class Environment(BaseModel):
3377
3381
  materialized_concepts: List[Concept] = Field(default_factory=list)
3378
3382
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3379
3383
 
3384
+ def __init__(self, **data):
3385
+ super().__init__(**data)
3386
+ self.concepts["_env_working_path"] = Concept(
3387
+ name="_env_working_path",
3388
+ namespace=self.namespace,
3389
+ lineage=Function(
3390
+ operator=FunctionType.CONSTANT,
3391
+ arguments=[str(self.working_path)],
3392
+ output_datatype=DataType.STRING,
3393
+ output_purpose=Purpose.CONSTANT,
3394
+ ),
3395
+ datatype=DataType.STRING,
3396
+ purpose=Purpose.CONSTANT,
3397
+ )
3398
+
3380
3399
  @classmethod
3381
3400
  def from_file(cls, path: str | Path) -> "Environment":
3382
3401
  with open(path, "r") as f:
@@ -3550,16 +3569,7 @@ class Environment(BaseModel):
3550
3569
  target = target.with_suffix(".preql")
3551
3570
  else:
3552
3571
  target = path
3553
- if alias in self.imports:
3554
- imports = self.imports[alias]
3555
- for x in imports:
3556
- if x.path == target:
3557
- return imports
3558
- if env:
3559
- self.imports[alias].append(
3560
- ImportStatement(alias=alias, path=target, environment=env)
3561
- )
3562
- else:
3572
+ if not env:
3563
3573
  parse_address = gen_cache_lookup(str(target), alias, str(self.working_path))
3564
3574
  try:
3565
3575
  with open(target, "r", encoding="utf-8") as f:
@@ -3583,13 +3593,8 @@ class Environment(BaseModel):
3583
3593
  f"Unable to import file {target.parent}, parsing error: {e}"
3584
3594
  )
3585
3595
  env = nparser.environment
3586
- for _, concept in env.concepts.items():
3587
- self.add_concept(concept.with_namespace(alias))
3588
-
3589
- for _, datasource in env.datasources.items():
3590
- self.add_datasource(datasource.with_namespace(alias))
3591
3596
  imps = ImportStatement(alias=alias, path=target, environment=env)
3592
- self.imports[alias].append(imps)
3597
+ self.add_import(alias, source=env, imp_stm=imps)
3593
3598
  return imps
3594
3599
 
3595
3600
  def parse(
trilogy/dialect/base.py CHANGED
@@ -337,6 +337,13 @@ class BaseDialect:
337
337
  " target grain"
338
338
  )
339
339
  rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.function.operator](args)}"
340
+ elif (
341
+ isinstance(c.lineage, Function)
342
+ and c.lineage.operator == FunctionType.CONSTANT
343
+ and CONFIG.rendering.parameters is True
344
+ and c.datatype.data_type != DataType.MAP
345
+ ):
346
+ rval = f":{c.safe_address}"
340
347
  else:
341
348
  args = [
342
349
  self.render_expr(
@@ -541,7 +548,7 @@ class BaseDialect:
541
548
  else:
542
549
  raise ValueError(f"Unable to render type {type(e)} {e}")
543
550
 
544
- def render_cte(self, cte: CTE, auto_sort: bool = True):
551
+ def render_cte(self, cte: CTE, auto_sort: bool = True) -> CompiledCTE:
545
552
  if self.UNNEST_MODE in (
546
553
  UnnestMode.CROSS_APPLY,
547
554
  UnnestMode.CROSS_JOIN,
trilogy/executor.py CHANGED
@@ -20,10 +20,16 @@ from trilogy.core.models import (
20
20
  ConceptDeclarationStatement,
21
21
  Datasource,
22
22
  CopyStatement,
23
+ ImportStatement,
24
+ MergeStatementV2,
25
+ Function,
26
+ FunctionType,
27
+ MapWrapper,
28
+ ListWrapper,
23
29
  )
24
30
  from trilogy.dialect.base import BaseDialect
25
31
  from trilogy.dialect.enums import Dialects
26
- from trilogy.core.enums import IOType
32
+ from trilogy.core.enums import IOType, Granularity
27
33
  from trilogy.parser import parse_text
28
34
  from trilogy.hooks.base_hook import BaseHook
29
35
  from pathlib import Path
@@ -104,6 +110,7 @@ class Executor(object):
104
110
  ProcessedShowStatement,
105
111
  ProcessedQueryPersist,
106
112
  ProcessedCopyStatement,
113
+ ProcessedRawSQLStatement,
107
114
  ),
108
115
  ):
109
116
  return None
@@ -142,7 +149,6 @@ class Executor(object):
142
149
 
143
150
  @execute_query.register
144
151
  def _(self, query: str) -> CursorResult:
145
-
146
152
  return self.execute_text(query)[-1]
147
153
 
148
154
  @execute_query.register
@@ -181,6 +187,34 @@ class Executor(object):
181
187
  ],
182
188
  )
183
189
 
190
+ @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"],
200
+ )
201
+
202
+ @execute_query.register
203
+ def _(self, query: MergeStatementV2) -> CursorResult:
204
+
205
+ self.environment.merge_concept(
206
+ query.source, query.target, modifiers=query.modifiers
207
+ )
208
+ return MockResult(
209
+ [
210
+ {
211
+ "source": query.source.address,
212
+ "target": query.target.address,
213
+ }
214
+ ],
215
+ ["source", "target"],
216
+ )
217
+
184
218
  @execute_query.register
185
219
  def _(self, query: ProcessedRawSQLStatement) -> CursorResult:
186
220
  return self.execute_raw_sql(query.text)
@@ -188,8 +222,7 @@ class Executor(object):
188
222
  @execute_query.register
189
223
  def _(self, query: ProcessedQuery) -> CursorResult:
190
224
  sql = self.generator.compile_statement(query)
191
- # connection = self.engine.connect()
192
- output = self.connection.execute(text(sql))
225
+ output = self.execute_raw_sql(sql)
193
226
  return output
194
227
 
195
228
  @execute_query.register
@@ -197,14 +230,14 @@ class Executor(object):
197
230
 
198
231
  sql = self.generator.compile_statement(query)
199
232
 
200
- output = self.connection.execute(text(sql))
233
+ output = self.execute_raw_sql(sql)
201
234
  self.environment.add_datasource(query.datasource)
202
235
  return output
203
236
 
204
237
  @execute_query.register
205
238
  def _(self, query: ProcessedCopyStatement) -> CursorResult:
206
239
  sql = self.generator.compile_statement(query)
207
- output: CursorResult = self.connection.execute(text(sql))
240
+ output: CursorResult = self.execute_raw_sql(sql)
208
241
  if query.target_type == IOType.CSV:
209
242
  import csv
210
243
 
@@ -213,7 +246,7 @@ class Executor(object):
213
246
  outcsv.writerow(output.keys())
214
247
  outcsv.writerows(output)
215
248
  else:
216
- raise NotImplementedError(f"Unsupported IOType {query.target_type}")
249
+ raise NotImplementedError(f"Unsupported IO Type {query.target_type}")
217
250
  # now return the query we ran through IO
218
251
  # TODO: instead return how many rows were written?
219
252
  return generate_result_set(
@@ -339,13 +372,50 @@ class Executor(object):
339
372
  if persist and isinstance(x, ProcessedQueryPersist):
340
373
  self.environment.add_datasource(x.datasource)
341
374
 
375
+ def _hydrate_param(self, param: str) -> Any:
376
+ matched = [
377
+ v
378
+ for v in self.environment.concepts.values()
379
+ if v.safe_address == param or v.address == param
380
+ ]
381
+ if not matched:
382
+ raise SyntaxError(f"No concept found for parameter {param}")
383
+
384
+ concept: Concept = matched.pop()
385
+ if not concept.granularity == Granularity.SINGLE_ROW:
386
+ raise SyntaxError(f"Cannot bind non-singleton concept {concept.address}")
387
+ if (
388
+ isinstance(concept.lineage, Function)
389
+ and concept.lineage.operator == FunctionType.CONSTANT
390
+ ):
391
+ rval = concept.lineage.arguments[0]
392
+ if isinstance(rval, ListWrapper):
393
+ return [x for x in rval]
394
+ if isinstance(rval, MapWrapper):
395
+ return {k: v for k, v in rval.items()}
396
+ return rval
397
+ else:
398
+ results = self.execute_query(f"select {concept.name} limit 1;").fetchone()
399
+ if not results:
400
+ return None
401
+ return results[0]
402
+
342
403
  def execute_raw_sql(
343
404
  self, command: str, variables: dict | None = None
344
405
  ) -> CursorResult:
345
406
  """Run a command against the raw underlying
346
407
  execution engine"""
408
+ final_params = None
409
+ q = text(command)
347
410
  if variables:
348
- return self.connection.execute(text(command), variables)
411
+ final_params = variables
412
+ else:
413
+ params = q.compile().params
414
+ if params:
415
+ final_params = {x: self._hydrate_param(x) for x in params}
416
+
417
+ if final_params:
418
+ return self.connection.execute(text(command), final_params)
349
419
  return self.connection.execute(
350
420
  text(command),
351
421
  )
@@ -302,6 +302,9 @@ class ParseToObjects(Transformer):
302
302
  def IDENTIFIER(self, args) -> str:
303
303
  return args.value
304
304
 
305
+ def QUOTED_IDENTIFIER(self, args) -> str:
306
+ return args.value[1:-1]
307
+
305
308
  @v_args(meta=True)
306
309
  def concept_lit(self, meta: Meta, args) -> Concept:
307
310
  return self.environment.concepts.__getitem__(args[0], meta.line)
@@ -386,7 +389,7 @@ class ParseToObjects(Transformer):
386
389
  modifiers += concept_list[:-1]
387
390
  concept = concept_list[-1]
388
391
  resolved = self.environment.concepts.__getitem__( # type: ignore
389
- key=concept, line_no=meta.line
392
+ key=concept, line_no=meta.line, file=self.token_address
390
393
  )
391
394
  return ColumnAssignment(alias=alias, modifiers=modifiers, concept=resolved)
392
395
 
@@ -801,7 +804,8 @@ class ParseToObjects(Transformer):
801
804
 
802
805
  @v_args(meta=True)
803
806
  def rawsql_statement(self, meta: Meta, args) -> RawSQLStatement:
804
- return RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
807
+ statement = RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
808
+ return statement
805
809
 
806
810
  def COPY_TYPE(self, args) -> IOType:
807
811
  return IOType(args.value)
trilogy/parsing/render.py CHANGED
@@ -368,9 +368,15 @@ class Renderer:
368
368
 
369
369
  @to_string.register
370
370
  def _(self, arg: "ImportStatement"):
371
- if arg.alias == DEFAULT_NAMESPACE:
372
- return f"import {arg.path};"
373
- return f"import {arg.path} as {arg.alias};"
371
+ path: str = str(arg.path).replace("\\", ".")
372
+ path = path.replace("/", ".")
373
+ if path.endswith(".preql"):
374
+ path = path.rsplit(".", 1)[0]
375
+ if path.startswith("."):
376
+ path = path[1:]
377
+ if arg.alias == DEFAULT_NAMESPACE or not arg.alias:
378
+ return f"import {path};"
379
+ return f"import {path} as {arg.alias};"
374
380
 
375
381
  @to_string.register
376
382
  def _(self, arg: "Concept"):
@@ -49,7 +49,7 @@
49
49
 
50
50
  //column_assignment
51
51
  //figure out if we want static
52
- column_assignment: (raw_column_assignment | IDENTIFIER | _static_functions ) ":" concept_assignment
52
+ column_assignment: (raw_column_assignment | IDENTIFIER | QUOTED_IDENTIFIER | _static_functions ) ":" concept_assignment
53
53
 
54
54
  RAW_ENTRY.1: /raw\s*\(/s
55
55
 
@@ -292,6 +292,7 @@
292
292
  // base language constructs
293
293
  concept_lit: IDENTIFIER
294
294
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.]*/
295
+ QUOTED_IDENTIFIER: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:\s]*`/
295
296
  QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:]*`/
296
297
  ADDRESS: IDENTIFIER
297
298
 
@@ -301,7 +302,7 @@
301
302
  SINGLE_STRING_CHARS: /(?:(?!\${)([^'\\]|\\.))+/+ // any character except '
302
303
  _single_quote: "'" ( SINGLE_STRING_CHARS )* "'"
303
304
  _double_quote: "\"" ( DOUBLE_STRING_CHARS )* "\""
304
- string_lit: _single_quote | _double_quote
305
+ string_lit: _single_quote | _double_quote | MULTILINE_STRING
305
306
 
306
307
  MINUS: "-"
307
308