pytrilogy 0.0.3.48__tar.gz → 0.0.3.52__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 (140) hide show
  1. {pytrilogy-0.0.3.48/pytrilogy.egg-info → pytrilogy-0.0.3.52}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_functions.py +4 -0
  4. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_parse_engine.py +23 -1
  5. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_parsing.py +12 -4
  6. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/enums.py +3 -0
  8. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/functions.py +27 -2
  9. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/author.py +12 -9
  10. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/build.py +18 -12
  11. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/execute.py +29 -13
  12. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/concept_strategies_v3.py +1 -1
  13. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/common.py +3 -4
  14. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/filter_node.py +142 -91
  15. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/group_node.py +3 -4
  16. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/base_node.py +4 -1
  17. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/statements/author.py +0 -2
  18. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/base.py +7 -2
  19. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/bigquery.py +2 -0
  20. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/duckdb.py +2 -0
  21. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/common.py +28 -18
  22. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/parse_engine.py +45 -12
  23. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/trilogy.lark +10 -4
  24. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/README.md +0 -0
  26. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/setup.cfg +0 -0
  33. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/setup.py +0 -0
  34. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_executor.py +0 -0
  41. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_failure.py +0 -0
  42. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_imports.py +0 -0
  43. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_metadata.py +0 -0
  44. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_models.py +0 -0
  45. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_multi_join_assignments.py +0 -0
  46. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_parsing_failures.py +0 -0
  47. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_query_render.py +0 -0
  50. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_select.py +0 -0
  51. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_show.py +0 -0
  52. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_statements.py +0 -0
  53. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_typing.py +0 -0
  54. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_undefined_concept.py +0 -0
  55. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_user_functions.py +0 -0
  56. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/tests/test_where_clause.py +0 -0
  57. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/authoring/__init__.py +0 -0
  58. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/constants.py +0 -0
  60. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/__init__.py +0 -0
  61. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/constants.py +0 -0
  62. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/env_processor.py +0 -0
  63. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/environment_helpers.py +0 -0
  64. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/ergonomics.py +0 -0
  65. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/exceptions.py +0 -0
  66. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/graph_models.py +0 -0
  67. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/__init__.py +0 -0
  69. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/build_environment.py +0 -0
  70. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/core.py +0 -0
  71. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/datasource.py +0 -0
  72. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/models/environment.py +0 -0
  73. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/optimization.py +0 -0
  74. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/optimizations/__init__.py +0 -0
  75. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/optimizations/base_optimization.py +0 -0
  76. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/optimizations/inline_datasource.py +0 -0
  77. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  78. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/__init__.py +0 -0
  79. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/graph_utils.py +0 -0
  80. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/__init__.py +0 -0
  81. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  82. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  83. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  84. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  85. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  86. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  87. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  88. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  89. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/select_node.py +0 -0
  90. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  91. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/union_node.py +0 -0
  92. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  93. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/node_generators/window_node.py +0 -0
  94. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/__init__.py +0 -0
  95. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/filter_node.py +0 -0
  96. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/group_node.py +0 -0
  97. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/merge_node.py +0 -0
  98. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  99. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/union_node.py +0 -0
  100. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  101. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/nodes/window_node.py +0 -0
  102. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/processing/utility.py +0 -0
  103. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/query_processor.py +0 -0
  104. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/statements/__init__.py +0 -0
  105. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/statements/build.py +0 -0
  106. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/statements/common.py +0 -0
  107. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/core/statements/execute.py +0 -0
  108. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/__init__.py +0 -0
  109. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/common.py +0 -0
  110. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/config.py +0 -0
  111. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/dataframe.py +0 -0
  112. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/enums.py +0 -0
  113. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/postgres.py +0 -0
  114. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/presto.py +0 -0
  115. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/snowflake.py +0 -0
  116. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/dialect/sql_server.py +0 -0
  117. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/engine.py +0 -0
  118. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/executor.py +0 -0
  119. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/hooks/__init__.py +0 -0
  120. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/hooks/base_hook.py +0 -0
  121. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/hooks/graph_hook.py +0 -0
  122. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/hooks/query_debugger.py +0 -0
  123. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/metadata/__init__.py +0 -0
  124. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parser.py +0 -0
  125. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/__init__.py +0 -0
  126. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/config.py +0 -0
  127. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/exceptions.py +0 -0
  128. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/helpers.py +0 -0
  129. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/parsing/render.py +0 -0
  130. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/py.typed +0 -0
  131. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/render.py +0 -0
  132. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/scripts/__init__.py +0 -0
  133. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/scripts/trilogy.py +0 -0
  134. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/__init__.py +0 -0
  135. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/std/report.preql +0 -0
  140. {pytrilogy-0.0.3.48 → pytrilogy-0.0.3.52}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.48
