pytrilogy 0.0.3.7__py3-none-any.whl → 0.0.3.8__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.2
2
2
  Name: pytrilogy
3
- Version: 0.0.3.7
3
+ Version: 0.0.3.8
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=nCVrjnf_bl_zZ7GmePdSoaRQ40QxFDkSFlafhQp8Cn8,302
1
+ trilogy/__init__.py,sha256=OtihFBeuWR7p7GBhiNMfyCpmu-x2kn_W2xSawos5ILM,302
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
4
4
  trilogy/engine.py,sha256=3etkm2RSVKO0IkgPKkrcs33X5gN_fIMyqMNfChcsR1E,1318
@@ -20,12 +20,12 @@ trilogy/core/internal.py,sha256=iicDBlC6nM8d7e7jqzf_ZOmpUsW8yrr2AA8AqEiLx-s,1577
20
20
  trilogy/core/optimization.py,sha256=xGO8piVsLrpqrx-Aid_Y56_5slSv4eZmlP64hCHRiEc,7957
21
21
  trilogy/core/query_processor.py,sha256=Do8YpdPBdsbKtl9n37hobzk8SORMGqH-e_zNNxd-BE4,19456
22
22
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- trilogy/core/models/author.py,sha256=oRCKWhz-i1fO1LlHWiHE3l1awCHdQ3yx6FKH9n9RxRU,67188
24
- trilogy/core/models/build.py,sha256=kiq31T8LtUtgmT37m617Q2MlMvQTuAxJzwb6947EiWU,56127
23
+ trilogy/core/models/author.py,sha256=S6riJx8Z4_orbohjK3fcZh5Ei6nCPYAS2FGDsI2njHk,69488
24
+ trilogy/core/models/build.py,sha256=z2QO7l2E2-1hHimmOGsLl42sTnEb2x9o45zkvOoJYpM,56687
25
25
  trilogy/core/models/build_environment.py,sha256=8UggvlPU708GZWYPJMc_ou2r7M3TY2g69eqGvz03YX0,5528
26
26
  trilogy/core/models/core.py,sha256=yie1uuq62uOQ5fjob9NMJbdvQPrCErXUT7JTCuYRyjI,9697
27
27
  trilogy/core/models/datasource.py,sha256=6RjJUd2u4nYmEwFBpJlM9LbHVYDv8iHJxqiBMZqUrwI,9422
28
- trilogy/core/models/environment.py,sha256=h06y1Dv7naw2GuFFAAyoFZmicG7a7Lu-dRoYPVfrOGo,25967
28
+ trilogy/core/models/environment.py,sha256=qFZ0_Op6zIhKc5oVS4EVYZ67f29wJhKP_xoEMV4kkuU,25991
29
29
  trilogy/core/models/execute.py,sha256=ABylFQgtavjjCfFkEsFdUwfMB4UBQLHjdzQ9E67QlAE,33521
30
30
  trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
31
31
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
@@ -40,9 +40,9 @@ trilogy/core/processing/node_generators/__init__.py,sha256=o8rOFHPSo-s_59hREwXMW
40
40
  trilogy/core/processing/node_generators/basic_node.py,sha256=UVsXMn6jTjm_ofVFt218jAS11s4RV4zD781vP4im-GI,3371
41
41
  trilogy/core/processing/node_generators/common.py,sha256=ZsDzThjm_mAtdQpKAg8QIJiPVZ4KuUkKyilj4eOhSDs,9439
42
42
  trilogy/core/processing/node_generators/filter_node.py,sha256=rlY7TbgjJlGhahYgdCIJpJbaSREAGVJEsyUIGaA38O0,8271
43
- trilogy/core/processing/node_generators/group_node.py,sha256=94uoGZWvBKJ1eqjbDHCbZuRqMkux_lfpfkGZgAJTNCY,5876
43
+ trilogy/core/processing/node_generators/group_node.py,sha256=3-TXVnRO9_jqE_e1kWLqbgtBShW8WFtKwQk8oOtOULs,5894
44
44
  trilogy/core/processing/node_generators/group_to_node.py,sha256=E5bEjovSx422d_MlAUCDFdY4P2WJVp61BmWwltkhzA8,3095
45
- trilogy/core/processing/node_generators/multiselect_node.py,sha256=z9FQOxxUvxW31a0TckFfAvnuvU8vP1GyN224RTbXUAk,7114
45
+ trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
46
46
  trilogy/core/processing/node_generators/node_merge_node.py,sha256=sv55oynfqgpHEpo1OEtVDri-5fywzPhDlR85qaWikvY,16195
47
47
  trilogy/core/processing/node_generators/rowset_node.py,sha256=8yeMWiyi9IFnza7qPn9YYC3WpA53weq3AY5WisIui8Y,6705
48
48
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=VHCPMbnKfg7AOfoYa6PKxpNni-j5JEfliNUiltmZhds,19698
@@ -63,7 +63,7 @@ trilogy/core/processing/nodes/union_node.py,sha256=fDFzLAUh5876X6_NM7nkhoMvHEdGJ
63
63
  trilogy/core/processing/nodes/unnest_node.py,sha256=oLKMMNMx6PLDPlt2V5neFMFrFWxET8r6XZElAhSNkO0,2181
64
64
  trilogy/core/processing/nodes/window_node.py,sha256=STvwheVttxSWVHB-yUQUSo-Pyz7Uk8G1txFDAbWMp-s,1380
65
65
  trilogy/core/statements/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- trilogy/core/statements/author.py,sha256=p3gLiPzXAHNNWVh8Xm9xECmywfG-LKDHB9U-Z6GdWCM,14246
66
+ trilogy/core/statements/author.py,sha256=9wKZDwQ-BeaUCMjD9l0ffMMv8zivaYcAg12UhVFi-0Y,14248
67
67
  trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
68
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
69
69
  trilogy/core/statements/execute.py,sha256=cSlvpHFOqpiZ89pPZ5GDp9Hu6j6uj-5_h21FWm_L-KM,1248
@@ -71,8 +71,8 @@ trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  trilogy/dialect/base.py,sha256=u00kIIl98as1QzcduiiyyoBzxRGVeBxfeO5hWlRCAJU,40222
72
72
  trilogy/dialect/bigquery.py,sha256=mKC3zoEU232h9RtIXJjqiZ72lWH8a6S28p6wAZKrAfg,2952
73
73
  trilogy/dialect/common.py,sha256=cbTo_vamdp8pj9spSjGSH-bSZpy4FpNJ12k5vMvyT2Y,3942
74
- trilogy/dialect/config.py,sha256=e-ZDVh7Z648JYz85JwSobTyo2cTi4lYGFMglZzB7atM,3184
75
- trilogy/dialect/dataframe.py,sha256=ei5y91XyZHI3ydUbdQ2sInnw2qHGtgb21DNX6qff0xw,1419
74
+ trilogy/dialect/config.py,sha256=EGYRQIbrkeMuud5Bkds7jSD5dCJR5hEYZUYcy-lYZl4,3308
75
+ trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
76
76
  trilogy/dialect/duckdb.py,sha256=2tH_OetgLJoKf_f4bdeeB0ozGC8f0h_xQ271I8qD-Oo,3690
77
77
  trilogy/dialect/enums.py,sha256=1KDgds_DC31hGxZzNI_TIggxXF7m9rIjn9KLgNf5WQU,4425
78
78
  trilogy/dialect/postgres.py,sha256=VH4EB4myjIeZTHeFU6vK00GxY9c53rCBjg2mLbdaCEE,3254
@@ -85,18 +85,18 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
85
85
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
86
86
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
87
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
- trilogy/parsing/common.py,sha256=yAE3x4SyO4PfAb7HhZ_l9sNPYaf_pcM1K8ioEy76SCU,20301
88
+ trilogy/parsing/common.py,sha256=RwO9CdNYy3KxJCjg5Ta_xJwfZHV2PuRErxg66Cs50ws,20490
89
89
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
90
90
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
91
91
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
92
- trilogy/parsing/parse_engine.py,sha256=32_yO_SreTjHxCkMziW2re15ilEZn01OUizVAvN9xHo,54656
92
+ trilogy/parsing/parse_engine.py,sha256=uZ6MYjg6kkTm5HFfOKLGvVvzHiGgH-vY7lV-AIlIBgY,55701
93
93
  trilogy/parsing/render.py,sha256=o_XuQWhcwx1lD9eGVqkqZEwkmQK0HdmWWokGBtdeH4I,17837
94
- trilogy/parsing/trilogy.lark,sha256=EazfEvYPuvkPkNjUnVzFi0uD9baavugbSI8CyfawShk,12573
94
+ trilogy/parsing/trilogy.lark,sha256=wZpqI1louDqm-t-TpmzW749dPA9w2EIAyowyEJIeXAM,12620
95
95
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
96
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
97
- pytrilogy-0.0.3.7.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
98
- pytrilogy-0.0.3.7.dist-info/METADATA,sha256=wvr0oUtX0As37OC9ljg5XnV7rblzMNvUppA4il2PtPI,8983
99
- pytrilogy-0.0.3.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
100
- pytrilogy-0.0.3.7.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
101
- pytrilogy-0.0.3.7.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
102
- pytrilogy-0.0.3.7.dist-info/RECORD,,
97
+ pytrilogy-0.0.3.8.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
98
+ pytrilogy-0.0.3.8.dist-info/METADATA,sha256=DAu0XOCyEgXpZj9-Znl0IbtouGXHELjt2EUZnp3IgEs,8983
99
+ pytrilogy-0.0.3.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
100
+ pytrilogy-0.0.3.8.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
101
+ pytrilogy-0.0.3.8.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
102
+ pytrilogy-0.0.3.8.dist-info/RECORD,,
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.7"
7
+ __version__ = "0.0.3.8"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -75,6 +75,9 @@ class Mergeable(ABC):
75
75
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
76
76
  raise NotImplementedError
77
77
 
78
+ def with_reference_replacement(self, source: str, target: Expr):
79
+ raise NotImplementedError(type(self))
80
+
78
81
 
79
82
  class ConceptArgs(ABC):
80
83
  @property
@@ -152,6 +155,11 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
152
155
  metadata=self.metadata,
153
156
  )
154
157
 
158
+ def with_reference_replacement(self, source: str, target: Expr):
159
+ if self.address == source:
160
+ return target
161
+ return self
162
+
155
163
 
156
164
  class UndefinedConcept(ConceptRef):
157
165
  pass
@@ -409,7 +417,7 @@ class Grain(Namespaced, BaseModel):
409
417
  ) -> Grain:
410
418
  from trilogy.parsing.common import concepts_to_grain_concepts
411
419
 
412
- return Grain.model_construct(
420
+ x = Grain.model_construct(
413
421
  components={
414
422
  c.address
415
423
  for c in concepts_to_grain_concepts(concepts, environment=environment)
@@ -417,6 +425,8 @@ class Grain(Namespaced, BaseModel):
417
425
  where_clause=where_clause,
418
426
  )
419
427
 
428
+ return x
429
+
420
430
  def with_namespace(self, namespace: str) -> "Grain":
421
431
  return Grain.model_construct(
422
432
  components={address_with_namespace(c, namespace) for c in self.components},
@@ -662,6 +672,21 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
662
672
  operator=self.operator,
663
673
  )
664
674
 
675
+ def with_reference_replacement(self, source, target):
676
+ return self.__class__.model_construct(
677
+ left=(
678
+ self.left.with_reference_replacement(source, target)
679
+ if isinstance(self.left, Mergeable)
680
+ else self.left
681
+ ),
682
+ right=(
683
+ self.right.with_reference_replacement(source, target)
684
+ if isinstance(self.right, Mergeable)
685
+ else self.right
686
+ ),
687
+ operator=self.operator,
688
+ )
689
+
665
690
  def with_namespace(self, namespace: str):
666
691
  return self.__class__.model_construct(
667
692
  left=(
@@ -1233,19 +1258,9 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1233
1258
  order=self.order,
1234
1259
  )
1235
1260
 
1236
- @property
1237
- def output(self):
1238
- return self.expr.output
1239
-
1240
1261
  @property
1241
1262
  def concept_arguments(self) -> Sequence[ConceptRef]:
1242
- base: List[ConceptRef] = []
1243
- x = self.expr
1244
- if isinstance(x, ConceptRef):
1245
- base += [x]
1246
- elif isinstance(x, ConceptArgs):
1247
- base += x.concept_arguments
1248
- return base
1263
+ return get_concept_arguments(self.expr)
1249
1264
 
1250
1265
  @property
1251
1266
  def row_arguments(self) -> Sequence[ConceptRef]:
@@ -1316,31 +1331,19 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1316
1331
 
1317
1332
  @property
1318
1333
  def concept_arguments(self) -> List[ConceptRef]:
1319
- return self.arguments
1320
-
1321
- @property
1322
- def arguments(self) -> List[ConceptRef]:
1323
1334
  output = [self.content]
1324
1335
  for order in self.order_by:
1325
- output += [order.output]
1336
+ output += get_concept_arguments(order)
1326
1337
  for item in self.over:
1327
- output += [item]
1338
+ output += get_concept_arguments(item)
1328
1339
  return output
1329
1340
 
1330
- @property
1331
- def output(self) -> ConceptRef:
1332
- return self.content
1333
-
1334
1341
  @property
1335
1342
  def output_datatype(self):
1336
1343
  if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
1337
1344
  return DataType.INTEGER
1338
1345
  return self.content.output_datatype
1339
1346
 
1340
- @property
1341
- def output_purpose(self):
1342
- return Purpose.PROPERTY
1343
-
1344
1347
 
1345
1348
  def get_basic_type(
1346
1349
  type: DataType | ListType | StructType | MapType | NumericType,
@@ -1407,12 +1410,28 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1407
1410
  ),
1408
1411
  )
1409
1412
 
1413
+ def with_reference_replacement(self, source, target):
1414
+ return CaseWhen.model_construct(
1415
+ comparison=self.comparison.with_reference_replacement(source, target),
1416
+ expr=(
1417
+ self.expr.with_reference_replacement(source, target)
1418
+ if isinstance(self.expr, Mergeable)
1419
+ else self.expr
1420
+ ),
1421
+ )
1422
+
1410
1423
 
1411
1424
  class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1412
1425
  expr: "Expr"
1413
1426
  # this ensures that it's easily differentiable from CaseWhen
1414
1427
  discriminant: ComparisonOperator = ComparisonOperator.ELSE
1415
1428
 
1429
+ def __str__(self):
1430
+ return self.__repr__()
1431
+
1432
+ def __repr__(self):
1433
+ return f"ELSE {str(self.expr)}"
1434
+
1416
1435
  @field_validator("expr", mode="before")
1417
1436
  def enforce_expr(cls, v):
1418
1437
  if isinstance(v, Concept):
@@ -1435,6 +1454,19 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1435
1454
  ),
1436
1455
  )
