pytrilogy 0.0.2.5__tar.gz → 0.0.2.7__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 (105) hide show
  1. {pytrilogy-0.0.2.5/pytrilogy.egg-info → pytrilogy-0.0.2.7}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_environment.py +10 -3
  4. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_models.py +55 -0
  5. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_parsing.py +45 -1
  6. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/constants.py +1 -2
  8. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/enums.py +1 -0
  9. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/models.py +76 -19
  10. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/inline_datasource.py +11 -7
  11. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/concept_strategies_v3.py +12 -2
  12. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/common.py +1 -0
  13. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/filter_node.py +19 -0
  14. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/group_node.py +1 -1
  15. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/group_to_node.py +0 -1
  16. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/node_merge_node.py +4 -0
  17. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/rowset_node.py +3 -2
  18. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/base_node.py +1 -1
  19. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/filter_node.py +1 -0
  20. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/merge_node.py +28 -23
  21. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/query_processor.py +24 -31
  22. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/base.py +6 -3
  23. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/common.py +2 -1
  24. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/duckdb.py +5 -3
  25. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/presto.py +2 -1
  26. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/common.py +6 -2
  27. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/parse_engine.py +6 -3
  28. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/trilogy.lark +3 -2
  29. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/LICENSE.md +0 -0
  30. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/README.md +0 -0
  31. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pyproject.toml +0 -0
  32. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/SOURCES.txt +0 -0
  33. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/dependency_links.txt +0 -0
  34. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/entry_points.txt +0 -0
  35. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/requires.txt +0 -0
  36. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/pytrilogy.egg-info/top_level.txt +0 -0
  37. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/setup.cfg +0 -0
  38. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/setup.py +0 -0
  39. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_datatypes.py +0 -0
  40. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_declarations.py +0 -0
  41. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_derived_concepts.py +0 -0
  42. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_discovery_nodes.py +0 -0
  43. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_functions.py +0 -0
  44. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_imports.py +0 -0
  45. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_metadata.py +0 -0
  46. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_multi_join_assignments.py +0 -0
  47. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_select.py +0 -0
  50. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_statements.py +0 -0
  51. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_undefined_concept.py +0 -0
  52. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/tests/test_where_clause.py +0 -0
  53. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/compiler.py +0 -0
  54. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/__init__.py +0 -0
  55. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/constants.py +0 -0
  56. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/env_processor.py +0 -0
  57. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/environment_helpers.py +0 -0
  58. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/ergonomics.py +0 -0
  59. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/exceptions.py +0 -0
  60. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/functions.py +0 -0
  61. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/graph_models.py +0 -0
  62. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/internal.py +0 -0
  63. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimization.py +0 -0
  64. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/__init__.py +0 -0
  65. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/base_optimization.py +0 -0
  66. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/inline_constant.py +0 -0
  67. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  68. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/__init__.py +0 -0
  69. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/graph_utils.py +0 -0
  70. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/__init__.py +0 -0
  71. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  72. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  73. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/select_node.py +0 -0
  74. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  75. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/node_generators/window_node.py +0 -0
  76. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/__init__.py +0 -0
  77. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/group_node.py +0 -0
  78. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  79. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  80. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/nodes/window_node.py +0 -0
  81. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/core/processing/utility.py +0 -0
  82. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/__init__.py +0 -0
  83. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/bigquery.py +0 -0
  84. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/config.py +0 -0
  85. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/enums.py +0 -0
  86. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/postgres.py +0 -0
  87. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/snowflake.py +0 -0
  88. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/dialect/sql_server.py +0 -0
  89. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/engine.py +0 -0
  90. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/executor.py +0 -0
  91. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/__init__.py +0 -0
  92. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/base_hook.py +0 -0
  93. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/graph_hook.py +0 -0
  94. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/hooks/query_debugger.py +0 -0
  95. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/metadata/__init__.py +0 -0
  96. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parser.py +0 -0
  97. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/__init__.py +0 -0
  98. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/config.py +0 -0
  99. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/exceptions.py +0 -0
  100. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/helpers.py +0 -0
  101. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/parsing/render.py +0 -0
  102. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/py.typed +0 -0
  103. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/scripts/__init__.py +0 -0
  104. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/scripts/trilogy.py +0 -0
  105. {pytrilogy-0.0.2.5 → pytrilogy-0.0.2.7}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.5
3
+ Version: 0.0.2.7
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.5
3
+ Version: 0.0.2.7
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -59,6 +59,13 @@ key order_id int;
59
59
 
60
60
  assert env1.concepts["order_id"] == env1.concepts["replacements.order_id"]
61
61
 
62
- order_id = env1.datasources["replacements.replacements"].columns[0]
63
- assert order_id.concept == env1.concepts["order_id"]
64
- assert order_id.modifiers == [Modifier.PARTIAL]
62
+ found = False
63
+ for x in env1.datasources["replacements.replacements"].columns:
64
+ if (
65
+ x.alias == "order_id"
66
+ and x.concept.address == env1.concepts["order_id"].address
67
+ ):
68
+ assert x.concept == env1.concepts["order_id"]
69
+ assert x.modifiers == [Modifier.PARTIAL]
70
+ found = True
71
+ assert found
@@ -10,6 +10,8 @@ from trilogy.core.models import (
10
10
  UndefinedConcept,
11
11
  BaseJoin,
12
12
  Comparison,
13
+ Join,
14
+ JoinKey,
13
15
  )
