pytrilogy 0.0.3.6__tar.gz → 0.0.3.8__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 (130) hide show
  1. {pytrilogy-0.0.3.6/pytrilogy.egg-info → pytrilogy-0.0.3.8}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pytrilogy.egg-info/SOURCES.txt +2 -0
  4. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_models.py +6 -2
  5. pytrilogy-0.0.3.8/tests/test_user_functions.py +17 -0
  6. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/functions.py +26 -6
  8. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/author.py +104 -39
  9. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/build.py +20 -3
  10. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/datasource.py +6 -0
  11. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/environment.py +2 -1
  12. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/group_node.py +1 -1
  13. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/multiselect_node.py +1 -1
  14. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/statements/author.py +2 -0
  15. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/config.py +15 -0
  16. pytrilogy-0.0.3.8/trilogy/dialect/dataframe.py +47 -0
  17. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/enums.py +12 -1
  18. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/engine.py +11 -4
  19. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/executor.py +10 -2
  20. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/common.py +57 -48
  21. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/parse_engine.py +53 -33
  22. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/trilogy.lark +6 -4
  23. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/LICENSE.md +0 -0
  24. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/README.md +0 -0
  25. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pyproject.toml +0 -0
  26. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pytrilogy.egg-info/dependency_links.txt +0 -0
  27. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pytrilogy.egg-info/entry_points.txt +0 -0
  28. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pytrilogy.egg-info/requires.txt +0 -0
  29. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/pytrilogy.egg-info/top_level.txt +0 -0
  30. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/setup.cfg +0 -0
  31. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/setup.py +0 -0
  32. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_datatypes.py +0 -0
  33. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_declarations.py +0 -0
  34. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_derived_concepts.py +0 -0
  35. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_discovery_nodes.py +0 -0
  36. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_enums.py +0 -0
  37. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_environment.py +0 -0
  38. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_executor.py +0 -0
  39. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_functions.py +0 -0
  40. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_imports.py +0 -0
  41. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_metadata.py +0 -0
  42. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_multi_join_assignments.py +0 -0
  43. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_parse_engine.py +0 -0
  44. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_parsing.py +0 -0
  45. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_partial_handling.py +0 -0
  46. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_query_processing.py +0 -0
  47. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_select.py +0 -0
  48. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_show.py +0 -0
  49. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_statements.py +0 -0
  50. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_undefined_concept.py +0 -0
  51. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/tests/test_where_clause.py +0 -0
  52. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/authoring/__init__.py +0 -0
  53. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/compiler.py +0 -0
  54. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/constants.py +0 -0
  55. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/__init__.py +0 -0
  56. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/constants.py +0 -0
  57. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/enums.py +0 -0
  58. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/env_processor.py +0 -0
  59. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/environment_helpers.py +0 -0
  60. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/ergonomics.py +0 -0
  61. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/exceptions.py +0 -0
  62. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/graph_models.py +0 -0
  63. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/internal.py +0 -0
  64. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/__init__.py +0 -0
  65. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/build_environment.py +0 -0
  66. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/core.py +0 -0
  67. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/models/execute.py +0 -0
  68. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimization.py +0 -0
  69. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimizations/__init__.py +0 -0
  70. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimizations/base_optimization.py +0 -0
  71. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimizations/inline_constant.py +0 -0
  72. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimizations/inline_datasource.py +0 -0
  73. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  74. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/__init__.py +0 -0
  75. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  76. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/graph_utils.py +0 -0
  77. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/__init__.py +0 -0
  78. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  79. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/common.py +0 -0
  80. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  81. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  82. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  83. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  84. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  85. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  86. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  87. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/select_node.py +0 -0
  88. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  89. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/union_node.py +0 -0
  90. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  91. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/node_generators/window_node.py +0 -0
  92. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/__init__.py +0 -0
  93. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/base_node.py +0 -0
  94. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/filter_node.py +0 -0
  95. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/group_node.py +0 -0
  96. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/merge_node.py +0 -0
  97. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  98. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/union_node.py +0 -0
  99. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  100. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/nodes/window_node.py +0 -0
  101. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/processing/utility.py +0 -0
  102. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/query_processor.py +0 -0
  103. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/statements/__init__.py +0 -0
  104. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/statements/build.py +0 -0
  105. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/statements/common.py +0 -0
  106. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/core/statements/execute.py +0 -0
  107. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/__init__.py +0 -0
  108. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/base.py +0 -0
  109. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/bigquery.py +0 -0
  110. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/common.py +0 -0
  111. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/duckdb.py +0 -0
  112. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/postgres.py +0 -0
  113. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/presto.py +0 -0
  114. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/snowflake.py +0 -0
  115. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/dialect/sql_server.py +0 -0
  116. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/hooks/__init__.py +0 -0
  117. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/hooks/base_hook.py +0 -0
  118. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/hooks/graph_hook.py +0 -0
  119. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/hooks/query_debugger.py +0 -0
  120. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/metadata/__init__.py +0 -0
  121. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parser.py +0 -0
  122. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/__init__.py +0 -0
  123. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/config.py +0 -0
  124. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/exceptions.py +0 -0
  125. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/helpers.py +0 -0
  126. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/parsing/render.py +0 -0
  127. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/py.typed +0 -0
  128. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/scripts/__init__.py +0 -0
  129. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/scripts/trilogy.py +0 -0
  130. {pytrilogy-0.0.3.6 → pytrilogy-0.0.3.8}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.6
