pytrilogy 0.0.2.36__tar.gz → 0.0.2.38__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 (110) hide show
  1. {pytrilogy-0.0.2.36/pytrilogy.egg-info → pytrilogy-0.0.2.38}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_select.py +1 -1
  4. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/constants.py +8 -0
  6. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/models.py +23 -20
  7. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/base.py +8 -1
  8. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/executor.py +77 -15
  9. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/parse_engine.py +46 -13
  10. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/render.py +3 -1
  11. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/trilogy.lark +4 -4
  12. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/LICENSE.md +0 -0
  13. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/README.md +0 -0
  14. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pyproject.toml +0 -0
  15. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pytrilogy.egg-info/SOURCES.txt +0 -0
  16. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pytrilogy.egg-info/dependency_links.txt +0 -0
  17. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pytrilogy.egg-info/entry_points.txt +0 -0
  18. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pytrilogy.egg-info/requires.txt +0 -0
  19. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/pytrilogy.egg-info/top_level.txt +0 -0
  20. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/setup.cfg +0 -0
  21. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/setup.py +0 -0
  22. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_datatypes.py +0 -0
  23. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_declarations.py +0 -0
  24. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_derived_concepts.py +0 -0
  25. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_discovery_nodes.py +0 -0
  26. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_enums.py +0 -0
  27. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_environment.py +0 -0
  28. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_executor.py +0 -0
  29. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_functions.py +0 -0
  30. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_imports.py +0 -0
  31. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_metadata.py +0 -0
  32. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_models.py +0 -0
  33. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_multi_join_assignments.py +0 -0
  34. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_parse_engine.py +0 -0
  35. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_parsing.py +0 -0
  36. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_partial_handling.py +0 -0
  37. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_query_processing.py +0 -0
  38. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_show.py +0 -0
  39. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_statements.py +0 -0
  40. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_undefined_concept.py +0 -0
  41. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/tests/test_where_clause.py +0 -0
  42. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/compiler.py +0 -0
  43. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/__init__.py +0 -0
  44. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/constants.py +0 -0
  45. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/enums.py +0 -0
  46. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/env_processor.py +0 -0
  47. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/environment_helpers.py +0 -0
  48. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/ergonomics.py +0 -0
  49. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/exceptions.py +0 -0
  50. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/functions.py +0 -0
  51. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/graph_models.py +0 -0
  52. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/internal.py +0 -0
  53. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimization.py +0 -0
  54. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimizations/__init__.py +0 -0
  55. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimizations/base_optimization.py +0 -0
  56. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimizations/inline_constant.py +0 -0
  57. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimizations/inline_datasource.py +0 -0
  58. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  59. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/__init__.py +0 -0
  60. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  61. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/graph_utils.py +0 -0
  62. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/__init__.py +0 -0
  63. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  64. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/common.py +0 -0
  65. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  66. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/group_node.py +0 -0
  67. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  68. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  69. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  70. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  71. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  72. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/select_node.py +0 -0
  73. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  74. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/node_generators/window_node.py +0 -0
  75. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/__init__.py +0 -0
  76. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/base_node.py +0 -0
  77. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/filter_node.py +0 -0
  78. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/group_node.py +0 -0
  79. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/merge_node.py +0 -0
  80. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  81. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  82. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/nodes/window_node.py +0 -0
  83. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/processing/utility.py +0 -0
  84. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/core/query_processor.py +0 -0
  85. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/__init__.py +0 -0
  86. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/bigquery.py +0 -0
  87. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/common.py +0 -0
  88. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/config.py +0 -0
  89. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/duckdb.py +0 -0
  90. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/enums.py +0 -0
  91. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/postgres.py +0 -0
  92. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/presto.py +0 -0
  93. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/snowflake.py +0 -0
  94. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/dialect/sql_server.py +0 -0
  95. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/engine.py +0 -0
  96. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/hooks/__init__.py +0 -0
  97. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/hooks/base_hook.py +0 -0
  98. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/hooks/graph_hook.py +0 -0
  99. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/hooks/query_debugger.py +0 -0
  100. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/metadata/__init__.py +0 -0
  101. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parser.py +0 -0
  102. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/__init__.py +0 -0
  103. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/common.py +0 -0
  104. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/config.py +0 -0
  105. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/exceptions.py +0 -0
  106. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/parsing/helpers.py +0 -0
  107. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/py.typed +0 -0
  108. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/scripts/__init__.py +0 -0
  109. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/scripts/trilogy.py +0 -0
  110. {pytrilogy-0.0.2.36 → pytrilogy-0.0.2.38}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.36
3
+ Version: 0.0.2.38
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.36
3
+ Version: 0.0.2.38
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -126,7 +126,7 @@ def test_modifiers():
126
126
  generator = BigqueryDialect()
127
127
 
128
128
  text = generator.compile_statement(query)
129
- assert "2 = 2" in text
129
+ assert ":b = 2" in text
130
130
  assert "as `b`" not in text
131
131
 
132
132
 
@@ -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.36"
7
+ __version__ = "0.0.2.38"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -39,6 +39,13 @@ class Comments:
39
39
  partial: bool = True
40
40
 
41
41
 
42
+ @dataclass
43
+ class Rendering:
44
+ """Control how the SQL is rendered"""
45
+
46
+ parameters: bool = True
47
+
48
+
42
49
  # TODO: support loading from environments
43
50
  @dataclass
44
51
  class Config:
@@ -48,6 +55,7 @@ class Config:
48
55
  validate_missing: bool = True
49
56
  comments: Comments = field(default_factory=Comments)
50
57
  optimizations: Optimizations = field(default_factory=Optimizations)
58
+ rendering: Rendering = field(default_factory=Rendering)
51
59
 
52
60
  @property
53
61
  def show_comments(self) -> bool:
@@ -2085,14 +2085,16 @@ class DatasourceMetadata(BaseModel):
2085
2085
 
2086
2086
 
2087
2087
  class MergeStatementV2(HasUUID, Namespaced, BaseModel):
2088
- source: Concept
2089
- target: Concept
2088
+ sources: list[Concept]
2089
+ targets: dict[str, Concept]
2090
+ source_wildcard: str | None = None
2091
+ target_wildcard: str | None = None
2090
2092
  modifiers: List[Modifier] = Field(default_factory=list)
2091
2093
 
2092
2094
  def with_namespace(self, namespace: str) -> "MergeStatementV2":
2093
2095
  new = MergeStatementV2(
2094
- source=self.source.with_namespace(namespace),
2095
- target=self.target.with_namespace(namespace),
2096
+ sources=[x.with_namespace(namespace) for x in self.sources],
2097
+ targets={k: v.with_namespace(namespace) for k, v in self.targets.items()},
2096
2098
  modifiers=self.modifiers,
2097
2099
  )
2098
2100
  return new
@@ -3381,6 +3383,21 @@ class Environment(BaseModel):
3381
3383
  materialized_concepts: List[Concept] = Field(default_factory=list)
3382
3384
  alias_origin_lookup: Dict[str, Concept] = Field(default_factory=dict)
3383
3385
 
3386
+ def __init__(self, **data):
3387
+ super().__init__(**data)
3388
+ self.concepts["_env_working_path"] = Concept(
3389
+ name="_env_working_path",
3390
+ namespace=self.namespace,
3391
+ lineage=Function(
3392
+ operator=FunctionType.CONSTANT,
3393
+ arguments=[str(self.working_path)],
3394
+ output_datatype=DataType.STRING,
3395
+ output_purpose=Purpose.CONSTANT,
3396
+ ),
3397
+ datatype=DataType.STRING,
3398
+ purpose=Purpose.CONSTANT,
3399
+ )
3400
+
3384
3401
  @classmethod
3385
3402
  def from_file(cls, path: str | Path) -> "Environment":
3386
3403
  with open(path, "r") as f:
@@ -3554,16 +3571,7 @@ class Environment(BaseModel):
3554
3571
  target = target.with_suffix(".preql")
3555
3572
  else:
3556
3573
  target = path
3557
- if alias in self.imports:
3558
- imports = self.imports[alias]
3559
- for x in imports:
3560
- if x.path == target:
3561
- return imports
3562
- if env:
3563
- self.imports[alias].append(
3564
- ImportStatement(alias=alias, path=target, environment=env)
3565
- )
3566
- else:
3574
+ if not env:
3567
3575
  parse_address = gen_cache_lookup(str(target), alias, str(self.working_path))