1437
1456
 
1457
+ def with_reference_replacement(self, source, target):
1458
+ return CaseElse.model_construct(
1459
+ discriminant=self.discriminant,
1460
+ expr=(
1461
+ self.expr.with_reference_replacement(
1462
+ source,
1463
+ target,
1464
+ )
1465
+ if isinstance(self.expr, Mergeable)
1466
+ else self.expr
1467
+ ),
1468
+ )
1469
+
1438
1470
  def with_namespace(self, namespace: str) -> CaseElse:
1439
1471
  return CaseElse.model_construct(
1440
1472
  discriminant=self.discriminant,
@@ -1510,8 +1542,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1510
1542
 
1511
1543
  def __init__(self, **kwargs):
1512
1544
  super().__init__(**kwargs)
1513
- if "datatype" in str(self):
1514
- raise SyntaxError(str(self))
1515
1545
 
1516
1546
  def __repr__(self):
1517
1547
  return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
@@ -1597,6 +1627,29 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1597
1627
  )
1598
1628
  return v
1599
1629
 
1630
+ def with_reference_replacement(self, source: str, target: Expr):
1631
+ return Function.model_construct(
1632
+ operator=self.operator,
1633
+ arguments=[
1634
+ (
1635
+ c.with_reference_replacement(
1636
+ source,
1637
+ target,
1638
+ )
1639
+ if isinstance(
1640
+ c,
1641
+ Mergeable,
1642
+ )
1643
+ else c
1644
+ )
1645
+ for c in self.arguments
1646
+ ],
1647
+ output_datatype=self.output_datatype,
1648
+ output_purpose=self.output_purpose,
1649
+ valid_inputs=self.valid_inputs,
1650
+ arg_count=self.arg_count,
1651
+ )
1652
+
1600
1653
  def with_namespace(self, namespace: str) -> "Function":
1601
1654
  return Function.model_construct(
1602
1655
  operator=self.operator,
@@ -1658,10 +1711,13 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1658
1711
  return base_grain
1659
1712
 
1660
1713
 
1661
- class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1714
+ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
1662
1715
  function: Function
1663
1716
  by: List[ConceptRef] = Field(default_factory=list)
1664
1717
 
1718
+ def __init__(self, **kwargs):
1719
+ super().__init__(**kwargs)
1720
+
1665
1721
  @field_validator("by", mode="before")
1666
1722
  @classmethod
1667
1723
  def enforce_concept_ref(cls, v):
@@ -1693,10 +1749,6 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1693
1749
  def output_purpose(self):
1694
1750
  return self.function.output_purpose
1695
1751
 
1696
- @property
1697
- def arguments(self):
1698
- return self.function.arguments
1699
-
1700
1752
  def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
1701
1753
  return AggregateWrapper.model_construct(
1702
1754
  function=self.function.with_merge(source, target, modifiers=modifiers),
@@ -1707,6 +1759,16 @@ class AggregateWrapper(Mergeable, ConceptArgs, Namespaced, BaseModel):
1707
1759
  ),
1708
1760
  )
1709
1761
 
1762
+ def with_reference_replacement(self, source, target):
1763
+ return AggregateWrapper.model_construct(
1764
+ function=self.function.with_reference_replacement(source, target),
1765
+ by=(
1766
+ [c.with_reference_replacement(source, target) for c in self.by]
1767
+ if self.by
1768
+ else []
1769
+ ),
1770
+ )
1771
+
1710
1772
  def with_namespace(self, namespace: str) -> "AggregateWrapper":
1711
1773
  return AggregateWrapper.model_construct(
1712
1774
  function=self.function.with_namespace(namespace),
@@ -1796,11 +1858,6 @@ class RowsetItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1796
1858
  rowset=self.rowset.with_namespace(namespace),
1797
1859
  )
1798
1860
 
1799
- @property
1800
- def arguments(self) -> List[ConceptRef]:
1801
- output = [self.content]
1802
- return output
1803
-
1804
1861
  @property
1805
1862
  def output(self) -> ConceptRef:
1806
1863
  return self.content
@@ -1831,7 +1888,10 @@ class OrderBy(Mergeable, Namespaced, BaseModel):
1831
1888
 
1832
1889
  @property
1833
1890
  def concept_arguments(self):
1834
- return [x.expr for x in self.items]
1891
+ base = []
1892
+ for x in self.items:
1893
+ base += x.concept_arguments
1894
+ return base
1835
1895
 
1836
1896
 
1837
1897
  class AlignClause(Namespaced, BaseModel):
@@ -2088,6 +2148,11 @@ class Comment(BaseModel):
2088
2148
  text: str
2089
2149
 
2090
2150
 
2151
+ class ArgBinding(BaseModel):
2152
+ name: str
2153
+ default: Expr | None = None
2154
+
2155
+
2091
2156
  Expr = (
2092
2157
  MagicConstants
2093
2158
  | bool
@@ -1639,9 +1639,26 @@ class Factory:
1639
1639
 
1640
1640
  @build.register
1641
1641
  def _(self, base: Comparison) -> BuildComparison:
1642
+ from trilogy.parsing.common import arbitrary_to_concept
1643
+
1644
+ left = base.left
1645
+ if isinstance(left, AggregateWrapper):
1646
+ left_c = arbitrary_to_concept(
1647
+ left,
1648
+ environment=self.environment,
1649
+ )
1650
+ left = left_c # type: ignore
1651
+ right = base.right
1652
+ if isinstance(right, AggregateWrapper):
1653
+ right_c = arbitrary_to_concept(
1654
+ right,
1655
+ environment=self.environment,
1656
+ )
1657
+
1658
+ right = right_c # type: ignore
1642
1659
  return BuildComparison.model_construct(
1643
- left=(self.build(base.left)),
1644
- right=(self.build(base.right)),
1660
+ left=(self.build(left)),
1661
+ right=(self.build(right)),
1645
1662
  operator=base.operator,
1646
1663
  )
1647
1664
 
@@ -1660,7 +1677,7 @@ class Factory:
1660
1677
  )
1661
1678
 
1662
1679
  @build.register
1663
- def _(self, base: RowsetItem):
1680
+ def _(self, base: RowsetItem) -> BuildRowsetItem:
1664
1681
 
1665
1682
  factory = Factory(
1666
1683
  environment=self.environment,
@@ -9,6 +9,7 @@ from typing import (
9
9
  TYPE_CHECKING,
10
10
  Annotated,
11
11
  Any,
12
+ Callable,
12
13
  Dict,
13
14
  ItemsView,
14
15
  List,
@@ -189,7 +190,7 @@ class Environment(BaseModel):
189
190
  datasources: Annotated[
190
191
  EnvironmentDatasourceDict, PlainValidator(validate_datasources)
191
192
  ] = Field(default_factory=EnvironmentDatasourceDict)
192
- functions: Dict[str, Function] = Field(default_factory=dict)
193
+ functions: Dict[str, Callable[..., Any]] = Field(default_factory=dict)
193
194
  data_types: Dict[str, DataType] = Field(default_factory=dict)
194
195
  named_statements: Dict[str, SelectLineage] = Field(default_factory=dict)
195
196
  imports: Dict[str, list[Import]] = Field(
@@ -37,7 +37,7 @@ def gen_group_node(
37
37
  resolve_function_parent_concepts(concept, environment=environment), "address"
38
38
  )
39
39
  logger.info(
40
- f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
40
+ f"{padding(depth)}{LOGGER_PREFIX} parent concepts for {concept} {concept.lineage} are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
41
41
  )
42
42
 
43
43
  # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
@@ -76,7 +76,7 @@ def gen_multiselect_node(
76
76
  for select in lineage.selects:
77
77
 
78
78
  snode: StrategyNode = get_query_node(history.base_environment, select)
79
- # raise SyntaxError(select.output_components)
79
+
80
80
  logger.info(
81
81
  f"{padding(depth)}{LOGGER_PREFIX} Fetched parent node with outputs {select.output_components}"
82
82
  )
@@ -127,7 +127,9 @@ class SelectStatement(HasUUID, SelectTypeMixin, BaseModel):
127
127
  order_by=order_by,
128
128
  meta=meta or Metadata(),
129
129
  )
130
+
130
131
  output.grain = output.calculate_grain(environment)
132
+
131
133
  for x in selection:
132
134
 
133
135
  if x.is_undefined and environment.concepts.fail_on_missing:
trilogy/dialect/config.py CHANGED
@@ -1,4 +1,10 @@
1
- from pandas import DataFrame
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ if TYPE_CHECKING:
4
+ try:
5
+ from pandas import DataFrame
6
+ except ImportError:
7
+ DataFrame = Any
2
8
 
3
9
 
4
10
  class DialectConfig:
@@ -110,6 +116,6 @@ class TrinoConfig(PrestoConfig):
110
116
 
111
117
 
112
118
  class DataFrameConfig(DuckDBConfig):
113
- def __init__(self, dataframes: dict[str, DataFrame]):
119
+ def __init__(self, dataframes: dict[str, "DataFrame"]):
114
120
  super().__init__()
115
121
  self.dataframes = dataframes
@@ -1,19 +1,24 @@
1
- from typing import Any
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
- from pandas import DataFrame
4
3
  from sqlalchemy import text
5
4
 
6
5
  from trilogy.core.models.environment import Environment
7
6
  from trilogy.dialect.duckdb import DuckDBDialect
8
7
  from trilogy.engine import ExecutionEngine
9
8
 
9
+ if TYPE_CHECKING:
10
+ try:
11
+ from pandas import DataFrame
12
+ except ImportError:
13
+ DataFrame = Any
14
+
10
15
 
11
16
  class DataframeDialect(DuckDBDialect):
12
17
  pass
13
18
 
14
19
 
15
20
  class DataframeConnectionWrapper(ExecutionEngine):
16
- def __init__(self, engine: ExecutionEngine, dataframes: dict[str, DataFrame]):
21
+ def __init__(self, engine: ExecutionEngine, dataframes: dict[str, "DataFrame"]):
17
22
  self.engine = engine
18
23
  self.dataframes = dataframes
19
24
  self.connection = None
@@ -34,7 +39,7 @@ class DataframeConnectionWrapper(ExecutionEngine):
34
39
  )
35
40
  pass
36
41
 
37
- def add_dataframe(self, name: str, df: DataFrame, connection, env: Environment):
42
+ def add_dataframe(self, name: str, df: "DataFrame", connection, env: Environment):
38
43
  self.dataframes[name] = df
39
44
  self._register_dataframes(env, connection)
40
45
 
trilogy/parsing/common.py CHANGED
@@ -46,6 +46,62 @@ from trilogy.core.statements.author import RowsetDerivationStatement, SelectStat
46
46
  from trilogy.utility import string_to_hash, unique
47
47
 
48
48
 
49
+ def process_function_arg(
50
+ arg,
51
+ meta: Meta | None,
52
+ environment: Environment,
53
+ ):
54
+ # if a function has an anonymous function argument
55
+ # create an implicit concept
56
+ if isinstance(arg, Parenthetical):
57
+ processed = process_function_args([arg.content], meta, environment)
58
+ return Function(
59
+ operator=FunctionType.PARENTHETICAL,
60
+ arguments=processed,
61
+ output_datatype=arg_to_datatype(processed[0]),
62
+ output_purpose=function_args_to_output_purpose(processed),
63
+ )
64
+ elif isinstance(arg, Function):
65
+ # if it's not an aggregate function, we can skip the virtual concepts
66
+ # to simplify anonymous function handling
67
+ if (
68
+ arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
69
+ and arg.operator != FunctionType.UNNEST
70
+ ):
71
+ return arg
72
+ id_hash = string_to_hash(str(arg))
73
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}"
74
+ if f"{environment.namespace}.{name}" in environment.concepts:
75
+ return environment.concepts[f"{environment.namespace}.{name}"]
76
+ concept = function_to_concept(
77
+ arg,
78
+ name=name,
79
+ environment=environment,
80
+ )
81
+ # to satisfy mypy, concept will always have metadata
82
+ if concept.metadata and meta:
83
+ concept.metadata.line_number = meta.line
84
+ environment.add_concept(concept, meta=meta)
85
+ return concept
86
+ elif isinstance(
87
+ arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
88
+ ):
89
+ id_hash = string_to_hash(str(arg))
90
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}"
91
+ if f"{environment.namespace}.{name}" in environment.concepts:
92
+ return environment.concepts[f"{environment.namespace}.{name}"]
93
+ concept = arbitrary_to_concept(
94
+ arg,
95
+ name=name,
96
+ environment=environment,
97
+ )
98
+ if concept.metadata and meta:
99
+ concept.metadata.line_number = meta.line
100
+ environment.add_concept(concept, meta=meta)
101
+ return concept
102
+ return arg
103
+
104
+
49
105
  def process_function_args(
50
106
  args,
51
107
  meta: Meta | None,
@@ -53,54 +109,7 @@ def process_function_args(
53
109
  ) -> List[Concept | Function | str | int | float | date | datetime]:
54
110
  final: List[Concept | Function | str | int | float | date | datetime] = []
55
111
  for arg in args:
56
- # if a function has an anonymous function argument
57
- # create an implicit concept
58
- if isinstance(arg, Parenthetical):
59
- processed = process_function_args([arg.content], meta, environment)
60
- final.append(
61
- Function(
62
- operator=FunctionType.PARENTHETICAL,
63
- arguments=processed,
64
- output_datatype=arg_to_datatype(processed[0]),
65
- output_purpose=function_args_to_output_purpose(processed),
66
- )
67
- )
68
- elif isinstance(arg, Function):
69
- # if it's not an aggregate function, we can skip the virtual concepts
70
- # to simplify anonymous function handling
71
- if (
72
- arg.operator not in FunctionClass.AGGREGATE_FUNCTIONS.value
73
- and arg.operator != FunctionType.UNNEST
74
- ):
75
- final.append(arg)
76
- continue
77
- id_hash = string_to_hash(str(arg))
78
- concept = function_to_concept(
79
- arg,
80
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}",
81
- environment=environment,
82
- )
83
- # to satisfy mypy, concept will always have metadata
84
- if concept.metadata and meta:
85
- concept.metadata.line_number = meta.line
86
- environment.add_concept(concept, meta=meta)
87
- final.append(concept)
88
- elif isinstance(
89
- arg, (FilterItem, WindowItem, AggregateWrapper, ListWrapper, MapWrapper)
90
- ):
91
- id_hash = string_to_hash(str(arg))
92
- concept = arbitrary_to_concept(
93
- arg,
94
- name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
95
- environment=environment,
96
- )
97
- if concept.metadata and meta:
98
- concept.metadata.line_number = meta.line
99
- environment.add_concept(concept, meta=meta)
100
- final.append(concept)
101
-
102
- else:
103
- final.append(arg)
112
+ final.append(process_function_arg(arg, meta, environment))
104
113
  return final
105
114
 
106
115
 
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from os.path import dirname, join
5
5
  from pathlib import Path
6
6
  from re import IGNORECASE
7
- from typing import List, Optional, Tuple, Union
7
+ from typing import Callable, List, Optional, Tuple, Union
8
8
 
9
9
  from lark import Lark, ParseTree, Transformer, Tree, v_args
10
10
  from lark.exceptions import (
@@ -49,6 +49,7 @@ from trilogy.core.models.author import (
49
49
  AggregateWrapper,
50
50
  AlignClause,
51
51
  AlignItem,
52
+ ArgBinding,
52
53
  CaseElse,
53
54
  CaseWhen,
54
55
  Comment,
@@ -61,6 +62,7 @@ from trilogy.core.models.author import (
61
62
  Function,
62
63
  Grain,
63
64
  HavingClause,
65
+ Mergeable,
64
66
  Metadata,
65
67
  OrderBy,
66
68
  OrderItem,
@@ -1075,27 +1077,45 @@ class ParseToObjects(Transformer):
1075
1077
  return HavingClause(conditional=root)
1076
1078
 
1077
1079
  @v_args(meta=True)
1078
- def function_binding_list(self, meta: Meta, args) -> Concept:
1080
+ def function_binding_list(self, meta: Meta, args) -> list[ArgBinding]:
1079
1081
  return args
1080
1082
 
1081
1083
  @v_args(meta=True)
1082
- def function_binding_item(self, meta: Meta, args) -> Concept:
1083
- return args
1084
+ def function_binding_item(self, meta: Meta, args) -> ArgBinding:
1085
+ if len(args) == 2:
1086
+ return ArgBinding(name=args[0], default=args[1])
1087
+ return ArgBinding(name=args[0], default=None)
1084
1088
 
1085
1089
  @v_args(meta=True)
1086
- def raw_function(self, meta: Meta, args) -> Function:
1090
+ def raw_function(self, meta: Meta, args) -> Callable[[list[Expr]], Expr]:
1087
1091
  identity = args[0]
1088
- fargs = args[1]
1092
+ function_arguments: list[ArgBinding] = args[1]
1089
1093
  output = args[2]
1090
- item = Function(
1091
- operator=FunctionType.SUM,
1092
- arguments=[x[1] for x in fargs],
1093
- output_datatype=output,
1094
- output_purpose=Purpose.PROPERTY,
1095
- arg_count=len(fargs) + 1,
1096
- )
1097
- self.environment.functions[identity] = item
1098
- return item
1094
+
1095
+ def function_factory(*creation_args: list[Expr]):
1096
+ nout = output.copy(deep=True)
1097
+ creation_arg_list: list[Expr] = list(creation_args)
1098
+ if len(creation_args) < len(function_arguments):
1099
+ for binding in function_arguments[len(creation_arg_list) :]:
1100
+ if binding.default is None:
1101
+ raise ValueError(f"Missing argument {binding.name}")
1102
+ creation_arg_list.append(binding.default)
1103
+ if isinstance(nout, Mergeable):
1104
+ for idx, x in enumerate(creation_arg_list):
1105
+ # these will always be local namespace
1106
+ nout = nout.with_reference_replacement(
1107
+ f"{DEFAULT_NAMESPACE}.{function_arguments[idx].name}", x
1108
+ )
1109
+ return nout
1110
+
1111
+ self.environment.functions[identity] = function_factory
1112
+ return function_factory
1113
+
1114
+ def custom_function(self, args):
1115
+ name = args[0]
1116
+ args = args[1:]
1117
+ remapped = self.environment.functions[name](*args)
1118
+ return remapped
1099
1119
 
1100
1120
  @v_args(meta=True)
1101
1121
  def function(self, meta: Meta, args) -> Function:
@@ -1141,24 +1161,24 @@ class ParseToObjects(Transformer):
1141
1161
  def comparison(self, args) -> Comparison:
1142
1162
  if args[1] == ComparisonOperator.IN:
1143
1163
  raise SyntaxError
1144
- if isinstance(args[0], AggregateWrapper):
1145
- left_c = arbitrary_to_concept(
1146
- args[0],
1147
- environment=self.environment,
1148
- )
1149
- self.environment.add_concept(left_c)
1150
- left = left_c.reference
1151
- else:
1152
- left = args[0]
1153
- if isinstance(args[2], AggregateWrapper):
1154
- right_c = arbitrary_to_concept(
1155
- args[2],
1156
- environment=self.environment,
1157
- )
1158
- self.environment.add_concept(right_c)
1159
- right = right_c.reference
1160
- else:
1161
- right = args[2]
1164
+ # if isinstance(args[0], AggregateWrapper):
1165
+ # left_c = arbitrary_to_concept(
1166
+ # args[0],
1167
+ # environment=self.environment,
1168
+ # )
1169
+ # self.environment.add_concept(left_c)
1170
+ # left = left_c.reference
1171
+ # else:
1172
+ left = args[0]
1173
+ # if isinstance(args[2], AggregateWrapper):
1174
+ # right_c = arbitrary_to_concept(
1175
+ # args[2],
1176
+ # environment=self.environment,
1177
+ # )
1178
+ # self.environment.add_concept(right_c)
1179
+ # right = right_c.reference
1180
+ # else:
1181
+ right = args[2]
1162
1182
  return Comparison(left=left, right=right, operator=args[1])
1163
1183
 
1164
1184
  def between_comparison(self, args) -> Conditional:
@@ -89,9 +89,9 @@
89
89
 
90
90
  // FUNCTION blocks
91
91
  function: raw_function
92
- function_binding_item: IDENTIFIER ":" data_type
93
- function_binding_list: function_binding_item ("," function_binding_item )* ","?
94
- raw_function: "bind" "sql" IDENTIFIER "(" function_binding_list ")" "->" data_type "as"i MULTILINE_STRING
92
+ function_binding_item: IDENTIFIER ("=" literal)?
93
+ function_binding_list: (function_binding_item ",")* function_binding_item ","?
94
+ raw_function: "def" IDENTIFIER "(" function_binding_list ")" "->" expr
95
95
 
96
96
  // user_id where state = Mexico
97
97
  _filter_alt: IDENTIFIER "?" conditional
@@ -178,7 +178,7 @@
178
178
  map_key_access: expr "[" string_lit "]"
179
179
  attr_access: expr "." string_lit
180
180
 
181
- expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | union | _static_functions | literal | concept_lit | index_access | map_key_access | attr_access | parenthetical | expr_tuple | comparison | alt_like
181
+ expr: _constant_functions | window_item | filter_item | subselect_comparison | between_comparison | fgroup | aggregate_functions | unnest | union | _static_functions | literal | concept_lit | index_access | map_key_access | attr_access | parenthetical | expr_tuple | comparison | alt_like | custom_function
182
182
 
183
183
  // functions
184
184
 
@@ -297,6 +297,8 @@
297
297
 
298
298
  _static_functions: _string_functions | _math_functions | _generic_functions | _date_functions
299
299
 
300
+ custom_function: "@" IDENTIFIER "(" (expr ",")* expr ")"
301
+
300
302
  // base language constructs
301
303
  concept_lit: IDENTIFIER
302
304
  IDENTIFIER: /[a-zA-Z\_][a-zA-Z0-9\_\.]*/