3
+ Version: 0.0.3.52
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.48
3
+ Version: 0.0.3.52
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -214,6 +214,8 @@ def test_math_functions(test_environment):
214
214
  property order_id.order_nested <- revenue * 2/2;
215
215
  property order_id.rounded <- round(revenue + 2.01,2);
216
216
  property order_id.rounded_default <- round(revenue + 2.01);
217
+ property order_id.floor <- floor(revenue + 2.01);
218
+ property order_id.ceil <- ceil(revenue + 2.01);
217
219
  constant random <- random(1);
218
220
  select
219
221
  order_id,
@@ -224,6 +226,8 @@ def test_math_functions(test_environment):
224
226
  order_add,
225
227
  rounded,
226
228
  rounded_default,
229
+ floor,
230
+ ceil,
227
231
  random,
228
232
  ;"""
229
233
  env, parsed = parse(declarations, environment=test_environment)
@@ -2,7 +2,12 @@ from pytest import raises
2
2
 
3
3
  from trilogy.core.exceptions import UndefinedConceptException
4
4
  from trilogy.core.models.environment import Environment
5
- from trilogy.parsing.parse_engine import PARSER, ParseToObjects, unpack_visit_error
5
+ from trilogy.parsing.parse_engine import (
6
+ PARSER,
7
+ InvalidSyntaxException,
8
+ ParseToObjects,
9
+ unpack_visit_error,
10
+ )
6
11
 
7
12
  TEXT = """
8
13
  const a <- 1;
@@ -29,3 +34,20 @@ def test_parser():
29
34
  with raises(UndefinedConceptException):
30
35
  unpack_visit_error(e)
31
36
  assert failed
37
+
38
+
39
+ TEXT2 = """
40
+ const a <- 1;
41
+
42
+ select
43
+ a,
44
+ FROM a
45
+ ;
46
+ """
47
+
48
+
49
+ def test_from_error():
50
+ env = Environment()
51
+
52
+ with raises(InvalidSyntaxException):
53
+ env.parse(TEXT2)
@@ -92,7 +92,12 @@ def test_arg_to_datatype():
92
92
 
93
93
 
94
94
  def test_argument_to_purpose(test_environment: Environment):
95
- assert argument_to_purpose(1.00) == Purpose.CONSTANT
95
+ assert (
96
+ argument_to_purpose(
97
+ 1.00,
98
+ )
99
+ == Purpose.CONSTANT
100
+ )
96
101
  assert argument_to_purpose("test") == Purpose.CONSTANT
97
102
  assert argument_to_purpose(test_environment.concepts["order_id"]) == Purpose.KEY
98
103
  assert (
@@ -100,19 +105,22 @@ def test_argument_to_purpose(test_environment: Environment):
100
105
  [
101
106
  "test",
102
107
  1.00,
103
- ]
108
+ ],
109
+ test_environment,
104
110
  )
105
111
  == Purpose.CONSTANT
106
112
  )
107
113
  assert (
108
114
  function_args_to_output_purpose(
109
- ["test", 1.00, test_environment.concepts["order_id"]]
115
+ ["test", 1.00, test_environment.concepts["order_id"]], test_environment
110
116
  )
111
117
  == Purpose.PROPERTY
112
118
  )
113
119
  unnest_env, parsed = parse_text("const random <- unnest([1,2,3,4]);")
