pytrilogy 0.0.2.27__tar.gz → 0.0.2.29__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 (108) hide show
  1. {pytrilogy-0.0.2.27/pytrilogy.egg-info → pytrilogy-0.0.2.29}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pytrilogy.egg-info/SOURCES.txt +1 -0
  4. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_environment.py +1 -1
  5. pytrilogy-0.0.2.29/tests/test_executor.py +8 -0
  6. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_parsing.py +31 -3
  7. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_undefined_concept.py +1 -1
  8. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/__init__.py +1 -1
  9. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/models.py +38 -11
  10. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimization.py +1 -15
  11. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/concept_strategies_v3.py +2 -3
  12. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/select_merge_node.py +16 -2
  13. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/base_node.py +0 -3
  14. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/select_node_v2.py +1 -4
  15. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/utility.py +41 -6
  16. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/base.py +13 -9
  17. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/executor.py +17 -1
  18. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/LICENSE.md +0 -0
  19. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/README.md +0 -0
  20. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pyproject.toml +0 -0
  21. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pytrilogy.egg-info/dependency_links.txt +0 -0
  22. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pytrilogy.egg-info/entry_points.txt +0 -0
  23. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pytrilogy.egg-info/requires.txt +0 -0
  24. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/pytrilogy.egg-info/top_level.txt +0 -0
  25. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/setup.cfg +0 -0
  26. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/setup.py +0 -0
  27. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_datatypes.py +0 -0
  28. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_declarations.py +0 -0
  29. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_derived_concepts.py +0 -0
  30. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_discovery_nodes.py +0 -0
  31. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_functions.py +0 -0
  32. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_imports.py +0 -0
  33. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_metadata.py +0 -0
  34. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_models.py +0 -0
  35. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_multi_join_assignments.py +0 -0
  36. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_partial_handling.py +0 -0
  37. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_query_processing.py +0 -0
  38. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_select.py +0 -0
  39. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_show.py +0 -0
  40. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_statements.py +0 -0
  41. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/tests/test_where_clause.py +0 -0
  42. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/compiler.py +0 -0
  43. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/constants.py +0 -0
  44. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/__init__.py +0 -0
  45. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/constants.py +0 -0
  46. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/enums.py +0 -0
  47. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/env_processor.py +0 -0
  48. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/environment_helpers.py +0 -0
  49. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/ergonomics.py +0 -0
  50. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/exceptions.py +0 -0
  51. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/functions.py +0 -0
  52. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/graph_models.py +0 -0
  53. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/internal.py +0 -0
  54. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimizations/__init__.py +0 -0
  55. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimizations/base_optimization.py +0 -0
  56. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimizations/inline_constant.py +0 -0
  57. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimizations/inline_datasource.py +0 -0
  58. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  59. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/__init__.py +0 -0
  60. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/graph_utils.py +0 -0
  61. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/__init__.py +0 -0
  62. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  63. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/common.py +0 -0
  64. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  65. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/group_node.py +0 -0
  66. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  67. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  68. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  69. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  70. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/select_node.py +0 -0
  71. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  72. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/node_generators/window_node.py +0 -0
  73. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/__init__.py +0 -0
  74. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/filter_node.py +0 -0
  75. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/group_node.py +0 -0
  76. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/merge_node.py +0 -0
  77. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  78. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/processing/nodes/window_node.py +0 -0
  79. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/core/query_processor.py +0 -0
  80. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/__init__.py +0 -0
  81. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/bigquery.py +0 -0
  82. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/common.py +0 -0
  83. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/config.py +0 -0
  84. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/duckdb.py +0 -0
  85. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/enums.py +0 -0
  86. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/postgres.py +0 -0
  87. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/presto.py +0 -0
  88. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/snowflake.py +0 -0
  89. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/dialect/sql_server.py +0 -0
  90. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/engine.py +0 -0
  91. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/hooks/__init__.py +0 -0
  92. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/hooks/base_hook.py +0 -0
  93. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/hooks/graph_hook.py +0 -0
  94. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/hooks/query_debugger.py +0 -0
  95. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/metadata/__init__.py +0 -0
  96. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parser.py +0 -0
  97. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/__init__.py +0 -0
  98. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/common.py +0 -0
  99. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/config.py +0 -0
  100. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/exceptions.py +0 -0
  101. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/helpers.py +0 -0
  102. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/parse_engine.py +0 -0
  103. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/render.py +0 -0
  104. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/parsing/trilogy.lark +0 -0
  105. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/py.typed +0 -0
  106. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/scripts/__init__.py +0 -0
  107. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/scripts/trilogy.py +0 -0
  108. {pytrilogy-0.0.2.27 → pytrilogy-0.0.2.29}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.27
