pytrilogy 0.0.3.43__py3-none-any.whl → 0.0.3.44__py3-none-any.whl

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.43
3
+ Version: 0.0.3.44
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.43.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=4cXB9506hb2gprGWKbo-5R8LE3DuFBSrMfMr82Gwvn4,303
1
+ pytrilogy-0.0.3.44.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=_DLr4-giprU_EZXs7LGyoeoIt8LaSs4wYqbdAajaVr4,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=5eQxk1A0pv-TQk3CCvgZCFA9_K-6nxrOm7E5Lxd7KIY,1652
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -22,8 +22,8 @@ trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
22
22
  trilogy/core/optimization.py,sha256=aihzx4-2-mSjx5td1TDTYGvc7e9Zvy-_xEyhPqLS-Ig,8314
23
23
  trilogy/core/query_processor.py,sha256=Vl-u0F0rbqI2liv82yJgiZCB255Kx_KiuzZVHL6aeTM,19459
24
24
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- trilogy/core/models/author.py,sha256=hS1caD8y7XWRBlHfwgZOrBcz3TisDPba8joFaiEXxX0,77072
26
- trilogy/core/models/build.py,sha256=NdDRcgvNoa2CHoXcPvWAUkCA2TRUKYm0zIl5NBx0sfo,61796
25
+ trilogy/core/models/author.py,sha256=7lPUVm1uCXYsyV85p34AvtLLzHqDGCeesTUT8ZHCwo4,76859
26
+ trilogy/core/models/build.py,sha256=EsI7BLmFXdxj1an3NnKR_Qm79tcjlFKjmLjmt3_v2eA,61829
27
27
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
28
28
  trilogy/core/models/core.py,sha256=wx6hJcFECMG-Ij972ADNkr-3nFXkYESr82ObPiC46_U,10875
29
29
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
@@ -40,7 +40,7 @@ trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuX
40
40
  trilogy/core/processing/utility.py,sha256=rfzdgl-vWkCyhLzXNNuWgPLK59eiYypQb6TdZKymUqk,21469
41
41
  trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW6gjUJCsiXumdbJNozHUf-Y,800
42
42
  trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
43
- trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
43
+ trilogy/core/processing/node_generators/common.py,sha256=nVeH_AdO58ygtNSO0wNgMR7_h2D0dFSGM_rh1fJd4Yc,9468
44
44
  trilogy/core/processing/node_generators/filter_node.py,sha256=lT167yBgy3P9sDBM1Cjj0PKSXro8dvGtBmc8nwsUjig,8366
45
45
  trilogy/core/processing/node_generators/group_node.py,sha256=kO-ersxIL04rZwX5-vFIFQQnp357PFo_7ZKXoGq3wyc,5989
46
46
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
@@ -52,7 +52,7 @@ trilogy/core/processing/node_generators/select_node.py,sha256=Y-zO0AFkTrpi2Lyebj
52
52
  trilogy/core/processing/node_generators/synonym_node.py,sha256=9LHK2XHDjbyTLjmDQieskG8fqbiSpRnFOkfrutDnOTE,2258
53
53
  trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
54
54
  trilogy/core/processing/node_generators/unnest_node.py,sha256=cOEKnMRzXUW3bwmiOlgn3E1-B38osng0dh2pDykwITY,2410
55
- trilogy/core/processing/node_generators/window_node.py,sha256=lj94LRcJaypyfLEucQwIn65ZQsSAkYV_r1esFhPRUDc,6047
55
+ trilogy/core/processing/node_generators/window_node.py,sha256=RUHgpYovQObFod1xRIMWtDzMcxwlm4-1Fdrf_Cuw5W4,6346
56
56
  trilogy/core/processing/node_generators/select_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  trilogy/core/processing/node_generators/select_helpers/datasource_injection.py,sha256=GMW07bb6hXurhF0hZLYoMAKSIS65tat5hwBjvqqPeSA,6516
58
58
  trilogy/core/processing/nodes/__init__.py,sha256=Lxr3rs_bqOAtMtn3DHIkY058ZzjyLM5mSfGMKW2z0NY,5555
@@ -87,13 +87,13 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
87
87
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
88
88
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- trilogy/parsing/common.py,sha256=2KwR86ZNH04tcs500l02jLPkFaStEYc9wtR1NVO6cYo,26411
90
+ trilogy/parsing/common.py,sha256=U9RNi1GyPTQaitZGwXy1QftdC5PWYArP7V8t-v3H8Po,27157
91
91
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
92
92
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
93
93
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
94
- trilogy/parsing/parse_engine.py,sha256=tsEBRyG2fzHUdiO1oW2mB_36vNZOfjFA4MXuf0zpF2E,65547
94
+ trilogy/parsing/parse_engine.py,sha256=9SO2q8m5MlZo_Eho-_r6hmTSm5VH38k47C2iHTtYwjU,68224
95
95
  trilogy/parsing/render.py,sha256=hI4y-xjXrEXvHslY2l2TQ8ic0zAOpN41ADH37J2_FZY,19047
96
- trilogy/parsing/trilogy.lark,sha256=2Noe58vGYteilKd6w-np3fb4lzWI-G9Gt0AMyOMVw3k,13735
96
+ trilogy/parsing/trilogy.lark,sha256=zbDAIG7gpsImxBtteD8E2pKwcJCGpM-rEQDRqpgzoSQ,13717
97
97
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
99
99
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -102,8 +102,8 @@ trilogy/std/display.preql,sha256=2BbhvqR4rcltyAbOXAUo7SZ_yGFYZgFnurglHMbjW2g,40
102
102
  trilogy/std/geography.preql,sha256=-fqAGnBL6tR-UtT8DbSek3iMFg66ECR_B_41pODxv-k,504
103
103
  trilogy/std/money.preql,sha256=ZHW-csTX-kYbOLmKSO-TcGGgQ-_DMrUXy0BjfuJSFxM,80
104
104
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
105
- pytrilogy-0.0.3.43.dist-info/METADATA,sha256=0cC502qzl6dYCgRsnS4Gi8LqcpV1Ozu7JKdYaqc69mM,9100
106
- pytrilogy-0.0.3.43.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
107
- pytrilogy-0.0.3.43.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
- pytrilogy-0.0.3.43.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
- pytrilogy-0.0.3.43.dist-info/RECORD,,
105
+ pytrilogy-0.0.3.44.dist-info/METADATA,sha256=-TUyQi3fENhsnYhqsy2HiD-thLyAz6S8TAmgdVuaCZo,9100
106
+ pytrilogy-0.0.3.44.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
107
+ pytrilogy-0.0.3.44.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
108
+ pytrilogy-0.0.3.44.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
109
+ pytrilogy-0.0.3.44.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.43"
7
+ __version__ = "0.0.3.44"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -629,18 +629,8 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
629
629
  )
630
630
  elif self.operator in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
631
631
  right_type = arg_to_datatype(self.right)
632
- if not any(
633
- [
634
- isinstance(self.right, ConceptRef),
635
- right_type in (DataType.LIST,),
636
- isinstance(right_type, (ListType, ListWrapper, TupleWrapper)),
637
- ]
638
- ):
639
- raise SyntaxError(
640
- f"Cannot use {self.operator.value} with non-list, non-tuple, non-concept object {self.right} in {str(self)}"
641
- )
642
632
 
643
- elif isinstance(right_type, ListType) and not is_compatible_datatype(
633
+ if isinstance(right_type, ListType) and not is_compatible_datatype(
644
634
  arg_to_datatype(self.left), right_type.value_data_type
645
635
  ):
646
636
  raise SyntaxError(
@@ -1916,7 +1906,7 @@ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel)
1916
1906
 
1917
1907
 
1918
1908
  class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1919
- content: ConceptRef
1909
+ content: Expr
1920
1910
  where: "WhereClause"
1921
1911
 
1922
1912
  @field_validator("content", mode="before")
@@ -1932,13 +1922,21 @@ class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
1932
1922
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1933
1923
  ) -> "FilterItem":
1934
1924
  return FilterItem.model_construct(
1935
- content=self.content.with_merge(source, target, modifiers),
1925
+ content=(
1926
+ self.content.with_merge(source, target, modifiers)
1927
+ if isinstance(self.content, Mergeable)
1928
+ else self.content
1929
+ ),
1936
1930
  where=self.where.with_merge(source, target, modifiers),
1937
1931
  )
1938
1932
 
1939
1933
  def with_namespace(self, namespace: str) -> "FilterItem":
1940
1934
  return FilterItem.model_construct(
1941
- content=self.content.with_namespace(namespace),
1935
+ content=(
1936
+ self.content.with_namespace(namespace)
1937
+ if isinstance(self.content, Namespaced)
1938
+ else self.content
1939
+ ),
1942
1940
  where=self.where.with_namespace(namespace),
1943
1941
  )
1944
1942
 
@@ -1496,22 +1496,43 @@ class Factory:
1496
1496
  ):
1497
1497
  return base
1498
1498
 
1499
+ def instantiate_concept(
1500
+ self,
1501
+ arg: (
1502
+ AggregateWrapper
1503
+ | FunctionCallWrapper
1504
+ | WindowItem
1505
+ | FilterItem
1506
+ | Function
1507
+ | ListWrapper[Any]
1508
+ | MapWrapper[Any, Any]
1509
+ | int
1510
+ | float
1511
+ | str
1512
+ ),
1513
+ ) -> tuple[Concept, BuildConcept]:
1514
+ from trilogy.parsing.common import arbitrary_to_concept
1515
+
1516
+ new = arbitrary_to_concept(
1517
+ arg,
1518
+ environment=self.environment,
1519
+ )
1520
+ built = self.build(new)
1521
+ self.local_concepts[new.address] = built
1522
+ return new, built
1523
+
1499
1524
  @build.register
1500
1525
  def _(self, base: None) -> None:
1501
1526
  return base
1502
1527
 
1503
1528
  @build.register