3568
3576
  try:
3569
3577
  with open(target, "r", encoding="utf-8") as f:
@@ -3587,13 +3595,8 @@ class Environment(BaseModel):
3587
3595
  f"Unable to import file {target.parent}, parsing error: {e}"
3588
3596
  )
3589
3597
  env = nparser.environment
3590
- for _, concept in env.concepts.items():
3591
- self.add_concept(concept.with_namespace(alias))
3592
-
3593
- for _, datasource in env.datasources.items():
3594
- self.add_datasource(datasource.with_namespace(alias))
3595
3598
  imps = ImportStatement(alias=alias, path=target, environment=env)
3596
- self.imports[alias].append(imps)
3599
+ self.add_import(alias, source=env, imp_stm=imps)
3597
3600
  return imps
3598
3601
 
3599
3602
  def parse(
@@ -337,6 +337,13 @@ class BaseDialect:
337
337
  " target grain"
338
338
  )
339
339
  rval = f"{self.FUNCTION_GRAIN_MATCH_MAP[c.lineage.function.operator](args)}"
340
+ elif (
341
+ isinstance(c.lineage, Function)
342
+ and c.lineage.operator == FunctionType.CONSTANT
343
+ and CONFIG.rendering.parameters is True
344
+ and c.datatype.data_type != DataType.MAP
345
+ ):
346
+ rval = f":{c.safe_address}"
340
347
  else:
