pytrilogy 0.0.1.103__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 (99) hide show
  1. {pytrilogy-0.0.1.103/pytrilogy.egg-info → pytrilogy-0.0.1.104}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/__init__.py +1 -1
  4. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/models.py +77 -26
  5. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/concept_strategies_v3.py +5 -3
  6. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/filter_node.py +2 -0
  7. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/select_node.py +275 -53
  8. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/__init__.py +54 -1
  9. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/parse_engine.py +23 -12
  10. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/LICENSE.md +0 -0
  11. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/README.md +0 -0
  12. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pyproject.toml +0 -0
  13. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/SOURCES.txt +0 -0
  14. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/dependency_links.txt +0 -0
  15. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/entry_points.txt +0 -0
  16. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/requires.txt +0 -0
  17. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/pytrilogy.egg-info/top_level.txt +0 -0
  18. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/setup.cfg +0 -0
  19. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/setup.py +0 -0
  20. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_declarations.py +0 -0
  21. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_derived_concepts.py +0 -0
  22. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_discovery_nodes.py +0 -0
  23. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_environment.py +0 -0
  24. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_functions.py +0 -0
  25. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_imports.py +0 -0
  26. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_metadata.py +0 -0
  27. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_models.py +0 -0
  28. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_multi_join_assignments.py +0 -0
  29. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_parsing.py +0 -0
  30. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_partial_handling.py +0 -0
  31. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_query_processing.py +0 -0
  32. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_select.py +0 -0
  33. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_statements.py +0 -0
  34. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_undefined_concept.py +0 -0
  35. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/tests/test_where_clause.py +0 -0
  36. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/compiler.py +0 -0
  37. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/constants.py +0 -0
  38. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/__init__.py +0 -0
  39. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/constants.py +0 -0
  40. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/enums.py +0 -0
  41. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/env_processor.py +0 -0
  42. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/environment_helpers.py +0 -0
  43. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/ergonomics.py +0 -0
  44. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/exceptions.py +0 -0
  45. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/functions.py +0 -0
  46. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/graph_models.py +0 -0
  47. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/internal.py +0 -0
  48. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/__init__.py +0 -0
  49. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/graph_utils.py +0 -0
  50. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/__init__.py +0 -0
  51. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  52. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/common.py +0 -0
  53. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/concept_merge.py +0 -0
  54. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_node.py +0 -0
  55. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  56. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/merge_node.py +0 -0
  57. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  58. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  59. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  60. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/node_generators/window_node.py +0 -0
  61. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/base_node.py +0 -0
  62. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/filter_node.py +0 -0
  63. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/group_node.py +0 -0
  64. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/merge_node.py +0 -0
  65. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  66. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  67. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/nodes/window_node.py +0 -0
  68. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/processing/utility.py +0 -0
  69. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/core/query_processor.py +0 -0
  70. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/__init__.py +0 -0
  71. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/base.py +0 -0
  72. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/bigquery.py +0 -0
  73. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/common.py +0 -0
  74. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/config.py +0 -0
  75. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/duckdb.py +0 -0
  76. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/enums.py +0 -0
  77. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/postgres.py +0 -0
  78. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/presto.py +0 -0
  79. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/snowflake.py +0 -0
  80. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/dialect/sql_server.py +0 -0
  81. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/docs/__init__.py +0 -0
  82. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/engine.py +0 -0
  83. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/executor.py +0 -0
  84. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/__init__.py +0 -0
  85. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/base_hook.py +0 -0
  86. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/graph_hook.py +0 -0
  87. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/hooks/query_debugger.py +0 -0
  88. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/metadata/__init__.py +0 -0
  89. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parser.py +0 -0
  90. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/__init__.py +0 -0
  91. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/common.py +0 -0
  92. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/config.py +0 -0
  93. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/exceptions.py +0 -0
  94. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/helpers.py +0 -0
  95. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/parsing/render.py +0 -0
  96. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/py.typed +0 -0
  97. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/scripts/__init__.py +0 -0
  98. {pytrilogy-0.0.1.103 → pytrilogy-0.0.1.104}/trilogy/scripts/trilogy.py +0 -0
  99. {pytrilogy-0.0.1.103 → 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.103
3
+ Version: 0.0.1.104
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.104
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.104"
7
7
 
8
8
  __all__ = ["parse", "Executor", "Dialects", "Environment"]
@@ -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"]
@@ -2288,8 +2292,8 @@ class EnvironmentConceptDict(dict):
2288
2292
 
2289
2293
  class ImportStatement(BaseModel):
2290
2294
  alias: str
2291
- path: str
2292
- # environment: "Environment" | None = None
2295
+ path: Path
2296
+ environment: Union["Environment", None] = None
2293
2297
  # TODO: this might result in a lot of duplication
2294
2298
  # environment:"Environment"
2295
2299
 
@@ -2324,6 +2328,9 @@ class Environment(BaseModel):
2324
2328
  version: str = Field(default_factory=get_version)
2325
2329
  cte_name_map: Dict[str, str] = Field(default_factory=dict)
2326
2330
 
2331
+ materialized_concepts: List[Concept] = Field(default_factory=list)
2332
+ _parse_count: int = 0
2333
+
2327
2334
  @classmethod
2328
2335
  def from_file(cls, path: str | Path) -> "Environment":
2329
2336
  with open(path, "r") as f:
@@ -2349,20 +2356,14 @@ class Environment(BaseModel):
2349
2356
  f.write(self.model_dump_json())
2350
2357
  return ppath
2351
2358
 
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
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
+ ]
2366
2367
 
2367
2368
  def validate_concept(self, lookup: str, meta: Meta | None = None):
2368
2369
  existing: Concept = self.concepts.get(lookup) # type: ignore
@@ -2392,12 +2393,61 @@ class Environment(BaseModel):
2392
2393
 
2393
2394
  def add_import(self, alias: str, environment: Environment):
2394
2395
  self.imports[alias] = ImportStatement(
2395
- alias=alias, path=str(environment.working_path)
2396
+ alias=alias, path=Path(environment.working_path)
2396
2397
  )
2397
2398
  for key, concept in environment.concepts.items():
2398
2399
  self.concepts[f"{alias}.{key}"] = concept.with_namespace(alias)
2399
2400
  for key, datasource in environment.datasources.items():
2400
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
2401
2451
 
2402
2452
  def parse(
2403
2453
  self, input: str, namespace: str | None = None, persist: bool = False
@@ -2448,21 +2498,22 @@ class Environment(BaseModel):
2448
2498
  from trilogy.core.environment_helpers import generate_related_concepts
2449
2499
 
2450
2500
  generate_related_concepts(concept, self)
2501
+ self.gen_materialized_concepts()
2451
2502
  return concept
2452
2503
 
2453
2504
  def add_datasource(
2454
2505
  self,
2455
2506
  datasource: Datasource,
2507
+ meta: Meta | None = None,
2456
2508
  ):
2457
- if datasource.namespace == DEFAULT_NAMESPACE:
2458
- self.datasources[datasource.name] = datasource
2459
- return datasource
2460
- if not datasource.namespace:
2509
+ if not datasource.namespace or datasource.namespace == DEFAULT_NAMESPACE:
2461
2510
  self.datasources[datasource.name] = datasource
2511
+ self.gen_materialized_concepts()
2462
2512
  return datasource
2463
2513
  self.datasources[datasource.namespace + "." + datasource.identifier] = (
2464
2514
  datasource
2465
2515
  )
2516
+ self.gen_materialized_concepts()
2466
2517
  return datasource
2467
2518
 
2468
2519
 
@@ -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
 
@@ -320,7 +321,7 @@ def generate_node(
320
321
  logger.info(
321
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=[
@@ -2,7 +2,13 @@ from itertools import combinations
2
2
  from typing import List, Optional
3
3
 
4
4
  from trilogy.core.enums import PurposeLineage
5
- from trilogy.core.models import Concept, Environment, Grain, LooseConceptList
5
+ from trilogy.core.models import (
6
+ Concept,
7
+ Environment,
8
+ Grain,
9
+ LooseConceptList,
10
+ Datasource,
11
+ )
6
12
  from trilogy.core.processing.nodes import (
7
13
  StrategyNode,
8
14
  SelectNode,
@@ -15,10 +21,211 @@ import networkx as nx
15
21
  from trilogy.core.graph_models import concept_to_node, datasource_to_node
16
22
  from trilogy.constants import logger
17
23
  from trilogy.core.processing.utility import padding
24
+ from dataclasses import dataclass
18
25
 
19
26
  LOGGER_PREFIX = "[GEN_SELECT_NODE]"
20
27
 
21
28
 
29
+ @dataclass
30
+ class DatasourceMatch:
31
+ key: str
32
+ datasource: Datasource
33
+ matched: LooseConceptList
34
+ partial: LooseConceptList
35
+
36
+ def __repr__(self):
37
+ return f"DatasourceMatch({self.key}, {self.datasource.identifier}, {str(self.matched)}, {str(self.partial)})"
38
+
39
+
40
+ def dm_to_strategy_node(
41
+ dm: DatasourceMatch,
42
+ target_grain: Grain,
43
+ environment: Environment,
44
+ g: nx.DiGraph,
45
+ depth: int,
46
+ accept_partial: bool = False,
47
+ ) -> StrategyNode:
48
+ datasource = dm.datasource
49
+ if target_grain and target_grain.issubset(datasource.grain):
50
+ if all([x in dm.matched for x in target_grain.components]):
51
+ force_group = False
52
+ # if we are not returning the grain
53
+ # we have to group
54
+ else:
55
+ logger.info(
56
+ f"{padding(depth)}{LOGGER_PREFIX} not all grain components are in output {str(dm.matched)}, group to actual grain"
57
+ )
58
+ force_group = True
59
+ elif all([x in dm.matched for x in datasource.grain.components]):
60
+ logger.info(
61
+ f"{padding(depth)}{LOGGER_PREFIX} query output includes all grain components, no reason to group further"
62
+ )
63
+ force_group = False
64
+ else:
65
+ logger.info(
66
+ f"{padding(depth)}{LOGGER_PREFIX} target grain is not subset of datasource grain {datasource.grain}, required to group"
67
+ )
68
+ force_group = True
69
+ bcandidate: StrategyNode = SelectNode(
70
+ input_concepts=[c.concept for c in datasource.columns],
71
+ output_concepts=dm.matched.concepts,
72
+ environment=environment,
73
+ g=g,
74
+ parents=[],
75
+ depth=depth,
76
+ partial_concepts=dm.partial.concepts,
77
+ accept_partial=accept_partial,
78
+ datasource=datasource,
79
+ grain=Grain(components=dm.matched.concepts),
80
+ )
81
+ # we need to nest the group node one further
82
+ if force_group is True:
83
+ candidate: StrategyNode = GroupNode(
84
+ output_concepts=dm.matched.concepts,
85
+ input_concepts=dm.matched.concepts,
86
+ environment=environment,
87
+ g=g,
88
+ parents=[bcandidate],
89
+ depth=depth,
90
+ partial_concepts=bcandidate.partial_concepts,
91
+ )
92
+ else:
93
+ candidate = bcandidate
94
+ return candidate
95
+
96
+
97
+ def gen_select_nodes_from_tables_v2(
98
+ mandatory_concept: Concept,
99
+ all_concepts: List[Concept],
100
+ g: nx.DiGraph,
101
+ environment: Environment,
102
+ depth: int,
103
+ target_grain: Grain,
104
+ accept_partial: bool = False,
105
+ ) -> tuple[bool, list[Concept], list[StrategyNode]]:
106
+ # if we have only constants
107
+ # we don't need a table
108
+ # so verify nothing, select node will render
109
+ all_lcl = LooseConceptList(concepts=all_concepts)
110
+ if all([c.derivation == PurposeLineage.CONSTANT for c in all_lcl]):
111
+ logger.info(
112
+ f"{padding(depth)}{LOGGER_PREFIX} All concepts {[x.address for x in all_lcl]} are constants, returning constant node"
113
+ )
114
+ return (
115
+ True,
116
+ all_lcl.concepts,
117
+ [
118
+ ConstantNode(
119
+ output_concepts=all_lcl.concepts,
120
+ input_concepts=[],
121
+ environment=environment,
122
+ g=g,
123
+ parents=[],
124
+ depth=depth,
125
+ # no partial for constants
126
+ partial_concepts=[],
127
+ force_group=False,
128
+ )
129
+ ],
130
+ )
131
+ # otherwise, we need to look for a table
132
+ nodes_to_find = [concept_to_node(x.with_default_grain()) for x in all_lcl.concepts]
133
+ matches: dict[str, DatasourceMatch] = {}
134
+ for k, datasource in environment.datasources.items():
135
+ matched = []
136
+ matched_paths = []
137
+ for idx, req_concept in enumerate(nodes_to_find):
138
+ try:
139
+ path = nx.shortest_path(
140
+ g,
141
+ source=datasource_to_node(datasource),
142
+ target=req_concept,
143
+ )
144
+ ds_valid = (
145
+ sum(
146
+ [
147
+ 1 if g.nodes[node]["type"] == "datasource" else 0
148
+ for node in path
149
+ ]
150
+ )
151
+ == 1
152
+ )
153
+ address_valid = (
154
+ sum(
155
+ [
156
+ (
157
+ 1
158
+ if g.nodes[node]["type"] == "concept"
159
+ and g.nodes[node]["concept"].address
160
+ != all_lcl.concepts[idx].address
161
+ else 0
162
+ )
163
+ for node in path
164
+ ]
165
+ )
166
+ == 0
167
+ )
168
+ if ds_valid and address_valid:
169
+ matched_paths.append(path)
170
+ matched.append(all_lcl.concepts[idx])
171
+ except nx.NodeNotFound:
172
+ continue
173
+ except nx.exception.NetworkXNoPath:
174
+ continue
175
+ dm = DatasourceMatch(
176
+ key=k,
177
+ datasource=datasource,
178
+ matched=LooseConceptList(concepts=matched),
179
+ partial=LooseConceptList(
180
+ concepts=[
181
+ c.concept
182
+ for c in datasource.columns
183
+ if not c.is_complete and c.concept.address in all_lcl
184
+ ]
185
+ ),
186
+ )
187
+ if not matched:
188
+ continue
189
+ if mandatory_concept.address not in dm.matched:
190
+ continue
191
+ if not accept_partial and dm.partial.addresses:
192
+ continue
193
+ matches[k] = dm
194
+ found: set[str] = set()
195
+ all_found = False
196
+ all_checked = False
197
+ to_return: list[StrategyNode] = []
198
+ if not matches:
199
+ return False, [], []
200
+ while not all_found and not all_checked:
201
+ final_key: str = max(
202
+ matches,
203
+ key=lambda x: len(
204
+ [m for m in matches[x].matched.addresses if m not in found]
205
+ )
206
+ - 0.1 * len(matches[x].partial.addresses),
207
+ )
208
+ final: DatasourceMatch = matches[final_key]
209
+ candidate = dm_to_strategy_node(
210
+ final,
211
+ target_grain=Grain(
212
+ components=[
213
+ x for x in target_grain.components if x.address in final.matched
214
+ ]
215
+ ),
216
+ environment=environment,
217
+ g=g,
218
+ depth=depth,
219
+ accept_partial=accept_partial,
220
+ )
221
+ to_return.append(candidate)
222
+ del matches[final_key]
223
+ found = found.union(final.matched.addresses)
224
+ all_found = all_lcl.addresses.issubset(found)
225
+ all_checked = len(matches) == 0
226
+ return all_found, [x for x in all_concepts if x.address in found], to_return
227
+
228
+
22
229
  def gen_select_node_from_table(
23
230
  target_concept: Concept,
24
231
  all_concepts: List[Concept],
@@ -166,58 +373,15 @@ def gen_select_node_from_table(
166
373
  return candidates[final]
167
374
 
168
375
 
169
- def gen_select_node(
170
- concept: Concept,
376
+ def gen_select_nodes_from_tables(
171
377
  local_optional: List[Concept],
172
- environment: Environment,
173
- g,
174
378
  depth: int,
175
- accept_partial: bool = False,
176
- fail_if_not_found: bool = True,
177
- accept_partial_optional: bool = True,
178
- target_grain: Grain | None = None,
179
- ) -> StrategyNode | None:
180
- all_concepts = [concept] + local_optional
181
- all_lcl = LooseConceptList(concepts=all_concepts)
182
- materialized_lcl = LooseConceptList(
183
- concepts=[
184
- x
185
- for x in all_concepts
186
- if x.address in [z.address for z in environment.materialized_concepts]
187
- or x.derivation == PurposeLineage.CONSTANT
188
- ]
189
- )
190
- if not target_grain:
191
- target_grain = Grain()
192
- for ac in all_concepts:
193
- target_grain += ac.grain
194
- if materialized_lcl != all_lcl:
195
- logger.info(
196
- f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concept.address} "
197
- f" as it + optional (looking for all {all_lcl}) includes non-materialized concepts {all_lcl.difference(materialized_lcl)} vs materialized: {materialized_lcl}"
198
- )
199
- if fail_if_not_found:
200
- raise NoDatasourceException(f"No datasource exists for {concept}")
201
- return None
202
-
203
- ds: StrategyNode | None = None
204
-
205
- # attempt to select all concepts from table
206
- ds = gen_select_node_from_table(
207
- concept,
208
- [concept] + local_optional,
209
- g=g,
210
- environment=environment,
211
- depth=depth,
212
- accept_partial=accept_partial,
213
- target_grain=target_grain,
214
- )
215
- if ds:
216
- logger.info(
217
- f"{padding(depth)}{LOGGER_PREFIX} Found select node with all target concepts, force group is {ds.force_group}, target grain {target_grain}"
218
- )
219
- return ds
220
- # if we cannot find a match
379
+ concept: Concept,
380
+ environment: Environment,
381
+ g: nx.DiGraph,
382
+ accept_partial: bool,
383
+ all_concepts: List[Concept],
384
+ ) -> tuple[bool, list[Concept], list[StrategyNode]]:
221
385
  parents: List[StrategyNode] = []
222
386
  found: List[Concept] = []
223
387
  logger.info(
@@ -238,7 +402,7 @@ def gen_select_node(
238
402
  )
239
403
  if not ds:
240
404
  unreachable.append(opt_con.address)
241
- # actual search
405
+ all_found = False
242
406
  for x in reversed(range(1, len(local_optional) + 1)):
243
407
  if all_found:
244
408
  break
@@ -275,6 +439,64 @@ def gen_select_node(
275
439
  f"{padding(depth)}{LOGGER_PREFIX} found all optional {[c.address for c in local_optional]}"
276
440
  )
277
441
  all_found = True
442
+ return all_found, found, parents
443
+
444
+
445
+ def gen_select_node(
446
+ concept: Concept,
447
+ local_optional: List[Concept],
448
+ environment: Environment,
449
+ g,
450
+ depth: int,
451
+ accept_partial: bool = False,
452
+ fail_if_not_found: bool = True,
453
+ accept_partial_optional: bool = True,
454
+ target_grain: Grain | None = None,
455
+ ) -> StrategyNode | None:
456
+ all_concepts = [concept] + local_optional
457
+ all_lcl = LooseConceptList(concepts=all_concepts)
458
+ materialized_lcl = LooseConceptList(
459
+ concepts=[
460
+ x
461
+ for x in all_concepts
462
+ if x.address in [z.address for z in environment.materialized_concepts]
463
+ or x.derivation == PurposeLineage.CONSTANT
464
+ ]
465
+ )
466
+ if not target_grain:
467
+ target_grain = Grain()
468
+ for ac in all_concepts:
469
+ target_grain += ac.grain
470
+ if materialized_lcl != all_lcl:
471
+ logger.info(
472
+ f"{padding(depth)}{LOGGER_PREFIX} Skipping select node generation for {concept.address} "
473
+ f" as it + optional (looking for all {all_lcl}) includes non-materialized concepts {all_lcl.difference(materialized_lcl)} vs materialized: {materialized_lcl}"
474
+ )
475
+ if fail_if_not_found:
476
+ raise NoDatasourceException(f"No datasource exists for {concept}")
477
+ return None
478
+
479
+ ds: StrategyNode | None = None
480
+
481
+ # attempt to select all concepts from table
482
+ ds = gen_select_node_from_table(
483
+ concept,
484
+ [concept] + local_optional,
485
+ g=g,
486
+ environment=environment,
487
+ depth=depth,
488
+ accept_partial=accept_partial,
489
+ target_grain=target_grain,
490
+ )
491
+ if ds:
492
+ logger.info(
493
+ f"{padding(depth)}{LOGGER_PREFIX} Found select node with all target concepts, force group is {ds.force_group}, target grain {target_grain}"
494
+ )
495
+ return ds
496
+ # if we cannot find a match
497
+ all_found, found, parents = gen_select_nodes_from_tables_v2(
498
+ concept, all_concepts, g, environment, depth, target_grain, accept_partial
499
+ )
278
500
  if parents and (all_found or accept_partial_optional):
279
501
  if all_found:
280
502
  logger.info(
@@ -282,7 +504,7 @@ def gen_select_node(
282
504
  )
283
505
  else:
284
506
  logger.info(
285
- f"{padding(depth)}{LOGGER_PREFIX} found some optional {[c.address for c in found]}, returning"
507
+ f"{padding(depth)}{LOGGER_PREFIX} found some optional, returning"
286
508
  )
287
509
  all_partial = [
288
510
  c
@@ -6,11 +6,12 @@ from .window_node import WindowNode
6
6
  from .base_node import StrategyNode, NodeJoin
7
7
  from .unnest_node import UnnestNode
8
8
  from pydantic import BaseModel, Field, ConfigDict
9
- from trilogy.core.models import Concept
9
+ from trilogy.core.models import Concept, Environment
10
10
 
11
11
 
12
12
  class History(BaseModel):
13
13
  history: dict[str, StrategyNode | None] = Field(default_factory=dict)
14
+ select_history: dict[str, StrategyNode | None] = Field(default_factory=dict)
14
15
  started: set[str] = Field(default_factory=set)
15
16
  model_config = ConfigDict(arbitrary_types_allowed=True)
16
17
 
@@ -60,6 +61,58 @@ class History(BaseModel):
60
61
  in self.started
61
62
  )
62
63
 
64
+ def _select_concepts_to_lookup(
65
+ self,
66
+ main: Concept,
67
+ search: list[Concept],
68
+ accept_partial: bool,
69
+ fail_if_not_found: bool,
70
+ accept_partial_optional: bool,
71
+ ) -> str:
72
+ return (
73
+ str(main.address)
74
+ + "|"
75
+ + "-".join([c.address for c in search])
76
+ + str(accept_partial)
77
+ + str(fail_if_not_found)
78
+ + str(accept_partial_optional)
79
+ )
80
+
81
+ def gen_select_node(
82
+ self,
83
+ concept: Concept,
84
+ local_optional: list[Concept],
85
+ environment: Environment,
86
+ g,
87
+ depth: int,
88
+ fail_if_not_found: bool = False,
89
+ accept_partial: bool = False,
90
+ accept_partial_optional: bool = False,
91
+ ) -> StrategyNode | None:
92
+ from trilogy.core.processing.node_generators.select_node import gen_select_node
93
+
94
+ fingerprint = self._select_concepts_to_lookup(
95
+ concept,
96
+ local_optional,
97
+ accept_partial,
98
+ fail_if_not_found,
99
+ accept_partial_optional,
100
+ )
101
+ if fingerprint in self.select_history:
102
+ return self.select_history[fingerprint]
103
+ gen = gen_select_node(
104
+ concept,
105
+ local_optional,
106
+ environment,
107
+ g,
108
+ depth + 1,
109
+ fail_if_not_found=fail_if_not_found,
110
+ accept_partial=accept_partial,
111
+ accept_partial_optional=accept_partial_optional,
112
+ )
113
+ self.select_history[fingerprint] = gen
114
+ return gen
115
+
63
116
 
64
117
  __all__ = [
65
118
  "FilterNode",
@@ -9,6 +9,7 @@ from lark.exceptions import (
9
9
  UnexpectedToken,
10
10
  VisitError,
11
11
  )
12
+ from pathlib import Path
12
13
  from lark.tree import Meta
13
14
  from pydantic import ValidationError
14
15
  from trilogy.core.internal import INTERNAL_NAMESPACE, ALL_ROWS_CONCEPT
@@ -466,25 +467,34 @@ class ParseToObjects(Transformer):
466
467
  text,
467
468
  environment: Environment,
468
469
  parse_address: str | None = None,
469
- parsed: dict | None = None,
470
+ parsed: dict[str, "ParseToObjects"] | None = None,
470
471
  ):
471
472
  Transformer.__init__(self, visit_tokens)
472
473
  self.text = text
473
474
  self.environment: Environment = environment
474
- self.imported: set[str] = set()
475
475
  self.parse_address = parse_address or "root"
476
476
  self.parsed: dict[str, ParseToObjects] = parsed if parsed else {}
477
477
  # we do a second pass to pick up circular dependencies
478
478
  # after initial parsing
479
479
  self.pass_count = 1
480
+ self._results_stash = None
481
+
482
+ def transform(self, tree):
483
+ results = super().transform(tree)
484
+ self._results_stash = results
485
+ self.environment._parse_count += 1
486
+ return results
480
487
 
481
488
  def hydrate_missing(self):
482
489
  self.pass_count = 2
483
490
  for k, v in self.parsed.items():
491
+
484
492
  if v.pass_count == 2:
485
493
  continue
486
494
  v.hydrate_missing()
487
495
  self.environment.concepts.fail_on_missing = True
496
+ # if not self.environment.concepts.undefined:
497
+ # return self._results_stash
488
498
  reparsed = self.transform(PARSER.parse(self.text))
489
499
  self.environment.concepts.undefined = {}
490
500
  return reparsed
@@ -932,7 +942,7 @@ class ParseToObjects(Transformer):
932
942
  )
933
943
  for column in columns:
934
944
  column.concept = column.concept.with_grain(datasource.grain)
935
- self.environment.datasources[datasource.identifier] = datasource
945
+ self.environment.add_datasource(datasource, meta=meta)
936
946
  return datasource
937
947
 
938
948
  @v_args(meta=True)
@@ -1046,12 +1056,11 @@ class ParseToObjects(Transformer):
1046
1056
  self.environment.add_concept(new, meta=meta)
1047
1057
  return merge
1048
1058
 
1049
- def import_statement(self, args: list[str]):
1059
+ def import_statement(self, args: list[str]) -> ImportStatement:
1050
1060
  alias = args[-1]
1051
1061
  path = args[0].split(".")
1052
1062
 
1053
1063
  target = join(self.environment.working_path, *path) + ".preql"
1054
- self.imported.add(target)
1055
1064
  if target in self.parsed:
1056
1065
  nparser = self.parsed[target]
1057
1066
  else:
@@ -1070,21 +1079,23 @@ class ParseToObjects(Transformer):
1070
1079
  )
1071
1080
  nparser.transform(PARSER.parse(text))
1072
1081
  self.parsed[target] = nparser
1082
+ # add the parsed objects of the import in
1083
+ self.parsed = {**self.parsed, **nparser.parsed}
1073
1084
  except Exception as e:
1074
1085
  raise ImportError(
1075
1086
  f"Unable to import file {dirname(target)}, parsing error: {e}"
1076
1087
  )
1077
1088
 
1078
- for key, concept in nparser.environment.concepts.items():
1079
- # self.environment.concepts[f"{alias}.{key}"] = concept.with_namespace(new_namespace)
1089
+ for _, concept in nparser.environment.concepts.items():
1080
1090
  self.environment.add_concept(concept.with_namespace(alias))
1081
1091
 
1082
- for key, datasource in nparser.environment.datasources.items():
1092
+ for _, datasource in nparser.environment.datasources.items():
1083
1093
  self.environment.add_datasource(datasource.with_namespace(alias))
1084
- # self.environment.datasources[f"{alias}.{key}"] = datasource.with_namespace(new_namespace)
1085
-
1086
- self.environment.imports[alias] = ImportStatement(alias=alias, path=args[0])
1087
- return None
1094
+ imps = ImportStatement(
1095
+ alias=alias, path=Path(args[0]), environment=nparser.environment
1096
+ )
1097
+ self.environment.imports[alias] = imps
1098
+ return imps
1088
1099
 
1089
1100
  @v_args(meta=True)
1090
1101
  def show_category(self, meta: Meta, args) -> ShowCategory:
File without changes
File without changes
File without changes
File without changes