1504
1529
  def _(self, base: Function) -> BuildFunction | BuildAggregateWrapper:
1505
- from trilogy.parsing.common import arbitrary_to_concept
1506
1530
 
1507
1531
  raw_args: list[Concept | FuncArgs] = []
1508
1532
  for arg in base.arguments:
1509
1533
  # to do proper discovery, we need to inject virtual intermediate ocncepts
1510
1534
  if isinstance(arg, (AggregateWrapper, FilterItem, WindowItem)):
1511
- narg = arbitrary_to_concept(
1512
- arg,
1513
- environment=self.environment,
1514
- )
1535
+ narg, _ = self.instantiate_concept(arg)
1515
1536
  raw_args.append(narg)
1516
1537
  else:
1517
1538
  raw_args.append(arg)
@@ -1532,24 +1553,16 @@ class Factory:
1532
1553
  for x in arguments:
1533
1554
  if isinstance(x, (ConceptRef, Concept)):
1534
1555
  final_args.append(x)
1535
- elif isinstance(x, (AggregateWrapper, FilterItem, WindowItem)):
1536
- newx = arbitrary_to_concept(
1537
- x,
1538
- environment=self.environment,
1539
- )
1540
- final_args.append(newx)
1541
1556
  else:
1542
1557
  # constants, etc, can be ignored for group
1543
1558
  continue
1544
- group_base = arbitrary_to_concept(
1559
+ _, rval = self.instantiate_concept(
1545
1560
  AggregateWrapper(
1546
1561
  function=group_base.lineage.function,
1547
1562
  by=final_args,
1548
- ),
1549
- environment=self.environment,
1563
+ )
1550
1564
  )
1551
1565
 
