pytrilogy 0.0.2.32__py3-none-any.whl → 0.0.2.34__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.32
3
+ Version: 0.0.2.34
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
- trilogy/__init__.py,sha256=CPOHf23-XKufDWmGAC9X5pKiKQAepzaB-rNzkGplJNo,291
1
+ trilogy/__init__.py,sha256=KM64JjCIbIm0t7no7TfLDsHkXU-HC5d2by7YePMKmq8,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=KiyYnctoZen4Hzv8WG2jeN-IE-dfQbWHdVCUeTZYjBg,1270
3
+ trilogy/constants.py,sha256=HQAnGUqJ5uMri7TWtqXHhz8iVWBzi2LCfRG8vKnBIB8,1269
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
5
  trilogy/executor.py,sha256=VcZ2U3RUU2al_VJ75AKVwmCJQLltYouxlgTjq4oxPB0,12577
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
@@ -8,7 +8,7 @@ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
11
- trilogy/core/enums.py,sha256=y0Z0m-xtcVw1ktkQ5yD3fJYWfOa4ncN_MzCTpREAxy0,6374
11
+ trilogy/core/enums.py,sha256=GG51K-bP3HTVuvJQFCjbmVt7iBOGpmF_qtnRVf0_obI,6396
12
12
  trilogy/core/env_processor.py,sha256=SHVB3nkidIlFc5dz-sofRMKXx66stpLQNuVdQSjC-So,2586
13
13
  trilogy/core/environment_helpers.py,sha256=DIsoo-GcXmXVPB1JbNh8Oku25Nyef9mexPIdy2ur_sk,7159
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
@@ -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=d0nmbSrR7LObYOpfx9Q5GYrXg-hV0OrBDwrIj90eGX8,159625
19
+ trilogy/core/models.py,sha256=xZHi9IYo_vSG35rF4fp7bLrGIu8J5QXqDObpZgN0_cI,160541
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
@@ -27,7 +27,7 @@ trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  trilogy/core/processing/concept_strategies_v3.py,sha256=Qy3WHiKaF371dDmbA0DWVQqSMKptH_gK714Uc41wbqc,36132
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=VFArfoUY5EiacspwQ03uWiKRr5SzEDhIB_iMrOIPBAg,18540
30
+ trilogy/core/processing/utility.py,sha256=_sWLDuV8WAEWdEIcAPM8-FcXgBai30UV12i4O9u5Mpc,18680
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
32
  trilogy/core/processing/node_generators/basic_node.py,sha256=WQNgJ1MwrMS_BQ-b3XwGGB6eToDykelAVj_fesJuqe0,2069
33
33
  trilogy/core/processing/node_generators/common.py,sha256=eslHTTPFTkmwHwKIuUsbFn54jxj-Avtt-QScqtNwzdg,8945
@@ -66,18 +66,18 @@ trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2
66
66
  trilogy/hooks/query_debugger.py,sha256=787umJjdGA057DCC714dqFstzJRUbwmz3MNr66IdpQI,4404
67
67
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
68
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
- trilogy/parsing/common.py,sha256=t7yiL_3f6rz_rouF9et84v5orAgs-EprV4V9ghQ6ql4,10024
69
+ trilogy/parsing/common.py,sha256=_GW9LU6_4RuUgcdcr8EE1ybCRd-7cz3idZtjHZ66pYA,10182
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=eCEJJ06zgWhj4DNja5CEhlwv-PrGlVMXDHUX9Q3aITM,64688
73
+ trilogy/parsing/parse_engine.py,sha256=mMMP0TPLAHx0m4c7scXxosDIgfPYyUYmlgwAFtVeRe0,66699
74
74
  trilogy/parsing/render.py,sha256=VKyo8wEOuiOzUtJ6w9EoGGmkhlqDQyy8wFj_Q_h6EfE,15263
75
75
  trilogy/parsing/trilogy.lark,sha256=Tuqw5oGMwOYt3TYOEx_hZqGpsAp-PiAKiMW8S3EFRcg,12236
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.32.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.32.dist-info/METADATA,sha256=iOdK_We6Z-kEQP6SnEzowkN089iEjUCqq_Q7yGxLEkQ,8403
80
- pytrilogy-0.0.2.32.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
81
- pytrilogy-0.0.2.32.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.32.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.32.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.34.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.34.dist-info/METADATA,sha256=jvasI3x4y_xwYh4TToePk_EQIwEr0Kfw2zqESs65bQk,8403
80
+ pytrilogy-0.0.2.34.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
81
+ pytrilogy-0.0.2.34.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.34.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.34.dist-info/RECORD,,
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.32"
7
+ __version__ = "0.0.2.34"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -36,7 +36,7 @@ class Comments:
36
36
  basic: bool = True
37
37
  joins: bool = True
38
38
  nullable: bool = False
39
- partial: bool = False
39
+ partial: bool = True
40
40
 
41
41
 
42
42
  # TODO: support loading from environments
trilogy/core/enums.py CHANGED
@@ -13,6 +13,7 @@ class UnnestMode(Enum):
13
13
  class ConceptSource(Enum):
14
14
  MANUAL = "manual"
15
15
  CTE = "cte"
16
+ SELECT = "select"
16
17
  PERSIST_STATEMENT = "persist_statement"
17
18
  AUTO_DERIVED = "auto_derived"
18
19
 
trilogy/core/models.py CHANGED
@@ -2077,6 +2077,11 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2077
2077
  self, source: Concept, target: Concept, modifiers: List[Modifier]
2078
2078
  ):
2079
2079
  original = [c for c in self.columns if c.concept.address == source.address]
2080
+ early_exit_check = [
2081
+ c for c in self.columns if c.concept.address == target.address
2082
+ ]
2083
+ if early_exit_check:
2084
+ return None
2080
2085
  if len(original) != 1:
2081
2086
  raise ValueError(
2082
2087
  f"Expected exactly one column to merge, got {len(original)} for {source.address}, {[x.alias for x in original]}"
@@ -3213,7 +3218,7 @@ class EnvironmentConceptDict(dict):
3213
3218
  def __init__(self, *args, **kwargs) -> None:
3214
3219
  super().__init__(self, *args, **kwargs)
3215
3220
  self.undefined: dict[str, UndefinedConcept] = {}
3216
- self.fail_on_missing: bool = False
3221
+ self.fail_on_missing: bool = True
3217
3222
  self.populate_default_concepts()
3218
3223
 
3219
3224
  def populate_default_concepts(self):
@@ -3332,7 +3337,6 @@ class Environment(BaseModel):
3332
3337
 
3333
3338
  materialized_concepts: List[Concept] = Field(default_factory=list)
3334
3339
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3335
- _parse_count: int = 0
3336
3340
 
3337
3341
  @classmethod
3338
3342
  def from_file(cls, path: str | Path) -> "Environment":
@@ -3442,19 +3446,22 @@ class Environment(BaseModel):
3442
3446
  exists = False
3443
3447
  existing = self.imports[alias]
3444
3448
  if imp_stm:
3445
- if any([x.path == imp_stm.path for x in existing]):
3449
+ if any(
3450
+ [x.path == imp_stm.path and x.alias == imp_stm.alias for x in existing]
3451
+ ):
3446
3452
  exists = True
3447
-
3448
3453
  else:
3449
- if any([x.path == source.working_path for x in existing]):
3454
+ if any(
3455
+ [x.path == source.working_path and x.alias == alias for x in existing]
3456
+ ):
3450
3457
  exists = True
3451
3458
  imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
3452
-
3453
3459
  same_namespace = alias == self.namespace
3454
3460
 
3455
3461
  if not exists:
3456
3462
  self.imports[alias].append(imp_stm)
3457
-
3463
+ # we can't exit early
3464
+ # as there may be new concepts
3458
3465
  for k, concept in source.concepts.items():
3459
3466
  if same_namespace:
3460
3467
  new = self.add_concept(concept, _ignore_cache=True)
@@ -3485,13 +3492,25 @@ class Environment(BaseModel):
3485
3492
  self.gen_concept_list_caches()
3486
3493
  return self
3487
3494
 
3488
- def add_file_import(self, path: str, alias: str, env: Environment | None = None):
3489
- from trilogy.parsing.parse_engine import ParseToObjects, PARSER
3490
-
3491
- apath = path.split(".")
3492
- apath[-1] = apath[-1] + ".preql"
3495
+ def add_file_import(
3496
+ self, path: str | Path, alias: str, env: Environment | None = None
3497
+ ):
3498
+ from trilogy.parsing.parse_engine import (
3499
+ ParseToObjects,
3500
+ PARSER,
3501
+ gen_cache_lookup,
3502
+ )
3493
3503
 
3494
- target: Path = Path(self.working_path, *apath)
3504
+ if isinstance(path, str):
3505
+ if path.endswith(".preql"):
3506
+ path = path.rsplit(".", 1)[0]
3507
+ if "." not in path:
3508
+ target = Path(self.working_path, path)
3509
+ else:
3510
+ target = Path(self.working_path, *path.split("."))
3511
+ target = target.with_suffix(".preql")
3512
+ else:
3513
+ target = path
3495
3514
  if alias in self.imports:
3496
3515
  imports = self.imports[alias]
3497
3516
  for x in imports:
@@ -3502,18 +3521,24 @@ class Environment(BaseModel):
3502
3521
  ImportStatement(alias=alias, path=target, environment=env)
3503
3522
  )
3504
3523
  else:
3524
+ parse_address = gen_cache_lookup(str(target), alias, str(self.working_path))
3505
3525
  try:
3506
3526
  with open(target, "r", encoding="utf-8") as f:
3507
3527
  text = f.read()
3528
+ nenv = Environment(
3529
+ working_path=target.parent,
3530
+ )
3531
+ nenv.concepts.fail_on_missing = False
3508
3532
  nparser = ParseToObjects(
3509
- visit_tokens=True,
3510
- text=text,
3511
3533
  environment=Environment(
3512
3534
  working_path=target.parent,
3513
3535
  ),
3514
- parse_address=str(target),
3536
+ parse_address=parse_address,
3537
+ token_address=target,
3515
3538
  )
3539
+ nparser.set_text(text)
3516
3540
  nparser.transform(PARSER.parse(text))
3541
+
3517
3542
  except Exception as e:
3518
3543
  raise ImportError(
3519
3544
  f"Unable to import file {target.parent}, parsing error: {e}"
@@ -425,6 +425,8 @@ def is_scalar_condition(
425
425
  return True
426
426
  if element.lineage and isinstance(element.lineage, AggregateWrapper):
427
427
  return is_scalar_condition(element.lineage, materialized)
428
+ if element.lineage and isinstance(element.lineage, Function):
429
+ return is_scalar_condition(element.lineage, materialized)
428
430
  return True
429
431
  elif isinstance(element, AggregateWrapper):
430
432
  return is_scalar_condition(element.function, materialized)
trilogy/parsing/common.py CHANGED
@@ -133,7 +133,9 @@ def constant_to_concept(
133
133
  )
134
134
 
135
135
 
136
- def function_to_concept(parent: Function, name: str, namespace: str) -> Concept:
136
+ def function_to_concept(
137
+ parent: Function, name: str, namespace: str, metadata: Metadata | None = None
138
+ ) -> Concept:
137
139
  pkeys: List[Concept] = []
138
140
  for x in parent.arguments:
139
141
  pkeys += [
@@ -159,6 +161,7 @@ def function_to_concept(parent: Function, name: str, namespace: str) -> Concept:
159
161
  purpose = Purpose.CONSTANT
160
162
  else:
161
163
  purpose = parent.output_purpose
164
+ fmetadata = metadata or Metadata()
162
165
  if grain is not None:
163
166
  return Concept(
164
167
  name=name,
@@ -169,6 +172,7 @@ def function_to_concept(parent: Function, name: str, namespace: str) -> Concept:
169
172
  keys=keys,
170
173
  modifiers=modifiers,
171
174
  grain=grain,
175
+ metadata=fmetadata,
172
176
  )
173
177
 
174
178
  return Concept(
@@ -179,6 +183,7 @@ def function_to_concept(parent: Function, name: str, namespace: str) -> Concept:
179
183
  namespace=namespace,
180
184
  keys=keys,
181
185
  modifiers=modifiers,
186
+ metadata=fmetadata,
182
187
  )
183
188
 
184
189
 
@@ -305,7 +310,7 @@ def arbitrary_to_concept(
305
310
  elif isinstance(parent, Function):
306
311
  if not name:
307
312
  name = f"{VIRTUAL_CONCEPT_PREFIX}_func_{parent.operator.value}_{string_to_hash(str(parent))}"
308
- return function_to_concept(parent, name, namespace)
313
+ return function_to_concept(parent, name, namespace, metadata=metadata)
309
314
  elif isinstance(parent, ListWrapper):
310
315
  if not name:
311
316
  name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
@@ -1,7 +1,7 @@
1
1
  from os.path import dirname, join
2
2
  from typing import List, Optional, Tuple, Union
3
3
  from re import IGNORECASE
4
- from lark import Lark, Transformer, v_args, Tree
4
+ from lark import Lark, Transformer, v_args, Tree, ParseTree
5
5
  from lark.exceptions import (
6
6
  UnexpectedCharacters,
7
7
  UnexpectedEOF,
@@ -32,6 +32,7 @@ from trilogy.core.enums import (
32
32
  ShowCategory,
33
33
  FunctionClass,
34
34
  IOType,
35
+ ConceptSource,
35
36
  )
36
37
  from trilogy.core.exceptions import InvalidSyntaxException, UndefinedConceptException
37
38
  from trilogy.core.functions import (
@@ -126,13 +127,16 @@ from trilogy.parsing.common import (
126
127
  from dataclasses import dataclass
127
128
 
128
129
 
130
+ CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
131
+
132
+ SELF_LABEL = "root"
133
+
134
+
129
135
  @dataclass
130
136
  class WholeGrainWrapper:
131
137
  where: WhereClause
132
138
 
133
139
 
134
- CONSTANT_TYPES = (int, float, str, bool, list, ListWrapper, MapWrapper)
135
-
136
140
  with open(join(dirname(__file__), "trilogy.lark"), "r") as f:
137
141
  PARSER = Lark(
138
142
  f.read(),
@@ -144,6 +148,10 @@ with open(join(dirname(__file__), "trilogy.lark"), "r") as f:
144
148
  )
145
149
 
146
150
 
151
+ def gen_cache_lookup(path: str, alias: str, parent: str) -> str:
152
+ return path + alias + parent
153
+
154
+
147
155
  def parse_concept_reference(
148
156
  name: str, environment: Environment, purpose: Optional[Purpose] = None
149
157
  ) -> Tuple[str, str, str, str | None]:
@@ -219,39 +227,47 @@ def unwrap_transformation(
219
227
  class ParseToObjects(Transformer):
220
228
  def __init__(
221
229
  self,
222
- visit_tokens,
223
- text,
224
230
  environment: Environment,
225
231
  parse_address: str | None = None,
232
+ token_address: Path | None = None,
226
233
  parsed: dict[str, "ParseToObjects"] | None = None,
234
+ tokens: dict[Path | str, ParseTree] | None = None,
235
+ text_lookup: dict[Path | str, str] | None = None,
227
236
  ):
228
- Transformer.__init__(self, visit_tokens)
229
- self.text = text
237
+ Transformer.__init__(self, True)
230
238
  self.environment: Environment = environment
231
- self.parse_address = parse_address or "root"
239
+ self.parse_address: str = parse_address or SELF_LABEL
240
+ self.token_address: Path | str = token_address or SELF_LABEL
232
241
  self.parsed: dict[str, ParseToObjects] = parsed if parsed else {}
242
+ self.tokens: dict[Path | str, ParseTree] = tokens or {}
243
+ self.text_lookup: dict[Path | str, str] = text_lookup or {}
233
244
  # we do a second pass to pick up circular dependencies
234
245
  # after initial parsing
235
246
  self.pass_count = 1
236
- self._results_stash = None
237
247
 
238
- def transform(self, tree):
248
+ def set_text(self, text: str):
249
+ self.text_lookup[self.token_address] = text
250
+
251
+ def transform(self, tree: Tree):
239
252
  results = super().transform(tree)
240
- self._results_stash = results
241
- self.environment._parse_count += 1
253
+ self.tokens[self.token_address] = tree
242
254
  return results
243
255
 
256
+ def prepare_parse(self):
257
+ self.pass_count = 1
258
+ self.environment.concepts.fail_on_missing = False
259
+ for _, v in self.parsed.items():
260
+ v.prepare_parse()
261
+
244
262
  def hydrate_missing(self):
245
263
  self.pass_count = 2
246
264
  for k, v in self.parsed.items():
247
-
248
265
  if v.pass_count == 2:
249
266
  continue
267
+ print(f"Hydrating {k}")
250
268
  v.hydrate_missing()
251
269
  self.environment.concepts.fail_on_missing = True
252
- # if not self.environment.concepts.undefined:
253
- # return self._results_stash
254
- reparsed = self.transform(PARSER.parse(self.text))
270
+ reparsed = self.transform(self.tokens[self.token_address])
255
271
  self.environment.concepts.undefined = {}
256
272
  return reparsed
257
273
 
@@ -266,6 +282,18 @@ class ParseToObjects(Transformer):
266
282
  output.concept.metadata.description
267
283
  or args[1].text.split("#")[1].strip()
268
284
  )
285
+ if isinstance(output, ImportStatement):
286
+ if len(args) > 1 and isinstance(args[1], Comment):
287
+ comment = args[1].text.split("#")[1].strip()
288
+ namespace = output.alias
289
+ for _, v in self.environment.concepts.items():
290
+ if v.namespace == namespace:
291
+ if v.metadata.description:
292
+ v.metadata.description = (
293
+ f"{comment}: {v.metadata.description}"
294
+ )
295
+ else:
296
+ v.metadata.description = comment
269
297
 
270
298
  return args[0]
271
299
 
@@ -647,45 +675,41 @@ class ParseToObjects(Transformer):
647
675
  return Comment(text=args.value)
648
676
 
649
677
  @v_args(meta=True)
650
- def select_transform(self, meta, args) -> ConceptTransform:
678
+ def select_transform(self, meta: Meta, args) -> ConceptTransform:
651
679
 
652
680
  output: str = args[1]
653
- function = unwrap_transformation(args[0])
681
+ transformation = unwrap_transformation(args[0])
654
682
  lookup, namespace, output, parent = parse_concept_reference(
655
683
  output, self.environment
656
684
  )
657
685
 
658
- if isinstance(function, AggregateWrapper):
659
- concept = agg_wrapper_to_concept(function, namespace=namespace, name=output)
660
- elif isinstance(function, WindowItem):
661
- concept = window_item_to_concept(function, namespace=namespace, name=output)
662
- elif isinstance(function, FilterItem):
663
- concept = filter_item_to_concept(function, namespace=namespace, name=output)
664
- elif isinstance(function, CONSTANT_TYPES):
665
- concept = constant_to_concept(function, namespace=namespace, name=output)
666
- elif isinstance(function, Function):
667
- concept = function_to_concept(function, namespace=namespace, name=output)
668
- else:
669
- if function.output_purpose == Purpose.PROPERTY:
670
- pkeys = [x for x in function.arguments if isinstance(x, Concept)]
671
- grain = Grain(components=pkeys)
672
- keys = tuple(grain.components_copy)
673
- else:
674
- grain = None
675
- keys = None
676
- concept = Concept(
677
- name=output,
678
- datatype=function.output_datatype,
679
- purpose=function.output_purpose,
680
- lineage=function,
681
- namespace=namespace,
682
- grain=Grain(components=[]) if not grain else grain,
683
- keys=keys,
686
+ metadata = Metadata(line_number=meta.line, concept_source=ConceptSource.SELECT)
687
+
688
+ if isinstance(transformation, AggregateWrapper):
689
+ concept = agg_wrapper_to_concept(
690
+ transformation, namespace=namespace, name=output, metadata=metadata
684
691
  )
685
- if concept.metadata:
686
- concept.metadata.line_number = meta.line
692
+ elif isinstance(transformation, WindowItem):
693
+ concept = window_item_to_concept(
694
+ transformation, namespace=namespace, name=output, metadata=metadata
695
+ )
696
+ elif isinstance(transformation, FilterItem):
697
+ concept = filter_item_to_concept(
698
+ transformation, namespace=namespace, name=output, metadata=metadata
699
+ )
700
+ elif isinstance(transformation, CONSTANT_TYPES):
701
+ concept = constant_to_concept(
702
+ transformation, namespace=namespace, name=output, metadata=metadata
703
+ )
704
+ elif isinstance(transformation, Function):
705
+ concept = function_to_concept(
706
+ transformation, namespace=namespace, name=output, metadata=metadata
707
+ )
708
+ else:
709
+ raise SyntaxError("Invalid transformation")
710
+
687
711
  self.environment.add_concept(concept, meta=meta)
688
- return ConceptTransform(function=function, output=concept)
712
+ return ConceptTransform(function=transformation, output=concept)
689
713
 
690
714
  @v_args(meta=True)
691
715
  def concept_nullable_modifier(self, meta: Meta, args) -> Modifier:
@@ -709,7 +733,7 @@ class ParseToObjects(Transformer):
709
733
  if len(args) != 1:
710
734
  raise ParseError(
711
735
  "Malformed select statement"
712
- f" {args} {self.text[meta.start_pos:meta.end_pos]}"
736
+ f" {args} {self.text_lookup[self.parse_address][meta.start_pos:meta.end_pos]}"
713
737
  )
714
738
  content = args[0]
715
739
  if isinstance(content, ConceptTransform):
@@ -807,25 +831,46 @@ class ParseToObjects(Transformer):
807
831
  path = args[0].split(".")
808
832
 
809
833
  target = join(self.environment.working_path, *path) + ".preql"
810
- if target in self.parsed:
811
- nparser = self.parsed[target]
834
+
835
+ # tokens + text are cached by path
836
+ token_lookup = Path(target)
837
+
838
+ # cache lookups by the target, the alias, and the file we're importing it from
839
+ cache_lookup = gen_cache_lookup(
840
+ path=target, alias=alias, parent=str(self.token_address)
841
+ )
842
+ if token_lookup in self.tokens:
843
+ raw_tokens = self.tokens[token_lookup]
844
+ text = self.text_lookup[token_lookup]
845
+ else:
846
+ text = self.resolve_import_address(target)
847
+ self.text_lookup[token_lookup] = text
848
+
849
+ raw_tokens = PARSER.parse(text)
850
+ self.tokens[token_lookup] = raw_tokens
851
+
852
+ if cache_lookup in self.parsed:
853
+ nparser = self.parsed[cache_lookup]
812
854
  else:
813
855
  try:
814
- text = self.resolve_import_address(target)
856
+ new_env = Environment(
857
+ working_path=dirname(target),
858
+ )
859
+ new_env.concepts.fail_on_missing = False
815
860
  nparser = ParseToObjects(
816
- visit_tokens=True,
817
- text=text,
818
- environment=Environment(
819
- working_path=dirname(target),
820
- # namespace=alias,
821
- ),
822
- parse_address=target,
861
+ environment=new_env,
862
+ parse_address=cache_lookup,
863
+ token_address=token_lookup,
823
864
  parsed={**self.parsed, **{self.parse_address: self}},
865
+ tokens={**self.tokens, **{token_lookup: raw_tokens}},
866
+ text_lookup={**self.text_lookup, **{token_lookup: text}},
824
867
  )
825
- nparser.transform(PARSER.parse(text))
826
- self.parsed[target] = nparser
868
+ nparser.transform(raw_tokens)
869
+ self.parsed[cache_lookup] = nparser
827
870
  # add the parsed objects of the import in
828
871
  self.parsed = {**self.parsed, **nparser.parsed}
872
+ self.tokens = {**self.tokens, **nparser.tokens}
873
+ self.text_lookup = {**self.text_lookup, **nparser.text_lookup}
829
874
  except Exception as e:
830
875
  raise ImportError(f"Unable to import file {target}, parsing error: {e}")
831
876
 
@@ -1912,11 +1957,14 @@ def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1912
1957
  ],
1913
1958
  ]:
1914
1959
  environment = environment or Environment()
1915
- parser = ParseToObjects(visit_tokens=True, text=text, environment=environment)
1960
+ parser = ParseToObjects(environment=environment)
1916
1961
 
1917
1962
  try:
1963
+ parser.set_text(text)
1964
+ # disable fail on missing to allow for circular dependencies
1965
+ parser.prepare_parse()
1918
1966
  parser.transform(PARSER.parse(text))
1919
- # handle circular dependencies
1967
+ # this will reset fail on missing
1920
1968
  pass_two = parser.hydrate_missing()
1921
1969
  output = [v for v in pass_two if v]
1922
1970
  except VisitError as e: