pytrilogy 0.0.3.70__tar.gz → 0.0.3.72__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 (150) hide show
  1. {pytrilogy-0.0.3.70/pytrilogy.egg-info → pytrilogy-0.0.3.72}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pytrilogy.egg-info/SOURCES.txt +1 -0
  4. pytrilogy-0.0.3.72/tests/test_execute_models.py +61 -0
  5. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/enums.py +1 -0
  7. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/functions.py +5 -0
  8. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/graph_models.py +6 -2
  9. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/build.py +35 -19
  10. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/datasource.py +5 -0
  11. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/execute.py +38 -39
  12. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/concept_strategies_v3.py +6 -4
  13. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/node_merge_node.py +3 -1
  14. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/rowset_node.py +0 -1
  15. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +69 -20
  16. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/select_merge_node.py +70 -33
  17. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/window_node.py +2 -1
  18. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/base_node.py +16 -12
  19. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/union_node.py +9 -1
  20. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/query_processor.py +5 -3
  21. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/base.py +5 -1
  22. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/common.py +10 -16
  23. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/parse_engine.py +9 -0
  24. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/net.preql +2 -1
  25. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/LICENSE.md +0 -0
  26. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/README.md +0 -0
  27. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pyproject.toml +0 -0
  28. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/setup.cfg +0 -0
  33. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/setup.py +0 -0
  34. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_executor.py +0 -0
  41. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_failure.py +0 -0
  42. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_functions.py +0 -0
  43. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_imports.py +0 -0
  44. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_metadata.py +0 -0
  45. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_models.py +0 -0
  46. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_multi_join_assignments.py +0 -0
  47. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_parse_engine.py +0 -0
  48. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_parsing.py +0 -0
  49. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_parsing_failures.py +0 -0
  50. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_partial_handling.py +0 -0
  51. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_query_processing.py +0 -0
  52. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_query_render.py +0 -0
  53. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_select.py +0 -0
  54. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_show.py +0 -0
  55. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_statements.py +0 -0
  56. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_typing.py +0 -0
  57. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_undefined_concept.py +0 -0
  58. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_user_functions.py +0 -0
  59. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/tests/test_where_clause.py +0 -0
  60. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/authoring/__init__.py +0 -0
  61. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/compiler.py +0 -0
  62. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/constants.py +0 -0
  63. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/__init__.py +0 -0
  64. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/constants.py +0 -0
  65. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/env_processor.py +0 -0
  66. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/environment_helpers.py +0 -0
  67. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/ergonomics.py +0 -0
  68. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/exceptions.py +0 -0
  69. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/internal.py +0 -0
  70. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/__init__.py +0 -0
  71. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/author.py +0 -0
  72. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/build_environment.py +0 -0
  73. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/core.py +0 -0
  74. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/models/environment.py +0 -0
  75. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/optimization.py +0 -0
  76. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/optimizations/__init__.py +0 -0
  77. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/optimizations/base_optimization.py +0 -0
  78. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/optimizations/inline_datasource.py +0 -0
  79. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  80. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/__init__.py +0 -0
  81. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/discovery_loop.py +0 -0
  82. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/discovery_node_factory.py +0 -0
  83. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/discovery_utility.py +0 -0
  84. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/discovery_validation.py +0 -0
  85. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/graph_utils.py +0 -0
  86. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/__init__.py +0 -0
  87. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  88. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/common.py +0 -0
  89. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  90. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/group_node.py +0 -0
  91. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  92. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  93. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  94. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  95. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/select_node.py +0 -0
  96. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  97. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/union_node.py +0 -0
  98. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  99. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/__init__.py +0 -0
  100. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/filter_node.py +0 -0
  101. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/group_node.py +0 -0
  102. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/merge_node.py +0 -0
  103. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  104. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  105. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  106. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/nodes/window_node.py +0 -0
  107. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/processing/utility.py +0 -0
  108. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/statements/__init__.py +0 -0
  109. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/statements/author.py +0 -0
  110. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/statements/build.py +0 -0
  111. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/statements/common.py +0 -0
  112. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/statements/execute.py +0 -0
  113. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/core/utility.py +0 -0
  114. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/__init__.py +0 -0
  115. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/bigquery.py +0 -0
  116. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/common.py +0 -0
  117. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/config.py +0 -0
  118. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/dataframe.py +0 -0
  119. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/duckdb.py +0 -0
  120. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/enums.py +0 -0
  121. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/postgres.py +0 -0
  122. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/presto.py +0 -0
  123. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/snowflake.py +0 -0
  124. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/dialect/sql_server.py +0 -0
  125. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/engine.py +0 -0
  126. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/executor.py +0 -0
  127. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/hooks/__init__.py +0 -0
  128. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/hooks/base_hook.py +0 -0
  129. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/hooks/graph_hook.py +0 -0
  130. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/hooks/query_debugger.py +0 -0
  131. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/metadata/__init__.py +0 -0
  132. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parser.py +0 -0
  133. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/__init__.py +0 -0
  134. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/config.py +0 -0
  135. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/exceptions.py +0 -0
  136. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/helpers.py +0 -0
  137. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/render.py +0 -0
  138. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/parsing/trilogy.lark +0 -0
  139. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/py.typed +0 -0
  140. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/render.py +0 -0
  141. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/scripts/__init__.py +0 -0
  142. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/scripts/trilogy.py +0 -0
  143. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/__init__.py +0 -0
  144. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/date.preql +0 -0
  145. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/display.preql +0 -0
  146. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/geography.preql +0 -0
  147. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/money.preql +0 -0
  148. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/ranking.preql +0 -0
  149. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/std/report.preql +0 -0
  150. {pytrilogy-0.0.3.70 → pytrilogy-0.0.3.72}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.70
3
+ Version: 0.0.3.72
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.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.70
3
+ Version: 0.0.3.72
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -14,6 +14,7 @@ tests/test_derived_concepts.py
14
14
  tests/test_discovery_nodes.py
15
15
  tests/test_enums.py
16
16
  tests/test_environment.py
17
+ tests/test_execute_models.py
17
18
  tests/test_executor.py
18
19
  tests/test_failure.py
19
20
  tests/test_functions.py
@@ -0,0 +1,61 @@
1
+ from pytest import raises
2
+
3
+ from trilogy.core.exceptions import InvalidSyntaxException
4
+ from trilogy.core.models.execute import (
5
+ BuildConcept,
6
+ BuildDatasource,
7
+ BuildGrain,
8
+ DataType,
9
+ Purpose,
10
+ raise_helpful_join_validation_error,
11
+ )
12
+
13
+
14
+ def test_raise_helpful_join_validation_error():
15
+
16
+ with raises(InvalidSyntaxException):
17
+ raise_helpful_join_validation_error(
18
+ concepts=[
19
+ BuildConcept(
20
+ name="test_concept",
21
+ datatype=DataType.INTEGER,
22
+ purpose=Purpose.KEY,
23
+ build_is_aggregate=False,
24
+ grain=BuildGrain(),
25
+ )
26
+ ],
27
+ left_datasource=BuildDatasource(name="left_ds", columns=[], address="agsg"),
28
+ right_datasource=BuildDatasource(
29
+ name="right_ds", columns=[], address="agsg"
30
+ ),
31
+ )
32
+ with raises(InvalidSyntaxException):
33
+ raise_helpful_join_validation_error(
34
+ concepts=[
35
+ BuildConcept(
36
+ name="test_concept",
37
+ datatype=DataType.INTEGER,
38
+ purpose=Purpose.KEY,
39
+ build_is_aggregate=False,
40
+ grain=BuildGrain(),
41
+ )
42
+ ],
43
+ left_datasource=None,
44
+ right_datasource=BuildDatasource(
45
+ name="right_ds", columns=[], address="agsg"
46
+ ),
47
+ )
48
+ with raises(InvalidSyntaxException):
49
+ raise_helpful_join_validation_error(
50
+ concepts=[
51
+ BuildConcept(
52
+ name="test_concept",
53
+ datatype=DataType.INTEGER,
54
+ purpose=Purpose.KEY,
55
+ build_is_aggregate=False,
56
+ grain=BuildGrain(),
57
+ )
58
+ ],
59
+ left_datasource=BuildDatasource(name="left_ds", columns=[], address="agsg"),
60
+ right_datasource=None,
61
+ )
@@ -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.3.70"
7
+ __version__ = "0.0.3.72"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -134,6 +134,7 @@ class FunctionType(Enum):
134
134
  CAST = "cast"