3
+ Version: 0.0.2.29
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.27
3
+ Version: 0.0.2.29
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -13,6 +13,7 @@ tests/test_declarations.py
13
13
  tests/test_derived_concepts.py
14
14
  tests/test_discovery_nodes.py
15
15
  tests/test_environment.py
16
+ tests/test_executor.py
16
17
  tests/test_functions.py
17
18
  tests/test_imports.py
18
19
  tests/test_metadata.py
@@ -20,7 +20,7 @@ def test_environment_from_path():
20
20
 
21
21
  env = Environment.from_file(Path(__file__).parent / "test_env.preql")
22
22
 
23
- assert "id" in env.concepts
23
+ assert "local.id" in env.concepts
24
24
 
25
25
 
26
26
  def test_environment_merge():
@@ -0,0 +1,8 @@
1
+ from trilogy import Dialects
2
+ from pathlib import Path
3
+
4
+
5
+ def test_file_parsing():
6
+ target = Path(__file__).parent / "test_env.preql"
7
+ parsed = Dialects.DUCK_DB.default_executor().parse_file(target)
8
+ assert len(list(parsed)) == 1
@@ -7,6 +7,7 @@ from trilogy.core.models import (
7
7
  Environment,
8
8
  Comparison,
9
9
  TupleWrapper,
10
+ Datasource,
10
11
  )
11
12
  from trilogy.core.functions import argument_to_purpose, function_args_to_output_purpose
12
13
  from trilogy.parsing.parse_engine import (
@@ -178,7 +179,7 @@ select
178
179
  )
179
180
 
180
181
  for name in ["name_alphabetical", "name_alphabetical_2"]:
181
- assert name in env.concepts
182
+ assert f"local.{name}" in env.concepts
182
183
  assert env.concepts[name].purpose == Purpose.PROPERTY
183
184
  assert env.concepts[name].keys == (env.concepts["id"],)
184
185
 
@@ -197,7 +198,6 @@ select
197
198
  )
198
199
 
199
200
  for name in ["join_id"]:
200
- assert name in env.concepts
201
201
  assert env.concepts[name].purpose == Purpose.PROPERTY
202
202
  assert env.concepts[name].keys == (
203
203
  env.concepts["id"],
@@ -225,7 +225,6 @@ select
225
225
  )
226
226
  # assert output_purpose == Purpose.METRIC
227
227
  for name in ["test_name_count"]:
228
- assert name in env.concepts
229
228
  assert env.concepts[name].purpose == Purpose.METRIC
230
229
 
231
230
 
@@ -513,6 +512,35 @@ address `abc:def`
513
512
  assert ds.non_partial_for.conditional.right == 10
514
513
 
515
514
 
515
+ def test_datasource_from_persist():
516
+
517
+ text = """
518
+ key x int;
519
+ key y int;
520
+
521
+ datasource test (
522
+ x:x,
523
+ y:y)
524
+ grain(x)
525
+ address `abc:def`
526
+ ;
527
+
528
+ persist alias into tbl_alias from
529
+ select
530
+ x,
531
+ y
532
+ where y>10;
533
+
534
+
535
+
536
+ """
537
+ env, parsed = parse_text(text)
538
+
539
+ ds: Datasource = parsed[-1].datasource
540
+ assert ds.non_partial_for.conditional.right == 10
541
+ assert not ds.where
542
+
543
+
516
544
  def test_filter_concise():