114
120
  assert (
115
- function_args_to_output_purpose([unnest_env.concepts["random"]])
121
+ function_args_to_output_purpose(
122
+ [unnest_env.concepts["random"]], test_environment
123
+ )
116
124
  == Purpose.PROPERTY
117
125
  )
118
126
 
@@ -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.48"
7
+ __version__ = "0.0.3.52"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -131,6 +131,7 @@ class FunctionType(Enum):
131
131
  CONSTANT = "constant"
132
132
  COALESCE = "coalesce"
133
133
  IS_NULL = "isnull"
134
+ NULLIF = "nullif"
134
135
  BOOL = "bool"
135
136
 
136
137
  # COMPLEX
@@ -156,6 +157,8 @@ class FunctionType(Enum):
156
157
  ABS = "abs"
157
158
  SQRT = "sqrt"
158
159
  RANDOM = "random"
160
+ FLOOR = "floor"
161
+ CEIL = "ceil"
159
162
 
160
163
  # Aggregates
161
164
  ## group is not a real aggregate - it just means group by this + some other set of fields
@@ -279,6 +279,12 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
279
279
  output_purpose=Purpose.PROPERTY,
280
280
  arg_count=1,
281
281
  ),
282
+ FunctionType.NULLIF: FunctionConfig(
283
+ valid_inputs={*DataType},
284
+ output_purpose=Purpose.PROPERTY,
285
+ output_type_function=lambda args: get_output_type_at_index(args, 0),
286
+ arg_count=2,
287
+ ),
282
288
  FunctionType.COALESCE: FunctionConfig(
283
289
  valid_inputs={*DataType},
284
290
  output_purpose=Purpose.PROPERTY,
@@ -637,6 +643,22 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
637
643
  output_type_function=lambda args: get_output_type_at_index(args, 0),
638
644
  arg_count=2,
639
645
  ),
646
+ FunctionType.FLOOR: FunctionConfig(
647
+ valid_inputs=[
648
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
649
+ ],
650
+ output_purpose=Purpose.PROPERTY,
651
+ output_type=DataType.INTEGER,
652
+ arg_count=1,
653
+ ),
654
+ FunctionType.CEIL: FunctionConfig(
655
+ valid_inputs=[
656
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
657
+ ],
658
+ output_purpose=Purpose.PROPERTY,
659
+ output_type=DataType.INTEGER,
660
+ arg_count=1,
661
+ ),
640
662
  FunctionType.CUSTOM: FunctionConfig(
641
663
  output_purpose=Purpose.PROPERTY,
642
664
  arg_count=InfiniteFunctionArgs,
@@ -787,13 +809,14 @@ def create_function_derived_concept(
787
809
  namespace: str,
788
810
  operator: FunctionType,
789
811
  arguments: list[Concept],
812
+ environment: Environment,
790
813
  output_type: Optional[
791
814
  DataType | ListType | StructType | MapType | NumericType | TraitDataType
792
815
  ] = None,
793
816
  output_purpose: Optional[Purpose] = None,
794
817
  ) -> Concept:
795
818
  purpose = (
796
- function_args_to_output_purpose(arguments)
819
+ function_args_to_output_purpose(arguments, environment=environment)
797
820
  if output_purpose is None
798
821
  else output_purpose
799
822
  )
@@ -846,13 +869,15 @@ def argument_to_purpose(arg) -> Purpose:
846
869
  raise ValueError(f"Cannot parse arg purpose for {arg} of type {type(arg)}")
847
870
 
848
871
 
849
- def function_args_to_output_purpose(args) -> Purpose:
872
+ def function_args_to_output_purpose(args, environment: Environment) -> Purpose:
850
873
  has_metric = False
851
874
  has_non_constant = False
852
875
  has_non_single_row_constant = False
853
876
  if not args:
854
877
  return Purpose.CONSTANT
855
878
  for arg in args:
879
+ if isinstance(arg, ConceptRef):
880
+ arg = environment.concepts[arg.address]
856
881
  purpose = argument_to_purpose(arg)
857
882
  if purpose == Purpose.METRIC:
858
883
  has_metric = True
@@ -25,6 +25,7 @@ from pydantic import (
25
25
  ValidationInfo,
26
26
  computed_field,
27
27
  field_validator,
28
+ model_validator,
28
29
  )
29
30
 
30
31
  from trilogy.constants import DEFAULT_NAMESPACE, MagicConstants
@@ -621,8 +622,8 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
621
622
  return v.reference
622
623
  return v
623
624
 
624
- def __init__(self, *args, **kwargs) -> None:
625
- super().__init__(*args, **kwargs)
625
+ @model_validator(mode="after")
626
+ def validate_comparison(self):
626
627
  if self.operator in (ComparisonOperator.IS, ComparisonOperator.IS_NOT):
627
628
  if self.right != MagicConstants.NULL and DataType.BOOL != arg_to_datatype(
628
629
  self.right
@@ -632,7 +633,6 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
632
633
  )
633
634
  elif self.operator in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
634
635
  right_type = arg_to_datatype(self.right)
635
-
636
636
  if isinstance(right_type, ListType) and not is_compatible_datatype(
637
637
  arg_to_datatype(self.left), right_type.value_data_type
638
638
  ):
@@ -653,6 +653,8 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
653
653
  f"Cannot compare {arg_to_datatype(self.left)} and {arg_to_datatype(self.right)} of different types with operator {self.operator} in {str(self)}"
654
654
  )
655
655
 
656
+ return self
657
+
656
658
  def __add__(self, other):
657
659
  if other is None:
658
660
  return self
@@ -1022,7 +1024,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1022
1024
  keys = self.keys
1023
1025
 
1024
1026
  if self.is_aggregate and isinstance(new_lineage, Function) and grain.components:
1025
- grain_components = [
1027
+ grain_components: list[ConceptRef | Concept] = [
1026
1028
  environment.concepts[c].reference for c in grain.components
1027
1029
  ]
1028
1030
  new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
@@ -1847,9 +1849,6 @@ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel)
1847
1849
  function: Function
1848
1850
  by: List[ConceptRef | Concept] = Field(default_factory=list)
1849
1851
 
1850
- def __init__(self, **kwargs):
1851
- super().__init__(**kwargs)
1852
-
1853
1852
  @field_validator("by", mode="before")
1854
1853
  @classmethod
1855
1854
  def enforce_concept_ref(cls, v):
@@ -1945,11 +1944,15 @@ class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1945
1944
 
1946
1945
  @property
1947
1946
  def output_datatype(self):
1948
- return self.content.datatype
1947
+ return arg_to_datatype(self.content)
1949
1948
 
1950
1949
  @property
1951
1950
  def concept_arguments(self):
1952
- return [self.content] + self.where.concept_arguments
1951
+ if isinstance(self.content, ConceptRef):
1952
+ return [self.content] + self.where.concept_arguments
1953
+ elif isinstance(self.content, ConceptArgs):
1954
+ return self.content.concept_arguments + self.where.concept_arguments
1955
+ return self.where.concept_arguments
1953
1956
 
1954
1957
 
1955
1958
  class RowsetLineage(Namespaced, Mergeable, BaseModel):
@@ -262,11 +262,8 @@ class BuildGrain(BaseModel):
262
262
  components: set[str] = Field(default_factory=set)
263
263
  where_clause: Optional[BuildWhereClause] = None
264
264
 
265
- def __init__(self, **kwargs):
266
- super().__init__(**kwargs)
267
-
268
265
  def without_condition(self):
269
- return BuildGrain(components=self.components)
266
+ return BuildGrain.model_construct(components=self.components)
270
267
 
271
268
  @classmethod
272
269
  def from_concepts(
@@ -321,12 +318,12 @@ class BuildGrain(BaseModel):
321
318
  # raise NotImplementedError(
322
319
  # f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
323
320
  # )
324
- return BuildGrain(
321
+ return BuildGrain.model_construct(
325
322
  components=self.components.union(other.components), where_clause=where
326
323
  )
327
324
 
328
325
  def __sub__(self, other: "BuildGrain") -> "BuildGrain":
329
- return BuildGrain(
326
+ return BuildGrain.model_construct(
330
327
  components=self.components.difference(other.components),
331
328
  where_clause=self.where_clause,
332
329
  )
@@ -637,9 +634,6 @@ class BuildComparison(BuildConceptArgs, ConstantInlineable, BaseModel):
637
634
  ]
638
635
  operator: ComparisonOperator
639
636
 
640
- def __init__(self, *args, **kwargs) -> None:
641
- super().__init__(*args, **kwargs)
642
-
643
637
  def __add__(self, other):
644
638
  if other is None:
645
639
  return self
@@ -1173,7 +1167,7 @@ class BuildAggregateWrapper(BuildConceptArgs, DataTyped, BaseModel):
1173
1167
 
1174
1168
 
1175
1169
  class BuildFilterItem(BuildConceptArgs, BaseModel):
1176
- content: BuildConcept
1170
+ content: "BuildExpr"
1177
1171
  where: BuildWhereClause
1178
1172
 
1179
1173
  def __str__(self):
@@ -1181,15 +1175,27 @@ class BuildFilterItem(BuildConceptArgs, BaseModel):
1181
1175
 
1182
1176
  @property
1183
1177
  def output_datatype(self):
1184
- return self.content.datatype
1178
+ return arg_to_datatype(self.content)
1185
1179
 
1186
1180
  @property
1187
1181
  def output_purpose(self):
1188
1182
  return self.content.purpose
1189
1183
 
1184
+ @property
1185
+ def content_concept_arguments(self):
1186
+ if isinstance(self.content, BuildConcept):
1187
+ return [self.content]
1188
+ elif isinstance(self.content, BuildConceptArgs):
1189
+ return self.content.concept_arguments
1190
+ return []
1191
+
1190
1192
  @property
1191
1193
  def concept_arguments(self):
1192
- return [self.content] + self.where.concept_arguments
1194
+ if isinstance(self.content, BuildConcept):
1195
+ return [self.content] + self.where.concept_arguments
1196
+ elif isinstance(self.content, BuildConceptArgs):
1197
+ return self.content.concept_arguments + self.where.concept_arguments
1198
+ return self.where.concept_arguments
1193
1199
 
1194
1200
 
1195
1201
  class BuildRowsetLineage(BuildConceptArgs, BaseModel):
@@ -1,9 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
- from typing import Any, Dict, List, Optional, Set, Union
5
-
6
- from pydantic import BaseModel, Field, ValidationInfo, computed_field, field_validator
4
+ from typing import Dict, List, Optional, Set, Union
5
+
6
+ from pydantic import (
7
+ BaseModel,
8
+ Field,
9
+ ValidationInfo,
10
+ computed_field,
11
+ field_validator,
12
+ model_validator,
13
+ )
7
14
 
8
15
  from trilogy.constants import CONFIG, logger
9
16
  from trilogy.core.constants import CONSTANT_DATASET
@@ -473,8 +480,8 @@ class BaseJoin(BaseModel):
473
480
  left_datasource: Optional[Union[BuildDatasource, "QueryDatasource"]] = None
474
481
  concept_pairs: list[ConceptPair] | None = None
475
482
 
476
- def __init__(self, **data: Any):
477
- super().__init__(**data)
483
+ @model_validator(mode="after")
484
+ def validate_join(self) -> "BaseJoin":
478
485
  if (
479
486
  self.left_datasource
480
487
  and self.left_datasource.identifier == self.right_datasource.identifier
@@ -483,14 +490,18 @@ class BaseJoin(BaseModel):
483
490
  f"Cannot join a dataself to itself, joining {self.left_datasource} and"
484
491
  f" {self.right_datasource}"
485
492
  )
486
- final_concepts = []
487
493
 
488
- # if we have a list of concept pairs
494
+ # Early returns maintained as in original code
489
495
  if self.concept_pairs:
490
- return
496
+ return self
497
+
491
498
  if self.concepts == []:
492
- return
499
+ return self
500
+
501
+ # Validation logic
502
+ final_concepts = []
493
503
  assert self.left_datasource and self.right_datasource
504
+
494
505
  for concept in self.concepts or []:
495
506
  include = True
496
507
  for ds in [self.left_datasource, self.right_datasource]:
@@ -507,6 +518,7 @@ class BaseJoin(BaseModel):
507
518
  )
508
519
  if include:
509
520
  final_concepts.append(concept)
521
+
510
522
  if not final_concepts and self.concepts:
511
523
  # if one datasource only has constants
512
524
  # we can join on 1=1
@@ -519,11 +531,11 @@ class BaseJoin(BaseModel):
519
531
  ]
