pytrilogy 0.0.3.7__py3-none-any.whl → 0.0.3.9__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.9
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,8 +1,8 @@
1
- trilogy/__init__.py,sha256=nCVrjnf_bl_zZ7GmePdSoaRQ40QxFDkSFlafhQp8Cn8,302
1
+ trilogy/__init__.py,sha256=oZcEJCenpaskC-ViU-OpC-03zsr2chE8ggGCpg2Duws,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
5
- trilogy/executor.py,sha256=YgSCeeYVecI9526LGSLVe2apOo7Ddsttvs_nDC9yElQ,17194
5
+ trilogy/executor.py,sha256=9HhdLQoou1Cy9KSDgpdYxK6uyg-UPkgx9jXJBuK5ITc,17271
6
6
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
@@ -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=eJOz-p20Am5IQPDeBntgIkncB1nUZGhuwcy7WBSbCmM,70240
24
+ trilogy/core/models/build.py,sha256=bO1qYvuGl6LeNGgsfS6ZHAzZBR2lBPLg-QJymp9hgkU,57235
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=IgZ2K3LzJ0usLIwxRCRmS-4luP6uwmM-f1oqGNyGbm0,21306
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.9.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
98
+ pytrilogy-0.0.3.9.dist-info/METADATA,sha256=8Bv3VIgAPpBlaRGc1UlrWrMPLb0HYNpy-coiUXMzzK0,8983
99
+ pytrilogy-0.0.3.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
100
+ pytrilogy-0.0.3.9.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
101
+ pytrilogy-0.0.3.9.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
102
+ pytrilogy-0.0.3.9.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.9"
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},
@@ -537,6 +547,7 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
537
547
  MagicConstants,
538
548
  WindowItem,
539
549
  AggregateWrapper,
550
+ FilterItem,
540
551
  ]
541
552
  right: Union[
542
553
  int,
@@ -556,6 +567,7 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
556
567
  WindowItem,
557
568
  AggregateWrapper,
558
569
  TupleWrapper,
570
+ FilterItem,
559
571
  ]
560
572
  operator: ComparisonOperator
561
573
 
@@ -662,6 +674,21 @@ class Comparison(ConceptArgs, Mergeable, Namespaced, BaseModel):
662
674
  operator=self.operator,
663
675
  )
664
676
 
677
+ def with_reference_replacement(self, source, target):
678
+ return self.__class__.model_construct(
679
+ left=(
680
+ self.left.with_reference_replacement(source, target)
681
+ if isinstance(self.left, Mergeable)
682
+ else self.left
683
+ ),
684
+ right=(
685
+ self.right.with_reference_replacement(source, target)
686
+ if isinstance(self.right, Mergeable)
687
+ else self.right
688
+ ),
689
+ operator=self.operator,
690
+ )
691
+
665
692
  def with_namespace(self, namespace: str):
666
693
  return self.__class__.model_construct(
667
694
  left=(
@@ -1224,7 +1251,7 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1224
1251
  def with_merge(
1225
1252
  self, source: Concept, target: Concept, modifiers: List[Modifier]
1226
1253
  ) -> "OrderItem":
1227
- return OrderItem(
1254
+ return OrderItem.model_construct(
1228
1255
  expr=(
1229
1256
  self.expr.with_merge(source, target, modifiers)
1230
1257
  if isinstance(self.expr, Mergeable)
@@ -1233,19 +1260,19 @@ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1233
1260
  order=self.order,
1234
1261
  )
1235
1262
 
1236
- @property
1237
- def output(self):
1238
- return self.expr.output
1263
+ def with_reference_replacement(self, source, target):
1264
+ return OrderItem.model_construct(
1265
+ expr=(
1266
+ self.expr.with_reference_replacement(source, target)
1267
+ if isinstance(self.expr, Mergeable)
1268
+ else self.expr
1269
+ ),
1270
+ order=self.order,
1271
+ )
1239
1272
 
1240
1273
  @property
1241
1274
  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
1275
+ return get_concept_arguments(self.expr)
1249
1276
 
1250
1277
  @property
1251
1278
  def row_arguments(self) -> Sequence[ConceptRef]:
@@ -1305,6 +1332,17 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1305
1332
  )
1306
1333
  return output
1307
1334
 
1335
+ def with_reference_replacement(self, source, target):
1336
+ return WindowItem.model_construct(
1337
+ type=self.type,
1338
+ content=self.content.with_reference_replacement(source, target),
1339
+ over=[x.with_reference_replacement(source, target) for x in self.over],
1340
+ order_by=[
1341
+ x.with_reference_replacement(source, target) for x in self.order_by
1342
+ ],
1343
+ index=self.index,
1344
+ )
1345
+
1308
1346
  def with_namespace(self, namespace: str) -> "WindowItem":
1309
1347
  return WindowItem.model_construct(
1310
1348
  type=self.type,
@@ -1316,31 +1354,19 @@ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1316
1354
 
1317
1355
  @property
1318
1356
  def concept_arguments(self) -> List[ConceptRef]:
1319
- return self.arguments
1320
-
1321
- @property
1322
- def arguments(self) -> List[ConceptRef]:
1323
1357
  output = [self.content]
1324
1358
  for order in self.order_by:
1325
- output += [order.output]
1359
+ output += get_concept_arguments(order)
1326
1360
  for item in self.over:
1327
- output += [item]
1361
+ output += get_concept_arguments(item)
1328
1362
  return output
1329
1363
 
1330
- @property
1331
- def output(self) -> ConceptRef:
1332
- return self.content
1333
-
1334
1364
  @property
1335
1365
  def output_datatype(self):
1336
1366
  if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
1337
1367
  return DataType.INTEGER
1338
1368
  return self.content.output_datatype
1339
1369
 
1340
- @property
1341
- def output_purpose(self):
1342
- return Purpose.PROPERTY
1343
-
1344
1370
 
1345
1371
  def get_basic_type(
1346
1372
  type: DataType | ListType | StructType | MapType | NumericType,
@@ -1407,12 +1433,28 @@ class CaseWhen(Namespaced, ConceptArgs, Mergeable, BaseModel):
1407
1433
  ),
1408
1434
  )
1409
1435
 
1436
+ def with_reference_replacement(self, source, target):
1437
+ return CaseWhen.model_construct(
1438
+ comparison=self.comparison.with_reference_replacement(source, target),
1439
+ expr=(
1440
+ self.expr.with_reference_replacement(source, target)
1441
+ if isinstance(self.expr, Mergeable)
1442
+ else self.expr
1443
+ ),
1444
+ )
1445
+
1410
1446
 
1411
1447
  class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1412
1448
  expr: "Expr"
1413
1449
  # this ensures that it's easily differentiable from CaseWhen
1414
1450
  discriminant: ComparisonOperator = ComparisonOperator.ELSE
1415
1451
 
1452
+ def __str__(self):
1453
+ return self.__repr__()
1454
+
1455
+ def __repr__(self):
1456
+ return f"ELSE {str(self.expr)}"
1457
+
1416
1458
  @field_validator("expr", mode="before")
1417
1459
  def enforce_expr(cls, v):
1418
1460
  if isinstance(v, Concept):
@@ -1435,6 +1477,19 @@ class CaseElse(Namespaced, ConceptArgs, Mergeable, BaseModel):
1435
1477
  ),
1436
1478
  )
1437
1479
 
1480
+ def with_reference_replacement(self, source, target):
1481
+ return CaseElse.model_construct(
1482
+ discriminant=self.discriminant,
1483
+ expr=(
1484
+ self.expr.with_reference_replacement(
1485
+ source,
1486
+ target,
1487
+ )
1488
+ if isinstance(self.expr, Mergeable)
1489
+ else self.expr
1490
+ ),
1491
+ )
1492
+
1438
1493
  def with_namespace(self, namespace: str) -> CaseElse:
1439
1494
  return CaseElse.model_construct(
1440
1495
  discriminant=self.discriminant,
@@ -1483,35 +1538,10 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1483
1538
  List[Set[DataType]],
1484
1539
  ]
1485
1540
  ] = None
1486
- arguments: Sequence[
1487
- Union[
1488
- ConceptRef,
1489
- AggregateWrapper,
1490
- Function,
1491
- Parenthetical,
1492
- CaseWhen,
1493
- CaseElse,
1494
- WindowItem,
1495
- int,
1496
- float,
1497
- str,
1498
- date,
1499
- datetime,
1500
- MapWrapper[Any, Any],
1501
- DataType,
1502
- ListType,
1503
- MapType,
1504
- NumericType,
1505
- DatePart,
1506
- list,
1507
- ListWrapper[Any],
1508
- ]
1509
- ]
1541
+ arguments: Sequence[FuncArgs]
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
@@ -2099,6 +2164,7 @@ Expr = (
2099
2164
  | datetime
2100
2165
  | TupleWrapper
2101
2166
  | ListWrapper
2167
+ | MapWrapper
2102
2168
  | WindowItem
2103
2169
  | FilterItem
2104
2170
  | ConceptRef
@@ -2107,4 +2173,30 @@ Expr = (
2107
2173
  | Parenthetical
2108
2174
  | Function
2109
2175
  | AggregateWrapper
2176
+ | CaseWhen
2177
+ | CaseElse
2178
+ )
2179
+
2180
+ FuncArgs = (
2181
+ ConceptRef
2182
+ | AggregateWrapper
2183
+ | Function
2184
+ | Parenthetical
2185
+ | CaseWhen
2186
+ | CaseElse
2187
+ | WindowItem
2188
+ | FilterItem
2189
+ | int
2190
+ | float
2191
+ | str
2192
+ | date
2193
+ | datetime
2194
+ | MapWrapper[Any, Any]
2195
+ | DataType
2196
+ | ListType
2197
+ | MapType
2198
+ | NumericType
2199
+ | DatePart
2200
+ | list
2201
+ | ListWrapper[Any]
2110
2202
  )
@@ -51,6 +51,7 @@ from trilogy.core.models.author import (
51
51
  ConceptRef,
52
52
  Conditional,
53
53
  FilterItem,
54
+ FuncArgs,
54
55
  Function,
55
56
  Grain,
56
57
  HavingClause,
@@ -1498,10 +1499,22 @@ class Factory:
1498
1499
 
1499
1500
  @build.register
1500
1501
  def _(self, base: Function) -> BuildFunction:
1501
-
1502
+ from trilogy.parsing.common import arbitrary_to_concept
1503
+
1504
+ raw_args: list[Concept | FuncArgs] = []
1505
+ for arg in base.arguments:
1506
+ # to do proper discovery, we need to inject virtual intermediate ocncepts
1507
+ if isinstance(arg, (AggregateWrapper, FilterItem, WindowItem)):
1508
+ narg = arbitrary_to_concept(
1509
+ arg,
1510
+ environment=self.environment,
1511
+ )
1512
+ raw_args.append(narg)
1513
+ else:
1514
+ raw_args.append(arg)
1502
1515
  new = BuildFunction.model_construct(
1503
1516
  operator=base.operator,
1504
- arguments=[self.build(c) for c in base.arguments],
1517
+ arguments=[self.build(c) for c in raw_args],
1505
1518
  output_datatype=base.output_datatype,
1506
1519
  output_purpose=base.output_purpose,
1507
1520
  valid_inputs=base.valid_inputs,
@@ -1639,9 +1652,26 @@ class Factory:
1639
1652
 
1640
1653
  @build.register
1641
1654
  def _(self, base: Comparison) -> BuildComparison:
1655
+ from trilogy.parsing.common import arbitrary_to_concept
1656
+
1657
+ left = base.left
1658
+ if isinstance(left, AggregateWrapper):
1659
+ left_c = arbitrary_to_concept(
1660
+ left,
1661
+ environment=self.environment,
1662
+ )
1663
+ left = left_c # type: ignore
1664
+ right = base.right
1665
+ if isinstance(right, AggregateWrapper):
1666
+ right_c = arbitrary_to_concept(
1667
+ right,
1668
+ environment=self.environment,
1669
+ )
1670
+
1671
+ right = right_c # type: ignore
1642
1672
  return BuildComparison.model_construct(
1643
- left=(self.build(base.left)),
1644
- right=(self.build(base.right)),
1673
+ left=(self.build(left)),
1674
+ right=(self.build(right)),
1645
1675
  operator=base.operator,
1646
1676
  )
1647
1677
 
@@ -1660,7 +1690,7 @@ class Factory:
1660
1690
  )
1661
1691
 
1662
1692
  @build.register
1663
- def _(self, base: RowsetItem):
1693
+ def _(self, base: RowsetItem) -> BuildRowsetItem:
1664
1694
 
1665
1695
  factory = Factory(
1666
1696
  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/executor.py CHANGED
@@ -166,8 +166,11 @@ class Executor(object):
166
166
  )
167
167
 
168
168
  @execute_query.register
169
- def _(self, query: str) -> CursorResult:
170
- return self.execute_text(query)[-1]
169
+ def _(self, query: str) -> CursorResult | None:
170
+ results = self.execute_text(query)
171
+ if results:
172
+ return results[-1]
173
+ return None
171
174
 
172
175
  @execute_query.register
173
176
  def _(self, query: SelectStatement) -> CursorResult:
trilogy/parsing/common.py CHANGED
@@ -46,6 +46,63 @@ 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,
88
+ (ListWrapper, MapWrapper),
89
+ ):
90
+ id_hash = string_to_hash(str(arg))
91
+ name = f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}"
92
+ if f"{environment.namespace}.{name}" in environment.concepts:
93
+ return environment.concepts[f"{environment.namespace}.{name}"]
94
+ concept = arbitrary_to_concept(
95
+ arg,
96
+ name=name,
97
+ environment=environment,
98
+ )
99
+ if concept.metadata and meta:
100
+ concept.metadata.line_number = meta.line
101
+ environment.add_concept(concept, meta=meta)
102
+ return concept
103
+ return arg
104
+
105
+
49
106
  def process_function_args(
50
107
  args,
51
108
  meta: Meta | None,
@@ -53,54 +110,7 @@ def process_function_args(
53
110
  ) -> List[Concept | Function | str | int | float | date | datetime]:
54
111
  final: List[Concept | Function | str | int | float | date | datetime] = []
55
112
  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)