517
545
 
518
546
  text = """
@@ -32,6 +32,6 @@ def test_undefined_concept_dict():
32
32
  try:
33
33
  env["orid"]
34
34
  except UndefinedConceptException as e:
35
- assert e.suggestions == ["order_id"]
35
+ assert e.suggestions == ["local.order_id"]
36
36
  assert "suggestions" in e.message.lower()
37
37
  assert "order_id" in e.message.lower()
@@ -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.27"
7
+ __version__ = "0.0.2.29"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -606,6 +606,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
606
606
  return self.grain.components_copy if self.grain else []
607
607
 
608
608
  def with_namespace(self, namespace: str) -> "Concept":
609
+ if namespace == self.namespace:
610
+ return self
609
611
  return self.__class__(
610
612
  name=self.name,
611
613
  datatype=self.datatype,
@@ -1758,7 +1760,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1758
1760
  grain=grain or self.grain,
1759
1761
  columns=columns,
1760
1762
  namespace=namespace,
1761
- where=WhereClause(conditional=condition) if condition else None,
1762
1763
  non_partial_for=WhereClause(conditional=condition) if condition else None,
1763
1764
  )
1764
1765
  for column in columns:
@@ -3253,7 +3254,6 @@ class EnvironmentConceptDict(dict):
3253
3254
  )
3254
3255
  self.undefined[key] = undefined
3255
3256
  return undefined
3256
-
3257
3257
  matches = self._find_similar_concepts(key)
3258
3258
  message = f"Undefined concept: {key}."
3259
3259
  if matches:
@@ -3263,8 +3263,15 @@ class EnvironmentConceptDict(dict):
3263
3263
  raise UndefinedConceptException(f"line: {line_no}: " + message, matches)
3264
3264
  raise UndefinedConceptException(message, matches)
3265
3265
 
3266
- def _find_similar_concepts(self, concept_name):
3267
- matches = difflib.get_close_matches(concept_name, self.keys())
3266
+ def _find_similar_concepts(self, concept_name: str):
3267
+ def strip_local(input: str):
3268
+ if input.startswith(f"{DEFAULT_NAMESPACE}."):
3269
+ return input[len(DEFAULT_NAMESPACE) + 1 :]
3270
+ return input
3271
+
3272
+ matches = difflib.get_close_matches(
3273
+ strip_local(concept_name), [strip_local(x) for x in self.keys()]
3274
+ )
3268
3275
  return matches
3269
3276
 
3270
3277
  def items(self) -> ItemsView[str, Concept]: # type: ignore
@@ -3325,7 +3332,6 @@ class Environment(BaseModel):
3325
3332
 
3326
3333
  materialized_concepts: List[Concept] = Field(default_factory=list)
3327
3334
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3328
- canonical_map: Dict[str, str] = Field(default_factory=dict)
3329
3335
  _parse_count: int = 0
3330
3336
 
3331
3337
  @classmethod
@@ -3444,14 +3450,38 @@ class Environment(BaseModel):
3444
3450
  exists = True
3445
3451
  imp_stm = ImportStatement(alias=alias, path=Path(source.working_path))
3446
3452
 
3453
+ same_namespace = alias == self.namespace
3454
+
3447
3455
  if not exists:
3448
3456
  self.imports[alias].append(imp_stm)
3449
3457
 
3450
- for _, concept in source.concepts.items():
3451
- self.add_concept(concept.with_namespace(alias), _ignore_cache=True)
3458
+ for k, concept in source.concepts.items():
3459
+ if same_namespace:
3460
+ new = self.add_concept(concept, _ignore_cache=True)
3461
+ else:
3462
+ new = self.add_concept(
3463
+ concept.with_namespace(alias), _ignore_cache=True
3464
+ )
3465
+
3466
+ k = address_with_namespace(k, alias)
3467
+ # set this explicitly, to handle aliasing
3468
+ self.concepts[k] = new
3452
3469
 
3453
3470
  for _, datasource in source.datasources.items():
3454
- self.add_datasource(datasource.with_namespace(alias), _ignore_cache=True)
3471
+ if same_namespace:
3472
+ self.add_datasource(datasource, _ignore_cache=True)
3473
+ else:
3474
+ self.add_datasource(
3475
+ datasource.with_namespace(alias), _ignore_cache=True
3476
+ )
3477
+ for key, val in source.alias_origin_lookup.items():
3478
+ if same_namespace:
3479
+ self.alias_origin_lookup[key] = val
3480
+ else:
3481
+ self.alias_origin_lookup[address_with_namespace(key, alias)] = (
3482
+ val.with_namespace(alias)
3483
+ )
3484
+
3455
3485
  self.gen_concept_list_caches()
3456
3486
  return self
3457
3487
 
@@ -3542,8 +3572,6 @@ class Environment(BaseModel):
3542
3572
  existing = self.validate_concept(concept, meta=meta)
3543
3573
  if existing:
3544
3574
  concept = existing
3545
- if concept.namespace == DEFAULT_NAMESPACE:
3546
- self.concepts[concept.name] = concept
3547
3575
  self.concepts[concept.address] = concept
3548
3576
  from trilogy.core.environment_helpers import generate_related_concepts
3549
3577
 
@@ -3631,7 +3659,6 @@ class Environment(BaseModel):
3631
3659
  v.pseudonyms.add(source.address)
3632
3660
  if v.address == source.address:
3633
3661
  replacements[k] = target
3634
- self.canonical_map[k] = target.address
3635
3662
  v.pseudonyms.add(target.address)
3636
3663
  # we need to update keys and grains of all concepts
3637
3664
  else:
@@ -13,7 +13,7 @@ from trilogy.core.optimizations import (
13
13
  PredicatePushdownRemove,
14
14
  InlineDatasource,
15
15
  )
16
-
16
+ from trilogy.core.processing.utility import sort_select_output
17
17
 
18
18
  MAX_OPTIMIZATION_LOOPS = 100
19
19
 
@@ -154,20 +154,6 @@ def is_direct_return_eligible(cte: CTE) -> CTE | None:
154
154
  return direct_parent
155
155
 
156
156
 
157
- def sort_select_output(cte: CTE, query: SelectStatement | MultiSelectStatement):
158
- hidden_addresses = [c.address for c in query.hidden_components]
159
- output_addresses = [
160
- c.address for c in query.output_components if c.address not in hidden_addresses
161
- ]
162
-
163
- mapping = {x.address: x for x in cte.output_columns}
164
-
165
- new_output = []
166
- for x in output_addresses:
167
- new_output.append(mapping[x])
168
- cte.output_columns = new_output
169
-
170
-
171
157
  def optimize_ctes(
172
158
  input: list[CTE], root_cte: CTE, select: SelectStatement | MultiSelectStatement
173
159
  ) -> list[CTE]:
@@ -838,9 +838,8 @@ def _search_concepts(
838
838
  f" {accept_partial} (complete: {complete}), have {found} from {[n for n in stack]} (missing {missing} partial {partial} virtual {virtual}), attempted {attempted}, mandatory w/ filter {mandatory_completion}"
839
839
  )
840
840
  if complete == ValidationResult.INCOMPLETE_CONDITION:
841
- raise SyntaxError(
842
- {str(node): node.preexisting_conditions for node in stack}
843
- )
841
+ cond_dict = {str(node): node.preexisting_conditions for node in stack}
842
+ raise SyntaxError(f"Have {cond_dict} and need {str(conditions)}")
844
843
  # early exit if we have a complete stack with one node
845
844
  # we can only early exit if we have a complete stack
846
845
  # and we are not looking for more non-partial sources
@@ -233,7 +233,6 @@ def create_select_node(
233
233
  ]
234
234
  nullable_lcl = LooseConceptList(concepts=nullable_concepts)
235
235
  partial_is_full = conditions and (conditions == datasource.non_partial_for)
236
-
237
236
  bcandidate: StrategyNode = SelectNode(
238
237
  input_concepts=[c.concept for c in datasource.columns],
239
238
  output_concepts=all_concepts,
@@ -249,7 +248,9 @@ def create_select_node(
249
248
  datasource=datasource,
250
249
  grain=Grain(components=all_concepts),
251
250
  conditions=datasource.where.conditional if datasource.where else None,
252
- render_condition=not partial_is_full,
251
+ preexisting_conditions=(
252
+ conditions.conditional if partial_is_full and conditions else None
253
+ ),
253
254
  )
254
255
 
255
256
  # we need to nest the group node one further
@@ -263,6 +264,9 @@ def create_select_node(
263
264
  depth=depth,
264
265
  partial_concepts=bcandidate.partial_concepts,
265
266
  nullable_concepts=bcandidate.nullable_concepts,
267
+ preexisting_conditions=(
268
+ conditions.conditional if partial_is_full and conditions else None
269
+ ),
266
270
  )
267
271
  else:
268
272
  candidate = bcandidate
@@ -340,6 +344,15 @@ def gen_select_merge_node(
340
344
 
341
345
  if len(parents) == 1:
342
346
  return parents[0]
347
+ preexisting_conditions = None
348
+ if conditions and all(
349
+ [
350
+ x.preexisting_conditions
351
+ and x.preexisting_conditions == conditions.conditional
352
+ for x in parents
353
+ ]
354
+ ):
355
+ preexisting_conditions = conditions.conditional
343
356
  return MergeNode(
344
357
  output_concepts=all_concepts,
345
358
  input_concepts=non_constant,
@@ -347,4 +360,5 @@ def gen_select_merge_node(
347
360
  g=g,
348
361
  depth=depth,
349
362
  parents=parents,
363
+ preexisting_conditions=preexisting_conditions,
350
364
  )
@@ -165,7 +165,6 @@ class StrategyNode:
165
165
  hidden_concepts: List[Concept] | None = None,
166
166
  existence_concepts: List[Concept] | None = None,
167
167
  virtual_output_concepts: List[Concept] | None = None,
168
- render_condition: bool = True,
169
168
  ):
170
169
  self.input_concepts: List[Concept] = (
171
170
  unique(input_concepts, "address") if input_concepts else []
@@ -209,7 +208,6 @@ class StrategyNode:
209
208
  )
210
209
  self.validate_parents()
211
210
  self.log = True
212
- self.render_condition = render_condition
213
211
 
214
212
  def add_parents(self, parents: list["StrategyNode"]):
215
213
  self.parents += parents
@@ -382,7 +380,6 @@ class StrategyNode:
382
380
  hidden_concepts=list(self.hidden_concepts),
383
381
  existence_concepts=list(self.existence_concepts),
384
382
  virtual_output_concepts=list(self.virtual_output_concepts),
385
- render_condition=self.render_condition,
386
383
  )
387
384
 
388
385
 
@@ -49,7 +49,6 @@ class SelectNode(StrategyNode):
49
49
  conditions: Conditional | Comparison | Parenthetical | None = None,
50
50
  preexisting_conditions: Conditional | Comparison | Parenthetical | None = None,
51
51
  hidden_concepts: List[Concept] | None = None,
52
- render_condition: bool = True,
53
52
  ):
54
53
  super().__init__(
55
54
  input_concepts=input_concepts,
@@ -66,7 +65,6 @@ class SelectNode(StrategyNode):
66
65
  conditions=conditions,
67
66
  preexisting_conditions=preexisting_conditions,
68
67
  hidden_concepts=hidden_concepts,
69
- render_condition=render_condition,
70
68
  )
71
69
  self.accept_partial = accept_partial
72
70
  self.datasource = datasource
@@ -123,7 +121,7 @@ class SelectNode(StrategyNode):
123
121
  nullable_concepts=[c.concept for c in datasource.columns if c.is_nullable],
124
122
  source_type=SourceType.DIRECT_SELECT,
125
123
  # we can skip rendering conditions
126
- condition=self.conditions if self.render_condition else None,
124
+ condition=self.conditions,
127
125
  # select nodes should never group
128
126
  force_group=self.force_group,
129
127
  hidden_concepts=self.hidden_concepts,
@@ -208,7 +206,6 @@ class SelectNode(StrategyNode):
208
206
  conditions=self.conditions,
209
207
  preexisting_conditions=self.preexisting_conditions,
210
208
  hidden_concepts=self.hidden_concepts,
211
- render_condition=self.render_condition,
212
209
  )
213
210
 
214
211
 
@@ -29,6 +29,10 @@ from trilogy.core.models import (
29
29
  NumericType,
30
30
  ListType,
31
31
  TupleWrapper,
32
+ CTE,
33
+ MultiSelectStatement,
34
+ SelectStatement,
35
+ ProcessedQuery,
32
36
  )
33
37
 
34
38
  from trilogy.core.enums import Purpose, Granularity, BooleanOperator
@@ -66,9 +70,6 @@ def resolve_join_order_v2(
66
70
  ) -> list[JoinOrderOutput]:
67
71
  datasources = [x for x in g.nodes if x.startswith("ds~")]
68
72
  concepts = [x for x in g.nodes if x.startswith("c~")]
69
- # from trilogy.hooks.graph_hook import GraphHook
70
-
71
- # GraphHook().query_graph_built(g)
72
73
 
73
74
  output: list[JoinOrderOutput] = []
74
75
  pivot_map = {
@@ -78,7 +79,7 @@ def resolve_join_order_v2(
78
79
  pivots = list(
79
80
  sorted(
80
81
  [x for x in pivot_map if len(pivot_map[x]) > 1],
81
- key=lambda x: len(pivot_map[x]),
82
+ key=lambda x: (len(pivot_map[x]), len(x), x),
82
83
  )
83
84
  )
84
85
  solo = [x for x in pivot_map if len(pivot_map[x]) == 1]
@@ -95,7 +96,7 @@ def resolve_join_order_v2(
95
96
  root = pivots.pop()
96
97
 
97
98
  # sort so less partials is last and eligible lefts are
98
- def score_key(x: str) -> int:
99
+ def score_key(x: str) -> tuple[int, int, str]:
99
100
  base = 1
100
101
  # if it's left, higher weight
101
102
  if x in eligible_left:
@@ -103,7 +104,7 @@ def resolve_join_order_v2(
103
104
  # if it has the concept as a partial, lower weight
104
105
  if root in partials.get(x, []):
105
106
  base -= 1
106
- return base
107
+ return (base, len(x), x)
107
108
 
108
109
  # get remainig un-joined datasets
109
110
  to_join = sorted(
@@ -531,3 +532,37 @@ def find_nullable_concepts(
531
532
  if set(v).issubset(all_ds):
532
533
  final_nullable.add(k)
533
534
  return list(sorted(final_nullable))
535
+
536
+
537
+ def sort_select_output_processed(cte: CTE, query: ProcessedQuery) -> CTE:
538
+ hidden_addresses = [c.address for c in query.hidden_columns]
539
+ output_addresses = [
540
+ c.address for c in query.output_columns if c.address not in hidden_addresses
541
+ ]
542
+
543
+ mapping = {x.address: x for x in cte.output_columns}
544
+
545
+ new_output = []
546
+ for x in output_addresses:
547
+ new_output.append(mapping[x])
548
+ cte.output_columns = new_output
549
+ return cte
550
+
551
+
552
+ def sort_select_output(
553
+ cte: CTE, query: SelectStatement | MultiSelectStatement | ProcessedQuery
554
+ ) -> CTE:
555
+ if isinstance(query, ProcessedQuery):
556
+ return sort_select_output_processed(cte, query)
557
+ hidden_addresses = [c.address for c in query.hidden_components]
558
+ output_addresses = [
559
+ c.address for c in query.output_components if c.address not in hidden_addresses
560
+ ]
561
+
562
+ mapping = {x.address: x for x in cte.output_columns}
563
+
564
+ new_output = []
565
+ for x in output_addresses:
566
+ new_output.append(mapping[x])
567
+ cte.output_columns = new_output
568
+ return cte
@@ -2,7 +2,11 @@ from typing import List, Union, Optional, Dict, Any, Sequence, Callable
2
2
 
3
3
  from jinja2 import Template
4
4
 
5
- from trilogy.core.processing.utility import is_scalar_condition, decompose_condition
5
+ from trilogy.core.processing.utility import (
6
+ is_scalar_condition,
7
+ decompose_condition,
8
+ sort_select_output,
9
+ )
6
10
  from trilogy.constants import CONFIG, logger, MagicConstants
7
11
  from trilogy.core.internal import DEFAULT_CONCEPTS
8
12
  from trilogy.core.enums import (
@@ -537,7 +541,7 @@ class BaseDialect:
537
541
  else:
538
542
  raise ValueError(f"Unable to render type {type(e)} {e}")
539
543
 
540
- def render_cte(self, cte: CTE):
544
+ def render_cte(self, cte: CTE, auto_sort: bool = True):
541
545
  if self.UNNEST_MODE in (
542
546
  UnnestMode.CROSS_APPLY,
543
547
  UnnestMode.CROSS_JOIN,
@@ -561,6 +565,8 @@ class BaseDialect:
561
565
  for c in cte.output_columns
562
566
  if c.address not in [y.address for y in cte.hidden_concepts]
563
567
  ]
568
+ if auto_sort:
569
+ select_columns = sorted(select_columns, key=lambda x: x)
564
570
  source: str | None = cte.base_name
565
571
  if not cte.render_from_clause:
566
572
  if len(cte.joins) > 0:
@@ -657,8 +663,11 @@ class BaseDialect:
657
663
  def generate_ctes(
658
664
  self,
659
665
  query: ProcessedQuery,
660
- ):
661
- return [self.render_cte(cte) for cte in query.ctes]
666
+ ) -> List[CompiledCTE]:
667
+ return [self.render_cte(cte) for cte in query.ctes[:-1]] + [
668
+ # last CTE needs to respect the user output order
669
+ self.render_cte(sort_select_output(query.ctes[-1], query), auto_sort=False)
670
+ ]
662
671
 
663
672
  def generate_queries(
664
673
  self,
@@ -790,11 +799,6 @@ class BaseDialect:
790
799
 
791
800
  compiled_ctes = self.generate_ctes(query)
792
801
 
793
- # restort selections by the order they were written in
794
- sorted_select: List[str] = []
795
- for output_c in output_addresses:
796
- sorted_select.append(select_columns[output_c])
797
-
798
802
  final = self.SQL_TEMPLATE.render(
799
803
  output=(
800
804
  query.output_to if isinstance(query, ProcessedQueryPersist) else None
@@ -276,6 +276,20 @@ class Executor(object):
276
276
  output.append(compiled_sql)
277
277
  return output
278
278
 
279
+ def parse_file(self, file: str | Path, persist: bool = False) -> Generator[
280
+ ProcessedQuery
281
+ | ProcessedQueryPersist
282
+ | ProcessedShowStatement
283
+ | ProcessedRawSQLStatement
284
+ | ProcessedCopyStatement,
285
+ None,
286
+ None,
287
+ ]:
288
+ file = Path(file)
289
+ with open(file, "r") as f:
290
+ command = f.read()
291
+ return self.parse_text_generator(command, persist=persist)
292
+
279
293
  def parse_text(
280
294
  self, command: str, persist: bool = False
281
295
  ) -> List[
@@ -319,9 +333,11 @@ class Executor(object):
319
333
  x = self.generator.generate_queries(
320
334
  self.environment, [t], hooks=self.hooks
321
335
  )[0]
336
+
337
+ yield x
338
+
322
339
  if persist and isinstance(x, ProcessedQueryPersist):
323
340
  self.environment.add_datasource(x.datasource)
324
- yield x
325
341
 
326
342
  def execute_raw_sql(
327
343
  self, command: str, variables: dict | None = None
File without changes
File without changes
File without changes
File without changes