135
135
  CONCAT = "concat"
136
136
  CONSTANT = "constant"
137
+ TYPED_CONSTANT = "typed_constant"
137
138
  COALESCE = "coalesce"
138
139
  IS_NULL = "isnull"
139
140
  NULLIF = "nullif"
@@ -704,6 +704,11 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
704
704
  output_purpose=Purpose.CONSTANT,
705
705
  arg_count=1,
706
706
  ),
707
+ FunctionType.TYPED_CONSTANT: FunctionConfig(
708
+ output_purpose=Purpose.CONSTANT,
709
+ output_type_function=get_cast_output_type,
710
+ arg_count=2,
711
+ ),
707
712
  FunctionType.IS_NULL: FunctionConfig(
708
713
  output_purpose=Purpose.PROPERTY,
709
714
  output_type=DataType.BOOL,
@@ -4,7 +4,7 @@ from trilogy.core.models.build import BuildConcept, BuildDatasource, BuildWhereC
4
4
 
5
5
 
6
6
  def get_graph_exact_match(
7
- g: nx.DiGraph, conditions: BuildWhereClause | None
7
+ g: nx.DiGraph, accept_partial: bool, conditions: BuildWhereClause | None
8
8
  ) -> set[str]:
9
9
  datasources: dict[str, BuildDatasource | list[BuildDatasource]] = (
10
10
  nx.get_node_attributes(g, "datasource")
@@ -20,6 +20,9 @@ def get_graph_exact_match(
20
20
  if not conditions and not ds.non_partial_for:
21
21
  exact.add(node)
22
22
  continue
23
+ elif not conditions and accept_partial and ds.non_partial_for:
24
+ exact.add(node)
25
+ continue
23
26
  elif conditions:
24
27
  if not ds.non_partial_for:
25
28
  continue
@@ -34,10 +37,11 @@ def get_graph_exact_match(
34
37
 
35
38
  def prune_sources_for_conditions(
36
39
  g: nx.DiGraph,
40
+ accept_partial: bool,
37
41
  conditions: BuildWhereClause | None,
38
42
  ):
39
43
 
40
- complete = get_graph_exact_match(g, conditions)
44
+ complete = get_graph_exact_match(g, accept_partial, conditions)
41
45
  to_remove = []
42
46
  for node in g.nodes:
43
47
  if node.startswith("ds~") and node not in complete:
@@ -1486,6 +1486,17 @@ def get_canonical_pseudonyms(environment: Environment) -> dict[str, set[str]]:
1486
1486
  return roots
1487
1487
 
1488
1488
 
1489
+ def requires_concept_nesting(
1490
+ expr,
1491
+ ) -> AggregateWrapper | WindowItem | FilterItem | Function | None:
1492
+ if isinstance(expr, (AggregateWrapper, WindowItem, FilterItem)):
1493
+ return expr
1494
+ if isinstance(expr, Function) and expr.operator == FunctionType.GROUP:
1495
+ # group by requires nesting
1496
+ return expr
1497
+ return None
1498
+
1499
+
1489
1500
  class Factory:
1490
1501
 
1491
1502
  def __init__(
@@ -1509,11 +1520,12 @@ class Factory:
1509
1520
  | WindowItem
1510
1521
  | FilterItem
1511
1522
  | Function
1512
- | ListWrapper[Any]
1513
- | MapWrapper[Any, Any]
1523
+ | ListWrapper
1524
+ | MapWrapper
1514
1525
  | int
1515
1526
  | float
1516
1527
  | str
1528
+ | date
1517
1529
  ),
1518
1530
  ) -> tuple[Concept, BuildConcept]:
1519
1531
  from trilogy.parsing.common import arbitrary_to_concept
@@ -1572,7 +1584,8 @@ class Factory:
1572
1584
 
1573
1585
  raw_args: list[Concept | FuncArgs] = []
1574
1586
  for arg in base.arguments:
1575
- # to do proper discovery, we need to inject virtual intermediate ocncepts
1587
+ # to do proper discovery, we need to inject virtual intermediate concepts
1588
+ # we don't use requires_concept_nesting here by design
1576
1589
  if isinstance(arg, (AggregateWrapper, FilterItem, WindowItem)):
1577
1590
  narg, _ = self.instantiate_concept(arg)
1578
1591
  raw_args.append(narg)
@@ -1640,11 +1653,10 @@ class Factory:
1640
1653
  def _(self, base: CaseWhen) -> BuildCaseWhen:
1641
1654
 
1642
1655
  comparison = base.comparison
1643
- if isinstance(comparison, (AggregateWrapper, FilterItem, WindowItem)):
1644
- comparison, _ = self.instantiate_concept(comparison)
1645
1656
  expr: Concept | FuncArgs = base.expr
1646
- if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1647
- expr, _ = self.instantiate_concept(expr)
1657
+ validation = requires_concept_nesting(expr)
1658
+ if validation:
1659
+ expr, _ = self.instantiate_concept(validation)
1648
1660
  return BuildCaseWhen.model_construct(
1649
1661
  comparison=self.build(comparison),
1650
1662
  expr=self.build(expr),
@@ -1653,8 +1665,9 @@ class Factory:
1653
1665
  @build.register
1654
1666
  def _(self, base: CaseElse) -> BuildCaseElse:
1655
1667
  expr: Concept | FuncArgs = base.expr
1656
- if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1657
- expr, _ = self.instantiate_concept(expr)
1668
+ validation = requires_concept_nesting(expr)
1669
+ if validation:
1670
+ expr, _ = self.instantiate_concept(validation)
1658
1671
  return BuildCaseElse.model_construct(expr=self.build(expr))
1659
1672
 
1660
1673
  @build.register
@@ -1753,10 +1766,9 @@ class Factory:
1753
1766
  def _(self, base: OrderItem) -> BuildOrderItem:
1754
1767
 
1755
1768
  bexpr: Any
1756
- if isinstance(base.expr, (AggregateWrapper, WindowItem, FilterItem)) or (
1757
- isinstance(base.expr, Function) and base.expr.operator == FunctionType.GROUP
1758
- ):
1759
- bexpr, _ = self.instantiate_concept(base.expr)
1769
+ validation = requires_concept_nesting(base.expr)
1770
+ if validation:
1771
+ bexpr, _ = self.instantiate_concept(validation)
1760
1772
  else:
1761
1773
  bexpr = base.expr
1762
1774
  return BuildOrderItem.model_construct(
@@ -1781,8 +1793,9 @@ class Factory:
1781
1793
  def _(self, base: WindowItem) -> BuildWindowItem:
1782
1794
 
1783
1795
  content: Concept | FuncArgs = base.content
1784
- if isinstance(content, (AggregateWrapper, FilterItem, WindowItem)):
1785
- content, _ = self.instantiate_concept(content)
1796
+ validation = requires_concept_nesting(base.content)
1797
+ if validation:
1798
+ content, _ = self.instantiate_concept(validation)
1786
1799
  final_by = []
1787
1800
  for x in base.order_by:
1788
1801
  if (
@@ -1811,6 +1824,7 @@ class Factory:
1811
1824
  @build.register
1812
1825
  def _(self, base: SubselectComparison) -> BuildSubselectComparison:
1813
1826
  right: Any = base.right
1827
+ # this has specialized logic - include all Functions
1814
1828
  if isinstance(base.right, (AggregateWrapper, WindowItem, FilterItem, Function)):
1815
1829
  right_c, _ = self.instantiate_concept(base.right)
1816
1830
  right = right_c
@@ -1824,12 +1838,14 @@ class Factory:
1824
1838
  def _(self, base: Comparison) -> BuildComparison:
1825
1839
 
1826
1840
  left = base.left
1827
- if isinstance(left, (AggregateWrapper, WindowItem, FilterItem)):
1828
- left_c, _ = self.instantiate_concept(left)
1841
+ validation = requires_concept_nesting(base.left)
1842
+ if validation:
1843
+ left_c, _ = self.instantiate_concept(validation)
1829
1844
  left = left_c # type: ignore
1830
1845
  right = base.right
1831
- if isinstance(right, (AggregateWrapper, WindowItem, FilterItem)):
1832
- right_c, _ = self.instantiate_concept(right)
1846
+ validation = requires_concept_nesting(base.right)
1847
+ if validation:
1848
+ right_c, _ = self.instantiate_concept(validation)
1833
1849
  right = right_c # type: ignore
1834
1850
  return BuildComparison.model_construct(
1835
1851
  left=self.handle_constant(self.build(left)),
@@ -249,6 +249,11 @@ class Datasource(HasUUID, Namespaced, BaseModel):
249
249
  address=self.address,
250
250
  columns=[c.with_namespace(namespace) for c in self.columns],
251
251
  where=self.where.with_namespace(namespace) if self.where else None,
252
+ non_partial_for=(
253
+ self.non_partial_for.with_namespace(namespace)
254
+ if self.non_partial_for
255
+ else None
256
+ ),
252
257
  )
253
258
  return new
254
259
 
@@ -24,12 +24,12 @@ from trilogy.core.enums import (
24
24
  ComparisonOperator,
25
25
  Derivation,
26
26
  FunctionType,
27
- Granularity,
28
27
  JoinType,
29
28
  Modifier,
30
29
  Purpose,
31
30
  SourceType,
32
31
  )
32
+ from trilogy.core.exceptions import InvalidSyntaxException
33
33
  from trilogy.core.models.build import (
34
34
  BuildCaseElse,
35
35
  BuildCaseWhen,
@@ -438,7 +438,7 @@ class ConceptPair(BaseModel):
438
438
 
439
439
 
440
440
  class CTEConceptPair(ConceptPair):
441
- cte: CTE
441
+ cte: CTE | UnionCTE
442
442
 
443
443
 
444
444
  class InstantiatedUnnestJoin(BaseModel):
@@ -460,6 +460,30 @@ class UnnestJoin(BaseModel):
460
460
  return self.alias + "".join([str(s.address) for s in self.concepts])
461
461
 
462
462
 
463
+ def raise_helpful_join_validation_error(
464
+ concepts: List[BuildConcept],
465
+ left_datasource: BuildDatasource | QueryDatasource | None,
466
+ right_datasource: BuildDatasource | QueryDatasource | None,
467
+ ):
468
+
469
+ if not left_datasource or not right_datasource:
470
+ raise InvalidSyntaxException(
471
+ "No mutual keys found, and not two valid datasources"
472
+ )
473
+ left_keys = [c.address for c in left_datasource.output_concepts]
474
+ right_keys = [c.address for c in right_datasource.output_concepts]
475
+ match_concepts = [c.address for c in concepts]
476
+ assert left_datasource
477
+ assert right_datasource
478
+ raise InvalidSyntaxException(
479
+ "No mutual join keys found between"
480
+ f" {left_datasource.identifier} and"
481
+ f" {right_datasource.identifier}, left_keys {left_keys},"
482
+ f" right_keys {right_keys},"
483
+ f" provided join concepts {match_concepts}"
484
+ )
485
+
486
+
463
487
  class BaseJoin(BaseModel):
464
488
  right_datasource: Union[BuildDatasource, "QueryDatasource"]
465
489
  join_type: JoinType
@@ -479,27 +503,24 @@ class BaseJoin(BaseModel):
479
503
  )
480
504
 
481
505
  # Early returns maintained as in original code
482
- if self.concept_pairs:
483
- return self
484
-
485
- if self.concepts == []:
506
+ if self.concept_pairs or self.concepts == []:
486
507
  return self
487
508
 
488
- # Validation logic
509
+ # reduce concept list to just the mutual keys
489
510
  final_concepts = []
490
- assert self.left_datasource and self.right_datasource
491
-
492
511
  for concept in self.concepts or []:
493
512
  include = True
494
513
  for ds in [self.left_datasource, self.right_datasource]:
495
514
  synonyms = []
515
+ if not ds:
516
+ continue
496
517
  for c in ds.output_concepts:
497
518
  synonyms += list(c.pseudonyms)
498
519
  if (
499
- concept.address not in [c.address for c in ds.output_concepts]
520
+ concept.address not in ds.output_concepts
500
521
  and concept.address not in synonyms
501
522
  ):
502
- raise SyntaxError(
523
+ raise InvalidSyntaxException(
503
524
  f"Invalid join, missing {concept} on {ds.name}, have"
504
525
  f" {[c.address for c in ds.output_concepts]}"
505
526
  )
@@ -507,32 +528,10 @@ class BaseJoin(BaseModel):
507
528
  final_concepts.append(concept)
508
529
 
509
530
  if not final_concepts and self.concepts:
510
- # if one datasource only has constants
511
- # we can join on 1=1
512
- for ds in [self.left_datasource, self.right_datasource]:
513
- # single rows
514
- if all(
515
- [
516
- c.granularity == Granularity.SINGLE_ROW
517
- for c in ds.output_concepts
518
- ]
519
- ):
520
- self.concepts = []
521
- return self
522
- # if everything is at abstract grain, we can skip joins
523
- if all([c.grain.abstract for c in ds.output_concepts]):
524
- self.concepts = []
525
- return self
526
-
527
- left_keys = [c.address for c in self.left_datasource.output_concepts]
528
- right_keys = [c.address for c in self.right_datasource.output_concepts]
529
- match_concepts = [c.address for c in self.concepts]
530
- raise SyntaxError(
531
- "No mutual join keys found between"
532
- f" {self.left_datasource.identifier} and"
533
- f" {self.right_datasource.identifier}, left_keys {left_keys},"
534
- f" right_keys {right_keys},"
535
- f" provided join concepts {match_concepts}"
531
+ raise_helpful_join_validation_error(
532
+ self.concepts,
533
+ self.left_datasource,
534
+ self.right_datasource,
536
535
  )
537
536
 
538
537
  self.concepts = final_concepts
@@ -1087,7 +1086,7 @@ class UnionCTE(BaseModel):
1087
1086
  class Join(BaseModel):
1088
1087
  right_cte: CTE | UnionCTE
1089
1088
  jointype: JoinType
1090
- left_cte: CTE | None = None
1089
+ left_cte: CTE | UnionCTE | None = None
1091
1090
  joinkey_pairs: List[CTEConceptPair] | None = None
1092
1091
  inlined_ctes: set[str] = Field(default_factory=set)
1093
1092
  quote: str | None = None
@@ -1096,7 +1095,7 @@ class Join(BaseModel):
1096
1095
  def inline_cte(self, cte: CTE):
1097
1096
  self.inlined_ctes.add(cte.name)
1098
1097
 
1099
- def get_name(self, cte: CTE):
1098
+ def get_name(self, cte: CTE | UnionCTE) -> str:
1100
1099
  if cte.identifier in self.inlined_ctes:
1101
1100
  return cte.source.datasources[0].safe_identifier
1102
1101
  return cte.safe_identifier
@@ -291,7 +291,7 @@ def evaluate_loop_conditions(
291
291
 
292
292
 
293
293
  def check_for_early_exit(
294
- complete, partial, context: LoopContext, priority_concept: BuildConcept
294
+ complete, partial, missing, context: LoopContext, priority_concept: BuildConcept
295
295
  ) -> bool:
296
296
  if complete == ValidationResult.INCOMPLETE_CONDITION:
297
297
  cond_dict = {str(node): node.preexisting_conditions for node in context.stack}
@@ -321,7 +321,7 @@ def check_for_early_exit(
321
321
  )
322
322
  else:
323
323
  logger.info(
324
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Not complete, continuing search"
324
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Not complete (missing {missing}), continuing search"
325
325
  )
326
326
  # if we have attempted on root node, we've tried them all.
327
327
  # inject in another search with filter concepts
@@ -412,7 +412,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
412
412
  elif context.conditions:
413
413
  output.preexisting_conditions = context.conditions.conditional
414
414
  logger.info(
415
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts]} with {context.conditions}"
415
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node output {[x.address for x in output.usable_outputs]} partial {[c.address for c in output.partial_concepts or []]} with {context.conditions}"
416
416
  )
417
417
  if condition_required and context.conditions and non_virtual_different:
418
418
  logger.info(
@@ -532,7 +532,9 @@ def _search_concepts(
532
532
  )
533
533
  # assign
534
534
  context.found = found_c
535
- early_exit = check_for_early_exit(complete, partial, context, priority_concept)
535
+ early_exit = check_for_early_exit(
536
+ complete, partial, missing_c, context, priority_concept
537
+ )
536
538
  if early_exit:
537
539
  break
538
540
 
@@ -253,7 +253,9 @@ def resolve_weak_components(
253
253
  break_flag = False
254
254
  found = []
255
255
  search_graph = environment_graph.copy()
256
- prune_sources_for_conditions(search_graph, conditions=search_conditions)
256
+ prune_sources_for_conditions(
257
+ search_graph, accept_partial, conditions=search_conditions
258
+ )
257
259
  reduced_concept_sets: list[set[str]] = []
258
260
 
259
261
  # loop through, removing new nodes we find
@@ -181,6 +181,5 @@ def gen_rowset_node(
181
181
  node,
182
182
  enrich_node,
183
183
  ],
184
- partial_concepts=node.partial_concepts + enrich_node.partial_concepts,
185
184
  preexisting_conditions=conditions.conditional if conditions else None,
186
185
  )
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from collections import defaultdict
2
3
  from datetime import date, datetime, timedelta
3
4
  from typing import List, Tuple, TypeVar
@@ -14,7 +15,7 @@ from trilogy.core.models.build import (
14
15
  from trilogy.core.models.core import DataType
15
16
 
16
17
  # Define a generic type that ensures start and end are the same type
17
- T = TypeVar("T", int, date, datetime)
18
+ T = TypeVar("T", int, float, date, datetime)
18
19
 
19
20
 
20
21
  def reduce_expression(
@@ -37,21 +38,26 @@ def reduce_expression(
37
38
  elif var.datatype == DataType.DATETIME:
38
39
  lower_check = datetime.min # type: ignore
39
40
  upper_check = datetime.max # type: ignore
41
+ elif var.datatype == DataType.BOOL:
42
+ lower_check = False # type: ignore
43
+ upper_check = True # type: ignore
44
+ elif var.datatype == DataType.FLOAT:
45
+ lower_check = float("-inf") # type: ignore
46
+ upper_check = float("inf") # type: ignore
40
47
  else:
41
- raise ValueError(f"Invalid datatype: {var.datatype}")
48
+ return False
42
49
 
43
50
  ranges: list[Tuple[T, T]] = []
44
51
  for op, value in group_tuple:
45
- increment: int | timedelta
52
+ increment: int | timedelta | float
46
53
  if isinstance(value, date):
47
54
  increment = timedelta(days=1)
48
55
  elif isinstance(value, datetime):
49
56
  increment = timedelta(seconds=1)
50
57
  elif isinstance(value, int):
51
58
  increment = 1
52
- # elif isinstance(value, float):
53
- # value = Decimal(value)
54
- # increment = Decimal(0.0000000001)
59
+ elif isinstance(value, float):
60
+ increment = sys.float_info.epsilon
55
61
 
56
62
  if op == ">":
57
63
  ranges.append(
@@ -88,24 +94,43 @@ def reduce_expression(
88
94
  value,
89
95
  )
90
96
  )
97
+ elif op == ComparisonOperator.IS:
98
+ ranges.append(
99
+ (
100
+ value,
101
+ value,
102
+ )
103
+ )
104
+ elif op == ComparisonOperator.NE:
105
+ pass
91
106
  else:
92
- raise ValueError(f"Invalid operator: {op}")
107
+ return False
93
108
  return is_fully_covered(lower_check, upper_check, ranges, increment)
94
109
 
95
110
 
111
+ TARGET_TYPES = (
112
+ int,
113
+ date,
114
+ float,
115
+ datetime,
116
+ bool,
117
+ )
118
+ REDUCABLE_TYPES = (int, float, date, bool, datetime, BuildFunction)
119
+
120
+
96
121
  def simplify_conditions(
97
122
  conditions: list[BuildComparison | BuildConditional | BuildParenthetical],
98
123
  ) -> bool:
99
124
  # Group conditions by variable
100
125
  grouped: dict[
101
- BuildConcept, list[tuple[ComparisonOperator, datetime | int | date]]
126
+ BuildConcept, list[tuple[ComparisonOperator, datetime | int | date | float]]
102
127
  ] = defaultdict(list)
103
128
  for condition in conditions:
104
129
  if not isinstance(condition, BuildComparison):
105
130
  return False
106
- if not isinstance(
107
- condition.left, (int, date, datetime, BuildFunction)
108
- ) and not isinstance(condition.right, (int, date, datetime, BuildFunction)):
131
+ if not isinstance(condition.left, REDUCABLE_TYPES) and not isinstance(
132
+ condition.right, REDUCABLE_TYPES
133
+ ):
109
134
  return False
110
135
  if not isinstance(condition.left, BuildConcept) and not isinstance(
111
136
  condition.right, BuildConcept
@@ -113,15 +138,20 @@ def simplify_conditions(
113
138
  return False
114
139
  vars = [condition.left, condition.right]
115
140
  concept = [x for x in vars if isinstance(x, BuildConcept)][0]
116
- comparison = [x for x in vars if not isinstance(x, BuildConcept)][0]
117
- if isinstance(comparison, BuildFunction):
118
- if not comparison.operator == FunctionType.CONSTANT:
141
+ raw_comparison = [x for x in vars if not isinstance(x, BuildConcept)][0]
142
+ if isinstance(raw_comparison, BuildFunction):
143
+ if not raw_comparison.operator == FunctionType.CONSTANT:
119
144
  return False
120
- first_arg = comparison.arguments[0]
121
- if not isinstance(first_arg, (int, date, datetime)):
145
+ first_arg = raw_comparison.arguments[0]
146
+ if not isinstance(first_arg, TARGET_TYPES):
122
147
  return False
123
148
  comparison = first_arg
124
- if not isinstance(comparison, (int, date, datetime)):
149
+ else:
150
+ if not isinstance(raw_comparison, TARGET_TYPES):
151
+ return False
152
+ comparison = raw_comparison
153
+
154
+ if not isinstance(comparison, REDUCABLE_TYPES):
125
155
  return False
126
156
 
127
157
  var = concept
@@ -136,11 +166,25 @@ def simplify_conditions(
136
166
  return True if all(isinstance(s, bool) and s for s in simplified) else False
137
167
 
138
168
 
169
+ def boolean_fully_covered(
170
+ start: bool,
171
+ end: bool,
172
+ ranges: List[Tuple[bool, bool]],
173
+ ):
174
+ all = []
175
+ for r_start, r_end in ranges:
176
+ if r_start is True and r_end is True:
177
+ all.append(True)
178
+ elif r_start is False and r_end is False:
179
+ all.append(False)
180
+ return set(all) == {False, True}
181
+
182
+
139
183
  def is_fully_covered(
140
184
  start: T,
141
185
  end: T,
142
186
  ranges: List[Tuple[T, T]],
143
- increment: int | timedelta,
187
+ increment: int | timedelta | float,
144
188
  ):
145
189
  """
146
190
  Check if the list of range pairs fully covers the set [start, end].
@@ -153,6 +197,11 @@ def is_fully_covered(
153
197
  Returns:
154
198
  - bool: True if the ranges fully cover [start, end], False otherwise.
155
199
  """
200
+ if isinstance(start, bool) and isinstance(end, bool):
201
+ # convert each element of each tuple to a boolean
202
+ bool_ranges = [(bool(r_start), bool(r_end)) for r_start, r_end in ranges]
203
+
204
+ return boolean_fully_covered(start, end, bool_ranges)
156
205
  # Sort ranges by their start values (and by end values for ties)
157
206
  ranges.sort()
158
207
 
@@ -173,14 +222,14 @@ def get_union_sources(
173
222
  datasources: list[BuildDatasource], concepts: list[BuildConcept]
174
223
  ) -> List[list[BuildDatasource]]:
175
224
  candidates: list[BuildDatasource] = []
225
+
176
226
  for x in datasources:
177
- if all([c.address in x.output_concepts for c in concepts]):
227
+ if any([c.address in x.output_concepts for c in concepts]):
178
228
  if (
179
229
  any([c.address in x.partial_concepts for c in concepts])
180
230
  and x.non_partial_for
181
231
  ):
182
232
  candidates.append(x)
183
-
184
233
  assocs: dict[str, list[BuildDatasource]] = defaultdict(list[BuildDatasource])
185
234
  for x in candidates:
186
235
  if not x.non_partial_for: