pytrilogy 0.0.1.102__tar.gz → 0.0.1.104__tar.gz

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 (100) hide show
  1. {pytrilogy-0.0.1.102/pytrilogy.egg-info → pytrilogy-0.0.1.104}/PKG-INFO +2 -2
  2. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/README.md +1 -1
  3. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104/pytrilogy.egg-info}/PKG-INFO +2 -2
  4. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_models.py +19 -5
  5. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/env_processor.py +5 -1
  7. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/models.py +84 -29
  8. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/concept_strategies_v3.py +6 -4
  9. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/filter_node.py +2 -0
  10. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/rowset_node.py +10 -6
  11. pytrilogy-0.0.1.104/trilogy/core/processing/node_generators/select_node.py +564 -0
  12. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/__init__.py +54 -1
  13. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/parse_engine.py +23 -12
  14. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/scripts/trilogy.py +1 -1
  15. pytrilogy-0.0.1.102/trilogy/core/processing/node_generators/select_node.py +0 -328
  16. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/LICENSE.md +0 -0
  17. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pyproject.toml +0 -0
  18. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/SOURCES.txt +0 -0
  19. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/dependency_links.txt +0 -0
  20. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/entry_points.txt +0 -0
  21. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/requires.txt +0 -0
  22. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/top_level.txt +0 -0
  23. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/setup.cfg +0 -0
  24. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/setup.py +0 -0
  25. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_declarations.py +0 -0
  26. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_derived_concepts.py +0 -0
  27. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_discovery_nodes.py +0 -0
  28. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_environment.py +0 -0
  29. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_functions.py +0 -0
  30. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_imports.py +0 -0
  31. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_metadata.py +0 -0
  32. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_multi_join_assignments.py +0 -0
  33. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_parsing.py +0 -0
  34. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_partial_handling.py +0 -0
  35. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_query_processing.py +0 -0
  36. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_select.py +0 -0
  37. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_statements.py +0 -0
  38. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_undefined_concept.py +0 -0
  39. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/tests/test_where_clause.py +0 -0
  40. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/compiler.py +0 -0
  41. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/constants.py +0 -0
  42. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/__init__.py +0 -0
  43. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/constants.py +0 -0
  44. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/enums.py +0 -0
  45. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/environment_helpers.py +0 -0
  46. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/ergonomics.py +0 -0
  47. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/exceptions.py +0 -0
  48. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/functions.py +0 -0
  49. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/graph_models.py +0 -0
  50. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/internal.py +0 -0
  51. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/__init__.py +0 -0
  52. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/graph_utils.py +0 -0
  53. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/__init__.py +0 -0
  54. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  55. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/common.py +0 -0
  56. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/concept_merge.py +0 -0
  57. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_node.py +0 -0
  58. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  59. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/merge_node.py +0 -0
  60. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  61. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  62. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/window_node.py +0 -0
  63. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/base_node.py +0 -0
  64. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/filter_node.py +0 -0
  65. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/group_node.py +0 -0
  66. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/merge_node.py +0 -0
  67. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  68. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  69. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/window_node.py +0 -0
  70. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/processing/utility.py +0 -0
  71. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/core/query_processor.py +0 -0
  72. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/__init__.py +0 -0
  73. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/base.py +0 -0
  74. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/bigquery.py +0 -0
  75. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/common.py +0 -0
  76. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/config.py +0 -0
  77. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/duckdb.py +0 -0
  78. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/enums.py +0 -0
  79. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/postgres.py +0 -0
  80. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/presto.py +0 -0
  81. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/snowflake.py +0 -0
  82. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/dialect/sql_server.py +0 -0
  83. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/docs/__init__.py +0 -0
  84. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/engine.py +0 -0
  85. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/executor.py +0 -0
  86. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/hooks/__init__.py +0 -0
  87. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/hooks/base_hook.py +0 -0
  88. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/hooks/graph_hook.py +0 -0
  89. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/hooks/query_debugger.py +0 -0
  90. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/metadata/__init__.py +0 -0
  91. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parser.py +0 -0
  92. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/__init__.py +0 -0
  93. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/common.py +0 -0
  94. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/config.py +0 -0
  95. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/exceptions.py +0 -0
  96. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/helpers.py +0 -0
  97. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/parsing/render.py +0 -0
  98. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/py.typed +0 -0
  99. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/scripts/__init__.py +0 -0
  100. {pytrilogy-0.0.1.102 → pytrilogy-0.0.1.104}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.1.102
3
+ Version: 0.0.1.104
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -27,7 +27,7 @@ Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
27
27
  Provides-Extra: snowflake
28
28
  Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
29
29
 
30
- ##Trilogy
30
+ ## Trilogy
31
31
  [![Website](https://img.shields.io/badge/INTRO-WEB-orange?)](https://trilogydata.dev/)
32
32
  [![Discord](https://img.shields.io/badge/DISCORD-CHAT-red?logo=discord)](https://discord.gg/Z4QSSuqGEd)
33
33
 
@@ -1,4 +1,4 @@
1
- ##Trilogy
1
+ ## Trilogy
2
2
  [![Website](https://img.shields.io/badge/INTRO-WEB-orange?)](https://trilogydata.dev/)
3
3
  [![Discord](https://img.shields.io/badge/DISCORD-CHAT-red?logo=discord)](https://discord.gg/Z4QSSuqGEd)
4
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.1.102
3
+ Version: 0.0.1.104
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -27,7 +27,7 @@ Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
27
27
  Provides-Extra: snowflake
28
28
  Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
29
29
 
30
- ##Trilogy
30
+ ## Trilogy
31
31
  [![Website](https://img.shields.io/badge/INTRO-WEB-orange?)](https://trilogydata.dev/)
32
32
  [![Discord](https://img.shields.io/badge/DISCORD-CHAT-red?logo=discord)](https://discord.gg/Z4QSSuqGEd)
33
33
 
@@ -1,4 +1,4 @@
1
- from trilogy.core.enums import BooleanOperator, Purpose, JoinType
1
+ from trilogy.core.enums import BooleanOperator, Purpose, JoinType, ComparisonOperator
2
2
  from trilogy.core.models import (
3
3
  CTE,
4
4
  Grain,
@@ -9,6 +9,7 @@ from trilogy.core.models import (
9
9
  Address,
10
10
  UndefinedConcept,
11
11
  BaseJoin,
12
+ Comparison,
12
13
  )
13
14
 
14
15
 
@@ -73,11 +74,17 @@ def test_conditional(test_environment, test_environment_graph):
73
74
  condition_b = Conditional(
74
75
  left=test_concept, right=test_concept, operator=BooleanOperator.AND
75
76
  )
76
-
77
77
  merged = condition_a + condition_b
78
- assert merged.left == condition_a
79
- assert merged.right == condition_b
80
- assert merged.operator == BooleanOperator.AND
78
+ assert merged == condition_a
79
+
80
+ test_concept_two = list(test_environment.concepts.values())[-2]
81
+ condition_c = Conditional(
82
+ left=test_concept, right=test_concept_two, operator=BooleanOperator.AND
83
+ )
84
+ merged_two = condition_a + condition_c
85
+ assert merged_two.left == condition_a
86
+ assert merged_two.right == condition_c
87
+ assert merged_two.operator == BooleanOperator.AND
81
88
 
82
89
 
83
90
  def test_grain(test_environment):
@@ -177,3 +184,10 @@ def test_base_join(test_environment: Environment):
177
184
  exc3 = exc4
178
185
  pass
179
186
  assert isinstance(exc3, SyntaxError)
187
+
188
+
189
+ def test_comparison():
190
+ try:
191
+ Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
192
+ except Exception as exc:
193
+ assert isinstance(exc, SyntaxError)
@@ -3,6 +3,6 @@ from trilogy.dialect.enums import Dialects
3
3
  from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
 
6
- __version__ = "0.0.1.102"
6
+ __version__ = "0.0.1.104"
7
7
 
8
8
  __all__ = ["parse", "Executor", "Dialects", "Environment"]
@@ -1,4 +1,8 @@
1
- from trilogy.core.graph_models import ReferenceGraph, concept_to_node, datasource_to_node
1
+ from trilogy.core.graph_models import (
2
+ ReferenceGraph,
3
+ concept_to_node,
4
+ datasource_to_node,
5
+ )
2
6
  from trilogy.core.models import Environment
3
7
  from trilogy.core.enums import PurposeLineage
4
8
 
@@ -338,7 +338,7 @@ class Concept(Namespaced, SelectGrain, BaseModel):
338
338
 
339
339
  def __eq__(self, other: object):
340
340
  if isinstance(other, str):
341
- if self.address == str:
341
+ if self.address == other:
342
342
  return True
343
343
  if not isinstance(other, Concept):
344
344
  return False
@@ -355,7 +355,7 @@ class Concept(Namespaced, SelectGrain, BaseModel):
355
355
  grain = ",".join([str(c.address) for c in self.grain.components])
356
356
  return f"{self.namespace}.{self.name}<{grain}>"
357
357
 
358
- @property
358
+ @cached_property
359
359
  def address(self) -> str:
360
360
  return f"{self.namespace}.{self.name}"
361
361
 
@@ -436,7 +436,8 @@ class Concept(Namespaced, SelectGrain, BaseModel):
436
436
  modifiers=self.modifiers,
437
437
  )
438
438
 
439
- def with_default_grain(self) -> "Concept":
439
+ @cached_property
440
+ def _with_default_grain(self) -> "Concept":
440
441
  if self.purpose == Purpose.KEY:
441
442
  # we need to make this abstract
442
443
  grain = Grain(components=[self.with_grain(Grain())], nested=True)
@@ -473,6 +474,9 @@ class Concept(Namespaced, SelectGrain, BaseModel):
473
474
  modifiers=self.modifiers,
474
475
  )
475
476
 
477
+ def with_default_grain(self) -> "Concept":
478
+ return self._with_default_grain
479
+
476
480
  @property
477
481
  def sources(self) -> List["Concept"]:
478
482
  if self.lineage:
@@ -610,7 +614,7 @@ class Grain(BaseModel):
610
614
  [c.name == ALL_ROWS_CONCEPT for c in self.components]
611
615
  )
612
616
 
613
- @property
617
+ @cached_property
614
618
  def set(self):
615
619
  return set([c.address for c in self.components_copy])
616
620
 
@@ -1585,7 +1589,7 @@ class Datasource(Namespaced, BaseModel):
1585
1589
  columns=[c.with_namespace(namespace) for c in self.columns],
1586
1590
  )
1587
1591
 
1588
- @property
1592
+ @cached_property
1589
1593
  def concepts(self) -> List[Concept]:
1590
1594
  return [c.concept for c in self.columns]
1591
1595
 
@@ -1780,7 +1784,7 @@ class QueryDatasource(BaseModel):
1780
1784
 
1781
1785
  @field_validator("source_map")
1782
1786
  @classmethod
1783
- def validate_source_map(cls, v, info=ValidationInfo):
1787
+ def validate_source_map(cls, v, info: ValidationInfo):
1784
1788
  values = info.data
1785
1789
  expected = {c.address for c in values["output_concepts"]}.union(
1786
1790
  c.address for c in values["input_concepts"]
@@ -1887,7 +1891,9 @@ class QueryDatasource(BaseModel):
1887
1891
  else None
1888
1892
  ),
1889
1893
  source_type=self.source_type,
1890
- partial_concepts=self.partial_concepts + other.partial_concepts,
1894
+ partial_concepts=unique(
1895
+ self.partial_concepts + other.partial_concepts, "address"
1896
+ ),
1891
1897
  join_derived_concepts=self.join_derived_concepts,
1892
1898
  force_group=self.force_group,
1893
1899
  )
@@ -2286,8 +2292,8 @@ class EnvironmentConceptDict(dict):
2286
2292
 
2287
2293
  class ImportStatement(BaseModel):
2288
2294
  alias: str
2289
- path: str
2290
- # environment: "Environment" | None = None
2295
+ path: Path
2296
+ environment: Union["Environment", None] = None
2291
2297
  # TODO: this might result in a lot of duplication
2292
2298
  # environment:"Environment"
2293
2299
 
@@ -2322,6 +2328,9 @@ class Environment(BaseModel):
2322
2328
  version: str = Field(default_factory=get_version)
2323
2329
  cte_name_map: Dict[str, str] = Field(default_factory=dict)
2324
2330
 
2331
+ materialized_concepts: List[Concept] = Field(default_factory=list)
2332
+ _parse_count: int = 0
2333
+
2325
2334
  @classmethod
2326
2335
  def from_file(cls, path: str | Path) -> "Environment":
2327
2336
  with open(path, "r") as f:
@@ -2347,20 +2356,14 @@ class Environment(BaseModel):
2347
2356
  f.write(self.model_dump_json())
2348
2357
  return ppath
2349
2358
 
2350
- @property
2351
- def materialized_concepts(self) -> List[Concept]:
2352
- output = []
2353
- for concept in self.concepts.values():
2354
- found = False
2355
- # basic concepts are effectively materialized
2356
- # and can be found via join paths
2357
- for datasource in self.datasources.values():
2358
- if concept.address in [x.address for x in datasource.output_concepts]:
2359
- found = True
2360
- break
2361
- if found:
2362
- output.append(concept)
2363
- return output
2359
+ def gen_materialized_concepts(self) -> None:
2360
+ concrete_addresses = set()
2361
+ for datasource in self.datasources.values():
2362
+ for concept in datasource.output_concepts:
2363
+ concrete_addresses.add(concept.address)
2364
+ self.materialized_concepts = [
2365
+ c for c in self.concepts.values() if c.address in concrete_addresses
2366
+ ]
2364
2367
 
2365
2368
  def validate_concept(self, lookup: str, meta: Meta | None = None):
2366
2369
  existing: Concept = self.concepts.get(lookup) # type: ignore
@@ -2390,12 +2393,61 @@ class Environment(BaseModel):
2390
2393
 
2391
2394
  def add_import(self, alias: str, environment: Environment):
2392
2395
  self.imports[alias] = ImportStatement(
2393
- alias=alias, path=str(environment.working_path)
2396
+ alias=alias, path=Path(environment.working_path)
2394
2397
  )
2395
2398
  for key, concept in environment.concepts.items():
2396
2399
  self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
2397
2400
  for key, datasource in environment.datasources.items():
2398
2401
  self.datasources[f"{alias}.{key}"] = datasource.with_namespace(alias)
2402
+ self.gen_materialized_concepts()
2403
+ return self
2404
+
2405
+ def add_file_import(self, path: str, alias: str, env: Environment | None = None):
2406
+ from trilogy.parsing.parse_engine import ParseToObjects, PARSER
2407
+
2408
+ apath = path.split(".")
2409
+ apath[-1] = apath[-1] + ".preql"
2410
+
2411
+ target: Path = Path(self.working_path, *apath)
2412
+ if env:
2413
+ self.imports[alias] = ImportStatement(
2414
+ alias=alias, path=target, environment=env
2415
+ )
2416
+
2417
+ elif alias in self.imports:
2418
+ current = self.imports[alias]
2419
+ env = self.imports[alias].environment
2420
+ if current.path != target:
2421
+ raise ImportError(
2422
+ f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
2423
+ )
2424
+ else:
2425
+ try:
2426
+ with open(target, "r", encoding="utf-8") as f:
2427
+ text = f.read()
2428
+ nparser = ParseToObjects(
2429
+ visit_tokens=True,
2430
+ text=text,
2431
+ environment=Environment(
2432
+ working_path=target.parent,
2433
+ ),
2434
+ parse_address=str(target),
2435
+ )
2436
+ nparser.transform(PARSER.parse(text))
2437
+ except Exception as e:
2438
+ raise ImportError(
2439
+ f"Unable to import file {target.parent}, parsing error: {e}"
2440
+ )
2441
+ env = nparser.environment
2442
+ if env:
2443
+ for _, concept in env.concepts.items():
2444
+ self.add_concept(concept.with_namespace(alias))
2445
+
2446
+ for _, datasource in env.datasources.items():
2447
+ self.add_datasource(datasource.with_namespace(alias))
2448
+ imps = ImportStatement(alias=alias, path=target, environment=env)
2449
+ self.imports[alias] = imps
2450
+ return imps
2399
2451
 
2400
2452
  def parse(
2401
2453
  self, input: str, namespace: str | None = None, persist: bool = False
@@ -2446,21 +2498,22 @@ class Environment(BaseModel):
2446
2498
  from trilogy.core.environment_helpers import generate_related_concepts
2447
2499
 
2448
2500
  generate_related_concepts(concept, self)
2501
+ self.gen_materialized_concepts()
2449
2502
  return concept
2450
2503
 
2451
2504
  def add_datasource(
2452
2505
  self,
2453
2506
  datasource: Datasource,
2507
+ meta: Meta | None = None,
2454
2508
  ):
2455
- if datasource.namespace == DEFAULT_NAMESPACE:
2456
- self.datasources[datasource.name] = datasource
2457
- return datasource
2458
- if not datasource.namespace:
2509
+ if not datasource.namespace or datasource.namespace == DEFAULT_NAMESPACE:
2459
2510
  self.datasources[datasource.name] = datasource
2511
+ self.gen_materialized_concepts()
2460
2512
  return datasource
2461
2513
  self.datasources[datasource.namespace + "." + datasource.identifier] = (
2462
2514
  datasource
2463
2515
  )
2516
+ self.gen_materialized_concepts()
2464
2517
  return datasource
2465
2518
 
2466
2519
 
@@ -2530,7 +2583,7 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2530
2583
 
2531
2584
  def __post_init__(self):
2532
2585
  if arg_to_datatype(self.left) != arg_to_datatype(self.right):
2533
- raise ValueError(
2586
+ raise SyntaxError(
2534
2587
  f"Cannot compare {self.left} and {self.right} of different types"
2535
2588
  )
2536
2589
 
@@ -2704,6 +2757,8 @@ class Conditional(Namespaced, SelectGrain, BaseModel):
2704
2757
  def __add__(self, other) -> "Conditional":
2705
2758
  if other is None:
2706
2759
  return self
2760
+ elif str(other) == str(self):
2761
+ return self
2707
2762
  elif isinstance(other, (Comparison, Conditional, Parenthetical)):
2708
2763
  return Conditional(left=self, right=other, operator=BooleanOperator.AND)
2709
2764
  raise ValueError(f"Cannot add {self.__class__} and {type(other)}")
@@ -23,7 +23,6 @@ from trilogy.core.processing.node_generators import (
23
23
  gen_window_node,
24
24
  gen_group_node,
25
25
  gen_basic_node,
26
- gen_select_node,
27
26
  gen_unnest_node,
28
27
  gen_merge_node,
29
28
  gen_group_to_node,
@@ -208,7 +207,8 @@ def generate_node(
208
207
  history: History | None = None,
209
208
  ) -> StrategyNode | None:
210
209
  # first check in case there is a materialized_concept
211
- candidate = gen_select_node(
210
+ history = history or History()
211
+ candidate = history.gen_select_node(
212
212
  concept,
213
213
  local_optional,
214
214
  environment,
@@ -218,6 +218,7 @@ def generate_node(
218
218
  accept_partial=accept_partial,
219
219
  accept_partial_optional=False,
220
220
  )
221
+
221
222
  if candidate:
222
223
  return candidate
223
224
 
@@ -318,9 +319,9 @@ def generate_node(
318
319
  )
319
320
  elif concept.derivation == PurposeLineage.ROOT:
320
321
  logger.info(
321
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating select node"
322
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating select node with optional {[x.address for x in local_optional]}"
322
323
  )
323
- return gen_select_node(
324
+ return history.gen_select_node(
324
325
  concept,
325
326
  local_optional,
326
327
  environment,
@@ -328,6 +329,7 @@ def generate_node(
328
329
  depth + 1,
329
330
  fail_if_not_found=False,
330
331
  accept_partial=accept_partial,
332
+ accept_partial_optional=True,
331
333
  )
332
334
  else:
333
335
  raise ValueError(f"Unknown derivation {concept.derivation}")
@@ -55,6 +55,8 @@ def gen_filter_node(
55
55
  depth=depth + 1,
56
56
  history=history,
57
57
  )
58
+ if not enrich_node:
59
+ return filter_node
58
60
  x = MergeNode(
59
61
  input_concepts=[concept, immediate_parent] + local_optional,
60
62
  output_concepts=[
@@ -35,26 +35,30 @@ def gen_rowset_node(
35
35
  lineage: RowsetItem = concept.lineage
36
36
  rowset: RowsetDerivationStatement = lineage.rowset
37
37
  select: SelectStatement | MultiSelectStatement = lineage.rowset.select
38
+ if where := select.where_clause:
39
+ targets = select.output_components + where.conditional.concept_arguments
40
+ else:
41
+ targets = select.output_components
38
42
  node: StrategyNode = source_concepts(
39
- mandatory_list=select.output_components,
43
+ mandatory_list=targets,
40
44
  environment=environment,
41
45
  g=g,
42
46
  depth=depth + 1,
43
47
  history=history,
44
48
  )
45
- node.conditions = select.where_clause.conditional if select.where_clause else None
46
- # rebuild any cached info with the new condition clause
47
- node.rebuild_cache()
48
49
  if not node:
49
50
  logger.info(
50
51
  f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset node for {concept}"
51
52
  )
52
53
  return None
54
+ node.conditions = select.where_clause.conditional if select.where_clause else None
55
+ # rebuild any cached info with the new condition clause
56
+ node.rebuild_cache()
53
57
  enrichment = set([x.address for x in local_optional])
54
58
  rowset_relevant = [
55
59
  x
56
60
  for x in rowset.derived_concepts
57
- if x.address == concept.address or x.address in enrichment
61
+ # if x.address == concept.address or x.address in enrichment
58
62
  ]
59
63
  additional_relevant = [
60
64
  x for x in select.output_components if x.address in enrichment
@@ -68,7 +72,7 @@ def gen_rowset_node(
68
72
  for item in additional_relevant:
69
73
  node.partial_concepts.append(item)
70
74
 
71
- # assume grain to be outoput of select
75
+ # assume grain to be output of select
72
76
  # but don't include anything aggregate at this point
73
77
  assert node.resolution_cache
74
78
  node.resolution_cache.grain = concept_list_to_grain(