pytrilogy 0.0.1.103__tar.gz → 0.0.1.105__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 (99) hide show
  1. {pytrilogy-0.0.1.103/pytrilogy.egg-info → pytrilogy-0.0.1.105}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/__init__.py +1 -1
  4. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/models.py +176 -45
  5. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/concept_strategies_v3.py +6 -3
  6. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/common.py +19 -7
  7. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/filter_node.py +39 -10
  8. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/merge_node.py +11 -1
  9. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/select_node.py +275 -53
  10. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/__init__.py +54 -1
  11. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/base.py +12 -3
  12. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/common.py +30 -0
  13. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/parse_engine.py +65 -94
  14. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/render.py +0 -122
  15. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/LICENSE.md +0 -0
  16. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/README.md +0 -0
  17. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pyproject.toml +0 -0
  18. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pytrilogy.egg-info/SOURCES.txt +0 -0
  19. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pytrilogy.egg-info/dependency_links.txt +0 -0
  20. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pytrilogy.egg-info/entry_points.txt +0 -0
  21. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pytrilogy.egg-info/requires.txt +0 -0
  22. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/pytrilogy.egg-info/top_level.txt +0 -0
  23. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/setup.cfg +0 -0
  24. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/setup.py +0 -0
  25. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_declarations.py +0 -0
  26. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_derived_concepts.py +0 -0
  27. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_discovery_nodes.py +0 -0
  28. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_environment.py +0 -0
  29. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_functions.py +0 -0
  30. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_imports.py +0 -0
  31. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_metadata.py +0 -0
  32. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_models.py +0 -0
  33. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_multi_join_assignments.py +0 -0
  34. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_parsing.py +0 -0
  35. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_partial_handling.py +0 -0
  36. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_query_processing.py +0 -0
  37. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_select.py +0 -0
  38. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_statements.py +0 -0
  39. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_undefined_concept.py +0 -0
  40. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/tests/test_where_clause.py +0 -0
  41. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/compiler.py +0 -0
  42. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/constants.py +0 -0
  43. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/__init__.py +0 -0
  44. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/constants.py +0 -0
  45. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/enums.py +0 -0
  46. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/env_processor.py +0 -0
  47. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/environment_helpers.py +0 -0
  48. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/ergonomics.py +0 -0
  49. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/exceptions.py +0 -0
  50. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/functions.py +0 -0
  51. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/graph_models.py +0 -0
  52. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/internal.py +0 -0
  53. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/__init__.py +0 -0
  54. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/graph_utils.py +0 -0
  55. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/__init__.py +0 -0
  56. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  57. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/concept_merge.py +0 -0
  58. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/group_node.py +0 -0
  59. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  60. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  61. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  62. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  63. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/node_generators/window_node.py +0 -0
  64. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/base_node.py +0 -0
  65. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/filter_node.py +0 -0
  66. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/group_node.py +0 -0
  67. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/merge_node.py +0 -0
  68. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  69. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  70. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/nodes/window_node.py +0 -0
  71. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/processing/utility.py +0 -0
  72. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/core/query_processor.py +0 -0
  73. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/__init__.py +0 -0
  74. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/bigquery.py +0 -0
  75. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/common.py +0 -0
  76. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/config.py +0 -0
  77. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/duckdb.py +0 -0
  78. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/enums.py +0 -0
  79. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/postgres.py +0 -0
  80. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/presto.py +0 -0
  81. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/snowflake.py +0 -0
  82. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/dialect/sql_server.py +0 -0
  83. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/docs/__init__.py +0 -0
  84. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/engine.py +0 -0
  85. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/executor.py +0 -0
  86. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/hooks/__init__.py +0 -0
  87. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/hooks/base_hook.py +0 -0
  88. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/hooks/graph_hook.py +0 -0
  89. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/hooks/query_debugger.py +0 -0
  90. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/metadata/__init__.py +0 -0
  91. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parser.py +0 -0
  92. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/__init__.py +0 -0
  93. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/config.py +0 -0
  94. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/exceptions.py +0 -0
  95. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/parsing/helpers.py +0 -0
  96. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/py.typed +0 -0
  97. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/scripts/__init__.py +0 -0
  98. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/scripts/trilogy.py +0 -0
  99. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.105}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.1.103
3
+ Version: 0.0.1.105
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.1.103
3
+ Version: 0.0.1.105
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -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.103"
6
+ __version__ = "0.0.1.105"
7
7
 
8
8
  __all__ = ["parse", "Executor", "Dialects", "Environment"]
@@ -113,15 +113,27 @@ NAMESPACED_TYPES = Union[
113
113
 
114
114
 
115
115
  class Namespaced(ABC):
116
- pass
117
116
 
118
117
  def with_namespace(self, namespace: str):
119
118
  raise NotImplementedError
120
119
 
121
120
 
122
- class SelectGrain(ABC):
123
- pass
121
+ class ConceptArgs(ABC):
122
+
123
+ @property
124
+ def concept_arguments(self) -> List["Concept"]:
125
+ raise NotImplementedError
126
+
127
+ @property
128
+ def existence_arguments(self) -> List["Concept"]:
129
+ return []
130
+
131
+ @property
132
+ def row_arguments(self) -> List["Concept"]:
133
+ return self.concept_arguments
124
134
 
135
+
136
+ class SelectGrain(ABC):
125
137
  def with_select_grain(self, grain: Grain):
126
138
  raise NotImplementedError
127
139
 
@@ -355,7 +367,7 @@ class Concept(Namespaced, SelectGrain, BaseModel):
355
367
  grain = ",".join([str(c.address) for c in self.grain.components])
356
368
  return f"{self.namespace}.{self.name}<{grain}>"
357
369
 
358
- @property
370
+ @cached_property
359
371
  def address(self) -> str:
360
372
  return f"{self.namespace}.{self.name}"
361
373
 
@@ -436,7 +448,8 @@ class Concept(Namespaced, SelectGrain, BaseModel):
436
448
  modifiers=self.modifiers,
437
449
  )
438
450
 
439
- def with_default_grain(self) -> "Concept":
451
+ @cached_property
452
+ def _with_default_grain(self) -> "Concept":
440
453
  if self.purpose == Purpose.KEY:
441
454
  # we need to make this abstract
442
455
  grain = Grain(components=[self.with_grain(Grain())], nested=True)
@@ -473,6 +486,9 @@ class Concept(Namespaced, SelectGrain, BaseModel):
473
486
  modifiers=self.modifiers,
474
487
  )
475
488
 
489
+ def with_default_grain(self) -> "Concept":
490
+ return self._with_default_grain
491
+
476
492
  @property
477
493
  def sources(self) -> List["Concept"]:
478
494
  if self.lineage:
@@ -610,7 +626,7 @@ class Grain(BaseModel):
610
626
  [c.name == ALL_ROWS_CONCEPT for c in self.components]
611
627
  )
612
628
 
613
- @property
629
+ @cached_property
614
630
  def set(self):
615
631
  return set([c.address for c in self.components_copy])
616
632
 
@@ -1391,16 +1407,11 @@ class MultiSelectStatement(Namespaced, BaseModel):
1391
1407
  return output
1392
1408
 
1393
1409
  def find_source(self, concept: Concept, cte: CTE) -> Concept:
1394
- all = []
1395
1410
  for x in self.align.items:
1396
1411
  if concept.name == x.alias:
1397
1412
  for c in x.concepts:
1398
1413
  if c.address in cte.output_lcl:
1399
- all.append(c)
1400
-
1401
- if len(all) == 1:
1402
- return all[0]
1403
-
1414
+ return c
1404
1415
  raise SyntaxError(
1405
1416
  f"Could not find upstream map for multiselect {str(concept)} on cte ({cte})"
1406
1417
  )
@@ -1585,7 +1596,7 @@ class Datasource(Namespaced, BaseModel):
1585
1596
  columns=[c.with_namespace(namespace) for c in self.columns],
1586
1597
  )
1587
1598
 
1588
- @property
1599
+ @cached_property
1589
1600
  def concepts(self) -> List[Concept]:
1590
1601
  return [c.concept for c in self.columns]
1591
1602
 
@@ -1780,7 +1791,7 @@ class QueryDatasource(BaseModel):
1780
1791
 
1781
1792
  @field_validator("source_map")
1782
1793
  @classmethod
1783
- def validate_source_map(cls, v, info=ValidationInfo):
1794
+ def validate_source_map(cls, v, info: ValidationInfo):
1784
1795
  values = info.data
1785
1796
  expected = {c.address for c in values["output_concepts"]}.union(
1786
1797
  c.address for c in values["input_concepts"]
@@ -2288,8 +2299,8 @@ class EnvironmentConceptDict(dict):
2288
2299
 
2289
2300
  class ImportStatement(BaseModel):
2290
2301
  alias: str
2291
- path: str
2292
- # environment: "Environment" | None = None
2302
+ path: Path
2303
+ environment: Union["Environment", None] = None
2293
2304
  # TODO: this might result in a lot of duplication
2294
2305
  # environment:"Environment"
2295
2306
 
@@ -2324,6 +2335,9 @@ class Environment(BaseModel):
2324
2335
  version: str = Field(default_factory=get_version)
2325
2336
  cte_name_map: Dict[str, str] = Field(default_factory=dict)
2326
2337
 
2338
+ materialized_concepts: List[Concept] = Field(default_factory=list)
2339
+ _parse_count: int = 0
2340
+
2327
2341
  @classmethod
2328
2342
  def from_file(cls, path: str | Path) -> "Environment":
2329
2343
  with open(path, "r") as f:
@@ -2349,20 +2363,14 @@ class Environment(BaseModel):
2349
2363
  f.write(self.model_dump_json())
2350
2364
  return ppath
2351
2365
 
2352
- @property
2353
- def materialized_concepts(self) -> List[Concept]:
2354
- output = []
2355
- for concept in self.concepts.values():
2356
- found = False
2357
- # basic concepts are effectively materialized
2358
- # and can be found via join paths
2359
- for datasource in self.datasources.values():
2360
- if concept.address in [x.address for x in datasource.output_concepts]:
2361
- found = True
2362
- break
2363
- if found:
2364
- output.append(concept)
2365
- return output
2366
+ def gen_materialized_concepts(self) -> None:
2367
+ concrete_addresses = set()
2368
+ for datasource in self.datasources.values():
2369
+ for concept in datasource.output_concepts:
2370
+ concrete_addresses.add(concept.address)
2371
+ self.materialized_concepts = [
2372
+ c for c in self.concepts.values() if c.address in concrete_addresses
2373
+ ]
2366
2374
 
2367
2375
  def validate_concept(self, lookup: str, meta: Meta | None = None):
2368
2376
  existing: Concept = self.concepts.get(lookup) # type: ignore
@@ -2392,12 +2400,61 @@ class Environment(BaseModel):
2392
2400
 
2393
2401
  def add_import(self, alias: str, environment: Environment):
2394
2402
  self.imports[alias] = ImportStatement(
2395
- alias=alias, path=str(environment.working_path)
2403
+ alias=alias, path=Path(environment.working_path)
2396
2404
  )
2397
2405
  for key, concept in environment.concepts.items():
2398
2406
  self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
2399
2407
  for key, datasource in environment.datasources.items():
2400
2408
  self.datasources[f"{alias}.{key}"] = datasource.with_namespace(alias)
2409
+ self.gen_materialized_concepts()
2410
+ return self
2411
+
2412
+ def add_file_import(self, path: str, alias: str, env: Environment | None = None):
2413
+ from trilogy.parsing.parse_engine import ParseToObjects, PARSER
2414
+
2415
+ apath = path.split(".")
2416
+ apath[-1] = apath[-1] + ".preql"
2417
+
2418
+ target: Path = Path(self.working_path, *apath)
2419
+ if env:
2420
+ self.imports[alias] = ImportStatement(
2421
+ alias=alias, path=target, environment=env
2422
+ )
2423
+
2424
+ elif alias in self.imports:
2425
+ current = self.imports[alias]
2426
+ env = self.imports[alias].environment
2427
+ if current.path != target:
2428
+ raise ImportError(
2429
+ f"Attempted to import {target} with alias {alias} but {alias} is already imported from {current.path}"
2430
+ )
2431
+ else:
2432
+ try:
2433
+ with open(target, "r", encoding="utf-8") as f:
2434
+ text = f.read()
2435
+ nparser = ParseToObjects(
2436
+ visit_tokens=True,
2437
+ text=text,
2438
+ environment=Environment(
2439
+ working_path=target.parent,
2440
+ ),
2441
+ parse_address=str(target),
2442
+ )
2443
+ nparser.transform(PARSER.parse(text))
2444
+ except Exception as e:
2445
+ raise ImportError(
2446
+ f"Unable to import file {target.parent}, parsing error: {e}"
2447
+ )
2448
+ env = nparser.environment
2449
+ if env:
2450
+ for _, concept in env.concepts.items():
2451
+ self.add_concept(concept.with_namespace(alias))
2452
+
2453
+ for _, datasource in env.datasources.items():
2454
+ self.add_datasource(datasource.with_namespace(alias))
2455
+ imps = ImportStatement(alias=alias, path=target, environment=env)
2456
+ self.imports[alias] = imps
2457
+ return imps
2401
2458
 
2402
2459
  def parse(
2403
2460
  self, input: str, namespace: str | None = None, persist: bool = False
@@ -2448,21 +2505,22 @@ class Environment(BaseModel):
2448
2505
  from trilogy.core.environment_helpers import generate_related_concepts
2449
2506
 
2450
2507
  generate_related_concepts(concept, self)
2508
+ self.gen_materialized_concepts()
2451
2509
  return concept
2452
2510
 
2453
2511
  def add_datasource(
2454
2512
  self,
2455
2513
  datasource: Datasource,
2514
+ meta: Meta | None = None,
2456
2515
  ):
2457
- if datasource.namespace == DEFAULT_NAMESPACE:
2458
- self.datasources[datasource.name] = datasource
2459
- return datasource
2460
- if not datasource.namespace:
2516
+ if not datasource.namespace or datasource.namespace == DEFAULT_NAMESPACE:
2461
2517
  self.datasources[datasource.name] = datasource
2518
+ self.gen_materialized_concepts()
2462
2519
  return datasource
2463
2520
  self.datasources[datasource.namespace + "." + datasource.identifier] = (
2464
2521
  datasource
2465
2522
  )
2523
+ self.gen_materialized_concepts()
2466
2524
  return datasource
2467
2525
 
2468
2526
 
@@ -2495,7 +2553,7 @@ class LazyEnvironment(Environment):
2495
2553
  return super().__getattribute__(name)
2496
2554
 
2497
2555
 
2498
- class Comparison(Namespaced, SelectGrain, BaseModel):
2556
+ class Comparison(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2499
2557
  left: Union[
2500
2558
  int,
2501
2559
  str,
@@ -2547,7 +2605,7 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2547
2605
  return f"{str(self.left)} {self.operator.value} {str(self.right)}"
2548
2606
 
2549
2607
  def with_namespace(self, namespace: str):
2550
- return Comparison(
2608
+ return self.__class__(
2551
2609
  left=(
2552
2610
  self.left.with_namespace(namespace)
2553
2611
  if isinstance(self.left, Namespaced)
@@ -2562,7 +2620,7 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2562
2620
  )
2563
2621
 
2564
2622
  def with_select_grain(self, grain: Grain):
2565
- return Comparison(
2623
+ return self.__class__(
2566
2624
  left=(
2567
2625
  self.left.with_select_grain(grain)
2568
2626
  if isinstance(self.left, SelectGrain)
@@ -2581,7 +2639,9 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2581
2639
  output: List[Concept] = []
2582
2640
  if isinstance(self.left, (Concept,)):
2583
2641
  output += [self.left]
2584
- if isinstance(self.left, (Conditional, Parenthetical)):
2642
+ if isinstance(
2643
+ self.left, (Comparison, SubselectComparison, Conditional, Parenthetical)
2644
+ ):
2585
2645
  output += self.left.input
2586
2646
  if isinstance(self.left, FilterItem):
2587
2647
  output += self.left.concept_arguments
@@ -2590,7 +2650,9 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2590
2650
 
2591
2651
  if isinstance(self.right, (Concept,)):
2592
2652
  output += [self.right]
2593
- if isinstance(self.right, (Conditional, Parenthetical)):
2653
+ if isinstance(
2654
+ self.right, (Comparison, SubselectComparison, Conditional, Parenthetical)
2655
+ ):
2594
2656
  output += self.right.input
2595
2657
  if isinstance(self.right, FilterItem):
2596
2658
  output += self.right.concept_arguments
@@ -2607,8 +2669,31 @@ class Comparison(Namespaced, SelectGrain, BaseModel):
2607
2669
  return output
2608
2670
 
2609
2671
 
2672
+ class SubselectComparison(Comparison):
2673
+
2674
+ @property
2675
+ def row_arguments(self) -> List[Concept]:
2676
+ return get_concept_arguments(self.left)
2677
+
2678
+ @property
2679
+ def existence_arguments(self) -> List[Concept]:
2680
+ return get_concept_arguments(self.right)
2681
+
2682
+ def with_select_grain(self, grain: Grain):
2683
+ # there's no need to pass the select grain through to a subselect comparison
2684
+ return self.__class__(
2685
+ left=(
2686
+ self.left.with_select_grain(grain)
2687
+ if isinstance(self.left, SelectGrain)
2688
+ else self.left
2689
+ ),
2690
+ right=self.right,
2691
+ operator=self.operator,
2692
+ )
2693
+
2694
+
2610
2695
  class CaseWhen(Namespaced, SelectGrain, BaseModel):
2611
- comparison: Conditional | Comparison
2696
+ comparison: Conditional | SubselectComparison | Comparison
2612
2697
  expr: "Expr"
2613
2698
 
2614
2699
  @property
@@ -2675,7 +2760,7 @@ class CaseElse(Namespaced, SelectGrain, BaseModel):
2675
2760
  )
2676
2761
 
2677
2762
 
2678
- class Conditional(Namespaced, SelectGrain, BaseModel):
2763
+ class Conditional(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2679
2764
  left: Union[
2680
2765
  int,
2681
2766
  str,
@@ -2770,6 +2855,32 @@ class Conditional(Namespaced, SelectGrain, BaseModel):
2770
2855
  output += get_concept_arguments(self.right)
2771
2856
  return output
2772
2857
 
2858
+ @property
2859
+ def row_arguments(self) -> List[Concept]:
2860
+ output = []
2861
+ if isinstance(self.left, ConceptArgs):
2862
+ output += self.left.row_arguments
2863
+ else:
2864
+ output += get_concept_arguments(self.left)
2865
+ if isinstance(self.right, ConceptArgs):
2866
+ output += self.right.row_arguments
2867
+ else:
2868
+ output += get_concept_arguments(self.right)
2869
+ return output
2870
+
2871
+ @property
2872
+ def existence_arguments(self) -> List[Concept]:
2873
+ output = []
2874
+ if isinstance(self.left, ConceptArgs):
2875
+ output += self.left.existence_arguments
2876
+ else:
2877
+ output += get_concept_arguments(self.left)
2878
+ if isinstance(self.right, ConceptArgs):
2879
+ output += self.right.existence_arguments
2880
+ else:
2881
+ output += get_concept_arguments(self.right)
2882
+ return output
2883
+
2773
2884
 
2774
2885
  class AggregateWrapper(Namespaced, SelectGrain, BaseModel):
2775
2886
  function: Function
@@ -2813,8 +2924,8 @@ class AggregateWrapper(Namespaced, SelectGrain, BaseModel):
2813
2924
  return AggregateWrapper(function=self.function.with_select_grain(grain), by=by)
2814
2925
 
2815
2926
 
2816
- class WhereClause(Namespaced, SelectGrain, BaseModel):
2817
- conditional: Union[Comparison, Conditional, "Parenthetical"]
2927
+ class WhereClause(ConceptArgs, Namespaced, SelectGrain, BaseModel):
2928
+ conditional: Union[SubselectComparison, Comparison, Conditional, "Parenthetical"]
2818
2929
 
2819
2930
  @property
2820
2931
  def input(self) -> List[Concept]:
@@ -2824,6 +2935,14 @@ class WhereClause(Namespaced, SelectGrain, BaseModel):
2824
2935
  def concept_arguments(self) -> List[Concept]:
2825
2936
  return self.conditional.concept_arguments
2826
2937
 
2938
+ @property
2939
+ def row_arguments(self) -> List[Concept]:
2940
+ return self.conditional.row_arguments
2941
+
2942
+ @property
2943
+ def existence_arguments(self) -> List[Concept]:
2944
+ return self.conditional.existence_arguments
2945
+
2827
2946
  def with_namespace(self, namespace: str) -> WhereClause:
2828
2947
  return WhereClause(conditional=self.conditional.with_namespace(namespace))
2829
2948
 
@@ -3011,7 +3130,7 @@ class RowsetItem(Namespaced, BaseModel):
3011
3130
  return [self.content]
3012
3131
 
3013
3132
 
3014
- class Parenthetical(Namespaced, SelectGrain, BaseModel):
3133
+ class Parenthetical(ConceptArgs, Namespaced, SelectGrain, BaseModel):
3015
3134
  content: "Expr"
3016
3135
 
3017
3136
  def __str__(self):
@@ -3055,6 +3174,18 @@ class Parenthetical(Namespaced, SelectGrain, BaseModel):
3055
3174
  base.append(x)
3056
3175
  return base
3057
3176
 
3177
+ @property
3178
+ def row_arguments(self) -> List[Concept]:
3179
+ if isinstance(self.content, ConceptArgs):
3180
+ return self.content.row_arguments
3181
+ return self.concept_arguments
3182
+
3183
+ @property
3184
+ def existence_arguments(self) -> List[Concept]:
3185
+ if isinstance(self.content, ConceptArgs):
3186
+ return self.content.existence_arguments
3187
+ return self.concept_arguments
3188
+
3058
3189
  @property
3059
3190
  def input(self):
3060
3191
  base = []
@@ -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
 
@@ -316,11 +317,12 @@ def generate_node(
316
317
  return gen_basic_node(
317
318
  concept, local_optional, environment, g, depth + 1, source_concepts, history
318
319
  )
320
+
319
321
  elif concept.derivation == PurposeLineage.ROOT:
320
322
  logger.info(
321
323
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating select node with optional {[x.address for x in local_optional]}"
322
324
  )
323
- return gen_select_node(
325
+ return history.gen_select_node(
324
326
  concept,
325
327
  local_optional,
326
328
  environment,
@@ -328,6 +330,7 @@ def generate_node(
328
330
  depth + 1,
329
331
  fail_if_not_found=False,
330
332
  accept_partial=accept_partial,
333
+ accept_partial_optional=True,
331
334
  )
332
335
  else:
333
336
  raise ValueError(f"Unknown derivation {concept.derivation}")
@@ -45,21 +45,33 @@ def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
45
45
  return unique(concept.lineage.concept_arguments, "address")
46
46
 
47
47
 
48
- def resolve_filter_parent_concepts(concept: Concept) -> Tuple[Concept, List[Concept]]:
48
+ def resolve_filter_parent_concepts(
49
+ concept: Concept,
50
+ ) -> Tuple[Concept, List[Concept], List[Concept]]:
49
51
  if not isinstance(concept.lineage, FilterItem):
50
- raise ValueError
52
+ raise ValueError(
53
+ f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
54
+ )
51
55
  direct_parent = concept.lineage.content
52
- base = [direct_parent]
53
- base += concept.lineage.where.concept_arguments
56
+ base_existence = []
57
+ base_rows = [direct_parent]
58
+ base_rows += concept.lineage.where.row_arguments
59
+ base_existence += concept.lineage.where.existence_arguments
54
60
  if direct_parent.grain:
55
- base += direct_parent.grain.components_copy
61
+ base_rows += direct_parent.grain.components_copy
56
62
  if (
57
63
  isinstance(direct_parent, Concept)
58
64
  and direct_parent.purpose == Purpose.PROPERTY
59
65
  and direct_parent.keys
60
66
  ):
61
- base += direct_parent.keys
62
- return concept.lineage.content, unique(base, "address")
67
+ base_rows += direct_parent.keys
68
+ if concept.lineage.where.existence_arguments:
69
+ return (
70
+ concept.lineage.content,
71
+ unique(base_rows, "address"),
72
+ unique(base_existence, "address"),
73
+ )
74
+ return concept.lineage.content, unique(base_rows, "address"), []
63
75
 
64
76
 
65
77
  def gen_property_enrichment_node(
@@ -11,7 +11,7 @@ from trilogy.core.processing.node_generators.common import (
11
11
  resolve_filter_parent_concepts,
12
12
  )
13
13
  from trilogy.constants import logger
14
- from trilogy.core.processing.utility import padding
14
+ from trilogy.core.processing.utility import padding, unique
15
15
  from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
16
16
 
17
17
  LOGGER_PREFIX = "[GEN_FILTER_NODE]"
@@ -26,35 +26,64 @@ def gen_filter_node(
26
26
  source_concepts,
27
27
  history: History | None = None,
28
28
  ) -> MergeNode | FilterNode | None:
29
- immediate_parent, parent_concepts = resolve_filter_parent_concepts(concept)
29
+ immediate_parent, parent_row_concepts, parent_existence_concepts = (
30
+ resolve_filter_parent_concepts(concept)
31
+ )
30
32
 
31
- logger.info(f"{padding(depth)}{LOGGER_PREFIX} fetching filter node parents")
33
+ logger.info(
34
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node row parents {[x.address for x in parent_row_concepts]}"
35
+ )
36
+ core_parents = []
32
37
  parent = source_concepts(
33
- mandatory_list=parent_concepts,
38
+ mandatory_list=parent_row_concepts,
34
39
  environment=environment,
35
40
  g=g,
36
41
  depth=depth + 1,
37
42
  history=history,
38
43
  )
44
+
39
45
  if not parent:
40
46
  return None
47
+ core_parents.append(parent)
48
+ if parent_existence_concepts:
49
+ logger.info(
50
+ f"{padding(depth)}{LOGGER_PREFIX} fetching filter node existence parents {[x.address for x in parent_existence_concepts]}"
51
+ )
52
+ parent_existence = source_concepts(
53
+ mandatory_list=parent_existence_concepts,
54
+ environment=environment,
55
+ g=g,
56
+ depth=depth + 1,
57
+ history=history,
58
+ )
59
+ if not parent_existence:
60
+ return None
61
+ core_parents.append(parent_existence)
62
+
41
63
  filter_node = FilterNode(
42
- input_concepts=[immediate_parent] + parent_concepts,
43
- output_concepts=[concept, immediate_parent] + parent_concepts,
64
+ input_concepts=unique(
65
+ [immediate_parent] + parent_row_concepts + parent_existence_concepts,
66
+ "address",
67
+ ),
68
+ output_concepts=[concept, immediate_parent] + parent_row_concepts,
44
69
  environment=environment,
45
70
  g=g,
46
- parents=[parent],
71
+ parents=core_parents,
47
72
  )
48
- if not local_optional:
73
+ if not local_optional or all(
74
+ [x.address in [y.address for y in parent_row_concepts] for x in local_optional]
75
+ ):
49
76
  return filter_node
50
77
  enrich_node = source_concepts( # this fetches the parent + join keys
51
78
  # to then connect to the rest of the query
52
- mandatory_list=[immediate_parent] + parent_concepts + local_optional,
79
+ mandatory_list=[immediate_parent] + parent_row_concepts + local_optional,
53
80
  environment=environment,
54
81
  g=g,
55
82
  depth=depth + 1,
56
83
  history=history,
57
84
  )
85
+ if not enrich_node:
86
+ return filter_node
58
87
  x = MergeNode(
59
88
  input_concepts=[concept, immediate_parent] + local_optional,
60
89
  output_concepts=[
@@ -73,7 +102,7 @@ def gen_filter_node(
73
102
  left_node=enrich_node,
74
103
  right_node=filter_node,
75
104
  concepts=concept_to_relevant_joins(
76
- [immediate_parent] + parent_concepts
105
+ [immediate_parent] + parent_row_concepts
77
106
  ),
78
107
  join_type=JoinType.LEFT_OUTER,
79
108
  filter_to_mutual=False,
@@ -87,8 +87,18 @@ def gen_merge_node(
87
87
  ) -> Optional[MergeNode]:
88
88
  join_candidates: List[PathInfo] = []
89
89
  # anchor on datasources
90
+ final_all_concepts = []
91
+ # implicit_upstream = {}
92
+ for x in all_concepts:
93
+ # if x.derivation in (PurposeLineage.AGGREGATE, PurposeLineage.BASIC):
94
+ # final_all_concepts +=resolve_function_parent_concepts(x)
95
+ # elif x.derivation == PurposeLineage.FILTER:
96
+ # final_all_concepts +=resolve_filter_parent_concepts(x)
97
+ # else:
98
+ # final_all_concepts.append(x)
99
+ final_all_concepts.append(x)
90
100
  for datasource in environment.datasources.values():
91
- path = identify_ds_join_paths(all_concepts, g, datasource, accept_partial)
101
+ path = identify_ds_join_paths(final_all_concepts, g, datasource, accept_partial)
92
102
  if path and path.reduced_concepts:
93
103
  join_candidates.append(path)
94
104
  join_candidates.sort(key=lambda x: sum([len(v) for v in x.paths.values()]))