pytrilogy 0.0.2.55__tar.gz → 0.0.2.56__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 (114) hide show
  1. {pytrilogy-0.0.2.55/pytrilogy.egg-info → pytrilogy-0.0.2.56}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_select.py +33 -1
  4. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/environment_helpers.py +16 -5
  6. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/models.py +94 -2
  7. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/concept_strategies_v3.py +1 -0
  8. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/filter_node.py +5 -1
  9. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/base_node.py +2 -0
  10. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/merge_node.py +0 -3
  11. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/utility.py +45 -14
  12. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/query_processor.py +1 -1
  13. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/parse_engine.py +3 -71
  14. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/LICENSE.md +0 -0
  15. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/README.md +0 -0
  16. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pyproject.toml +0 -0
  17. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/SOURCES.txt +0 -0
  18. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/dependency_links.txt +0 -0
  19. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/entry_points.txt +0 -0
  20. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/requires.txt +0 -0
  21. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/pytrilogy.egg-info/top_level.txt +0 -0
  22. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/setup.cfg +0 -0
  23. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/setup.py +0 -0
  24. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_datatypes.py +0 -0
  25. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_declarations.py +0 -0
  26. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_derived_concepts.py +0 -0
  27. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_discovery_nodes.py +0 -0
  28. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_enums.py +0 -0
  29. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_environment.py +0 -0
  30. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_executor.py +0 -0
  31. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_functions.py +0 -0
  32. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_imports.py +0 -0
  33. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_metadata.py +0 -0
  34. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_models.py +0 -0
  35. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_multi_join_assignments.py +0 -0
  36. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_parse_engine.py +0 -0
  37. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_parsing.py +0 -0
  38. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_partial_handling.py +0 -0
  39. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_query_processing.py +0 -0
  40. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_show.py +0 -0
  41. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_statements.py +0 -0
  42. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_undefined_concept.py +0 -0
  43. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/tests/test_where_clause.py +0 -0
  44. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/compiler.py +0 -0
  45. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/constants.py +0 -0
  46. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/__init__.py +0 -0
  47. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/constants.py +0 -0
  48. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/enums.py +0 -0
  49. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/env_processor.py +0 -0
  50. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/ergonomics.py +0 -0
  51. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/exceptions.py +0 -0
  52. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/functions.py +0 -0
  53. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/graph_models.py +0 -0
  54. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/internal.py +0 -0
  55. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimization.py +0 -0
  56. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/__init__.py +0 -0
  57. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/base_optimization.py +0 -0
  58. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/inline_constant.py +0 -0
  59. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/inline_datasource.py +0 -0
  60. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  61. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/__init__.py +0 -0
  62. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/graph_utils.py +0 -0
  63. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/__init__.py +0 -0
  64. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  65. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/common.py +0 -0
  66. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_node.py +0 -0
  67. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  68. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  69. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  70. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  71. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  72. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  73. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  74. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/select_node.py +0 -0
  75. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/union_node.py +0 -0
  76. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  77. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/node_generators/window_node.py +0 -0
  78. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/__init__.py +0 -0
  79. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/filter_node.py +0 -0
  80. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/group_node.py +0 -0
  81. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  82. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/union_node.py +0 -0
  83. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  84. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/core/processing/nodes/window_node.py +0 -0
  85. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/__init__.py +0 -0
  86. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/base.py +0 -0
  87. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/bigquery.py +0 -0
  88. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/common.py +0 -0
  89. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/config.py +0 -0
  90. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/duckdb.py +0 -0
  91. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/enums.py +0 -0
  92. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/postgres.py +0 -0
  93. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/presto.py +0 -0
  94. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/snowflake.py +0 -0
  95. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/dialect/sql_server.py +0 -0
  96. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/engine.py +0 -0
  97. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/executor.py +0 -0
  98. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/hooks/__init__.py +0 -0
  99. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/hooks/base_hook.py +0 -0
  100. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/hooks/graph_hook.py +0 -0
  101. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/hooks/query_debugger.py +0 -0
  102. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/metadata/__init__.py +0 -0
  103. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parser.py +0 -0
  104. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/__init__.py +0 -0
  105. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/common.py +0 -0
  106. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/config.py +0 -0
  107. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/exceptions.py +0 -0
  108. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/helpers.py +0 -0
  109. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/render.py +0 -0
  110. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/parsing/trilogy.lark +0 -0
  111. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/py.typed +0 -0
  112. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/scripts/__init__.py +0 -0
  113. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/scripts/trilogy.py +0 -0
  114. {pytrilogy-0.0.2.55 → pytrilogy-0.0.2.56}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.55
3
+ Version: 0.0.2.56
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.2.55
3
+ Version: 0.0.2.56
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
1
  # from trilogy.compiler import compile
2
- from trilogy import Dialects
2
+ from trilogy import Dialects, Environment
3
3
  from trilogy.core.models import Grain, SelectStatement
4
4
  from trilogy.core.query_processor import process_query
5
5
  from trilogy.dialect.bigquery import BigqueryDialect
@@ -176,3 +176,35 @@ select id + 2 as three;
176
176
 
177
177
  result = Dialects.DUCK_DB.default_executor(environment=env).execute_text(q1)[-1]
178
178
  assert result.fetchone().three == 3
179
+
180
+
181
+ def test_select_from_components():
182
+ env = Environment()
183
+ q1 = """
184
+
185
+ key id int;
186
+ property id.class int;
187
+ property id.name string;
188
+
189
+ select
190
+ class,
191
+ upper(id.name)-> upper_name,
192
+ count(id) ->class_id_count,
193
+ ;
194
+ """
195
+ env, statements = env.parse(q1)
196
+
197
+ select: SelectStatement = statements[-1]
198
+
199
+ assert select.grain.components == {"local.class", "local.upper_name"}
200
+ assert select.local_concepts["local.class_id_count"].grain.components == {
201
+ "local.class",
202
+ "local.upper_name",
203
+ }
204
+
205
+ # SelectStatement.from_inputs(
206
+ # environment=env,
207
+ # selection=[SelectItem(concept=env.concepts["id"]),
208
+ # SelectItem(concept=env.concepts["id.class"])],
209
+ # input_components=[],
210
+ # )
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.2.55"
7
+ __version__ = "0.0.2.56"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -11,12 +11,23 @@ from trilogy.core.models import (
11
11
  )
12
12
  from trilogy.parsing.common import Meta, arg_to_datatype, process_function_args
13
13
 
14
+ FUNCTION_DESCRIPTION_MAPS = {
15
+ FunctionType.DATE: "The date part of a timestamp/date. Integer, 0-31 depending on month.",
16
+ FunctionType.MONTH: "The month part of a timestamp/date. Integer, 1-12.",
17
+ FunctionType.YEAR: "The year part of a timestamp/date. Integer.",
18
+ FunctionType.QUARTER: "The quarter part of a timestamp/date. Integer, 1-4.",
19
+ FunctionType.DAY_OF_WEEK: "The day of the week part of a timestamp/date. Integer, 0-6.",
20
+ FunctionType.HOUR: "The hour part of a timestamp. Integer, 0-23.",
21
+ FunctionType.MINUTE: "The minute part of a timestamp. Integer, 0-59.",
22
+ FunctionType.SECOND: "The second part of a timestamp. Integer, 0-59.",
23
+ }
24
+
14
25
 
15
26
  def generate_date_concepts(concept: Concept, environment: Environment):
16
27
  if concept.metadata and concept.metadata.description:
17
28
  base_description = concept.metadata.description
18
29
  else:
19
- base_description = f"a {concept.datatype.value}"
30
+ base_description = f"a {concept.address}"
20
31
  if concept.metadata and concept.metadata.line_number:
21
32
  base_line_number = concept.metadata.line_number
22
33
  else:
@@ -54,7 +65,7 @@ def generate_date_concepts(concept: Concept, environment: Environment):
54
65
  concept.address,
55
66
  ),
56
67
  metadata=Metadata(
57
- description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
68
+ description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}. ",
58
69
  line_number=base_line_number,
59
70
  concept_source=ConceptSource.AUTO_DERIVED,
60
71
  ),
@@ -68,7 +79,7 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
68
79
  if concept.metadata and concept.metadata.description:
69
80
  base_description = concept.metadata.description
70
81
  else:
71
- base_description = f"a {concept.datatype.value}"
82
+ base_description = concept.address
72
83
  if concept.metadata and concept.metadata.line_number:
73
84
  base_line_number = concept.metadata.line_number
74
85
  else:
@@ -105,7 +116,7 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
105
116
  concept.address,
106
117
  ),
107
118
  metadata=Metadata(
108
- description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
119
+ description=f"Auto-derived from {base_description}. {FUNCTION_DESCRIPTION_MAPS.get(ftype, ftype.value)}.",
109
120
  line_number=base_line_number,
110
121
  concept_source=ConceptSource.AUTO_DERIVED,
111
122
  ),
@@ -147,7 +158,7 @@ def generate_key_concepts(concept: Concept, environment: Environment):
147
158
  concept.address,
148
159
  },
149
160
  metadata=Metadata(
150
- description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
161
+ description=f"Auto-derived integer. The {ftype.value} of {concept.address}, {base_description}",
151
162
  line_number=base_line_number,
152
163
  concept_source=ConceptSource.AUTO_DERIVED,
153
164
  ),
@@ -620,7 +620,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
620
620
  )
621
621
  final_grain = self.grain or grain
622
622
  keys = self.keys if self.keys else None
623
- if self.is_aggregate and isinstance(new_lineage, Function):
623
+ if self.is_aggregate and isinstance(new_lineage, Function) and grain.components:
624
624
  grain_components = [environment.concepts[c] for c in grain.components]
625
625
  new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
626
626
  final_grain = grain
@@ -1015,6 +1015,7 @@ class EnvironmentConceptDict(dict):
1015
1015
  def raise_undefined(
1016
1016
  self, key: str, line_no: int | None = None, file: Path | str | None = None
1017
1017
  ) -> Never:
1018
+
1018
1019
  matches = self._find_similar_concepts(key)
1019
1020
  message = f"Undefined concept: {key}."
1020
1021
  if matches:
@@ -1660,6 +1661,96 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1660
1661
  ] = Field(default_factory=EnvironmentConceptDict)
1661
1662
  grain: Grain = Field(default_factory=Grain)
1662
1663
 
1664
+ @classmethod
1665
+ def from_inputs(
1666
+ cls,
1667
+ environment: Environment,
1668
+ selection: List[SelectItem],
1669
+ order_by: OrderBy | None = None,
1670
+ limit: int | None = None,
1671
+ meta: Metadata | None = None,
1672
+ where_clause: WhereClause | None = None,
1673
+ having_clause: HavingClause | None = None,
1674
+ ) -> "SelectStatement":
1675
+
1676
+ output = SelectStatement(
1677
+ selection=selection,
1678
+ where_clause=where_clause,
1679
+ having_clause=having_clause,
1680
+ limit=limit,
1681
+ order_by=order_by,
1682
+ meta=meta or Metadata(),
1683
+ )
1684
+ for parse_pass in [
1685
+ 1,
1686
+ 2,
1687
+ ]:
1688
+ # the first pass will result in all concepts being defined
1689
+ # the second will get grains appropriately
1690
+ # eg if someone does sum(x)->a, b+c -> z - we don't know if Z is a key to group by or an aggregate
1691
+ # until after the first pass, and so don't know the grain of a
1692
+
1693
+ if parse_pass == 1:
1694
+ grain = Grain.from_concepts(
1695
+ [
1696
+ x.content
1697
+ for x in output.selection
1698
+ if isinstance(x.content, Concept)
1699
+ ],
1700
+ where_clause=output.where_clause,
1701
+ )
1702
+ if parse_pass == 2:
1703
+ grain = Grain.from_concepts(
1704
+ output.output_components, where_clause=output.where_clause
1705
+ )
1706
+ output.grain = grain
1707
+ pass_grain = Grain() if parse_pass == 1 else grain
1708
+ for item in selection:
1709
+ # we don't know the grain of an aggregate at assignment time
1710
+ # so rebuild at this point in the tree
1711
+ # TODO: simplify
1712
+ if isinstance(item.content, ConceptTransform):
1713
+ new_concept = item.content.output.with_select_context(
1714
+ output.local_concepts,
1715
+ # the first pass grain will be incorrect
1716
+ pass_grain,
1717
+ environment=environment,
1718
+ )
1719
+ output.local_concepts[new_concept.address] = new_concept
1720
+ item.content.output = new_concept
1721
+ if parse_pass == 2 and CONFIG.select_as_definition:
1722
+ environment.add_concept(new_concept)
1723
+ elif isinstance(item.content, UndefinedConcept):
1724
+ environment.concepts.raise_undefined(
1725
+ item.content.address,
1726
+ line_no=item.content.metadata.line_number,
1727
+ file=environment.env_file_path,
1728
+ )
1729
+ elif isinstance(item.content, Concept):
1730
+ # Sometimes cached values here don't have the latest info
1731
+ # but we can't just use environment, as it might not have the right grain.
1732
+ item.content = item.content.with_select_context(
1733
+ output.local_concepts,
1734
+ pass_grain,
1735
+ environment=environment,
1736
+ )
1737
+ output.local_concepts[item.content.address] = item.content
1738
+
1739
+ if order_by:
1740
+ output.order_by = order_by.with_select_context(
1741
+ local_concepts=output.local_concepts,
1742
+ grain=output.grain,
1743
+ environment=environment,
1744
+ )
1745
+ if output.having_clause:
1746
+ output.having_clause = output.having_clause.with_select_context(
1747
+ local_concepts=output.local_concepts,
1748
+ grain=output.grain,
1749
+ environment=environment,
1750
+ )
1751
+ output.validate_syntax(environment)
1752
+ return output
1753
+
1663
1754
  def validate_syntax(self, environment: Environment):
1664
1755
  if self.where_clause:
1665
1756
  for x in self.where_clause.concept_arguments:
@@ -3264,6 +3355,7 @@ class Environment(BaseModel):
3264
3355
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3265
3356
  # TODO: support freezing environments to avoid mutation
3266
3357
  frozen: bool = False
3358
+ env_file_path: Path | None = None
3267
3359
 
3268
3360
  def freeze(self):
3269
3361
  self.frozen = True
@@ -3317,7 +3409,7 @@ class Environment(BaseModel):
3317
3409
  path = Path(path)
3318
3410
  with open(path, "r") as f:
3319
3411
  read = f.read()
3320
- return Environment(working_path=Path(path).parent).parse(read)[0]
3412
+ return Environment(working_path=path.parent, env_file_path=path).parse(read)[0]
3321
3413
 
3322
3414
  @classmethod
3323
3415
  def from_string(cls, input: str) -> "Environment":
@@ -866,6 +866,7 @@ def _search_concepts(
866
866
  )
867
867
  if complete == ValidationResult.INCOMPLETE_CONDITION:
868
868
  cond_dict = {str(node): node.preexisting_conditions for node in stack}
869
+ logger.error(f"Have {cond_dict} and need {str(conditions)}")
869
870
  raise SyntaxError(f"Have {cond_dict} and need {str(conditions)}")
870
871
  # early exit if we have a complete stack with one node
871
872
  # we can only early exit if we have a complete stack
@@ -60,7 +60,7 @@ def gen_filter_node(
60
60
  g=g,
61
61
  depth=depth + 1,
62
62
  history=history,
63
- # conditions=conditions,
63
+ conditions=conditions,
64
64
  )
65
65
 
66
66
  flattened_existence = [x for y in parent_existence_concepts for x in y]
@@ -194,6 +194,9 @@ def gen_filter_node(
194
194
  history=history,
195
195
  conditions=conditions,
196
196
  )
197
+ logger.info(
198
+ f"{padding(depth)}{LOGGER_PREFIX} returning filter node and enrich node with {enrich_node.output_concepts} and {enrich_node.input_concepts}"
199
+ )
197
200
  return MergeNode(
198
201
  input_concepts=[concept, immediate_parent] + local_optional,
199
202
  output_concepts=[
@@ -206,4 +209,5 @@ def gen_filter_node(
206
209
  filter_node,
207
210
  enrich_node,
208
211
  ],
212
+ preexisting_conditions=conditions.conditional if conditions else None,
209
213
  )
@@ -210,6 +210,8 @@ class StrategyNode:
210
210
  return self
211
211
 
212
212
  def add_condition(self, condition: Conditional | Comparison | Parenthetical):
213
+ if self.conditions and condition == self.conditions:
214
+ return self
213
215
  if self.conditions:
214
216
  self.conditions = Conditional(
215
217
  left=self.conditions, right=condition, operator=BooleanOperator.AND
@@ -224,9 +224,6 @@ class MergeNode(StrategyNode):
224
224
  f"{self.logging_prefix}{LOGGER_PREFIX} Final joins is not null {final_joins} but is empty, skipping join generation"
225
225
  )
226
226
  return []
227
-
228
- for join in joins:
229
- logger.info(f"{self.logging_prefix}{LOGGER_PREFIX} final join {str(join)}")
230
227
  return joins
231
228
 
232
229
  def _resolve(self) -> QueryDatasource:
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Set, Tuple
6
6
 
7
7
  import networkx as nx
8
8
 
9
+ from trilogy.constants import logger
9
10
  from trilogy.core.enums import BooleanOperator, FunctionClass, Granularity, Purpose
10
11
  from trilogy.core.models import (
11
12
  CTE,
@@ -160,6 +161,8 @@ def resolve_join_order_v2(
160
161
  final_join_type = JoinType.LEFT_OUTER
161
162
  elif any([x == JoinType.FULL for x in join_types]):
162
163
  final_join_type = JoinType.FULL
164
+ logger.info("JOIN DEBUG")
165
+ logger.info(joinkeys)
163
166
  output.append(
164
167
  JoinOrderOutput(
165
168
  # left=left_candidate,
@@ -306,11 +309,37 @@ def resolve_instantiated_concept(
306
309
  )
307
310
 
308
311
 
312
+ def reduce_concept_pairs(input: list[ConceptPair]) -> list[ConceptPair]:
313
+ left_keys = set()
314
+ right_keys = set()
315
+ for pair in input:
316
+ if pair.left.purpose == Purpose.KEY:
317
+ left_keys.add(pair.left.address)
318
+ if pair.right.purpose == Purpose.KEY:
319
+ right_keys.add(pair.right.address)
320
+ final: list[ConceptPair] = []
321
+ for pair in input:
322
+ if (
323
+ pair.left.purpose == Purpose.PROPERTY
324
+ and pair.left.keys
325
+ and pair.left.keys.issubset(left_keys)
326
+ ):
327
+ continue
328
+ if (
329
+ pair.right.purpose == Purpose.PROPERTY
330
+ and pair.right.keys
331
+ and pair.right.keys.issubset(right_keys)
332
+ ):
333
+ continue
334
+ final.append(pair)
335
+ return final
336
+
337
+
309
338
  def get_node_joins(
310
339
  datasources: List[QueryDatasource | Datasource],
311
340
  environment: Environment,
312
341
  # concepts:List[Concept],
313
- ):
342
+ ) -> List[BaseJoin]:
314
343
  graph = nx.Graph()
315
344
  partials: dict[str, list[str]] = {}
316
345
  ds_node_map: dict[str, QueryDatasource | Datasource] = {}
@@ -337,19 +366,21 @@ def get_node_joins(
337
366
  join_type=j.type,
338
367
  # preserve empty field for maps
339
368
  concepts=[] if not j.keys else None,
340
- concept_pairs=[
341
- ConceptPair(
342
- left=resolve_instantiated_concept(
343
- concept_map[concept], ds_node_map[k]
344
- ),
345
- right=resolve_instantiated_concept(
346
- concept_map[concept], ds_node_map[j.right]
347
- ),
348
- existing_datasource=ds_node_map[k],
349
- )
350
- for k, v in j.keys.items()
351
- for concept in v
352
- ],
369
+ concept_pairs=reduce_concept_pairs(
370
+ [
371
+ ConceptPair(
372
+ left=resolve_instantiated_concept(
373
+ concept_map[concept], ds_node_map[k]
374
+ ),
375
+ right=resolve_instantiated_concept(
376
+ concept_map[concept], ds_node_map[j.right]
377
+ ),
378
+ existing_datasource=ds_node_map[k],
379
+ )
380
+ for k, v in j.keys.items()
381
+ for concept in v
382
+ ]
383
+ ),
353
384
  )
354
385
  for j in joins
355
386
  ]
@@ -359,7 +359,7 @@ def get_query_node(
359
359
  environment.concepts[k] = v
360
360
  graph = generate_graph(environment)
361
361
  logger.info(
362
- f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and output {[str(c) for c in statement.output_components]}"
362
+ f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and grain {statement.grain}"
363
363
  )
364
364
  if not statement.output_components:
365
365
  raise ValueError(f"Statement has no output components {statement}")
@@ -17,7 +17,6 @@ from lark.tree import Meta
17
17
  from pydantic import ValidationError
18
18
 
19
19
  from trilogy.constants import (
20
- CONFIG,
21
20
  DEFAULT_NAMESPACE,
22
21
  NULL_VALUE,
23
22
  MagicConstants,
@@ -108,7 +107,6 @@ from trilogy.core.models import (
108
107
  StructType,
109
108
  SubselectComparison,
110
109
  TupleWrapper,
111
- UndefinedConcept,
112
110
  WhereClause,
113
111
  Window,
114
112
  WindowItem,
@@ -1030,81 +1028,15 @@ class ParseToObjects(Transformer):
1030
1028
  if not select_items:
1031
1029
  raise ValueError("Malformed select, missing select items")
1032
1030
 
1033
- output = SelectStatement(
1031
+ return SelectStatement.from_inputs(
1032
+ environment=self.environment,
1034
1033
  selection=select_items,
1034
+ order_by=order_by,
1035
1035
  where_clause=where,
1036
1036
  having_clause=having,
1037
1037
  limit=limit,
1038
- order_by=order_by,
1039
1038
  meta=Metadata(line_number=meta.line),
1040
1039
  )
1041
- for parse_pass in [
1042
- 1,
1043
- 2,
1044
- ]:
1045
- # the first pass will result in all concepts being defined
1046
- # the second will get grains appropriately
1047
- # eg if someone does sum(x)->a, b+c -> z - we don't know if Z is a key to group by or an aggregate
1048
- # until after the first pass, and so don't know the grain of a
1049
-
1050
- if parse_pass == 1:
1051
- grain = Grain.from_concepts(
1052
- [
1053
- x.content
1054
- for x in output.selection
1055
- if isinstance(x.content, Concept)
1056
- ],
1057
- where_clause=output.where_clause,
1058
- )
1059
- if parse_pass == 2:
1060
- grain = Grain.from_concepts(
1061
- output.output_components, where_clause=output.where_clause
1062
- )
1063
- output.grain = grain
1064
- for item in select_items:
1065
- # we don't know the grain of an aggregate at assignment time
1066
- # so rebuild at this point in the tree
1067
- # TODO: simplify
1068
- if isinstance(item.content, ConceptTransform):
1069
- new_concept = item.content.output.with_select_context(
1070
- output.local_concepts,
1071
- output.grain,
1072
- environment=self.environment,
1073
- )
1074
- output.local_concepts[new_concept.address] = new_concept
1075
- item.content.output = new_concept
1076
- if parse_pass == 2 and CONFIG.select_as_definition:
1077
- self.environment.add_concept(new_concept)
1078
- elif isinstance(item.content, UndefinedConcept):
1079
- self.environment.concepts.raise_undefined(
1080
- item.content.address,
1081
- line_no=item.content.metadata.line_number,
1082
- file=self.token_address,
1083
- )
1084
- elif isinstance(item.content, Concept):
1085
- # Sometimes cached values here don't have the latest info
1086
- # but we can't just use environment, as it might not have the right grain.
1087
- item.content = item.content.with_select_context(
1088
- output.local_concepts,
1089
- output.grain,
1090
- environment=self.environment,
1091
- )
1092
- output.local_concepts[item.content.address] = item.content
1093
-
1094
- if order_by:
1095
- output.order_by = order_by.with_select_context(
1096
- local_concepts=output.local_concepts,
1097
- grain=output.grain,
1098
- environment=self.environment,
1099
- )
1100
- if output.having_clause:
1101
- output.having_clause = output.having_clause.with_select_context(
1102
- local_concepts=output.local_concepts,
1103
- grain=output.grain,
1104
- environment=self.environment,
1105
- )
1106
- output.validate_syntax(self.environment)
1107
- return output
1108
1040
 
1109
1041
  @v_args(meta=True)
1110
1042
  def address(self, meta: Meta, args):
File without changes
File without changes
File without changes
File without changes