113
+ final.append(process_function_arg(arg, meta, environment))
104
114
  return final
105
115
 
106
116
 
@@ -240,6 +250,24 @@ def concepts_to_grain_concepts(
240
250
  return v2
241
251
 
242
252
 
253
+ def get_relevant_parent_concepts(arg) -> tuple[list[ConceptRef], bool]:
254
+ from trilogy.core.models.author import get_concept_arguments
255
+
256
+ is_metric = False
257
+ if isinstance(arg, Function):
258
+ all = []
259
+ for y in arg.arguments:
260
+ refs, local_flag = get_relevant_parent_concepts(y)
261
+ all += refs
262
+ is_metric = is_metric or local_flag
263
+ return all, is_metric
264
+ elif isinstance(arg, AggregateWrapper) and not arg.by:
265
+ return [], True
266
+ elif isinstance(arg, AggregateWrapper) and arg.by:
267
+ return arg.by, True
268
+ return get_concept_arguments(arg), False
269
+
270
+
243
271
  def function_to_concept(
244
272
  parent: Function,
245
273
  name: str,
@@ -247,14 +275,12 @@ def function_to_concept(
247
275
  namespace: str | None = None,
248
276
  metadata: Metadata | None = None,
249
277
  ) -> Concept:
278
+
250
279
  pkeys: List[Concept] = []
251
280
  namespace = namespace or environment.namespace
252
- concrete_args = [
253
- x
254
- for x in [environment.concepts[c.address] for c in parent.concept_arguments]
255
- if not isinstance(x, UndefinedConcept)
256
- ]
257
-
281
+ is_metric = False
282
+ ref_args, is_metric = get_relevant_parent_concepts(parent)
283
+ concrete_args = [environment.concepts[c.address] for c in ref_args]
258
284
  pkeys += [x for x in concrete_args if not x.derivation == Derivation.CONSTANT]
259
285
  grain: Grain | None = Grain()
260
286
  for x in pkeys:
@@ -265,12 +291,18 @@ def function_to_concept(
265
291
  modifiers = get_upstream_modifiers(pkeys, environment)
266
292
  key_grain: list[str] = []
267
293
  for x in pkeys:
268
- if x.keys:
294
+ # metrics will group to keys, so do no do key traversal
295
+ if is_metric:
296
+ key_grain.append(x.address)
297
+ # otherwse, for row ops, assume keys are transitive
298
+ elif x.keys:
269
299
  key_grain += [*x.keys]
270
300
  else:
271
301
  key_grain.append(x.address)
272
302
  keys = set(key_grain)
273
- if not pkeys:
303
+ if is_metric:
304
+ purpose = Purpose.METRIC
305
+ elif not pkeys:
274
306
  purpose = Purpose.CONSTANT
275
307
  else:
276
308
  purpose = parent.output_purpose
@@ -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\_\.]*/