14
16
 
15
17
 
@@ -54,6 +56,8 @@ def test_cte_merge(test_environment, test_environment_graph):
54
56
  merged = a + b
55
57
  assert merged.output_columns == outputs
56
58
 
59
+ assert "Target: Grain<Abstract>." in merged.comment
60
+
57
61
 
58
62
  def test_concept(test_environment, test_environment_graph):
59
63
  test_concept = list(test_environment.concepts.values())[0]
@@ -191,3 +195,54 @@ def test_comparison():
191
195
  Comparison(left=1, right="abc", operator=ComparisonOperator.EQ)
192
196
  except Exception as exc:
193
197
  assert isinstance(exc, SyntaxError)
198
+
199
+
200
+ def test_join(test_environment: Environment):
201
+ datasource = list(test_environment.datasources.values())[0]
202
+ outputs = [c.concept for c in datasource.columns]
203
+ output_map = {
204
+ c.address: {
205
+ datasource,
206
+ }
207
+ for c in outputs
208
+ }
209
+ a = CTE(
210
+ name="test",
211
+ output_columns=[outputs[0]],
212
+ grain=Grain(),
213
+ source=QueryDatasource(
214
+ input_concepts=[outputs[0]],
215
+ output_concepts=[outputs[0]],
216
+ datasources=[datasource],
217
+ grain=Grain(),
218
+ joins=[],
219
+ source_map={outputs[0].address: {datasource}},
220
+ ),
221
+ source_map={c.address: [datasource.identifier] for c in outputs},
222
+ )
223
+
224
+ b = CTE(
225
+ name="testb",
226
+ output_columns=outputs,
227
+ grain=Grain(),
228
+ source=QueryDatasource(
229
+ input_concepts=outputs,
230
+ output_concepts=outputs,
231
+ datasources=[datasource],
232
+ grain=Grain(),
233
+ joins=[],
234
+ source_map=output_map,
235
+ ),
236
+ source_map={c.address: [datasource.identifier] for c in outputs},
237
+ )
238
+ test = Join(
239
+ left_cte=a,
240
+ right_cte=b,
241
+ joinkeys=[JoinKey(concept=x) for x in outputs],
242
+ jointype=JoinType.RIGHT_OUTER,
243
+ )
244
+
245
+ assert (
246
+ str(test)
247
+ == "right outer JOIN test and testb on local.product_id<local.product_id>,local.category_id<local.category_id>"
248
+ ), str(test)
@@ -160,7 +160,8 @@ address `preqldata.analytics_411641820.events_*`
160
160
  ;"""
161
161
  )
162
162
  query = parsed[-1]
163
- assert query.address.location == "`preqldata.analytics_411641820.events_*`"
163
+ assert query.address.quoted is True
164
+ assert query.address.location == "preqldata.analytics_411641820.events_*"
164
165
 
165
166
 
166
167
  def test_purpose_and_keys():
@@ -448,3 +449,46 @@ select
448
449
  {"a": 1, "b": 2, "c": 3},
449
450
  1,
450
451
  )
452
+
453
+
454
+ def test_datasource_colon():
455
+
456
+ text = """
457
+ key x int;
458
+ key y int;
459
+
460
+ datasource test (
461
+ x:x,
462
+ y:y)
463
+ grain(x)
464
+ address `abc:def`
465
+ ;
466
+
467
+
468
+ select x;
469
+ """
470
+ env, parsed = parse_text(text)
471
+
472
+ results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
473
+
474
+ assert '"abc:def" as test' in results
475
+
476
+ text = """
477
+ key x int;
478
+ key y int;
479
+
480
+ datasource test (
481
+ x:x,
482
+ y:y)
483
+ grain(x)
484
+ address abcdef
485
+ ;
486
+
487
+
488
+ select x;
489
+ """
490
+ env, parsed = parse_text(text)
491
+
492
+ results = Dialects.DUCK_DB.default_executor().generate_sql(text)[0]
493
+
494
+ assert "abcdef as test" in results, results
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.5"
7
+ __version__ = "0.0.2.7"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -33,6 +33,7 @@ class Config:
33
33
  strict_mode: bool = True
34
34
  human_identifiers: bool = True
35
35
  validate_missing: bool = True
36
+ show_comments: bool = False
36
37
  optimizations: Optimizations = field(default_factory=Optimizations)
37
38
 
38
39
  def set_random_seed(self, seed: int):
@@ -42,5 +43,3 @@ class Config:
42
43
  CONFIG = Config()
43
44
 
44
45
  CONFIG.set_random_seed(42)
45
-
46
- CONFIG.strict_mode = True
@@ -7,6 +7,7 @@ class UnnestMode(Enum):
7
7
  DIRECT = "direct"
8
8
  CROSS_APPLY = "cross_apply"
9
9
  CROSS_JOIN = "cross_join"
10
+ CROSS_JOIN_ALIAS = "cross_join_alias"
10
11
 
11
12
 
12
13
  class ConceptSource(Enum):
@@ -945,12 +945,14 @@ class ColumnAssignment(BaseModel):
945
945
  )
946
946
 
947
947
  def with_merge(
948
- self, concept: Concept, modifiers: List[Modifier]
948
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
949
949
  ) -> "ColumnAssignment":
950
950
  return ColumnAssignment(
951
951
  alias=self.alias,
952
- concept=concept,
953
- modifiers=modifiers,
952
+ concept=self.concept.with_merge(source, target, modifiers),
953
+ modifiers=(
954
+ modifiers if self.concept.address == source.address else self.modifiers
955
+ ),
954
956
  )
955
957
 
956
958
 
@@ -1817,6 +1819,7 @@ class MultiSelectStatement(SelectTypeMixin, Mergeable, Namespaced, BaseModel):
1817
1819
  class Address(BaseModel):
1818
1820
  location: str
1819
1821
  is_query: bool = False
1822
+ quoted: bool = False
1820
1823
 
1821
1824
 
1822
1825
  class Query(BaseModel):
@@ -1889,20 +1892,22 @@ class Datasource(Namespaced, BaseModel):
1889
1892
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1890
1893
  ):
1891
1894
  original = [c for c in self.columns if c.concept.address == source.address]
1895
+ if len(original) != 1:
1896
+ raise ValueError(
1897
+ f"Expected exactly one column to merge, got {len(original)} for {source.address}, {[x.alias for x in original]}"
1898
+ )
1892
1899
  # map to the alias with the modifier, and the original
1893
1900
  self.columns = [
1894
- (
1895
- c.with_merge(target, modifiers)
1896
- if c.concept.address == source.address
1897
- else c
1898
- )
1901
+ c.with_merge(source, target, modifiers)
1899
1902
  for c in self.columns
1903
+ if c.concept.address != source.address
1900
1904
  ] + original
1901
1905
  self.grain = self.grain.with_merge(source, target, modifiers)
1902
1906
  self.where = (
1903
1907
  self.where.with_merge(source, target, modifiers) if self.where else None
1904
1908
  )
1905
- del self.output_lcl
1909
+
1910
+ self.add_column(target, original[0].alias, modifiers)
1906
1911
 
1907
1912
  @property
1908
1913
  def env_label(self) -> str:
@@ -1914,7 +1919,7 @@ class Datasource(Namespaced, BaseModel):
1914
1919
  def condition(self):
1915
1920
  return None
1916
1921
 
1917
- @cached_property
1922
+ @property
1918
1923
  def output_lcl(self) -> LooseConceptList:
1919
1924
  return LooseConceptList(concepts=self.output_concepts)
1920
1925
 
@@ -1922,9 +1927,9 @@ class Datasource(Namespaced, BaseModel):
1922
1927
  def can_be_inlined(self) -> bool:
1923
1928
  if isinstance(self.address, Address) and self.address.is_query:
1924
1929
  return False
1925
- for x in self.columns:
1926
- if not isinstance(x.alias, str):
1927
- return False
1930
+ # for x in self.columns:
1931
+ # if not isinstance(x.alias, str):
1932
+ # return False
1928
1933
  return True
1929
1934
 
1930
1935
  @property
@@ -1959,12 +1964,15 @@ class Datasource(Namespaced, BaseModel):
1959
1964
  )
1960
1965
  return grain
1961
1966
 
1962
- def add_column(self, concept: Concept, alias: str, modifiers=None):
1967
+ def add_column(
1968
+ self,
1969
+ concept: Concept,
1970
+ alias: str | RawColumnExpr | Function,
1971
+ modifiers: List[Modifier] | None = None,
1972
+ ):
1963
1973
  self.columns.append(
1964
- ColumnAssignment(alias=alias, concept=concept, modifiers=modifiers)
1974
+ ColumnAssignment(alias=alias, concept=concept, modifiers=modifiers or [])
1965
1975
  )
1966
- # force refresh
1967
- del self.output_lcl
1968
1976
 
1969
1977
  def __add__(self, other):
1970
1978
  if not other == self:
@@ -1997,7 +2005,7 @@ class Datasource(Namespaced, BaseModel):
1997
2005
  where=self.where.with_namespace(namespace) if self.where else None,
1998
2006
  )
1999
2007
 
2000
- @cached_property
2008
+ @property
2001
2009
  def concepts(self) -> List[Concept]:
2002
2010
  return [c.concept for c in self.columns]
2003
2011
 
@@ -2148,6 +2156,12 @@ class BaseJoin(BaseModel):
2148
2156
  )
2149
2157
 
2150
2158
  def __str__(self):
2159
+ if self.concept_pairs:
2160
+ return (
2161
+ f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
2162
+ f" {self.right_datasource.identifier} on"
2163
+ f" {','.join([str(k[0])+'='+str(k[1]) for k in self.concept_pairs])}"
2164
+ )
2151
2165
  return (
2152
2166
  f"{self.join_type.value} JOIN {self.left_datasource.identifier} and"
2153
2167
  f" {self.right_datasource.identifier} on"
@@ -2459,6 +2473,19 @@ class CTE(BaseModel):
2459
2473
  self.base_alias_override = candidates[0] if candidates else None
2460
2474
  return True
2461
2475
 
2476
+ @property
2477
+ def comment(self) -> str:
2478
+ base = f"Target: {str(self.grain)}."
2479
+ if self.parent_ctes:
2480
+ base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
2481
+ if self.joins:
2482
+ base += f"\n-- Joins: {', '.join([str(x) for x in self.joins])}."
2483
+ if self.partial_concepts:
2484
+ base += (
2485
+ f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
2486
+ )
2487
+ return base
2488
+
2462
2489
  def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
2463
2490
  qds_being_inlined = parent.source
2464
2491
  ds_being_inlined = qds_being_inlined.datasources[0]
@@ -2549,6 +2576,10 @@ class CTE(BaseModel):
2549
2576
  self.hidden_concepts = unique(
2550
2577
  self.hidden_concepts + other.hidden_concepts, "address"
2551
2578
  )
2579
+ self.existence_source_map = {
2580
+ **self.existence_source_map,
2581
+ **other.existence_source_map,
2582
+ }
2552
2583
  return self
2553
2584
 
2554
2585
  @property
@@ -2579,6 +2610,16 @@ class CTE(BaseModel):
2579
2610
  return self.relevant_base_ctes[0].name
2580
2611
  return self.source.name
2581
2612
 
2613
+ @property
2614
+ def quote_address(self) -> bool:
2615
+ if self.is_root_datasource:
2616
+ candidate = self.source.datasources[0]
2617
+ if isinstance(candidate, Datasource) and isinstance(
2618
+ candidate.address, Address
2619
+ ):
2620
+ return candidate.address.quoted
2621
+ return False
2622
+
2582
2623
  @property
2583
2624
  def base_alias(self) -> str:
2584
2625
  if self.base_alias_override:
@@ -2730,6 +2771,12 @@ class Join(BaseModel):
2730
2771
  return self.left_name + self.right_name + self.jointype.value
2731
2772
 
2732
2773
  def __str__(self):
2774
+ if self.joinkey_pairs:
2775
+ return (
2776
+ f"{self.jointype.value} JOIN {self.left_name} and"
2777
+ f" {self.right_name} on"
2778
+ f" {','.join([str(k[0])+'='+str(k[1]) for k in self.joinkey_pairs])}"
2779
+ )
2733
2780
  return (
2734
2781
  f"{self.jointype.value} JOIN {self.left_name} and"
2735
2782
  f" {self.right_name} on {','.join([str(k) for k in self.joinkeys])}"
@@ -2991,6 +3038,7 @@ class Environment(BaseModel):
2991
3038
 
2992
3039
  materialized_concepts: List[Concept] = Field(default_factory=list)
2993
3040
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3041
+ canonical_map: Dict[str, str] = Field(default_factory=dict)
2994
3042
  _parse_count: int = 0
2995
3043
 
2996
3044
  @classmethod
@@ -3039,7 +3087,7 @@ class Environment(BaseModel):
3039
3087
  if x.address not in current_mat
3040
3088
  ]
3041
3089
  if new:
3042
- logger.info(f"Environment added new materialized concepts {new}")
3090
+ logger.debug(f"Environment added new materialized concepts {new}")
3043
3091
 
3044
3092
  def validate_concept(self, lookup: str, meta: Meta | None = None):
3045
3093
  existing: Concept = self.concepts.get(lookup) # type: ignore
@@ -3202,13 +3250,22 @@ class Environment(BaseModel):
3202
3250
  self, source: Concept, target: Concept, modifiers: List[Modifier]
3203
3251
  ):
3204
3252
  replacements = {}
3253
+ # exit early if we've run this
3254
+ if source.address in self.alias_origin_lookup:
3255
+ if self.concepts[source.address] == target:
3256
+ return
3205
3257
  self.alias_origin_lookup[source.address] = source
3206
3258
  for k, v in self.concepts.items():
3259
+
3207
3260
  if v.address == target.address:
3208
3261
  v.pseudonyms[source.address] = source
3209
3262
  if v.address == source.address:
3210
3263
  replacements[k] = target
3264
+ self.canonical_map[k] = target.address
3211
3265
  v.pseudonyms[target.address] = target
3266
+ # we need to update keys and grains of all concepts
3267
+ else:
3268
+ replacements[k] = v.with_merge(source, target, modifiers)
3212
3269
  self.concepts.update(replacements)
3213
3270
 
3214
3271
  for k, ds in self.datasources.items():
@@ -42,19 +42,18 @@ class InlineDatasource(OptimizationRule):
42
42
  self.log(f"parent {parent_cte.name} datasource is not inlineable")
43
43
  continue
44
44
  root_outputs = {x.address for x in root.output_concepts}
45
- cte_outputs = {x.address for x in cte.output_columns}
46
- inherited = {x for x, v in cte.source_map.items() if v}
47
- # cte_inherited_outputs = {x.address for x in parent_cte.output_columns if parent_cte.source_map.get(x.address)}
48
- grain_components = {x.address for x in root.grain.components}
45
+ inherited = {
46
+ x for x, v in cte.source_map.items() if v and parent_cte.name in v
47
+ }
49
48
  if not inherited.issubset(root_outputs):
50
49
  cte_missing = inherited - root_outputs
51
50
  self.log(
52
51
  f"Not all {parent_cte.name} require inputs are found on datasource, missing {cte_missing}"
53
52
  )
54
53
  continue
55
- if not grain_components.issubset(cte_outputs):
56
- self.log("Not all datasource components in cte outputs, forcing group")
57
- force_group = True
54
+ if not root.grain.issubset(parent_cte.grain):
55
+ self.log(f"Not all {parent_cte.name} is at wrong grain to inline")
56
+ continue
58
57
  to_inline.append(parent_cte)
59
58
 
60
59
  optimized = False
@@ -68,6 +67,11 @@ class InlineDatasource(OptimizationRule):
68
67
  f"Skipping inlining raw datasource {replaceable.source.name} ({replaceable.name}) due to multiple references"
69
68
  )
70
69
  continue
70
+ if not replaceable.source.datasources[0].grain.issubset(replaceable.grain):
71
+ self.log(
72
+ f"Forcing group ({parent_cte.grain} being replaced by inlined source {root.grain})"
73
+ )
74
+ force_group = True
71
75
  result = cte.inline_parent_datasource(replaceable, force_group=force_group)
72
76
  if result:
73
77
  self.log(
@@ -612,9 +612,19 @@ def _search_concepts(
612
612
  )
613
613
 
614
614
  if expanded:
615
- expanded.resolve()
615
+ # we don't need to return the entire list; just the ones we needed pre-expansion
616
+ ex_resolve = expanded.resolve()
617
+ extra = [
618
+ x
619
+ for x in ex_resolve.output_concepts
620
+ if x.address not in [y.address for y in mandatory_list]
621
+ and x not in ex_resolve.grain.components
622
+ ]
623
+ expanded.output_concepts = mandatory_list
624
+ expanded.rebuild_cache()
625
+
616
626
  logger.info(
617
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition;"
627
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition; removing extra {[c.address for c in extra]}"
618
628
  )
619
629
  return expanded
620
630
  # if we can't find it after expanding to a merge, then
@@ -209,6 +209,7 @@ def gen_enrichment_node(
209
209
  environment=environment,
210
210
  g=g,
211
211
  parents=[enrich_node, base_node],
212
+ force_group=False,
212
213
  node_joins=[
213
214
  NodeJoin(
214
215
  left_node=enrich_node,
@@ -105,10 +105,29 @@ def gen_filter_node(
105
105
  environment=environment,
106
106
  g=g,
107
107
  parents=core_parents,
108
+ grain=Grain(
109
+ components=[immediate_parent] + parent_row_concepts,
110
+ ),
111
+ )
112
+
113
+ assert filter_node.resolve().grain == Grain(
114
+ components=[immediate_parent] + parent_row_concepts,
108
115
  )
109
116
  if not local_optional or all(
110
117
  [x.address in [y.address for y in parent_row_concepts] for x in local_optional]
111
118
  ):
119
+ outputs = [
120
+ x
121
+ for x in filter_node.output_concepts
122
+ if x.address in [y.address for y in local_optional]
123
+ ]
124
+ logger.info(
125
+ f"{padding(depth)}{LOGGER_PREFIX} no extra enrichment needed for filter node"
126
+ )
127
+ filter_node.output_concepts = [
128
+ concept,
129
+ ] + outputs
130
+ filter_node.rebuild_cache()
112
131
  return filter_node
113
132
  enrich_node = source_concepts( # this fetches the parent + join keys
114
133
  # to then connect to the rest of the query
@@ -29,7 +29,7 @@ def gen_group_node(
29
29
  resolve_function_parent_concepts(concept), "address"
30
30
  )
31
31
  logger.info(
32
- f"{padding(depth)}{LOGGER_PREFIX} parent_concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
32
+ f"{padding(depth)}{LOGGER_PREFIX} parent concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
33
33
  )
34
34
 
35
35
  # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
@@ -95,5 +95,4 @@ def gen_group_to_node(
95
95
  ],
96
96
  whole_grain=True,
97
97
  depth=depth,
98
- partial_concepts=group_node.partial_concepts,
99
98
  )
@@ -260,6 +260,7 @@ def subgraphs_to_merge_node(
260
260
  source_concepts,
261
261
  history,
262
262
  conditions,
263
+ enable_early_exit: bool = True,
263
264
  ):
264
265
  parents: List[StrategyNode] = []
265
266
  logger.info(
@@ -290,6 +291,8 @@ def subgraphs_to_merge_node(
290
291
  for x in parents:
291
292
  for y in x.output_concepts:
292
293
  input_c.append(y)
294
+ if len(parents) == 1 and enable_early_exit:
295
+ return parents[0]
293
296
 
294
297
  return MergeNode(
295
298
  input_concepts=unique(input_c, "address"),
@@ -350,6 +353,7 @@ def gen_merge_node(
350
353
  source_concepts=source_concepts,
351
354
  history=history,
352
355
  conditions=conditions,
356
+ enable_early_exit=False,
353
357
  )
354
358
  if test:
355
359
  return test
@@ -74,8 +74,9 @@ def gen_rowset_node(
74
74
  if existence_parents:
75
75
  node.parents += existence_parents
76
76
  # we don't need to join to any existence parents
77
- if isinstance(node, MergeNode):
78
- node.node_joins = []
77
+ # if isinstance(node, MergeNode) and node.node_joins is None:
78
+ # # set it explicitly to empty to avoid inference
79
+ # node.node_joins = []
79
80
  for parent in existence_parents:
80
81
  for x in parent.output_concepts:
81
82
  if x.address not in node.output_lcl:
@@ -192,7 +192,7 @@ class StrategyNode:
192
192
  p.resolve() for p in self.parents
193
193
  ]
194
194
 
195
- grain = Grain(components=self.output_concepts)
195
+ grain = self.grain if self.grain else Grain(components=self.output_concepts)
196
196
  source_map = resolve_concept_map(
197
197
  parent_sources,
198
198
  self.output_concepts,
@@ -63,4 +63,5 @@ class FilterNode(StrategyNode):
63
63
  conditions=self.conditions,
64
64
  partial_concepts=list(self.partial_concepts),
65
65
  force_group=self.force_group,
66
+ grain=self.grain,
66
67
  )
@@ -78,7 +78,7 @@ def deduplicate_nodes_and_joins(
78
78
  duplicates = False
79
79
  duplicates, merged, removed = deduplicate_nodes(merged, logging_prefix)
80
80
  # filter out any removed joins
81
- if joins:
81
+ if joins is not None:
82
82
  joins = [
83
83
  j
84
84
  for j in joins
@@ -138,6 +138,16 @@ class MergeNode(StrategyNode):
138
138
  continue
139
139
  final_joins.append(join)
140
140
  self.node_joins = final_joins
141
+ partial_lookup: list[Concept] = []
142
+ non_partial: List[Concept] = []
143
+ for node in parents or []:
144
+ partial_lookup += node.partial_concepts
145
+ non_partial += [
146
+ x for x in node.output_concepts if x not in node.partial_concepts
147
+ ]
148
+
149
+ final_partial = [x for x in partial_lookup if x not in non_partial]
150
+ self.partial_concepts = final_partial
141
151
 
142
152
  def translate_node_joins(self, node_joins: List[NodeJoin]) -> List[BaseJoin]:
143
153
  joins = []
@@ -219,12 +229,13 @@ class MergeNode(StrategyNode):
219
229
  )
220
230
  joins = self.translate_node_joins(final_joins)
221
231
  else:
232
+ logger.info(
233
+ f"{self.logging_prefix}{LOGGER_PREFIX} Final joins is not null {final_joins} but is empty, skipping join generation"
234
+ )
222
235
  return []
223
236
 
224
237
  for join in joins:
225
- logger.info(
226
- f"{self.logging_prefix}{LOGGER_PREFIX} final join {join.join_type} {[str(c) for c in join.concepts]}"
227
- )
238
+ logger.info(f"{self.logging_prefix}{LOGGER_PREFIX} final join {str(join)}")
228
239
  return joins
229
240
 
230
241
  def _resolve(self) -> QueryDatasource:
@@ -249,6 +260,12 @@ class MergeNode(StrategyNode):
249
260
  # early exit if we can just return the parent
250
261
  final_datasets: List[QueryDatasource | Datasource] = list(merged.values())
251
262
 
263
+ existence_final = [
264
+ x
265
+ for x in final_datasets
266
+ if all([y in self.existence_concepts for y in x.output_concepts])
267
+ ]
268
+
252
269
  if len(merged.keys()) == 1:
253
270
  final: QueryDatasource | Datasource = list(merged.values())[0]
254
271
  if (
@@ -288,34 +305,25 @@ class MergeNode(StrategyNode):
288
305
  for source in final_datasets:
289
306
  pregrain += source.grain
290
307
 
291
- grain = (
292
- self.grain
293
- if self.grain
294
- else Grain(
295
- components=[
296
- c
297
- for c in pregrain.components
298
- if c.address in [x.address for x in self.output_concepts]
299
- ]
300
- )
301
- )
308
+ grain = self.grain if self.grain else pregrain
302
309
 
303
310
  logger.info(
304
311
  f"{self.logging_prefix}{LOGGER_PREFIX} has pre grain {pregrain} and final merge node grain {grain}"
305
312
  )
306
-
307
- if len(final_datasets) > 1:
313
+ join_candidates = [x for x in final_datasets if x not in existence_final]
314
+ if len(join_candidates) > 1:
308
315
  joins = self.generate_joins(
309
- final_datasets, final_joins, pregrain, grain, self.environment
316
+ join_candidates, final_joins, pregrain, grain, self.environment
310
317
  )
311
318
  else:
312
319
  joins = []
313
-
320
+ logger.info(
321
+ f"{self.logging_prefix}{LOGGER_PREFIX} Final join count for CTE parent count {len(join_candidates)} is {len(joins)}"
322
+ )
314
323
  full_join_concepts = []
315
324
  for join in joins:
316
325
  if join.join_type == JoinType.FULL:
317
326
  full_join_concepts += join.concepts
318
-
319
327
  if self.whole_grain:
320
328
  force_group = False
321
329
  elif self.force_group is False:
@@ -337,9 +345,6 @@ class MergeNode(StrategyNode):
337
345
  inherited_inputs=self.input_concepts + self.existence_concepts,
338
346
  full_joins=full_join_concepts,
339
347
  )
340
- logger.info(
341
- f"{self.logging_prefix}{LOGGER_PREFIX} source_map {str(source_map)}"
342
- )
343
348
  qds = QueryDatasource(
344
349
  input_concepts=unique(self.input_concepts, "address"),
345
350
  output_concepts=unique(self.output_concepts, "address"),
@@ -183,49 +183,42 @@ def generate_cte_name(full_name: str, name_map: dict[str, str]) -> str:
183
183
  return full_name.replace("<", "").replace(">", "").replace(",", "_")
184
184
 
185
185
 
186
- def resolve_cte_base_name_and_alias(
186
+ def resolve_cte_base_name_and_alias_v2(
187
187
  name: str,
188
188
  source: QueryDatasource,
189
- parents: List[CTE],
190
- joins: List[Join | InstantiatedUnnestJoin],
189
+ source_map: Dict[str, list[str]],
190
+ raw_joins: List[Join | InstantiatedUnnestJoin],
191
191
  ) -> Tuple[str | None, str | None]:
192
-
193
- valid_joins: List[Join] = [join for join in joins if isinstance(join, Join)]
194
- relevant_parent_sources = set()
195
- for k, v in source.source_map.items():
196
- if v:
197
- relevant_parent_sources.update(v)
198
- eligible = [x for x in source.datasources if x in relevant_parent_sources]
192
+ joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
199
193
  if (
200
- len(eligible) == 1
201
- and isinstance(eligible[0], Datasource)
202
- and not eligible[0].name == CONSTANT_DATASET
194
+ len(source.datasources) == 1
195
+ and isinstance(source.datasources[0], Datasource)
196
+ and not source.datasources[0].name == CONSTANT_DATASET
203
197
  ):
204
- ds = eligible[0]
198
+ ds = source.datasources[0]
205
199
  return ds.safe_location, ds.identifier
206
200
 
207
- # if we have multiple joined CTEs, pick the base
208
- # as the root
209
- elif len(eligible) == 1 and len(parents) == 1:
210
- return parents[0].name, parents[0].name
211
- elif valid_joins and len(valid_joins) > 0:
212
- candidates = [x.left_cte.name for x in valid_joins]
213
- disallowed = [x.right_cte.name for x in valid_joins]
201
+ if joins and len(joins) > 0:
202
+ candidates = [x.left_cte.name for x in joins]
203
+ disallowed = [x.right_cte.name for x in joins]
214
204
  try:
215
205
  cte = [y for y in candidates if y not in disallowed][0]
216
206
  return cte, cte
217
207
  except IndexError:
218
208
  raise SyntaxError(
219
- f"Invalid join configuration {candidates} {disallowed} with all parents {[x.base_name for x in parents]}"
209
+ f"Invalid join configuration {candidates} {disallowed} for {name}",
220
210
  )
221
- elif eligible:
222
- matched = [x for x in parents if x.source.name == eligible[0].name]
223
- if matched:
224
- return matched[0].name, matched[0].name
225
211
 
226
- logger.info(
227
- f"Could not determine CTE base name for {name} with relevant sources {relevant_parent_sources}"
228
- )
212
+ counts: dict[str, int] = defaultdict(lambda: 0)
213
+ output_addresses = [x.address for x in source.output_concepts]
214
+ for k, v in source_map.items():
215
+ for vx in v:
216
+ if k in output_addresses:
217
+ counts[vx] = counts[vx] + 1
218
+ else:
219
+ counts[vx] = counts[vx]
220
+ if counts:
221
+ return max(counts, key=counts.get), max(counts, key=counts.get) # type: ignore
229
222
  return None, None
230
223
 
231
224
 
@@ -274,8 +267,8 @@ def datasource_to_ctes(
274
267
  for x in [base_join_to_join(join, parents) for join in query_datasource.joins]
275
268
  if x
276
269
  ]
277
- base_name, base_alias = resolve_cte_base_name_and_alias(
278
- human_id, query_datasource, parents, final_joins
270
+ base_name, base_alias = resolve_cte_base_name_and_alias_v2(
271
+ human_id, query_datasource, source_map, final_joins
279
272
  )
280
273
  cte = CTE(
281
274
  name=human_id,
@@ -499,10 +499,12 @@ class BaseDialect:
499
499
  for c in cte.output_columns
500
500
  if c.address not in [y.address for y in cte.hidden_concepts]
501
501
  ]
502
- if cte.base_name == cte.base_alias:
503
- source = cte.base_name
502
+ if cte.quote_address:
503
+ source = f"{self.QUOTE_CHARACTER}{cte.base_name}{self.QUOTE_CHARACTER}"
504
504
  else:
505
- source = f"{cte.base_name} as {cte.base_alias}"
505
+ source = cte.base_name
506
+ if cte.base_name != cte.base_alias:
507
+ source = f"{source} as {cte.base_alias}"
506
508
  return CompiledCTE(
507
509
  name=cte.name,
508
510
  statement=self.SQL_TEMPLATE.render(
@@ -511,6 +513,7 @@ class BaseDialect:
511
513
  grain=cte.grain,
512
514
  limit=cte.limit,
513
515
  # some joins may not need to be rendered
516
+ comment=cte.comment if CONFIG.show_comments else None,
514
517
  joins=[
515
518
  j
516
519
  for j in [
@@ -26,7 +26,8 @@ def render_join(
26
26
  raise ValueError("must provide a cte to build an unnest joins")
27
27
  if unnest_mode == UnnestMode.CROSS_JOIN:
28
28
  return f"CROSS JOIN {render_func(join.concept, cte, False)} as {quote_character}{join.concept.safe_address}{quote_character}"
29
-
29
+ if unnest_mode == UnnestMode.CROSS_JOIN_ALIAS:
30
+ return f"CROSS JOIN {render_func(join.concept, cte, False)} as array_unnest ({quote_character}{join.concept.safe_address}{quote_character})"
30
31
  return f"FULL JOIN {render_func(join.concept, cte, False)} as unnest_wrapper({quote_character}{join.concept.safe_address}{quote_character})"
31
32
  left_name = join.left_name
32
33
  right_name = join.right_name
@@ -47,8 +47,9 @@ CREATE OR REPLACE TABLE {{ output.address.location }} AS
47
47
  {% endif %}{%- if ctes %}
48
48
  WITH {% for cte in ctes %}
49
49
  {{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
50
- {% if full_select -%}{{full_select}}
51
- {% else -%}
50
+ {%- if full_select -%}{{full_select}}
51
+ {%- else -%}{%- if comment %}
52
+ -- {{ comment }}{% endif %}
52
53
  SELECT
53
54
  {%- for select in select_columns %}
54
55
  {{ select }}{% if not loop.last %},{% endif %}{% endfor %}
@@ -56,7 +57,8 @@ SELECT
56
57
  {{ base }}{% endif %}{% if joins %}
57
58
  {%- for join in joins %}
58
59
  {{ join }}{% endfor %}{% endif %}
59
- {% if where %}WHERE
60
+ {%- if where %}
61
+ WHERE
60
62
  {{ where }}
61
63
  {% endif -%}{%- if group_by %}
62
64
  GROUP BY {% for group in group_by %}
@@ -5,7 +5,7 @@ from jinja2 import Template
5
5
  from trilogy.core.enums import FunctionType, WindowType
6
6
  from trilogy.dialect.base import BaseDialect
7
7
  from trilogy.core.models import DataType
8
-
8
+ from trilogy.core.enums import UnnestMode
9
9
 
10
10
  WINDOW_FUNCTION_MAP: Mapping[WindowType, Callable[[Any, Any, Any], str]] = {}
11
11
 
@@ -86,6 +86,7 @@ class PrestoDialect(BaseDialect):
86
86
  QUOTE_CHARACTER = '"'
87
87
  SQL_TEMPLATE = SQL_TEMPLATE
88
88
  DATATYPE_MAP = {**BaseDialect.DATATYPE_MAP, DataType.NUMERIC: "DECIMAL"}
89
+ UNNEST_MODE = UnnestMode.CROSS_JOIN
89
90
 
90
91
 
91
92
  class TrinoDialect(PrestoDialect):
@@ -112,12 +112,16 @@ def filter_item_to_concept(
112
112
  return Concept(
113
113
  name=name,
114
114
  datatype=parent.content.datatype,
115
- purpose=parent.content.purpose,
115
+ purpose=Purpose.PROPERTY,
116
116
  lineage=parent,
117
117
  metadata=fmetadata,
118
118
  namespace=namespace,
119
119
  # filtered copies cannot inherit keys
120
- keys=None,
120
+ keys=(
121
+ parent.content.keys
122
+ if parent.content.purpose == Purpose.PROPERTY
123
+ else (parent.content,)
124
+ ),
121
125
  grain=(
122
126
  parent.content.grain
123
127
  if parent.content.purpose == Purpose.PROPERTY
@@ -297,8 +297,11 @@ class ParseToObjects(Transformer):
297
297
  def concept_lit(self, args) -> Concept:
298
298
  return self.environment.concepts.__getitem__(args[0])
299
299
 
300
- def ADDRESS(self, args) -> str:
301
- return args.value
300
+ def ADDRESS(self, args) -> Address:
301
+ return Address(location=args.value, quoted=False)
302
+
303
+ def QUOTED_ADDRESS(self, args) -> Address:
304
+ return Address(location=args.value[1:-1], quoted=True)
302
305
 
303
306
  def STRING_CHARS(self, args) -> str:
304
307
  return args.value
@@ -1010,7 +1013,7 @@ class ParseToObjects(Transformer):
1010
1013
 
1011
1014
  @v_args(meta=True)
1012
1015
  def address(self, meta: Meta, args):
1013
- return Address(location=args[0])
1016
+ return args[0]
1014
1017
 
1015
1018
  @v_args(meta=True)
1016
1019
  def query(self, meta: Meta, args):
@@ -39,7 +39,7 @@
39
39
 
40
40
  grain_clause: "grain" "(" column_list ")"
41
41
 
42
- address: "address" ADDRESS
42
+ address: "address" (QUOTED_ADDRESS | ADDRESS)
43
43
 
44
44
  query: "query" MULTILINE_STRING
45
45
 
@@ -258,7 +258,8 @@
258
258
  // base language constructs
259
259
  concept_lit: IDENTIFIER
260
260
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.\-]*/
261
- ADDRESS: IDENTIFIER | /`[a-zA-Z\_][a-zA-Z0-9\_\-\.\-\*]*`/
261
+ QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\-\.\-\*\:]*`/
262
+ ADDRESS: IDENTIFIER
262
263
 
263
264
  MULTILINE_STRING: /\'{3}(.*?)\'{3}/s
264
265
 
File without changes
File without changes
File without changes
File without changes
File without changes