3
+ Version: 0.0.3.8
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.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.6
3
+ Version: 0.0.3.8
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -28,6 +28,7 @@ tests/test_select.py
28
28
  tests/test_show.py
29
29
  tests/test_statements.py
30
30
  tests/test_undefined_concept.py
31
+ tests/test_user_functions.py
31
32
  tests/test_where_clause.py
32
33
  trilogy/__init__.py
33
34
  trilogy/compiler.py
@@ -103,6 +104,7 @@ trilogy/dialect/base.py
103
104
  trilogy/dialect/bigquery.py
104
105
  trilogy/dialect/common.py
105
106
  trilogy/dialect/config.py
107
+ trilogy/dialect/dataframe.py
106
108
  trilogy/dialect/duckdb.py
107
109
  trilogy/dialect/enums.py
108
110
  trilogy/dialect/postgres.py
@@ -219,10 +219,14 @@ def test_comparison():
219
219
  # this should not error
220
220
  Comparison(left=1, right=[1, 2, 3], operator=ComparisonOperator.IN)
221
221
 
222
- Comparison(left=1, right=ListWrapper([1, 2, 3]), operator=ComparisonOperator.IN)
222
+ Comparison(
223
+ left=1,
224
+ right=ListWrapper([1, 2, 3], type=DataType.INTEGER),
225
+ operator=ComparisonOperator.IN,
226
+ )
223
227
 
224
228
 
225
- def test_comparison():
229
+ def test_comparison_invalid_type():
226
230
  try:
227
231
  Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
228
232
  except Exception as exc:
@@ -0,0 +1,17 @@
1
+ from trilogy import Dialects
2
+
3
+
4
+ def test_user_function_def():
5
+ x = Dialects.DUCK_DB.default_executor()
6
+
7
+ results = x.execute_query(
8
+ """
9
+ def percent_ratio(a, b, digits=3) -> round(a::float / b * 100, digits);
10
+
11
+
12
+ select @percent_ratio(10, 100) as ratio;
13
+
14
+ """
15
+ )
16
+
17
+ assert results.fetchall()[0].ratio == 10.0
@@ -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.6"
7
+ __version__ = "0.0.3.8"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -503,32 +503,52 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
503
503
  arg_count=1,
504
504
  ),
505
505
  FunctionType.ADD: FunctionConfig(
506
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
506
+ valid_inputs={
507
+ DataType.INTEGER,
508
+ DataType.FLOAT,
509
+ DataType.NUMBER,
510
+ DataType.NUMERIC,
511
+ },
507
512
  output_purpose=Purpose.PROPERTY,
508
513
  output_type=DataType.INTEGER,
509
514
  arg_count=InfiniteFunctionArgs,
510
515
  ),
511
516
  FunctionType.SUBTRACT: FunctionConfig(
512
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
517
+ valid_inputs={
518
+ DataType.INTEGER,
519
+ DataType.FLOAT,
520
+ DataType.NUMBER,
521
+ DataType.NUMERIC,
522
+ },
513
523
  output_purpose=Purpose.PROPERTY,
514
524
  output_type=DataType.INTEGER,
515
525
  arg_count=InfiniteFunctionArgs,
516
526
  ),