520
532
  ):
521
533
  self.concepts = []
522
- return
534
+ return self
523
535
  # if everything is at abstract grain, we can skip joins
524
536
  if all([c.grain.abstract for c in ds.output_concepts]):
525
537
  self.concepts = []
526
- return
538
+ return self
527
539
 
528
540
  left_keys = [c.address for c in self.left_datasource.output_concepts]
529
541
  right_keys = [c.address for c in self.right_datasource.output_concepts]
@@ -535,7 +547,9 @@ class BaseJoin(BaseModel):
535
547
  f" right_keys {right_keys},"
536
548
  f" provided join concepts {match_concepts}"
537
549
  )
550
+
538
551
  self.concepts = final_concepts
552
+ return self
539
553
 
540
554
  @property
541
555
  def unique_id(self) -> str:
@@ -695,7 +709,7 @@ class QueryDatasource(BaseModel):
695
709
  "can only merge two datasources if the force_group flag is the same"
696
710
  )
697
711
  logger.debug(
698
- f"{LOGGER_PREFIX} merging {self.name} with"
712
+ f"[Query Datasource] merging {self.name} with"
699
713
  f" {[c.address for c in self.output_concepts]} concepts and"
700
714
  f" {other.name} with {[c.address for c in other.output_concepts]} concepts"
701
715
  )
@@ -759,7 +773,9 @@ class QueryDatasource(BaseModel):
759
773
  hidden_concepts=hidden,
760
774
  ordering=self.ordering,
761
775
  )
762
-
776
+ logger.debug(
777
+ f"[Query Datasource] merged with {[c.address for c in qds.output_concepts]} concepts"
778
+ )
763
779
  return qds
764
780
 
765
781
  @property
@@ -319,7 +319,7 @@ def generate_node(
319
319
  ]
320
320
 
321
321
  logger.info(
322
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating aggregate node with {[x.address for x in agg_optional]}"
322
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} for {concept.address}, generating aggregate node with {[x for x in agg_optional]}"
323
323
  )
324
324
  return gen_group_node(
325
325
  concept,
@@ -67,14 +67,14 @@ def resolve_condition_parent_concepts(
67
67
  def resolve_filter_parent_concepts(
68
68
  concept: BuildConcept,
69
69
  environment: BuildEnvironment,
70
- ) -> Tuple[BuildConcept, List[BuildConcept], List[Tuple[BuildConcept, ...]]]:
70
+ ) -> Tuple[List[BuildConcept], List[Tuple[BuildConcept, ...]]]:
71
71
  if not isinstance(concept.lineage, (BuildFilterItem,)):
72
72
  raise ValueError(
73
73
  f"Concept {concept} lineage is not filter item, is {type(concept.lineage)}"
74
74
  )
75
75
  direct_parent = concept.lineage.content
76
76
  base_existence = []
77
- base_rows = [direct_parent]
77
+ base_rows = [direct_parent] if isinstance(direct_parent, BuildConcept) else []
78
78
  condition_rows, condition_existence = resolve_condition_parent_concepts(
79
79
  concept.lineage.where
80
80
  )
@@ -90,11 +90,10 @@ def resolve_filter_parent_concepts(
90
90
 
91
91
  if concept.lineage.where.existence_arguments:
92
92
  return (
93
- concept.lineage.content,
94
93
  unique(base_rows, "address"),
95
94
  base_existence,
96
95
  )
97
- return concept.lineage.content, unique(base_rows, "address"), []
96
+ return unique(base_rows, "address"), []
98
97
 
99
98
 
100
99
  def gen_property_enrichment_node(