341
348
  args = [
342
349
  self.render_expr(
@@ -541,7 +548,7 @@ class BaseDialect:
541
548
  else:
542
549
  raise ValueError(f"Unable to render type {type(e)} {e}")
543
550
 
544
- def render_cte(self, cte: CTE, auto_sort: bool = True):
551
+ def render_cte(self, cte: CTE, auto_sort: bool = True) -> CompiledCTE:
545
552
  if self.UNNEST_MODE in (
546
553
  UnnestMode.CROSS_APPLY,
547
554
  UnnestMode.CROSS_JOIN,
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, Any, Generator
1
+ from typing import List, Optional, Any, Generator, Protocol
2
2
  from functools import singledispatchmethod
3
3
  from sqlalchemy import text
4
4
  from sqlalchemy.engine import Engine, CursorResult
@@ -22,16 +22,29 @@ from trilogy.core.models import (
22
22
  CopyStatement,
23
23
  ImportStatement,
24
24
  MergeStatementV2,
25
+ Function,
26
+ FunctionType,
27
+ MapWrapper,
28
+ ListWrapper,
25
29
  )
26
30
  from trilogy.dialect.base import BaseDialect
27
31
  from trilogy.dialect.enums import Dialects
28
- from trilogy.core.enums import IOType
32
+ from trilogy.core.enums import IOType, Granularity
29
33
  from trilogy.parser import parse_text
30
34
  from trilogy.hooks.base_hook import BaseHook
31
35
  from pathlib import Path
32
36
  from dataclasses import dataclass
33
37
 
34
38
 
39
+ class ResultProtocol(Protocol):
40
+ values: List[Any]
41
+ columns: List[str]
42
+
43
+ def fetchall(self) -> List[Any]: ...
44
+
45
+ def keys(self) -> List[str]: ...
46
+
47
+
35
48
  @dataclass
36
49
  class MockResult:
37
50
  values: list[Any]
@@ -185,7 +198,6 @@ class Executor(object):
185
198
 
186
199
  @execute_query.register
187
200
  def _(self, query: ImportStatement) -> CursorResult:
188
- self.environment.add_file_import(query.path, query.alias)
189
201
  return MockResult(
190
202
  [
191
203
  {
@@ -198,15 +210,16 @@ class Executor(object):
198
210
 
199
211
  @execute_query.register
200
212
  def _(self, query: MergeStatementV2) -> CursorResult:
213
+ for concept in query.sources:
214
+ self.environment.merge_concept(
215
+ concept, query.targets[concept.address], modifiers=query.modifiers
216
+ )
201
217
 
202
- self.environment.merge_concept(
203
- query.source, query.target, modifiers=query.modifiers
204
- )
205
218
  return MockResult(
206
219
  [
207
220
  {
208
- "source": query.source.address,
209
- "target": query.target.address,
221
+ "sources": ",".join([x.address for x in query.sources]),
222
+ "targets": ",".join([x.address for _, x in query.targets.items()]),
210
223
  }
211
224
  ],
212
225
  ["source", "target"],
@@ -219,8 +232,7 @@ class Executor(object):
219
232
  @execute_query.register
220
233
  def _(self, query: ProcessedQuery) -> CursorResult:
221
234
  sql = self.generator.compile_statement(query)
222
- # connection = self.engine.connect()
223
- output = self.connection.execute(text(sql))
235
+ output = self.execute_raw_sql(sql)
224
236
  return output
225
237
 
226
238
  @execute_query.register
@@ -228,14 +240,14 @@ class Executor(object):
228
240
 
229
241
  sql = self.generator.compile_statement(query)
230
242
 
231
- output = self.connection.execute(text(sql))
243
+ output = self.execute_raw_sql(sql)
232
244
  self.environment.add_datasource(query.datasource)
233
245
  return output
234
246
 
235
247
  @execute_query.register
236
248
  def _(self, query: ProcessedCopyStatement) -> CursorResult:
237
249
  sql = self.generator.compile_statement(query)
238
- output: CursorResult = self.connection.execute(text(sql))
250
+ output: CursorResult = self.execute_raw_sql(sql)
239
251
  if query.target_type == IOType.CSV:
240
252
  import csv
241
253
 
@@ -244,7 +256,7 @@ class Executor(object):
244
256
  outcsv.writerow(output.keys())
245
257
  outcsv.writerows(output)
246
258
  else:
247
- raise NotImplementedError(f"Unsupported IOType {query.target_type}")
259
+ raise NotImplementedError(f"Unsupported IO Type {query.target_type}")
248
260
  # now return the query we ran through IO
249
261
  # TODO: instead return how many rows were written?
250
262
  return generate_result_set(
@@ -307,7 +319,20 @@ class Executor(object):
307
319
  output.append(compiled_sql)
308
320
  return output
309
321
 
310
- def parse_file(self, file: str | Path, persist: bool = False) -> Generator[
322
+ def parse_file(
323
+ self, file: str | Path, persist: bool = False
324
+ ) -> list[
325
+ ProcessedQuery
326
+ | ProcessedQueryPersist
327
+ | ProcessedShowStatement
328
+ | ProcessedRawSQLStatement
329
+ | ProcessedCopyStatement,
330
+ ]:
331
+ return list(self.parse_file_generator(file, persist=persist))
332
+
333
+ def parse_file_generator(
334
+ self, file: str | Path, persist: bool = False
335
+ ) -> Generator[
311
336
  ProcessedQuery
312
337
  | ProcessedQueryPersist
313
338
  | ProcessedShowStatement
@@ -370,13 +395,50 @@ class Executor(object):
370
395
  if persist and isinstance(x, ProcessedQueryPersist):
371
396
  self.environment.add_datasource(x.datasource)
372
397
 
398
+ def _hydrate_param(self, param: str) -> Any:
399
+ matched = [
400
+ v
401
+ for v in self.environment.concepts.values()
402
+ if v.safe_address == param or v.address == param
403
+ ]
404
+ if not matched:
405
+ raise SyntaxError(f"No concept found for parameter {param}")
406
+
407
+ concept: Concept = matched.pop()
408
+ if not concept.granularity == Granularity.SINGLE_ROW:
409
+ raise SyntaxError(f"Cannot bind non-singleton concept {concept.address}")
410
+ if (
411
+ isinstance(concept.lineage, Function)
412
+ and concept.lineage.operator == FunctionType.CONSTANT
413
+ ):
414
+ rval = concept.lineage.arguments[0]
415
+ if isinstance(rval, ListWrapper):
416
+ return [x for x in rval]
417
+ if isinstance(rval, MapWrapper):
418
+ return {k: v for k, v in rval.items()}
419
+ return rval
420
+ else:
421
+ results = self.execute_query(f"select {concept.name} limit 1;").fetchone()
422
+ if not results:
423
+ return None
424
+ return results[0]
425
+
373
426
  def execute_raw_sql(
374
427
  self, command: str, variables: dict | None = None
375
428
  ) -> CursorResult:
376
429
  """Run a command against the raw underlying
377
430
  execution engine"""
431
+ final_params = None
432
+ q = text(command)
378
433
  if variables:
379
- return self.connection.execute(text(command), variables)
434
+ final_params = variables
435
+ else:
436
+ params = q.compile().params
437
+ if params:
438
+ final_params = {x: self._hydrate_param(x) for x in params}
439
+
440
+ if final_params:
441
+ return self.connection.execute(text(command), final_params)
380
442
  return self.connection.execute(
381
443
  text(command),
382
444
  )
@@ -302,6 +302,9 @@ class ParseToObjects(Transformer):
302
302
  def IDENTIFIER(self, args) -> str:
303
303
  return args.value
304
304
 
305
+ def WILDCARD_IDENTIFIER(self, args) -> str:
306
+ return args.value
307
+
305
308
  def QUOTED_IDENTIFIER(self, args) -> str:
306
309
  return args.value[1:-1]
307
310
 
@@ -786,25 +789,54 @@ class ParseToObjects(Transformer):
786
789
  return [x for x in args]
787
790
 
788
791
  @v_args(meta=True)
789
- def merge_statement_v2(self, meta: Meta, args) -> MergeStatementV2:
792
+ def merge_statement(self, meta: Meta, args) -> MergeStatementV2:
790
793
  modifiers = []
791
- concepts = []
794
+ cargs: list[str] = []
795
+ source_wildcard = None
796
+ target_wildcard = None
792
797
  for arg in args:
793
798
  if isinstance(arg, Modifier):
794
799
  modifiers.append(arg)
795
800
  else:
796
- concepts.append(self.environment.concepts[arg])
801
+ cargs.append(arg)
802
+ source, target = cargs
803
+ if source.endswith(".*"):
804
+ if not target.endswith(".*"):
805
+ raise ValueError("Invalid merge, source is wildcard, target is not")
806
+ source_wildcard = source[:-2]
807
+ target_wildcard = target[:-2]
808
+ sources = [
809
+ v
810
+ for k, v in self.environment.concepts.items()
811
+ if v.namespace == source_wildcard
812
+ ]
813
+ targets = {}
814
+ for x in sources:
815
+ target = target_wildcard + "." + x.name
816
+ if target in self.environment.concepts:
817
+ targets[x.address] = self.environment.concepts[target]
818
+ sources = [x for x in sources if x.address in targets]
819
+ else:
820
+ sources = [self.environment.concepts[source]]
821
+ targets = {sources[0].address: self.environment.concepts[target]}
797
822
  new = MergeStatementV2(
798
- source=concepts[0], target=concepts[1], modifiers=modifiers
823
+ sources=sources,
824
+ targets=targets,
825
+ modifiers=modifiers,
826
+ source_wildcard=source_wildcard,
827
+ target_wildcard=target_wildcard,
799
828
  )
800
-
801
- self.environment.merge_concept(new.source, new.target, modifiers)
829
+ for source_c in new.sources:
830
+ self.environment.merge_concept(
831
+ source_c, targets[source_c.address], modifiers
832
+ )
802
833
 
803
834
  return new
804
835
 
805
836
  @v_args(meta=True)
806
837
  def rawsql_statement(self, meta: Meta, args) -> RawSQLStatement:
807
- return RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
838
+ statement = RawSQLStatement(meta=Metadata(line_number=meta.line), text=args[0])
839
+ return statement
808
840
 
809
841
  def COPY_TYPE(self, args) -> IOType:
810
842
  return IOType(args.value)
@@ -1744,7 +1776,7 @@ class ParseToObjects(Transformer):
1744
1776
  arguments=args,
1745
1777
  output_datatype=output_datatype,
1746
1778
  output_purpose=function_args_to_output_purpose(args),
1747
- # valid_inputs={DataType.DATE, DataType.TIMESTAMP, DataType.DATETIME},
1779
+ valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
1748
1780
  arg_count=2,
1749
1781
  )
1750
1782
 
@@ -1757,7 +1789,7 @@ class ParseToObjects(Transformer):
1757
1789
  arguments=args,
1758
1790
  output_datatype=output_datatype,
1759
1791
  output_purpose=function_args_to_output_purpose(args),
1760
- # valid_inputs={DataType.DATE, DataType.TIMESTAMP, DataType.DATETIME},
1792
+ valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
1761
1793
  arg_count=2,
1762
1794
  )
1763
1795
 
@@ -1770,20 +1802,21 @@ class ParseToObjects(Transformer):
1770
1802
  arguments=args,
1771
1803
  output_datatype=output_datatype,
1772
1804
  output_purpose=function_args_to_output_purpose(args),
1773
- # valid_inputs={DataType.DATE, DataType.TIMESTAMP, DataType.DATETIME},
1805
+ valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
1774
1806
  arg_count=2,
1775
1807
  )
1776
1808
 
1777
1809
  @v_args(meta=True)
1778
1810
  def fdiv(self, meta: Meta, args):
1779
1811
  args = process_function_args(args, meta=meta, environment=self.environment)
1780
- output_datatype = merge_datatypes([arg_to_datatype(x) for x in args])
1812
+ # 2024-11-18 - this is a bit of a hack, but division always returns a float
1813
+ # output_datatype = merge_datatypes([arg_to_datatype(x) for x in args])
1781
1814
  return Function(
1782
1815
  operator=FunctionType.DIVIDE,
1783
1816
  arguments=args,
1784
- output_datatype=output_datatype,
1817
+ output_datatype=DataType.FLOAT, # division always returns a float
1785
1818
  output_purpose=function_args_to_output_purpose(args),
1786
- # valid_inputs={DataType.DATE, DataType.TIMESTAMP, DataType.DATETIME},
1819
+ valid_inputs={DataType.INTEGER, DataType.FLOAT, DataType.NUMBER},
1787
1820
  arg_count=2,
1788
1821
  )
1789
1822
 
@@ -432,7 +432,9 @@ class Renderer:
432
432
 
433
433
  @to_string.register
434
434
  def _(self, arg: MergeStatementV2):
435
- return f"MERGE {self.to_string(arg.source)} into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{self.to_string(arg.target)};"
435
+ if len(arg.sources) == 1:
436
+ return f"MERGE {self.to_string(arg.sources[0])} into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{self.to_string(arg.targets[arg.sources[0].address])};"
437
+ return f"MERGE {arg.source_wildcard}.* into {''.join([self.to_string(modifier) for modifier in arg.modifiers])}{arg.target_wildcard}.*;"
436
438
 
437
439
  @to_string.register
438
440
  def _(self, arg: Modifier):
@@ -9,7 +9,7 @@
9
9
  | rowset_derivation_statement
10
10
  | import_statement
11
11
  | copy_statement
12
- | merge_statement_v2
12
+ | merge_statement
13
13
  | rawsql_statement
14
14
 
15
15
  _TERMINATOR: ";"i /\s*/
@@ -74,8 +74,7 @@
74
74
 
75
75
  align_clause: align_item ("AND"i align_item)* "AND"i?
76
76
 
77
- // merge_v2
78
- merge_statement_v2: "merge"i IDENTIFIER "into"i SHORTHAND_MODIFIER? IDENTIFIER
77
+ merge_statement: "merge"i WILDCARD_IDENTIFIER "into"i SHORTHAND_MODIFIER? WILDCARD_IDENTIFIER
79
78
 
80
79
  // raw sql statement
81
80
  rawsql_statement: "raw_sql"i "(" MULTILINE_STRING ")"
@@ -292,6 +291,7 @@
292
291
  // base language constructs
293
292
  concept_lit: IDENTIFIER
294
293
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.]*/
294
+ WILDCARD_IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.\*]*/
295
295
  QUOTED_IDENTIFIER: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:\s]*`/
296
296
  QUOTED_ADDRESS: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:]*`/
297
297
  ADDRESS: IDENTIFIER
@@ -302,7 +302,7 @@
302
302
  SINGLE_STRING_CHARS: /(?:(?!\${)([^'\\]|\\.))+/+ // any character except '
303
303
  _single_quote: "'" ( SINGLE_STRING_CHARS )* "'"
304
304
  _double_quote: "\"" ( DOUBLE_STRING_CHARS )* "\""
305
- string_lit: _single_quote | _double_quote
305
+ string_lit: _single_quote | _double_quote | MULTILINE_STRING
306
306
 
307
307
  MINUS: "-"
308
308
 
File without changes
File without changes
File without changes
File without changes