517
527
  FunctionType.MULTIPLY: FunctionConfig(
518
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
528
+ valid_inputs={
529
+ DataType.INTEGER,
530
+ DataType.FLOAT,
531
+ DataType.NUMBER,
532
+ DataType.NUMERIC,
533
+ },
519
534
  output_purpose=Purpose.PROPERTY,
520
535
  output_type=DataType.INTEGER,
521
536
  arg_count=InfiniteFunctionArgs,
522
537
  ),
523
538
  FunctionType.DIVIDE: FunctionConfig(
524
- valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
539
+ valid_inputs={
540
+ DataType.INTEGER,
541
+ DataType.FLOAT,
542
+ DataType.NUMBER,
543
+ DataType.NUMERIC,
544
+ },
525
545
  output_purpose=Purpose.PROPERTY,
526
546
  output_type=DataType.INTEGER,
527
547
  arg_count=InfiniteFunctionArgs,
528
548
  ),
529
549
  FunctionType.MOD: FunctionConfig(
530
550
  valid_inputs=[
531
- {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
551
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
532
552
  {DataType.INTEGER},
533
553
  ],
534
554
  output_purpose=Purpose.PROPERTY,
@@ -537,7 +557,7 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
537
557
  ),
538
558
  FunctionType.ROUND: FunctionConfig(
539
559
  valid_inputs=[
540
- {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
560
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
541
561
  {DataType.INTEGER},
542
562
  ],
543
563
  output_purpose=Purpose.PROPERTY,
@@ -75,6 +75,9 @@ class Mergeable(ABC):
75
75
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
76
76
  raise NotImplementedError
77
77
 
78
+ def with_reference_replacement(self, source: str, target: Expr):
79
+ raise NotImplementedError(type(self))
80
+
78
81
 
79
82
  class ConceptArgs(ABC):
80
83
  @property
@@ -152,6 +155,11 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
152
155
  metadata=self.metadata,
153
156
  )
154
157
 
158
+ def with_reference_replacement(self, source: str, target: Expr):
159
+ if self.address == source:
160
+ return target
161
+ return self
162
+
155
163
 
156
164
  class UndefinedConcept(ConceptRef):
157
165
  pass
@@ -409,7 +417,7 @@ class Grain(Namespaced, BaseModel):
409
417
  ) -> Grain:
410
418
  from trilogy.parsing.common import concepts_to_grain_concepts
411
419
 
412
- return Grain.model_construct(
420
+ x = Grain.model_construct(
413
421
  components={
414
422
  c.address
415
423
  for c in concepts_to_grain_concepts(concepts, environment=environment)
@@ -417,6 +425,8 @@ class Grain(Namespaced, BaseModel):
417
425
  where_clause=where_clause,
418
426
  )
419
427
 
428
+ return x
429
+
420
430
  def with_namespace(self, namespace: str) -> "Grain":
421
431
  return Grain.model_construct(
422
432
  components={address_with_namespace(c, namespace) for c in self.components},
@@ -662,6 +672,21 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
662
672
  operator=self.operator,
663
673
  )
664
674
 
675
+ def with_reference_replacement(self, source, target):
676
+ return self.__class__.model_construct(
677
+ left=(
678
+ self.left.with_reference_replacement(source, target)
679
+ if isinstance(self.left, Mergeable)
680
+ else self.left
681
+ ),
682
+ right=(
683
+ self.right.with_reference_replacement(source, target)
684
+ if isinstance(self.right, Mergeable)
685
+ else self.right
686
+ ),
687
+ operator=self.operator,
688
+ )
689
+
665
690
  def with_namespace(self, namespace: str):
666
691
  return self.__class__.model_construct(
667
692
  left=(
@@ -1233,19 +1258,9 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1233
1258
  order=self.order,
1234
1259
  )
1235
1260
 
1236
- @property
1237
- def output(self):
1238
- return self.expr.output
1239
-
1240
1261
  @property
1241
1262
  def concept_arguments(self) -> Sequence[ConceptRef]:
1242
- base: List[ConceptRef] = []
1243
- x = self.expr
1244
- if isinstance(x, ConceptRef):
1245
- base += [x]
1246
- elif isinstance(x, ConceptArgs):
1247
- base += x.concept_arguments
1248
- return base
1263
+ return get_concept_arguments(self.expr)
1249
1264
 
1250
1265
  @property
1251
1266
  def row_arguments(self) -> Sequence[ConceptRef]:
@@ -1316,31 +1331,19 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1316
1331
 
1317
1332
  @property
1318
1333
  def concept_arguments(self) -> List[ConceptRef]:
1319
- return self.arguments
1320
-
1321
- @property
1322
- def arguments(self) -> List[ConceptRef]:
1323
1334
  output = [self.content]
1324
1335
  for order in self.order_by:
1325
- output += [order.output]
1336
+ output += get_concept_arguments(order)
1326
1337
  for item in self.over:
1327
- output += [item]
1338
+ output += get_concept_arguments(item)
1328
1339
  return output
1329
1340
 
1330
- @property
1331
- def output(self) -> ConceptRef:
1332
- return self.content
1333
-
1334
1341
  @property
1335
1342
  def output_datatype(self):
1336
1343
  if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
1337
1344
  return DataType.INTEGER
1338
1345
  return self.content.output_datatype
1339
1346
 
1340
- @property
1341
- def output_purpose(self):
1342
- return Purpose.PROPERTY
1343
-
1344
1347
 
1345
1348
  def get_basic_type(
1346
1349
  type: DataType | ListType | StructType | MapType | NumericType,
@@ -1407,12 +1410,28 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1407
1410
  ),
1408
1411
  )
1409
1412
 
1413
+ def with_reference_replacement(self, source, target):
1414
+ return CaseWhen.model_construct(
1415
+ comparison=self.comparison.with_reference_replacement(source, target),
1416
+ expr=(
1417
+ self.expr.with_reference_replacement(source, target)
1418
+ if isinstance(self.expr, Mergeable)
1419
+ else self.expr
1420
+ ),
1421
+ )
1422
+
1410
1423
 
1411
1424
  class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1412
1425
  expr: "Expr"
1413
1426
  # this ensures that it's easily differentiable from CaseWhen
1414
1427
  discriminant: ComparisonOperator = ComparisonOperator.ELSE
1415
1428
 
1429
+ def __str__(self):
1430
+ return self.__repr__()
1431
+
1432
+ def __repr__(self):
1433
+ return f"ELSE {str(self.expr)}"
1434
+
1416
1435
  @field_validator("expr", mode="before")
1417
1436
  def enforce_expr(cls, v):
1418
1437
  if isinstance(v, Concept):
@@ -1435,6 +1454,19 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1435
1454
  ),
1436
1455
  )
1437
1456
 
1457
+ def with_reference_replacement(self, source, target):
1458
+ return CaseElse.model_construct(
1459
+ discriminant=self.discriminant,
1460
+ expr=(
1461
+ self.expr.with_reference_replacement(
1462
+ source,
1463
+ target,
1464
+ )
1465
+ if isinstance(self.expr, Mergeable)
1466
+ else self.expr
1467
+ ),
1468
+ )
1469
+
1438
1470
  def with_namespace(self, namespace: str) -> CaseElse:
1439
1471
  return CaseElse.model_construct(
1440
1472
  discriminant=self.discriminant,
@@ -1510,8 +1542,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1510
1542
 
1511
1543
  def __init__(self, **kwargs):
1512
1544
  super().__init__(**kwargs)
1513
- if "datatype" in str(self):
1514
- raise SyntaxError(str(self))
1515
1545
 
1516
1546
  def __repr__(self):
1517
1547
  return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
@@ -1597,6 +1627,29 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1597
1627
  )
1598
1628
  return v
1599
1629
 
1630
+ def with_reference_replacement(self, source: str, target: Expr):
1631
+ return Function.model_construct(
1632
+ operator=self.operator,
1633
+ arguments=[
1634
+ (
1635
+ c.with_reference_replacement(
1636
+ source,
1637
+ target,
1638
+ )
1639
+ if isinstance(
1640
+ c,
1641
+ Mergeable,
1642
+ )
1643
+ else c
1644
+ )
1645
+ for c in self.arguments
1646
+ ],
1647
+ output_datatype=self.output_datatype,
1648
+ output_purpose=self.output_purpose,
1649
+ valid_inputs=self.valid_inputs,
1650
+ arg_count=self.arg_count,
1651
+ )
1652
+
1600
1653
  def with_namespace(self, namespace: str) -> "Function":
1601
1654
  return Function.model_construct(
1602
1655
  operator=self.operator,
@@ -1658,10 +1711,13 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1658
1711
  return base_grain
1659
1712
 
1660
1713
 
1661
- class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1714
+ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
1662
1715
  function: Function
1663
1716
  by: List[ConceptRef] = Field(default_factory=list)
1664
1717
 
1718
+ def __init__(self, **kwargs):
1719
+ super().__init__(**kwargs)
1720
+
1665
1721
  @field_validator("by", mode="before")
1666
1722
  @classmethod
1667
1723
  def enforce_concept_ref(cls, v):
@@ -1693,10 +1749,6 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1693
1749
  def output_purpose(self):
1694
1750
  return self.function.output_purpose
1695
1751
 
1696
- @property
1697
- def arguments(self):
1698
- return self.function.arguments
1699
-
1700
1752
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
1701
1753
  return AggregateWrapper.model_construct(
1702
1754
  function=self.function.with_merge(source, target, modifiers=modifiers),
@@ -1707,6 +1759,16 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1707
1759
  ),
1708
1760
  )
1709
1761
 
1762
+ def with_reference_replacement(self, source, target):
1763
+ return AggregateWrapper.model_construct(
1764
+ function=self.function.with_reference_replacement(source, target),
1765
+ by=(
1766
+ [c.with_reference_replacement(source, target) for c in self.by]
1767
+ if self.by
1768
+ else []
1769
+ ),
1770
+ )
1771
+
1710
1772
  def with_namespace(self, namespace: str) -> "AggregateWrapper":
1711
1773
  return AggregateWrapper.model_construct(
1712
1774
  function=self.function.with_namespace(namespace),
@@ -1796,11 +1858,6 @@ class RowsetItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1796
1858
  rowset=self.rowset.with_namespace(namespace),
1797
1859
  )
1798
1860
 
1799
- @property
1800
- def arguments(self) -> List[ConceptRef]:
1801
- output = [self.content]
1802
- return output
1803
-
1804
1861
  @property
1805
1862
  def output(self) -> ConceptRef:
1806
1863
  return self.content
@@ -1831,7 +1888,10 @@ class OrderBy(Mergeable, Namespaced, BaseModel):
1831
1888
 
1832
1889
  @property
1833
1890
  def concept_arguments(self):
1834
- return [x.expr for x in self.items]
1891
+ base = []
1892
+ for x in self.items:
1893
+ base += x.concept_arguments
1894
+ return base
1835
1895
 
1836
1896
 
1837
1897
  class AlignClause(Namespaced, BaseModel):
@@ -2088,6 +2148,11 @@ class Comment(BaseModel):
2088
2148
  text: str
2089
2149
 
2090
2150
 
2151
+ class ArgBinding(BaseModel):
2152
+ name: str
2153
+ default: Expr | None = None
2154
+
2155
+
2091
2156
  Expr = (
2092
2157
  MagicConstants
2093
2158
  | bool
@@ -1639,9 +1639,26 @@ class Factory:
1639
1639
 
1640
1640
  @build.register
1641
1641
  def _(self, base: Comparison) -> BuildComparison:
1642
+ from trilogy.parsing.common import arbitrary_to_concept
1643
+
1644
+ left = base.left
1645
+ if isinstance(left, AggregateWrapper):
1646
+ left_c = arbitrary_to_concept(
1647
+ left,
1648
+ environment=self.environment,
1649
+ )
1650
+ left = left_c # type: ignore
1651
+ right = base.right
1652
+ if isinstance(right, AggregateWrapper):
1653
+ right_c = arbitrary_to_concept(
1654
+ right,
1655
+ environment=self.environment,
1656
+ )
1657
+
1658
+ right = right_c # type: ignore
1642
1659
  return BuildComparison.model_construct(
1643
- left=(self.build(base.left)),
1644
- right=(self.build(base.right)),
1660
+ left=(self.build(left)),
1661
+ right=(self.build(right)),
1645
1662
  operator=base.operator,
1646
1663
  )
1647
1664
 
@@ -1660,7 +1677,7 @@ class Factory:
1660
1677
  )
1661
1678
 
1662
1679
  @build.register
1663
- def _(self, base: RowsetItem):
1680
+ def _(self, base: RowsetItem) -> BuildRowsetItem:
1664
1681
 
1665
1682
  factory = Factory(
1666
1683
  environment=self.environment,
@@ -117,6 +117,12 @@ class Datasource(HasUUID, Namespaced, BaseModel):
117
117
  where: Optional[WhereClause] = None
118
118
  non_partial_for: Optional[WhereClause] = None
119
119
 
120
+ @property
121
+ def safe_address(self) -> str:
122
+ if isinstance(self.address, Address):
123
+ return self.address.location
124
+ return self.address
125
+
120
126
  def __eq__(self, other):
121
127
  if not isinstance(other, Datasource):
122
128
  return False
@@ -9,6 +9,7 @@ from typing import (
9
9
  TYPE_CHECKING,
10
10
  Annotated,
11
11
  Any,
12
+ Callable,
12
13
  Dict,
13
14
  ItemsView,
14
15
  List,
@@ -189,7 +190,7 @@ class Environment(BaseModel):
189
190
  datasources: Annotated[
190
191
  EnvironmentDatasourceDict, PlainValidator(validate_datasources)
191
192
  ] = Field(default_factory=EnvironmentDatasourceDict)
192
- functions: Dict[str, Function] = Field(default_factory=dict)
193
+ functions: Dict[str, Callable[..., Any]] = Field(default_factory=dict)
193
194
  data_types: Dict[str, DataType] = Field(default_factory=dict)
194
195
  named_statements: Dict[str, SelectLineage] = Field(default_factory=dict)
195
196
  imports: Dict[str, list[Import]] = Field(
@@ -37,7 +37,7 @@ def gen_group_node(
37
37
  resolve_function_parent_concepts(concept, environment=environment), "address"
38
38
  )
39
39
  logger.info(
40
- f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
40
+ f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} {concept.lineage} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
41
41
  )
42
42
 
43
43
  # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
@@ -76,7 +76,7 @@ def gen_multiselect_node(
76
76
  for select in lineage.selects:
77
77
 
78
78
  snode: StrategyNode = get_query_node(history.base_environment, select)
79
- # raise SyntaxError(select.output_components)
79
+
80
80
  logger.info(
81
81
  f"{padding(depth)}{LOGGER_PREFIX} Fetched parent node with outputs {select.output_components}"
82
82
  )
@@ -127,7 +127,9 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
127
127
  order_by=order_by,
128
128
  meta=meta or Metadata(),
129
129
  )
130
+
130
131
  output.grain = output.calculate_grain(environment)
132
+
131
133
  for x in selection:
132
134
 
133
135
  if x.is_undefined and environment.concepts.fail_on_missing:
@@ -1,3 +1,12 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ if TYPE_CHECKING:
4
+ try:
5
+ from pandas import DataFrame
6
+ except ImportError:
7
+ DataFrame = Any
8
+
9
+
1
10
  class DialectConfig:
2
11
  def __init__(self):
3
12
  pass
@@ -104,3 +113,9 @@ class TrinoConfig(PrestoConfig):
104
113
  if self.schema:
105
114
  return f"trino://{self.username}:{self.password}@{self.host}:{self.port}/{self.catalog}/{self.schema}"
106
115
  return f"trino://{self.username}:{self.password}@{self.host}:{self.port}/{self.catalog}"
116
+
117
+
118
+ class DataFrameConfig(DuckDBConfig):
119
+ def __init__(self, dataframes: dict[str, "DataFrame"]):
120
+ super().__init__()
121
+ self.dataframes = dataframes
@@ -0,0 +1,47 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from sqlalchemy import text
4
+
5
+ from trilogy.core.models.environment import Environment
6
+ from trilogy.dialect.duckdb import DuckDBDialect
7
+ from trilogy.engine import ExecutionEngine
8
+
9
+ if TYPE_CHECKING:
10
+ try:
11
+ from pandas import DataFrame
12
+ except ImportError:
13
+ DataFrame = Any
14
+
15
+
16
+ class DataframeDialect(DuckDBDialect):
17
+ pass
18
+
19
+
20
+ class DataframeConnectionWrapper(ExecutionEngine):
21
+ def __init__(self, engine: ExecutionEngine, dataframes: dict[str, "DataFrame"]):
22
+ self.engine = engine
23
+ self.dataframes = dataframes
24
+ self.connection = None
25
+
26
+ def setup(self, env: Environment, connection):
27
+ self._register_dataframes(env, connection)
28
+
29
+ def _register_dataframes(self, env: Environment, connection):
30
+ for ds in env.datasources.values():
31
+ if ds.safe_address in self.dataframes:
32
+ connection.execute(
33
+ text("register(:name, :df)"),
34
+ {"name": ds.safe_address, "df": self.dataframes[ds.safe_address]},
35
+ )
36
+ else:
37
+ raise ValueError(
38
+ f"Dataframe {ds.safe_address} not found in dataframes on connection config, have {self.dataframes.keys()}"
39
+ )
40
+ pass
41
+
42
+ def add_dataframe(self, name: str, df: "DataFrame", connection, env: Environment):
43
+ self.dataframes[name] = df
44
+ self._register_dataframes(env, connection)
45
+
46
+ def connect(self) -> Any:
47
+ return self.engine.connect()
@@ -16,7 +16,7 @@ def default_factory(conf: DialectConfig, config_type):
16
16
 
17
17
  if not isinstance(conf, config_type):
18
18
  raise TypeError(
19
- f"Invalid dialect configuration for type {type(config_type).__name__}"
19
+ f"Invalid dialect configuration for type {type(config_type).__name__}, is {type(conf)}"
20
20
  )
21
21
  if conf.connect_args:
22
22
  return create_engine(
@@ -33,6 +33,7 @@ class Dialects(Enum):
33
33
  TRINO = "trino"
34
34
  POSTGRES = "postgres"
35
35
  SNOWFLAKE = "snowflake"
36
+ DATAFRAME = "dataframe"
36
37
 
37
38
  @classmethod
38
39
  def _missing_(cls, value):
@@ -88,6 +89,16 @@ class Dialects(Enum):
88
89
  from trilogy.dialect.config import TrinoConfig
89
90
 
90
91
  return _engine_factory(conf, TrinoConfig)
92
+ elif self == Dialects.DATAFRAME:
93
+ from trilogy.dialect.config import DataFrameConfig
94
+ from trilogy.dialect.dataframe import DataframeConnectionWrapper
95
+
96
+ if not conf:
97
+ conf = DataFrameConfig(dataframes={})
98
+
99
+ base = _engine_factory(conf, DataFrameConfig)
100
+
101
+ return DataframeConnectionWrapper(base, dataframes=conf.dataframes)
91
102
  else:
92
103
  raise ValueError(
93
104
  f"Unsupported dialect {self} for default engine creation; create one explicitly."
@@ -1,7 +1,9 @@
1
- from typing import Protocol
1
+ from typing import Any, Protocol
2
2
 
3
3
  from sqlalchemy.engine import Connection, CursorResult, Engine
4
4
 
5
+ from trilogy.core.models.environment import Environment
6
+
5
7
 
6
8
  class EngineResult(Protocol):
7
9
  pass
@@ -13,7 +15,7 @@ class EngineResult(Protocol):
13
15
  class EngineConnection(Protocol):
14
16
  pass
15
17
 
16
- def execute(self, statement: str) -> EngineResult:
18
+ def execute(self, statement: str, parameters: Any | None = None) -> EngineResult:
17
19
  pass
18
20
 
19
21
 
@@ -23,6 +25,9 @@ class ExecutionEngine(Protocol):
23
25
  def connect(self) -> EngineConnection:
24
26
  pass
25
27
 
28
+ def setup(self, env: Environment, connection):
29
+ pass
30
+
26
31
 
27
32
  ### Begin default SQLAlchemy implementation
28
33
  class SqlAlchemyResult(EngineResult):
@@ -37,8 +42,10 @@ class SqlAlchemyConnection(EngineConnection):
37
42
  def __init__(self, connection: Connection):
38
43
  self.connection = connection
39
44
 
40
- def execute(self, statement: str) -> SqlAlchemyResult:
41
- return SqlAlchemyResult(self.connection.execute(statement))
45
+ def execute(
46
+ self, statement: str, parameters: Any | None = None
47
+ ) -> SqlAlchemyResult:
48
+ return SqlAlchemyResult(self.connection.execute(statement, parameters))
42
49
 
43
50
 
44
51
  class SqlAlchemyEngine(ExecutionEngine):