pytrilogy 0.0.3.69__tar.gz → 0.0.3.71__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.69/pytrilogy.egg-info → pytrilogy-0.0.3.71}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/SOURCES.txt +1 -0
  4. pytrilogy-0.0.3.71/tests/test_execute_models.py +61 -0
  5. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/__init__.py +1 -1
  6. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/functions.py +2 -2
  7. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/graph_models.py +6 -2
  8. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/datasource.py +5 -0
  9. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/execute.py +38 -39
  10. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/concept_strategies_v3.py +6 -4
  11. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/node_merge_node.py +3 -1
  12. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/rowset_node.py +0 -1
  13. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +69 -20
  14. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_merge_node.py +70 -33
  15. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/base_node.py +16 -12
  16. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/union_node.py +9 -1
  17. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/query_processor.py +5 -3
  18. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/base.py +4 -1
  19. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/duckdb.py +17 -0
  20. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/parse_engine.py +3 -0
  21. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/trilogy.lark +1 -1
  22. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/LICENSE.md +0 -0
  23. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/README.md +0 -0
  24. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pyproject.toml +0 -0
  25. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/dependency_links.txt +0 -0
  26. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/entry_points.txt +0 -0
  27. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/requires.txt +0 -0
  28. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/pytrilogy.egg-info/top_level.txt +0 -0
  29. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/setup.cfg +0 -0
  30. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/setup.py +0 -0
  31. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_datatypes.py +0 -0
  32. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_declarations.py +0 -0
  33. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_derived_concepts.py +0 -0
  34. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_discovery_nodes.py +0 -0
  35. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_enums.py +0 -0
  36. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_environment.py +0 -0
  37. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_executor.py +0 -0
  38. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_failure.py +0 -0
  39. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_functions.py +0 -0
  40. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_imports.py +0 -0
  41. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_metadata.py +0 -0
  42. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_models.py +0 -0
  43. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_multi_join_assignments.py +0 -0
  44. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_parse_engine.py +0 -0
  45. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_parsing.py +0 -0
  46. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_parsing_failures.py +0 -0
  47. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_query_render.py +0 -0
  50. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_select.py +0 -0
  51. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_show.py +0 -0
  52. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_statements.py +0 -0
  53. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_typing.py +0 -0
  54. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_undefined_concept.py +0 -0
  55. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_user_functions.py +0 -0
  56. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/tests/test_where_clause.py +0 -0
  57. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/authoring/__init__.py +0 -0
  58. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/constants.py +0 -0
  60. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/__init__.py +0 -0
  61. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/constants.py +0 -0
  62. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/enums.py +0 -0
  63. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/env_processor.py +0 -0
  64. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/environment_helpers.py +0 -0
  65. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/ergonomics.py +0 -0
  66. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/exceptions.py +0 -0
  67. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/__init__.py +0 -0
  69. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/author.py +0 -0
  70. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/build.py +0 -0
  71. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/build_environment.py +0 -0
  72. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/core.py +0 -0
  73. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/models/environment.py +0 -0
  74. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/optimization.py +0 -0
  75. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/__init__.py +0 -0
  76. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/base_optimization.py +0 -0
  77. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/inline_datasource.py +0 -0
  78. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  79. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/__init__.py +0 -0
  80. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_loop.py +0 -0
  81. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_node_factory.py +0 -0
  82. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_utility.py +0 -0
  83. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/discovery_validation.py +0 -0
  84. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/graph_utils.py +0 -0
  85. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/__init__.py +0 -0
  86. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  87. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/common.py +0 -0
  88. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  89. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/group_node.py +0 -0
  90. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  91. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  92. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  93. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  94. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/select_node.py +0 -0
  95. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  96. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/union_node.py +0 -0
  97. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  98. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/node_generators/window_node.py +0 -0
  99. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/__init__.py +0 -0
  100. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/filter_node.py +0 -0
  101. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/group_node.py +0 -0
  102. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/merge_node.py +0 -0
  103. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  104. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  105. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  106. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/nodes/window_node.py +0 -0
  107. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/processing/utility.py +0 -0
  108. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/statements/__init__.py +0 -0
  109. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/statements/author.py +0 -0
  110. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/statements/build.py +0 -0
  111. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/statements/common.py +0 -0
  112. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/statements/execute.py +0 -0
  113. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/core/utility.py +0 -0
  114. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/__init__.py +0 -0
  115. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/bigquery.py +0 -0
  116. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/common.py +0 -0
  117. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/config.py +0 -0
  118. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/dataframe.py +0 -0
  119. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/enums.py +0 -0
  120. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/postgres.py +0 -0
  121. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/presto.py +0 -0
  122. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/snowflake.py +0 -0
  123. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/dialect/sql_server.py +0 -0
  124. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/engine.py +0 -0
  125. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/executor.py +0 -0
  126. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/hooks/__init__.py +0 -0
  127. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/hooks/base_hook.py +0 -0
  128. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/hooks/graph_hook.py +0 -0
  129. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/hooks/query_debugger.py +0 -0
  130. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/metadata/__init__.py +0 -0
  131. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parser.py +0 -0
  132. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/__init__.py +0 -0
  133. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/common.py +0 -0
  134. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/config.py +0 -0
  135. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/exceptions.py +0 -0
  136. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/helpers.py +0 -0
  137. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/parsing/render.py +0 -0
  138. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/py.typed +0 -0
  139. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/render.py +0 -0
  140. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/scripts/__init__.py +0 -0
  141. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/scripts/trilogy.py +0 -0
  142. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/__init__.py +0 -0
  143. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/date.preql +0 -0
  144. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/display.preql +0 -0
  145. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/geography.preql +0 -0
  146. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/money.preql +0 -0
  147. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/net.preql +0 -0
  148. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/ranking.preql +0 -0
  149. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/std/report.preql +0 -0
  150. {pytrilogy-0.0.3.69 → pytrilogy-0.0.3.71}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.69
3
+ Version: 0.0.3.71
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.69
3
+ Version: 0.0.3.71
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.69"
7
+ __version__ = "0.0.3.71"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -370,10 +370,10 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
370
370
  arg_count=2,
371
371
  ),
372
372
  FunctionType.REGEXP_EXTRACT: FunctionConfig(
373
- valid_inputs={DataType.STRING},
373
+ valid_inputs=[{DataType.STRING}, {DataType.STRING}, {DataType.INTEGER}],
374
374
  output_purpose=Purpose.PROPERTY,
375
375
  output_type=DataType.STRING,
376
- arg_count=2,
376
+ arg_count=3,
377
377
  ),
378
378
  FunctionType.REGEXP_REPLACE: FunctionConfig(
379
379
  valid_inputs={DataType.STRING},
@@ -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:
@@ -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: