pytrilogy 0.0.2.49__tar.gz → 0.0.2.50__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 (112) hide show
  1. {pytrilogy-0.0.2.49/pytrilogy.egg-info → pytrilogy-0.0.2.50}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_discovery_nodes.py +0 -1
  4. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_functions.py +32 -0
  5. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/enums.py +11 -0
  7. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/functions.py +4 -1
  8. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/models.py +11 -0
  9. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/concept_strategies_v3.py +0 -3
  10. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/common.py +0 -2
  11. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/filter_node.py +0 -3
  12. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/group_node.py +0 -1
  13. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/group_to_node.py +0 -2
  14. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/multiselect_node.py +0 -2
  15. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/node_merge_node.py +0 -1
  16. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/rowset_node.py +0 -1
  17. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/select_merge_node.py +138 -59
  18. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/union_node.py +0 -1
  19. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/unnest_node.py +0 -2
  20. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/window_node.py +0 -2
  21. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/base_node.py +0 -3
  22. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/filter_node.py +0 -3
  23. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/group_node.py +0 -3
  24. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/merge_node.py +0 -3
  25. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/select_node_v2.py +0 -4
  26. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/union_node.py +0 -3
  27. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/unnest_node.py +0 -3
  28. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/window_node.py +0 -3
  29. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/utility.py +3 -0
  30. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/query_processor.py +0 -1
  31. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/base.py +14 -2
  32. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/duckdb.py +7 -0
  33. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/hooks/graph_hook.py +14 -0
  34. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/common.py +14 -5
  35. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/parse_engine.py +31 -0
  36. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/trilogy.lark +3 -1
  37. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/LICENSE.md +0 -0
  38. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/README.md +0 -0
  39. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pyproject.toml +0 -0
  40. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pytrilogy.egg-info/SOURCES.txt +0 -0
  41. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pytrilogy.egg-info/dependency_links.txt +0 -0
  42. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pytrilogy.egg-info/entry_points.txt +0 -0
  43. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pytrilogy.egg-info/requires.txt +0 -0
  44. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/pytrilogy.egg-info/top_level.txt +0 -0
  45. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/setup.cfg +0 -0
  46. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/setup.py +0 -0
  47. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_datatypes.py +0 -0
  48. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_declarations.py +0 -0
  49. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_derived_concepts.py +0 -0
  50. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_enums.py +0 -0
  51. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_environment.py +0 -0
  52. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_executor.py +0 -0
  53. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_imports.py +0 -0
  54. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_metadata.py +0 -0
  55. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_models.py +0 -0
  56. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_multi_join_assignments.py +0 -0
  57. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_parse_engine.py +0 -0
  58. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_parsing.py +0 -0
  59. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_partial_handling.py +0 -0
  60. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_query_processing.py +0 -0
  61. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_select.py +0 -0
  62. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_show.py +0 -0
  63. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_statements.py +0 -0
  64. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_undefined_concept.py +0 -0
  65. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/tests/test_where_clause.py +0 -0
  66. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/compiler.py +0 -0
  67. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/constants.py +0 -0
  68. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/__init__.py +0 -0
  69. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/constants.py +0 -0
  70. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/env_processor.py +0 -0
  71. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/environment_helpers.py +0 -0
  72. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/ergonomics.py +0 -0
  73. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/exceptions.py +0 -0
  74. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/graph_models.py +0 -0
  75. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/internal.py +0 -0
  76. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimization.py +0 -0
  77. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimizations/__init__.py +0 -0
  78. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimizations/base_optimization.py +0 -0
  79. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimizations/inline_constant.py +0 -0
  80. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimizations/inline_datasource.py +0 -0
  81. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  82. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/__init__.py +0 -0
  83. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/graph_utils.py +0 -0
  84. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/__init__.py +0 -0
  85. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  86. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/node_generators/select_node.py +0 -0
  87. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/core/processing/nodes/__init__.py +0 -0
  88. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/__init__.py +0 -0
  89. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/bigquery.py +0 -0
  90. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/common.py +0 -0
  91. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/config.py +0 -0
  92. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/enums.py +0 -0
  93. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/postgres.py +0 -0
  94. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/presto.py +0 -0
  95. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/snowflake.py +0 -0
  96. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/dialect/sql_server.py +0 -0
  97. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/engine.py +0 -0
  98. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/executor.py +0 -0
  99. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/hooks/__init__.py +0 -0
  100. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/hooks/base_hook.py +0 -0
  101. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/hooks/query_debugger.py +0 -0
  102. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/metadata/__init__.py +0 -0
  103. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parser.py +0 -0
  104. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/__init__.py +0 -0
  105. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/config.py +0 -0
  106. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/exceptions.py +0 -0
  107. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/helpers.py +0 -0
  108. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/parsing/render.py +0 -0
  109. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/py.typed +0 -0
  110. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/scripts/__init__.py +0 -0
  111. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/scripts/trilogy.py +0 -0
  112. {pytrilogy-0.0.2.49 → pytrilogy-0.0.2.50}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.49
3
+ Version: 0.0.2.50
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.49
3
+ Version: 0.0.2.50
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -15,7 +15,6 @@ def test_group_node(test_environment, test_environment_graph):
15
15
  output_concepts=[total_revenue, category],
16
16
  input_concepts=[category, revenue],
17
17
  environment=test_environment,
18
- g=test_environment_graph,
19
18
  parents=[
20
19
  search_concepts(
21
20
  [category, revenue],
@@ -14,6 +14,8 @@ from trilogy.dialect.duckdb import DuckDBDialect
14
14
  from trilogy.dialect.snowflake import SnowflakeDialect
15
15
  from trilogy.dialect.sql_server import SqlServerDialect
16
16
  from trilogy.parser import parse
17
+ from trilogy import Dialects
18
+ from datetime import date, datetime
17
19
 
18
20
  logger.setLevel(INFO)
19
21
 
@@ -154,6 +156,36 @@ def test_explicit_cast(test_environment):
154
156
  dialect.compile_statement(process_query(test_environment, select))
155
157
 
156
158
 
159
+ def test_literal_cast(test_environment):
160
+ declarations = """
161
+ select
162
+ '1'::int -> one,
163
+ '1'::float -> one_float,
164
+ '1'::string -> one_string,
165
+ '2024-01-01'::date -> one_date,
166
+ '2024-01-01 01:01:01'::datetime -> one_datetime,
167
+ 'true'::bool -> one_bool,
168
+ ;"""
169
+ env, parsed = parse(declarations, environment=test_environment)
170
+
171
+ select: SelectStatement = parsed[-1]
172
+ z = (
173
+ Dialects.DUCK_DB.default_executor(environment=test_environment)
174
+ .execute_query(parsed[-1])
175
+ .fetchall()
176
+ )
177
+ assert z[0].one == 1
178
+ assert z[0].one_float == 1.0
179
+ assert z[0].one_string == "1"
180
+ assert z[0].one_date == date(year=2024, month=1, day=1)
181
+ assert z[0].one_datetime == datetime(
182
+ year=2024, month=1, day=1, hour=1, minute=1, second=1
183
+ )
184
+ assert z[0].one_bool == True
185
+ for dialect in TEST_DIALECTS:
186
+ dialect.compile_statement(process_query(test_environment, select))
187
+
188
+
157
189
  def test_math_functions(test_environment):
158
190
  declarations = """
159
191
 
@@ -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.49"
7
+ __version__ = "0.0.2.50"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -120,6 +120,8 @@ class FunctionType(Enum):
120
120
 
121
121
  ALIAS = "alias"
122
122
 
123
+ PARENTHETICAL = "parenthetical"
124
+
123
125
  # Generic
124
126
  CASE = "case"
125
127
  CAST = "cast"
@@ -135,6 +137,8 @@ class FunctionType(Enum):
135
137
  ATTR_ACCESS = "attr_access"
136
138
  STRUCT = "struct"
137
139
  ARRAY = "array"
140
+ DATE_LITERAL = "date_literal"
141
+ DATETIME_LITERAL = "datetime_literal"
138
142
 
139
143
  # TEXT AND MAYBE MORE
140
144
  SPLIT = "split"
@@ -260,6 +264,13 @@ class ComparisonOperator(Enum):
260
264
  CONTAINS = "contains"
261
265
  ELSE = "else"
262
266
 
267
+ def __eq__(self, other):
268
+ if isinstance(other, str):
269
+ return self.value == other
270
+ if not isinstance(other, ComparisonOperator):
271
+ return False
272
+ return self.value == other.value
273
+
263
274
  @classmethod
264
275
  def _missing_(cls, value):
265
276
  if not isinstance(value, list) and " " in str(value):
@@ -1,3 +1,4 @@
1
+ from datetime import date, datetime
1
2
  from typing import Optional
2
3
 
3
4
  from trilogy.constants import MagicConstants
@@ -17,6 +18,8 @@ from trilogy.core.models import (
17
18
  arg_to_datatype,
18
19
  )
19
20
 
21
+ GENERIC_ARGS = Concept | Function | str | int | float | date | datetime
22
+
20
23
 
21
24
  def create_function_derived_concept(
22
25
  name: str,
@@ -262,7 +265,7 @@ def get_attr_datatype(
262
265
  return arg.datatype
263
266
 
264
267
 
265
- def AttrAccess(args: list[Concept]):
268
+ def AttrAccess(args: list[GENERIC_ARGS]):
266
269
  return Function(
267
270
  operator=FunctionType.ATTR_ACCESS,
268
271
  arguments=args,
@@ -5,6 +5,7 @@ import hashlib
5
5
  import os
6
6
  from abc import ABC
7
7
  from collections import UserDict, UserList, defaultdict
8
+ from datetime import date, datetime
8
9
  from enum import Enum
9
10
  from functools import cached_property
10
11
  from pathlib import Path
@@ -1264,6 +1265,8 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1264
1265
  int,
1265
1266
  float,
1266
1267
  str,
1268
+ date,
1269
+ datetime,
1267
1270
  MapWrapper[Any, Any],
1268
1271
  DataType,
1269
1272
  ListType,
@@ -3868,6 +3871,8 @@ class Comparison(
3868
3871
  float,
3869
3872
  list,
3870
3873
  bool,
3874
+ datetime,
3875
+ date,
3871
3876
  Function,
3872
3877
  Concept,
3873
3878
  "Conditional",
@@ -3884,6 +3889,8 @@ class Comparison(
3884
3889
  float,
3885
3890
  list,
3886
3891
  bool,
3892
+ date,
3893
+ datetime,
3887
3894
  Concept,
3888
3895
  Function,
3889
3896
  "Conditional",
@@ -5008,5 +5015,9 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
5008
5015
  return ListType(type=wrapper.type)
5009
5016
  elif isinstance(arg, MapWrapper):
5010
5017
  return MapType(key_type=arg.key_type, value_type=arg.value_type)
5018
+ elif isinstance(arg, datetime):
5019
+ return DataType.DATETIME
5020
+ elif isinstance(arg, date):
5021
+ return DataType.DATE
5011
5022
  else:
5012
5023
  raise ValueError(f"Cannot parse arg datatype for arg of raw type {type(arg)}")
@@ -359,7 +359,6 @@ def generate_node(
359
359
  input_concepts=[],
360
360
  output_concepts=constant_targets,
361
361
  environment=environment,
362
- g=g,
363
362
  parents=[],
364
363
  depth=depth + 1,
365
364
  )
@@ -906,7 +905,6 @@ def _search_concepts(
906
905
  input_concepts=non_virtual,
907
906
  output_concepts=non_virtual,
908
907
  environment=environment,
909
- g=g,
910
908
  parents=stack,
911
909
  depth=depth,
912
910
  )
@@ -987,7 +985,6 @@ def source_query_concepts(
987
985
  x for x in root.output_concepts if x.address not in root.hidden_concepts
988
986
  ],
989
987
  environment=environment,
990
- g=g,
991
988
  parents=[root],
992
989
  partial_concepts=root.partial_concepts,
993
990
  )
@@ -130,7 +130,6 @@ def gen_property_enrichment_node(
130
130
  ),
131
131
  output_concepts=base_node.output_concepts + extra_properties,
132
132
  environment=environment,
133
- g=g,
134
133
  parents=[
135
134
  base_node,
136
135
  ]
@@ -209,7 +208,6 @@ def gen_enrichment_node(
209
208
  input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
210
209
  output_concepts=unique(join_keys + extra_required + non_hidden, "address"),
211
210
  environment=environment,
212
- g=g,
213
211
  parents=[enrich_node, base_node],
214
212
  force_group=False,
215
213
  preexisting_conditions=conditions.conditional if conditions else None,
@@ -117,7 +117,6 @@ def gen_filter_node(
117
117
  input_concepts=row_parent.output_concepts,
118
118
  output_concepts=[concept] + row_parent.output_concepts,
119
119
  environment=row_parent.environment,
120
- g=row_parent.g,
121
120
  parents=[row_parent],
122
121
  depth=row_parent.depth,
123
122
  partial_concepts=row_parent.partial_concepts,
@@ -161,7 +160,6 @@ def gen_filter_node(
161
160
  ),
162
161
  output_concepts=[concept, immediate_parent] + parent_row_concepts,
163
162
  environment=environment,
164
- g=g,
165
163
  parents=core_parents,
166
164
  grain=Grain(
167
165
  components=[immediate_parent] + parent_row_concepts,
@@ -202,7 +200,6 @@ def gen_filter_node(
202
200
  ]
203
201
  + local_optional,
204
202
  environment=environment,
205
- g=g,
206
203
  parents=[
207
204
  # this node fetches only what we need to filter
208
205
  filter_node,
@@ -100,7 +100,6 @@ def gen_group_node(
100
100
  output_concepts=output_concepts,
101
101
  input_concepts=parent_concepts,
102
102
  environment=environment,
103
- g=g,
104
103
  parents=parents,
105
104
  depth=depth,
106
105
  preexisting_conditions=conditions.conditional if conditions else None,
@@ -45,7 +45,6 @@ def gen_group_to_node(
45
45
  output_concepts=parent_concepts + [concept],
46
46
  input_concepts=parent_concepts,
47
47
  environment=environment,
48
- g=g,
49
48
  parents=parents,
50
49
  depth=depth,
51
50
  )
@@ -76,7 +75,6 @@ def gen_group_to_node(
76
75
  + [x for x in parent_concepts if x.address != concept.address],
77
76
  output_concepts=[concept] + local_optional,
78
77
  environment=environment,
79
- g=g,
80
78
  parents=[
81
79
  # this node gets the group
82
80
  group_node,
@@ -108,7 +108,6 @@ def gen_multiselect_node(
108
108
  input_concepts=[x for y in base_parents for x in y.output_concepts],
109
109
  output_concepts=[x for y in base_parents for x in y.output_concepts],
110
110
  environment=environment,
111
- g=g,
112
111
  depth=depth,
113
112
  parents=base_parents,
114
113
  node_joins=node_joins,
@@ -178,7 +177,6 @@ def gen_multiselect_node(
178
177
  input_concepts=enrich_node.output_concepts + node.output_concepts,
179
178
  output_concepts=node.output_concepts + local_optional,
180
179
  environment=environment,
181
- g=g,
182
180
  depth=depth,
183
181
  parents=[
184
182
  # this node gets the multiselect
@@ -333,7 +333,6 @@ def subgraphs_to_merge_node(
333
333
  input_concepts=unique(input_c, "address"),
334
334
  output_concepts=[x for x in all_concepts],
335
335
  environment=environment,
336
- g=g,
337
336
  parents=parents,
338
337
  depth=depth,
339
338
  # conditions=conditions,
@@ -140,7 +140,6 @@ def gen_rowset_node(
140
140
  input_concepts=non_hidden + non_hidden_enrich,
141
141
  output_concepts=non_hidden + local_optional,
142
142
  environment=environment,
143
- g=g,
144
143
  depth=depth,
145
144
  parents=[
146
145
  node,
@@ -13,6 +13,9 @@ from trilogy.core.models import (
13
13
  LooseConceptList,
14
14
  WhereClause,
15
15
  )
16
+ from trilogy.core.processing.node_generators.select_helpers.datasource_injection import (
17
+ get_union_sources,
18
+ )
16
19
  from trilogy.core.processing.nodes import (
17
20
  ConstantNode,
18
21
  GroupNode,
@@ -35,38 +38,66 @@ def extract_address(node: str):
35
38
  def get_graph_partial_nodes(
36
39
  g: nx.DiGraph, conditions: WhereClause | None
37
40
  ) -> dict[str, list[str]]:
38
- datasources: dict[str, Datasource] = nx.get_node_attributes(g, "datasource")
41
+ datasources: dict[str, Datasource | list[Datasource]] = nx.get_node_attributes(
42
+ g, "datasource"
43
+ )
39
44
  partial: dict[str, list[str]] = {}
40
45
  for node in g.nodes:
41
46
  if node in datasources:
42
47
  ds = datasources[node]
43
- partial[node] = [concept_to_node(c) for c in ds.partial_concepts]
44
- if ds.non_partial_for and conditions == ds.non_partial_for:
48
+ if not isinstance(ds, list):
49
+ if ds.non_partial_for and conditions == ds.non_partial_for:
50
+ partial[node] = []
51
+ continue
52
+ partial[node] = [concept_to_node(c) for c in ds.partial_concepts]
53
+ ds = [ds]
54
+ # assume union sources have no partial
55
+ else:
45
56
  partial[node] = []
46
57
 
47
58
  return partial
48
59
 
49
60
 
50
61
  def get_graph_grain_length(g: nx.DiGraph) -> dict[str, int]:
51
- datasources: dict[str, Datasource] = nx.get_node_attributes(g, "datasource")
52
- partial: dict[str, int] = {}
62
+ datasources: dict[str, Datasource | list[Datasource]] = nx.get_node_attributes(
63
+ g, "datasource"
64
+ )
65
+ grain_length: dict[str, int] = {}
53
66
  for node in g.nodes:
54
67
  if node in datasources:
55
- partial[node] = len(datasources[node].grain.components)
56
- return partial
68
+ lookup = datasources[node]
69
+ if not isinstance(lookup, list):
70
+ lookup = [lookup]
71
+ assert isinstance(lookup, list)
72
+ grain_length[node] = sum(len(x.grain.components) for x in lookup)
73
+ return grain_length
57
74
 
58
75
 
59
76
  def create_pruned_concept_graph(
60
77
  g: nx.DiGraph,
61
78
  all_concepts: List[Concept],
79
+ datasources: list[Datasource],
62
80
  accept_partial: bool = False,
63
81
  conditions: WhereClause | None = None,
64
82
  ) -> nx.DiGraph:
65
83
  orig_g = g
66
84
  g = g.copy()
85
+
86
+ union_options = get_union_sources(datasources, all_concepts)
87
+ for ds_list in union_options:
88
+ node_address = "ds~" + "-".join([x.name for x in ds_list])
89
+ common: set[Concept] = set.intersection(
90
+ *[set(x.output_concepts) for x in ds_list]
91
+ )
92
+ g.add_node(node_address, datasource=ds_list)
93
+ for c in common:
94
+ g.add_edge(node_address, concept_to_node(c))
95
+
67
96
  target_addresses = set([c.address for c in all_concepts])
68
97
  concepts: dict[str, Concept] = nx.get_node_attributes(orig_g, "concept")
69
- datasources: dict[str, Datasource] = nx.get_node_attributes(orig_g, "datasource")
98
+ datasource_map: dict[str, Datasource | list[Datasource]] = nx.get_node_attributes(
99
+ orig_g, "datasource"
100
+ )
70
101
  relevant_concepts_pre = {
71
102
  n: x.address
72
103
  for n in g.nodes()
@@ -81,13 +112,13 @@ def create_pruned_concept_graph(
81
112
  to_remove = []
82
113
  for edge in g.edges:
83
114
  if (
84
- edge[0] in datasources
115
+ edge[0] in datasource_map
85
116
  and (pnodes := partial.get(edge[0], []))
86
117
  and edge[1] in pnodes
87
118
  ):
88
119
  to_remove.append(edge)
89
120
  if (
90
- edge[1] in datasources
121
+ edge[1] in datasource_map
91
122
  and (pnodes := partial.get(edge[1], []))
92
123
  and edge[0] in pnodes
93
124
  ):
@@ -136,7 +167,9 @@ def create_pruned_concept_graph(
136
167
  for edge in orig_g.edges():
137
168
  if edge[0] in relevant and edge[1] in relevant:
138
169
  g.add_edge(edge[0], edge[1])
139
-
170
+ # if we have no ds nodes at all, for non constant, we can't find it
171
+ if not any([n.startswith("ds~") for n in g.nodes]):
172
+ return None
140
173
  return g
141
174
 
142
175
 
@@ -190,6 +223,54 @@ def resolve_subgraphs(
190
223
  return pruned_subgraphs
191
224
 
192
225
 
226
+ def create_datasource_node(
227
+ datasource: Datasource,
228
+ all_concepts: List[Concept],
229
+ accept_partial: bool,
230
+ environment: Environment,
231
+ depth: int,
232
+ conditions: WhereClause | None = None,
233
+ ) -> tuple[StrategyNode, bool]:
234
+ target_grain = Grain(components=all_concepts)
235
+ force_group = False
236
+ if not datasource.grain.issubset(target_grain):
237
+ force_group = True
238
+ partial_concepts = [
239
+ c.concept
240
+ for c in datasource.columns
241
+ if not c.is_complete and c.concept.address in all_concepts
242
+ ]
243
+ partial_lcl = LooseConceptList(concepts=partial_concepts)
244
+ nullable_concepts = [
245
+ c.concept
246
+ for c in datasource.columns
247
+ if c.is_nullable and c.concept.address in all_concepts
248
+ ]
249
+ nullable_lcl = LooseConceptList(concepts=nullable_concepts)
250
+ partial_is_full = conditions and (conditions == datasource.non_partial_for)
251
+ return (
252
+ SelectNode(
253
+ input_concepts=[c.concept for c in datasource.columns],
254
+ output_concepts=all_concepts,
255
+ environment=environment,
256
+ parents=[],
257
+ depth=depth,
258
+ partial_concepts=(
259
+ [] if partial_is_full else [c for c in all_concepts if c in partial_lcl]
260
+ ),
261
+ nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
262
+ accept_partial=accept_partial,
263
+ datasource=datasource,
264
+ grain=Grain(components=all_concepts),
265
+ conditions=datasource.where.conditional if datasource.where else None,
266
+ preexisting_conditions=(
267
+ conditions.conditional if partial_is_full and conditions else None
268
+ ),
269
+ ),
270
+ force_group,
271
+ )
272
+
273
+
193
274
  def create_select_node(
194
275
  ds_name: str,
195
276
  subgraph: list[str],
@@ -199,12 +280,11 @@ def create_select_node(
199
280
  depth: int,
200
281
  conditions: WhereClause | None = None,
201
282
  ) -> StrategyNode:
202
- ds_name = ds_name.split("~")[1]
283
+
203
284
  all_concepts = [
204
285
  environment.concepts[extract_address(c)] for c in subgraph if c.startswith("c~")
205
286
  ]
206
287
 
207
- all_lcl = LooseConceptList(concepts=all_concepts)
208
288
  if all([c.derivation == PurposeLineage.CONSTANT for c in all_concepts]):
209
289
  logger.info(
210
290
  f"{padding(depth)}{LOGGER_PREFIX} All concepts {[x.address for x in all_concepts]} are constants, returning constant node"
@@ -213,7 +293,6 @@ def create_select_node(
213
293
  output_concepts=all_concepts,
214
294
  input_concepts=[],
215
295
  environment=environment,
216
- g=g,
217
296
  parents=[],
218
297
  depth=depth,
219
298
  # no partial for constants
@@ -221,41 +300,44 @@ def create_select_node(
221
300
  force_group=False,
222
301
  )
223
302
 
224
- datasource = environment.datasources[ds_name]
225
- target_grain = Grain(components=all_concepts)
226
- force_group = False
227
- if not datasource.grain.issubset(target_grain):
228
- force_group = True
229
- partial_concepts = [
230
- c.concept
231
- for c in datasource.columns
232
- if not c.is_complete and c.concept in all_lcl
233
- ]
234
- partial_lcl = LooseConceptList(concepts=partial_concepts)
235
- nullable_concepts = [
236
- c.concept for c in datasource.columns if c.is_nullable and c.concept in all_lcl
237
- ]
238
- nullable_lcl = LooseConceptList(concepts=nullable_concepts)
239
- partial_is_full = conditions and (conditions == datasource.non_partial_for)
240
- bcandidate: StrategyNode = SelectNode(
241
- input_concepts=[c.concept for c in datasource.columns],
242
- output_concepts=all_concepts,
243
- environment=environment,
244
- g=g,
245
- parents=[],
246
- depth=depth,
247
- partial_concepts=(
248
- [] if partial_is_full else [c for c in all_concepts if c in partial_lcl]
249
- ),
250
- nullable_concepts=[c for c in all_concepts if c in nullable_lcl],
251
- accept_partial=accept_partial,
252
- datasource=datasource,
253
- grain=Grain(components=all_concepts),
254
- conditions=datasource.where.conditional if datasource.where else None,
255
- preexisting_conditions=(
256
- conditions.conditional if partial_is_full and conditions else None
257
- ),
258
- )
303
+ datasource: dict[str, Datasource | list[Datasource]] = nx.get_node_attributes(
304
+ g, "datasource"
305
+ )[ds_name]
306
+ if isinstance(datasource, Datasource):
307
+ bcandidate, force_group = create_datasource_node(
308
+ datasource,
309
+ all_concepts,
310
+ accept_partial,
311
+ environment,
312
+ depth,
313
+ conditions=conditions,
314
+ )
315
+
316
+ elif isinstance(datasource, list):
317
+ from trilogy.core.processing.nodes.union_node import UnionNode
318
+
319
+ force_group = False
320
+ parents = []
321
+ for x in datasource:
322
+ subnode, fg = create_datasource_node(
323
+ x,
324
+ all_concepts,
325
+ accept_partial,
326
+ environment,
327
+ depth,
328
+ conditions=conditions,
329
+ )
330
+ parents.append(subnode)
331
+ force_group = force_group or fg
332
+ bcandidate = UnionNode(
333
+ output_concepts=all_concepts,
334
+ input_concepts=all_concepts,
335
+ environment=environment,
336
+ parents=parents,
337
+ depth=depth,
338
+ )
339
+ else:
340
+ raise ValueError(f"Unknown datasource type {datasource}")
259
341
 
260
342
  # we need to nest the group node one further
261
343
  if force_group is True:
@@ -263,14 +345,11 @@ def create_select_node(
263
345
  output_concepts=all_concepts,
264
346
  input_concepts=all_concepts,
265
347
  environment=environment,
266
- g=g,
267
348
  parents=[bcandidate],
268
349
  depth=depth,
269
350
  partial_concepts=bcandidate.partial_concepts,
270
351
  nullable_concepts=bcandidate.nullable_concepts,
271
- preexisting_conditions=(
272
- conditions.conditional if partial_is_full and conditions else None
273
- ),
352
+ preexisting_conditions=bcandidate.preexisting_conditions,
274
353
  )
275
354
  else:
276
355
  candidate = bcandidate
@@ -292,7 +371,6 @@ def gen_select_merge_node(
292
371
  output_concepts=constants,
293
372
  input_concepts=[],
294
373
  environment=environment,
295
- g=g,
296
374
  parents=[],
297
375
  depth=depth,
298
376
  partial_concepts=[],
@@ -300,7 +378,11 @@ def gen_select_merge_node(
300
378
  )
301
379
  for attempt in [False, True]:
302
380
  pruned_concept_graph = create_pruned_concept_graph(
303
- g, non_constant, attempt, conditions
381
+ g,
382
+ non_constant,
383
+ accept_partial=attempt,
384
+ conditions=conditions,
385
+ datasources=list(environment.datasources.values()),
304
386
  )
305
387
  if pruned_concept_graph:
306
388
  logger.info(
@@ -321,7 +403,7 @@ def gen_select_merge_node(
321
403
  create_select_node(
322
404
  k,
323
405
  subgraph,
324
- g=g,
406
+ g=pruned_concept_graph,
325
407
  accept_partial=accept_partial,
326
408
  environment=environment,
327
409
  depth=depth,
@@ -338,7 +420,6 @@ def gen_select_merge_node(
338
420
  output_concepts=constants,
339
421
  input_concepts=[],
340
422
  environment=environment,
341
- g=g,
342
423
  parents=[],
343
424
  depth=depth,
344
425
  partial_concepts=[],
@@ -361,7 +442,6 @@ def gen_select_merge_node(
361
442
  output_concepts=all_concepts,
362
443
  input_concepts=non_constant,
363
444
  environment=environment,
364
- g=g,
365
445
  depth=depth,
366
446
  parents=parents,
367
447
  preexisting_conditions=preexisting_conditions,
@@ -372,7 +452,6 @@ def gen_select_merge_node(
372
452
  output_concepts=all_concepts,
373
453
  input_concepts=all_concepts,
374
454
  environment=environment,
375
- g=g,
376
455
  parents=[base],
377
456
  depth=depth,
378
457
  preexisting_conditions=preexisting_conditions,
@@ -70,6 +70,5 @@ def gen_union_node(
70
70
  input_concepts=[concept] + local_optional,
71
71
  output_concepts=[concept] + local_optional,
72
72
  environment=environment,
73
- g=g,
74
73
  parents=parents,
75
74
  )
@@ -46,7 +46,6 @@ def gen_unnest_node(
46
46
  input_concepts=arguments + non_equivalent_optional,
47
47
  output_concepts=[concept] + local_optional,
48
48
  environment=environment,
49
- g=g,
50
49
  parents=([parent] if (arguments or local_optional) else []),
51
50
  )
52
51
  # we need to sometimes nest an unnest node,
@@ -56,7 +55,6 @@ def gen_unnest_node(
56
55
  input_concepts=base.output_concepts,
57
56
  output_concepts=base.output_concepts,
58
57
  environment=environment,
59
- g=g,
60
58
  parents=[base],
61
59
  preexisting_conditions=conditions.conditional if conditions else None,
62
60
  )