1552
- rval = self.build(group_base)
1553
1566
  return BuildFunction.model_construct(
1554
1567
  operator=base.operator,
1555
1568
  arguments=[rval, *[self.build(c) for c in raw_args[1:]]],
@@ -1580,20 +1593,13 @@ class Factory:
1580
1593
 
1581
1594
  @build.register
1582
1595
  def _(self, base: CaseWhen) -> BuildCaseWhen:
1583
- from trilogy.parsing.common import arbitrary_to_concept
1584
1596
 
1585
1597
  comparison = base.comparison
1586
1598
  if isinstance(comparison, (AggregateWrapper, FilterItem, WindowItem)):
1587
- comparison = arbitrary_to_concept(
1588
- comparison,
1589
- environment=self.environment,
1590
- )
1599
+ comparison, _ = self.instantiate_concept(comparison)
1591
1600
  expr: Concept | FuncArgs = base.expr
1592
1601
  if isinstance(expr, (AggregateWrapper, FilterItem, WindowItem)):
1593
- expr = arbitrary_to_concept(
1594
- expr,
1595
- environment=self.environment,
1596
- )
1602
+ expr, _ = self.instantiate_concept(expr)
1597
1603
  return BuildCaseWhen.model_construct(
1598
1604
  comparison=self.build(comparison),
1599
1605
  expr=self.build(expr),
@@ -1615,6 +1621,7 @@ class Factory:
1615
1621
  else:
1616
1622
  build_lineage = None
1617
1623
  derivation = Concept.calculate_derivation(build_lineage, base.purpose)
1624
+
1618
1625
  granularity = Concept.calculate_granularity(
1619
1626
  derivation, final_grain, build_lineage
1620
1627
  )
@@ -1675,16 +1682,12 @@ class Factory:
1675
1682
 
1676
1683
  @build.register
1677
1684
  def _(self, base: OrderItem) -> BuildOrderItem:
1678
- from trilogy.parsing.common import arbitrary_to_concept
1679
1685
 
1680
1686
  bexpr: Any
1681
1687
  if isinstance(base.expr, (AggregateWrapper, WindowItem, FilterItem)) or (
1682
1688
  isinstance(base.expr, Function) and base.expr.operator == FunctionType.GROUP
1683
1689
  ):
1684
- bexpr = arbitrary_to_concept(
1685
- base.expr,
1686
- environment=self.environment,
1687
- )
1690
+ bexpr, _ = self.instantiate_concept(base.expr)
1688
1691
  else:
1689
1692
  bexpr = base.expr
1690
1693
  return BuildOrderItem.model_construct(
@@ -1707,15 +1710,10 @@ class Factory:
1707
1710
 
1708
1711
  @build.register
1709
1712
  def _(self, base: WindowItem) -> BuildWindowItem:
1710
- # to do proper discovery, we need to inject virtual intermediate ocncepts
1711
- from trilogy.parsing.common import arbitrary_to_concept
1712
1713
 
1713
1714
  content: Concept | FuncArgs = base.content
1714
1715
  if isinstance(content, (AggregateWrapper, FilterItem, WindowItem)):
1715
- content = arbitrary_to_concept(
1716
- content,
1717
- environment=self.environment,
1718
- )
1716
+ content, _ = self.instantiate_concept(content)
1719
1717
  final_by = []
1720
1718
  for x in base.order_by:
1721
1719
  if (
@@ -1743,30 +1741,26 @@ class Factory:
1743
1741
 
1744
1742
  @build.register
1745
1743
  def _(self, base: SubselectComparison) -> BuildSubselectComparison:
1744
+ right: Any = base.right
1745
+ if isinstance(base.right, (AggregateWrapper, WindowItem, FilterItem, Function)):
1746
+ right_c, _ = self.instantiate_concept(base.right)
1747
+ right = right_c
1746
1748
  return BuildSubselectComparison.model_construct(
1747
- left=(self.build(base.left)),
1748
- right=(self.build(base.right)),
1749
+ left=self.build(base.left),
1750
+ right=self.build(right),
1749
1751
  operator=base.operator,
1750
1752
  )
1751
1753
 
1752
1754
  @build.register
1753
1755
  def _(self, base: Comparison) -> BuildComparison:
1754
- from trilogy.parsing.common import arbitrary_to_concept
1755
1756
 
1756
1757
  left = base.left
1757
1758
  if isinstance(left, (AggregateWrapper, WindowItem, FilterItem)):
1758
- left_c = arbitrary_to_concept(
1759
- left,
1760
- environment=self.environment,
1761
- )
1759
+ left_c, _ = self.instantiate_concept(left)
1762
1760
  left = left_c # type: ignore
1763
1761
  right = base.right
1764
1762
  if isinstance(right, (AggregateWrapper, WindowItem, FilterItem)):
1765
- right_c = arbitrary_to_concept(
1766
- right,
1767
- environment=self.environment,
1768
- )
1769
-
1763
+ right_c, _ = self.instantiate_concept(right)
1770
1764
  right = right_c # type: ignore
1771
1765
  return BuildComparison.model_construct(
1772
1766
  left=(self.build(left)),
@@ -1826,6 +1820,13 @@ class Factory:
1826
1820
 
1827
1821
  @build.register
1828
1822
  def _(self, base: FilterItem) -> BuildFilterItem:
1823
+ if isinstance(
1824
+ base.content, (Function, AggregateWrapper, WindowItem, FilterItem)
1825
+ ):
1826
+ _, built = self.instantiate_concept(base.content)
1827
+ return BuildFilterItem.model_construct(
1828
+ content=built, where=self.build(base.where)
1829
+ )
1829
1830
  return BuildFilterItem.model_construct(
1830
1831
  content=self.build(base.content), where=self.build(base.where)
1831
1832
  )
@@ -1969,6 +1970,10 @@ class Factory:
1969
1970
  new.datasources[k] = self.build(d)
1970
1971
  for k, a in base.alias_origin_lookup.items():
1971
1972
  new.alias_origin_lookup[k] = self.build(a)
1973
+ # add in anything that was built as a side-effect
1974
+ for bk, bv in self.local_concepts.items():
1975
+ if bk not in new.concepts:
1976
+ new.concepts[bk] = bv
1972
1977
  new.gen_concept_list_caches()
1973
1978
  return new
1974
1979
 
@@ -178,7 +178,7 @@ def gen_enrichment_node(
178
178
  for x in extra_required
179
179
  ):
180
180
  log_lambda(
181
- f"{str(type(base_node).__name__)} returning property optimized enrichment node"
181
+ f"{str(type(base_node).__name__)} returning property optimized enrichment node for {extra_required[0].keys}"
182
182
  )
183
183
  return gen_property_enrichment_node(
184
184
  base_node,
@@ -32,7 +32,9 @@ def resolve_window_parent_concepts(
32
32
  if concept.lineage.order_by:
33
33
  for item in concept.lineage.order_by:
34
34
  base += item.concept_arguments
35
-
35
+ if concept.grain:
36
+ for gitem in concept.grain.components:
37
+ base.append(environment.concepts[gitem])
36
38
  return concept.lineage.content, unique(base, "address")
37
39
 
38
40
 
@@ -131,20 +133,24 @@ def gen_window_node(
131
133
  )
132
134
  _window_node.rebuild_cache()
133
135
  _window_node.resolve()
136
+
134
137
  window_node = StrategyNode(
135
138
  input_concepts=[concept] + additional_outputs + parent_concepts + targets,
136
139
  output_concepts=[concept] + additional_outputs + parent_concepts + targets,
137
140
  environment=environment,
138
141
  parents=[_window_node],
139
142
  preexisting_conditions=conditions.conditional if conditions else None,
140
- # hidden_concepts=[
141
- # x.address for x in parent_concepts if x.address not in local_optional
142
- # ],
143
+ grain=BuildGrain.from_concepts(
144
+ concepts=[concept] + additional_outputs + parent_concepts + targets,
145
+ environment=environment,
146
+ ),
143
147
  )
144
148
  if not non_equivalent_optional:
145
149
  logger.info(
146
150
  f"{padding(depth)}{LOGGER_PREFIX} no optional concepts, returning window node"
147
151
  )
152
+ # prune outputs if we don't need join keys
153
+ window_node.set_output_concepts([concept] + additional_outputs + targets)
148
154
  return window_node
149
155
 
150
156
  missing_optional = [
trilogy/parsing/common.py CHANGED
@@ -458,7 +458,25 @@ def filter_item_to_concept(
458
458
  metadata: Metadata | None = None,
459
459
  ) -> Concept:
460
460
  fmetadata = metadata or Metadata()
461
- cparent = environment.concepts[parent.content.address]
461
+ if isinstance(parent.content, ConceptRef):
462
+ cparent = environment.concepts[parent.content.address]
463
+ elif isinstance(
464
+ parent.content,
465
+ (
466
+ FilterItem,
467
+ AggregateWrapper,
468
+ FunctionCallWrapper,
469
+ WindowItem,
470
+ Function,
471
+ ListWrapper,
472
+ MapWrapper,
473
+ ),
474
+ ):
475
+ cparent = arbitrary_to_concept(parent.content, environment, namespace=namespace)
476
+ else:
477
+ raise NotImplementedError(
478
+ f"Filter item with non ref content {parent.content} not yet supported"
479
+ )
462
480
  modifiers = get_upstream_modifiers(
463
481
  cparent.concept_arguments, environment=environment
464
482
  )
@@ -494,24 +512,6 @@ def window_item_to_concept(
494
512
  metadata: Metadata | None = None,
495
513
  ) -> Concept:
496
514
  fmetadata = metadata or Metadata()
497
- # if isinstance(
498
- # parent.content,
499
- # (
500
- # AggregateWrapper
501
- # | FunctionCallWrapper
502
- # | WindowItem
503
- # | FilterItem
504
- # | Function
505
- # | ListWrapper
506
- # | MapWrapper
507
- # ),
508
- # ):
509
- # new_parent = arbitrary_to_concept(
510
- # parent.content, environment=environment, namespace=namespace
511
- # )
512
- # environment.add_concept(new_parent)
513
- # parent = parent.model_copy(update={"content": new_parent.reference})
514
-
515
515
  if not isinstance(parent.content, ConceptRef):
516
516
  raise NotImplementedError(
517
517
  f"Window function wiht non ref content {parent.content} not yet supported"
@@ -523,16 +523,26 @@ def window_item_to_concept(
523
523
  local_purpose, keys = get_purpose_and_keys(None, (bcontent,), environment)
524
524
  else:
525
525
  local_purpose = Purpose.PROPERTY
526
- keys = {
527
- bcontent.address,
528
- }
526
+ keys = Grain.from_concepts(
527
+ [bcontent.address] + [y.address for y in parent.over], environment
528
+ ).components
529
529
 
530
+ # when including the order by in discovery grain
530
531
  if parent.order_by:
531
532
  grain_components = parent.over + [bcontent.output]
532
533
  for item in parent.order_by:
533
- grain_components += item.concept_arguments
534
+ # confirm that it's not just an aggregate at the grain of the stuff we're already keying of of
535
+ # in which case we can ignore contributions
536
+ if (
537
+ isinstance(item.expr, AggregateWrapper)
538
+ and set([x.address for x in item.expr.by]) == keys
539
+ ):
540
+ continue
541
+ else:
542
+ grain_components += item.concept_arguments
534
543
  else:
535
544
  grain_components = parent.over + [bcontent.output]
545
+
536
546
  final_grain = Grain.from_concepts(grain_components, environment)
537
547
  modifiers = get_upstream_modifiers(bcontent.concept_arguments, environment)
538
548
  datatype = parent.content.datatype
@@ -651,7 +661,9 @@ def rowset_concept(
651
661
  orig_concept = environment.concepts[orig_address.address]
652
662
  name = orig_concept.name
653
663
  if isinstance(orig_concept.lineage, FilterItem):
654
- if orig_concept.lineage.where == rowset.select.where_clause:
664
+ if orig_concept.lineage.where == rowset.select.where_clause and isinstance(
665
+ orig_concept.lineage.content, (ConceptRef, Concept)
666
+ ):
655
667
  name = environment.concepts[orig_concept.lineage.content.address].name
656
668
  base_namespace = (
657
669
  f"{rowset.name}.{orig_concept.namespace}"
@@ -761,7 +773,10 @@ def arbitrary_to_concept(
761
773
  )
762
774
  elif isinstance(parent, FilterItem):
763
775
  if not name:
764
- name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{parent.content.name}_{string_to_hash(str(parent))}"
776
+ if isinstance(parent.content, ConceptRef):
777
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{parent.content.name}_{string_to_hash(str(parent))}"
778
+ else:
779
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_filter_{string_to_hash(str(parent))}"
765
780
  return filter_item_to_concept(
766
781
  parent,
767
782
  name,
@@ -495,7 +495,7 @@ class ParseToObjects(Transformer):
495
495
  return ComparisonOperator([x.value.lower() for x in args])
496
496
 
497
497
  def COMPARISON_OPERATOR(self, args) -> ComparisonOperator:
498
- return ComparisonOperator(args)
498
+ return ComparisonOperator(args.strip())
499
499
 
500
500
  def LOGICAL_OPERATOR(self, args) -> BooleanOperator:
501
501
  return BooleanOperator(args.lower())
@@ -682,8 +682,7 @@ class ParseToObjects(Transformer):
682
682
  return ConceptDerivationStatement(concept=concept)
683
683
 
684
684
  raise SyntaxError(
685
- f"Received invalid type {type(args[2])} {args[2]} as input to select"
686
- " transform"
685
+ f"Received invalid type {type(args[2])} {args[2]} as input to concept derivation: `{self.text_lookup[self.token_address][meta.start_pos:meta.end_pos]}`"
687
686
  )
688
687
 
689
688
  @v_args(meta=True)
@@ -1255,7 +1254,7 @@ class ParseToObjects(Transformer):
1255
1254
  intersection = base.locally_derived.intersection(pre_keys)
1256
1255
  if intersection:
1257
1256
  raise ParseError(
1258
- f"Select statement {base} has derived concepts {list(intersection)} that shadow existing environment concepts, which may cause unexpected behavior. Rename these."
1257
+ f"Select statement {base} creates new derived concepts {list(intersection)} from transformations with identical name(s) to existing concept(s). Do you mean to drop the calculation and directly use the existing concept? If not, alias these concept(s) under new names."
1259
1258
  )
1260
1259
  return base
1261
1260
 
@@ -1365,11 +1364,78 @@ class ParseToObjects(Transformer):
1365
1364
  def literal(self, args):
1366
1365
  return args[0]
1367
1366
 
1367
+ def product_operator(self, args) -> Function | Any:
1368
+ if len(args) == 1:
1369
+ return args[0]
1370
+ result = args[0]
1371
+ for i in range(1, len(args), 2):
1372
+ new_result = None
1373
+ op = args[i]
1374
+ right = args[i + 1]
1375
+ if op == "*":
1376
+ new_result = self.function_factory.create_function(
1377
+ [result, right], operator=FunctionType.MULTIPLY
1378
+ )
1379
+ elif op == "/":
1380
+ new_result = self.function_factory.create_function(
1381
+ [result, right], operator=FunctionType.DIVIDE
1382
+ )
1383
+ elif op == "%":
1384
+ new_result = self.function_factory.create_function(
1385
+ [result, right], operator=FunctionType.MOD
1386
+ )
1387
+ else:
1388
+ raise ValueError(f"Unknown operator: {op}")
1389
+ result = new_result
1390
+ return new_result
1391
+
1392
+ def PLUS_OR_MINUS(self, args) -> str:
1393
+ return args.value
1394
+
1395
+ def MULTIPLY_DIVIDE_PERCENT(self, args) -> str:
1396
+ return args[0]
1397
+
1398
+ @v_args(meta=True)
1399
+ def sum_operator(self, meta: Meta, args) -> Function | Any:
1400
+ if len(args) == 1:
1401
+ return args[0]
1402
+ result = args[0]
1403
+ for i in range(1, len(args), 2):
1404
+ new_result = None
1405
+ op = args[i]
1406
+ right = args[i + 1]
1407
+ if op == "+":
1408
+ new_result = self.function_factory.create_function(
1409
+ [result, right], operator=FunctionType.ADD, meta=meta
1410
+ )
1411
+ elif op == "-":
1412
+ new_result = self.function_factory.create_function(
1413
+ [result, right], operator=FunctionType.SUBTRACT, meta=meta
1414
+ )
1415
+ elif op == "||":
1416
+ new_result = self.function_factory.create_function(
1417
+ [result, right], operator=FunctionType.CONCAT, meta=meta
1418
+ )
1419
+ elif op == "like":
1420
+ new_result = self.function_factory.create_function(
1421
+ [result, right], operator=FunctionType.LIKE, meta=meta
1422
+ )
1423
+ else:
1424
+ raise ValueError(f"Unknown operator: {op}")
1425
+ result = new_result
1426
+ return result
1427
+
1368
1428
  def comparison(self, args) -> Comparison:
1369
- if args[1] == ComparisonOperator.IN:
1370
- raise SyntaxError
1429
+ if len(args) == 1:
1430
+ return args[0]
1371
1431
  left = args[0]
1372
1432
  right = args[2]
1433
+ if args[1] in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
1434
+ return SubselectComparison(
1435
+ left=left,
1436
+ right=right,
1437
+ operator=args[1],
1438
+ )
1373
1439
  return Comparison(left=left, right=right, operator=args[1])
1374
1440
 
1375
1441
  def between_comparison(self, args) -> Conditional:
@@ -1500,13 +1566,14 @@ class ParseToObjects(Transformer):
1500
1566
 
1501
1567
  def filter_item(self, args) -> FilterItem:
1502
1568
  where: WhereClause
1503
- string_concept, raw = args
1569
+ expr, raw = args
1504
1570
  if isinstance(raw, WhereClause):
1505
1571
  where = raw
1506
1572
  else:
1507
1573
  where = WhereClause(conditional=raw)
1508
- concept = self.environment.concepts[string_concept].reference
1509
- return FilterItem(content=concept, where=where)
1574
+ if isinstance(expr, str):
1575
+ expr = self.environment.concepts[expr].reference
1576
+ return FilterItem(content=expr, where=where)
1510
1577
 
1511
1578
  # BEGIN FUNCTIONS
1512
1579
  @v_args(meta=True)
@@ -1725,10 +1792,7 @@ class ParseToObjects(Transformer):
1725
1792
  def fyear(self, meta, args):
1726
1793
  return self.function_factory.create_function(args, FunctionType.YEAR, meta)
1727
1794
 
1728
- # utility functions
1729
- @v_args(meta=True)
1730
- def fcast(self, meta, args) -> Function:
1731
- # if it's casting a constant, we'll process that directly
1795
+ def internal_fcast(self, meta, args):
1732
1796
  args = process_function_args(args, meta=meta, environment=self.environment)
1733
1797
  if isinstance(args[0], str):
1734
1798
  processed: date | datetime | int | float | bool | str
@@ -1753,9 +1817,15 @@ class ParseToObjects(Transformer):
1753
1817
  )
1754
1818
  return self.function_factory.create_function(args, FunctionType.CAST, meta)
1755
1819
 
1820
+ # utility functions
1821
+ @v_args(meta=True)
1822
+ def fcast(self, meta, args) -> Function:
1823
+ return self.internal_fcast(meta, args)
1824
+
1756
1825
  # math functions
1757
1826
  @v_args(meta=True)
1758
1827
  def fadd(self, meta, args) -> Function:
1828
+
1759
1829
  return self.function_factory.create_function(args, FunctionType.ADD, meta)
1760
1830
 
1761
1831
  @v_args(meta=True)
@@ -52,7 +52,7 @@
52
52
 
53
53
  //column_assignment
54
54
  //figure out if we want static
55
- column_assignment: (raw_column_assignment | IDENTIFIER | QUOTED_IDENTIFIER | _static_functions ) ":" concept_assignment
55
+ column_assignment: (raw_column_assignment | IDENTIFIER | QUOTED_IDENTIFIER | expr ) ":" concept_assignment
56
56
 
57
57
  RAW_ENTRY.1: /raw\s*\(/s
58
58
 
@@ -102,7 +102,7 @@
102
102
  type_declaration: "type" IDENTIFIER data_type
103
103
 
104
104
  // user_id where state = Mexico
105
- _filter_alt: IDENTIFIER "?" conditional
105
+ _filter_alt: (IDENTIFIER | "(" expr ")") "?" conditional
106
106
  _filter_base: "filter"i IDENTIFIER where
107
107
  filter_item: _filter_base | _filter_alt
108
108
 
@@ -129,12 +129,6 @@
129
129
 
130
130
  limit: "LIMIT"i /[0-9]+/
131
131
 
132
- !window_order: /TOP|BOTTOM/i
133
-
134
- window: window_order /[0-9]+/
135
-
136
- window_order_by: "BY"i column_list
137
-
138
132
  order_list: expr ordering ("," expr ordering)* ","?
139
133
 
140
134
  over_list: concept_lit ("," concept_lit )* ","?
@@ -166,11 +160,9 @@
166
160
 
167
161
  !array_comparison: ( ("NOT"i "IN"i) | "IN"i)
168
162
 
169
- COMPARISON_OPERATOR: /(is\s+not|is|=|>=|<=|!=|>|<)/i
163
+ COMPARISON_OPERATOR: /(\s+is\s+not\s|\s+is\s|\s+in\s|\s+not\s+in\s|=|>=|<=|!=|>|<)/i
170
164
 
171
- comparison: expr COMPARISON_OPERATOR expr
172
-
173
- between_comparison: expr "between"i expr "and"i expr
165
+ between_comparison: "between"i expr "and"i expr
174
166
 
175
167
  subselect_comparison: expr array_comparison (literal | _constant_functions | _string_functions | concept_lit | filter_item | window_item | unnest | fgroup | expr_tuple | parenthetical )
176
168
 
@@ -187,31 +179,49 @@
187
179
  union: _UNION (expr ",")* expr ")"
188
180
 
189
181
  //indexing into an expression is a function
190
- index_access: expr "[" int_lit "]"
191
- map_key_access: expr "[" string_lit "]"
192
- attr_access: expr "." string_lit
182
+ index_access: atom "[" int_lit "]"
183
+ map_key_access: atom "[" string_lit "]"
184
+ attr_access: atom "." string_lit
193
185
 
186
+ ?expr: comparison_root | between_root
194
187
 
195
- expr: _basic_expr | _functional_expr | _operation_expr | _access_expr
188
+ ?comparison_root: sum_chain (COMPARISON_OPERATOR sum_chain)? -> comparison
189
+ ?between_root: sum_chain "between"i sum_chain "and"i sum_chain -> between_comparison
196
190
 
197
- # Most common/basic expressions
198
- _basic_expr: literal | concept_lit | parenthetical | expr_tuple
191
+ PLUS_OR_MINUS: ("+" | /-(?!>)/ | "||" | "like" )
199
192
 
200
- # Operations and comparisons
201
- _operation_expr: comparison | alt_like | between_comparison | subselect_comparison
193
+ ?sum_chain: product_chain (PLUS_OR_MINUS product_chain)* -> sum_operator
194
+
195
+ MULTIPLY_DIVIDE_PERCENT: ("*" | "/" | "%")
196
+
197
+ ?product_chain: atom ( MULTIPLY_DIVIDE_PERCENT atom)* -> product_operator
198
+
199
+ ?atom: literal | concept_lit | parenthetical
200
+ | expr_tuple
201
+ | custom_function
202
+ | _constant_functions
203
+ | _static_functions
204
+ | _generic_functions
205
+ | _date_functions
206
+ | aggregate_functions
207
+ | window_item
208
+ | unnest
209
+ | union
210
+ | fgroup
211
+ | filter_item
212
+ | _access_expr
213
+ | aggregate_by
202
214
 
203
- # Function-like expressions
204
- _functional_expr: _constant_functions | _static_functions | filter_item | aggregate_functions | window_item | custom_function | fgroup | unnest | union | aggregate_by
205
215
 
206
216
  # Access patterns
207
217
  _access_expr: index_access | map_key_access | attr_access
208
218
  // functions
209
219
 
210
- fadd: (/add\(/ expr "," expr ")" ) | ( expr "+" expr )
211
- fsub: ("subtract"i "(" expr "," expr ")" ) | ( expr "-" expr )
212
- fmul: ("multiply"i "(" expr "," expr ")" ) | ( expr "*" expr )
213
- fdiv: ( "divide"i "(" expr "," expr ")") | ( expr "/" expr )
214
- fmod: ( "mod"i "(" expr "," (int_lit | concept_lit ) ")") | ( expr "%" (int_lit | concept_lit ) )
220
+ fadd: (/add\(/ expr "," expr ")" )
221
+ fsub: ("subtract"i "(" expr "," expr ")" )
222
+ fmul: ("multiply"i "(" expr "," expr ")" )
223
+ fdiv: ( "divide"i "(" expr "," expr ")")
224
+ fmod: ( "mod"i "(" expr "," (int_lit | concept_lit ) ")")
215
225
  _ROUND.1: "round"i "("
216
226
  fround: _ROUND expr "," expr ")"
217
227
  fabs: "abs"i "(" expr ")"
@@ -224,9 +234,9 @@
224
234
 
225
235
  //generic
226
236
  _fcast_primary: "cast"i "(" expr "as"i data_type ")"
227
- _fcast_alt: expr "::" data_type
237
+ _fcast_alt: atom "::" data_type
228
238
  fcast: _fcast_primary | _fcast_alt
229
- concat: ("concat"i "(" (expr ",")* expr ")") | (expr "||" expr)
239
+ concat: ("concat"i "(" (expr ",")* expr ")")
230
240
  fcoalesce: "coalesce"i "(" (expr ",")* expr ")"
231
241
  fcase_when: "WHEN"i conditional "THEN"i expr
232
242
  fcase_else: "ELSE"i expr
@@ -235,7 +245,7 @@
235
245
  fnot: "NOT"i expr
236
246
  fbool: "bool"i "(" expr ")"
237
247
 
238
- _generic_functions: fcast | concat | fcoalesce | fcase | len | fnot | fbool
248
+ _generic_functions: fcast | concat | fcoalesce | fcase | len | fnot | fbool
239
249
 
240
250
  //constant
241
251
  CURRENT_DATE.1: /current_date\(\)/
@@ -333,12 +343,12 @@
333
343
 
334
344
  _date_functions: fdate | fdate_add | fdate_sub | fdate_diff | fdatetime | ftimestamp | fsecond | fminute | fhour | fday | fday_of_week | fweek | fmonth | fquarter | fyear | fdate_part | fdate_trunc
335
345
 
336
- _static_functions: _string_functions | _math_functions | _generic_functions | _date_functions
346
+ _static_functions: _string_functions | _math_functions
337
347
 
338
348
  custom_function: "@" IDENTIFIER "(" (expr ",")* expr ")"
339
349
 
340
350
  // base language constructs
341
- concept_lit: MINUS? IDENTIFIER
351
+ concept_lit: IDENTIFIER
342
352
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\.]*/
343
353
  WILDCARD_IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\-\.\*]*/
344
354
  QUOTED_IDENTIFIER: /`[a-zA-Z\_][a-zA-Z0-9\_\.\-\*\:\s]*`/
@@ -353,9 +363,9 @@
353
363
  _double_quote: "\"" ( DOUBLE_STRING_CHARS )* "\""
354
364
  string_lit: _single_quote | _double_quote | MULTILINE_STRING
355
365
 
356
- MINUS: "-"
357
366
 
358
- int_lit: MINUS? /[0-9]+/
367
+
368
+ int_lit: /\-?[0-9]+/
359
369
 
360
370
  float_lit: /[0-9]*\.[0-9]+/
361
371