pytrilogy 0.0.2.11__py3-none-any.whl → 0.0.2.13__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.

Files changed (31) hide show
  1. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/RECORD +31 -31
  3. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/WHEEL +1 -1
  4. trilogy/__init__.py +1 -1
  5. trilogy/constants.py +5 -0
  6. trilogy/core/enums.py +3 -1
  7. trilogy/core/environment_helpers.py +44 -6
  8. trilogy/core/models.py +51 -27
  9. trilogy/core/optimization.py +31 -3
  10. trilogy/core/optimizations/__init__.py +2 -1
  11. trilogy/core/optimizations/predicate_pushdown.py +60 -42
  12. trilogy/core/processing/concept_strategies_v3.py +6 -4
  13. trilogy/core/processing/node_generators/basic_node.py +22 -9
  14. trilogy/core/processing/node_generators/common.py +13 -23
  15. trilogy/core/processing/node_generators/node_merge_node.py +22 -1
  16. trilogy/core/processing/node_generators/unnest_node.py +10 -3
  17. trilogy/core/processing/nodes/base_node.py +18 -11
  18. trilogy/core/processing/nodes/group_node.py +0 -1
  19. trilogy/core/processing/nodes/merge_node.py +12 -5
  20. trilogy/core/processing/nodes/unnest_node.py +13 -9
  21. trilogy/core/processing/utility.py +3 -1
  22. trilogy/core/query_processor.py +14 -12
  23. trilogy/dialect/base.py +95 -52
  24. trilogy/dialect/common.py +3 -3
  25. trilogy/executor.py +8 -2
  26. trilogy/parsing/common.py +73 -2
  27. trilogy/parsing/parse_engine.py +88 -132
  28. trilogy/parsing/trilogy.lark +3 -3
  29. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/LICENSE.md +0 -0
  30. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/entry_points.txt +0 -0
  31. {pytrilogy-0.0.2.11.dist-info → pytrilogy-0.0.2.13.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.11
3
+ Version: 0.0.2.13
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,57 +1,57 @@
1
- trilogy/__init__.py,sha256=rSlG7hlvpZjtLdU_7j6ldvO5bmiafhgPB40sdcOu3j0,291
1
+ trilogy/__init__.py,sha256=U1mVKIIGVmjHu6kwRMyNhb3buZHWWzbk-c9Hu7FiBGQ,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- trilogy/constants.py,sha256=HRQq4i3cpSEJCywt61QKEzRO1jd4tEPZNSBuxUA_7yg,922
3
+ trilogy/constants.py,sha256=rZJh3fAx3ljxf_QZNECR-devR6QXkYc9mpLCxIWNqB0,960
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
5
- trilogy/executor.py,sha256=5cRbU4Rj7p1pNV76rfp1pz704Hx_0q8_O8HFURjgXxQ,11016
5
+ trilogy/executor.py,sha256=PZr7IF8wS1Oi2WJGE-B3lp70Y8ue2uuauODw02chjdQ,11175
6
6
  trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
7
7
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
11
- trilogy/core/enums.py,sha256=D5HUKiknRcx_OBoX21IAM0JFQ-vQNBgad2GOvJfXi3o,5964
11
+ trilogy/core/enums.py,sha256=BRYqy-NgIacCYTJo0B11m5XQWSHq5pfxhoLd8pzA3ho,6025
12
12
  trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
13
- trilogy/core/environment_helpers.py,sha256=mzBDHhdF9ssZ_-LY8CcaM_ddfJavkpRYrFImUd3cjXI,5972
13
+ trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
14
14
  trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
15
15
  trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
16
16
  trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
17
17
  trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=rIeYw173UjfTfc7194y-jqv7u51Uy6LufwbiwKI2Cgs,143834
20
- trilogy/core/optimization.py,sha256=RJmDr2f9hyFePF-B6LpmHkj69-AzUm-nNtvO59899O8,6601
21
- trilogy/core/query_processor.py,sha256=50FHJ9Rw89ZNBflZK8R9jn7O_WdDHplS1hZSNXCozN8,19352
22
- trilogy/core/optimizations/__init__.py,sha256=pxRzNzd2g8oRMy4f_ub5va6bNS2pd4hnyp9JBzTKc1E,300
19
+ trilogy/core/models.py,sha256=9-1kYfq4NqAthe50Jgs0WjBnZtEWJw0ReCBfFhVxD0Y,144799
20
+ trilogy/core/optimization.py,sha256=7E-Ol51u6ZAxF56F_bzLxgRO-Hu6Yl1ZbPopZJB2tqk,7533
21
+ trilogy/core/query_processor.py,sha256=JJFBVBmT5QNsJ9rSDKNJBINLm7YW7i5fjAp98H0Wcd8,19281
22
+ trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
23
23
  trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
24
24
  trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
25
25
  trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
26
- trilogy/core/optimizations/predicate_pushdown.py,sha256=4Y6zfJN3VVexkD6p9IYWN4BTL8RwW6hyNb0VQZ8ETdI,8020
26
+ trilogy/core/optimizations/predicate_pushdown.py,sha256=3hSS1i1itR5lEmLwebuhz4FiPIyFjr2pBeydZfHHPSk,8433
27
27
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- trilogy/core/processing/concept_strategies_v3.py,sha256=5yZQRWlEyNR2sddq7kx12_PRUNhekEu6bWO2yIvjjUA,25511
28
+ trilogy/core/processing/concept_strategies_v3.py,sha256=ae6FmwiKNiEbOU2GhnzggFMh82MhqxBj9bgr0ituT2w,25633
29
29
  trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
30
- trilogy/core/processing/utility.py,sha256=15Qi68ktmnQmMt5jxrpueQ34oG5fSSgst3kIEejwF8A,14519
30
+ trilogy/core/processing/utility.py,sha256=QKaZL5yJzGJBWCirgB1cAKgcDOibhyk7ETvHveb3GOE,14604
31
31
  trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
32
- trilogy/core/processing/node_generators/basic_node.py,sha256=4242PNGTCm2tklqMIkqVu5Iv4m_IeTnOYXxDveuCDZM,2856
33
- trilogy/core/processing/node_generators/common.py,sha256=lDBRq9X6dQ_xSwXxLLNDq2pW8D-XwAY-ylTJLMugkLw,9525
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=IHj5jEloUe5yojGRLAzt35FcfHqGviWQdS8ETyvr39Q,3292
33
+ trilogy/core/processing/node_generators/common.py,sha256=WY41zjxSCG13n3mdUHmcE0mUQ0gtWxz4nugBgxHONd8,9327
34
34
  trilogy/core/processing/node_generators/filter_node.py,sha256=Ij2WqyOsu-TFxhAcL50PLMGpghsSWXJnWEJ8yTqOwrY,8228
35
35
  trilogy/core/processing/node_generators/group_node.py,sha256=Du-9uFXD0M-aHq2MV7v5R3QCrAL0JZBFMW-YQwgb6Bw,3135
36
36
  trilogy/core/processing/node_generators/group_to_node.py,sha256=nzITnhaALIT7FMonyo16nNo-kSrLfefa9sZBYecrvkU,2887
37
37
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=vP84dnLQy6dtypi6mUbt9sMAcmmrTgQ1Oz4GI6X1IEo,6421
38
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=qS8VKppi10NKfqNb7BXTKyLBBn4wJuMmFYhrtMUkYb8,12580
38
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=D_jsnfoLMrQc08_JvT0wEDvjyzJAxBpdcZFyDN-feV0,13192
39
39
  trilogy/core/processing/node_generators/rowset_node.py,sha256=6KVnuk75mRzWJ-jIk7e8azN8BIPPuCn-VxPlxDqfPVE,4616
40
40
  trilogy/core/processing/node_generators/select_node.py,sha256=E8bKOAUpwLwZy1iiaFVD5sM4XK-eFpHgijdyIWLMyH4,18904
41
- trilogy/core/processing/node_generators/unnest_node.py,sha256=gHjurhr86JFkbq6vxTHDQGDKt95EdotSFHVKgrR_Z3Q,1860
41
+ trilogy/core/processing/node_generators/unnest_node.py,sha256=aZeixbOzMtXi7BPahKr9bOkIhTciyD9Klsj0kZ56F6s,2189
42
42
  trilogy/core/processing/node_generators/window_node.py,sha256=lFfmEjX_mLB7MuOM6CuKNnks1CabokGImpwhbQzjnkE,3283
43
43
  trilogy/core/processing/nodes/__init__.py,sha256=jyduHk96j5fpju72sc8swOiBjR3Md866kt8JZGkp3ZU,4866
44
- trilogy/core/processing/nodes/base_node.py,sha256=KVLAPLlo5QQq9Cm6lTLsqLN7l8EyXGxQkKrZFHdyRp8,12871
44
+ trilogy/core/processing/nodes/base_node.py,sha256=P4VyOhbgPgTLaXEIftbVVmmEPviScJJhi9v6hSxjC7M,13155
45
45
  trilogy/core/processing/nodes/filter_node.py,sha256=DBOSGFfkiILrZa1BlLv2uxUSkgWtSIKiZplqyKXPjg8,2132
46
- trilogy/core/processing/nodes/group_node.py,sha256=RKqODYxbY8wmClvI8-k3UgXWL7qQv5Hxhz5AB-RVUu0,6344
47
- trilogy/core/processing/nodes/merge_node.py,sha256=ZUcFIm5v4hip_Ml_oJXqVMrevUkE62wvPrvCrO9FWAc,14272
46
+ trilogy/core/processing/nodes/group_node.py,sha256=wE6tgyCUL74v76O8jACDm4oYMov4dAlwzLa5xMYReAA,6294
47
+ trilogy/core/processing/nodes/merge_node.py,sha256=KOTdYli_T3c5RwMaK_73W5UNtKPA0F8hjDJF9jQx2fs,14491
48
48
  trilogy/core/processing/nodes/select_node_v2.py,sha256=QuXNcwgjTRYamOoIooGrp4ie6INcqA9whtC5LZWjD8s,7180
49
- trilogy/core/processing/nodes/unnest_node.py,sha256=JFtm90IVM-46aCYkTNIaJah6v9ApAfonjVhcVM1HmDE,1903
49
+ trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
50
50
  trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
51
51
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- trilogy/dialect/base.py,sha256=xbZXcWvwdRadyb77J2qat67R_gRAmKeeeI4JRH4P928,30726
52
+ trilogy/dialect/base.py,sha256=kQek_ufZC9HDVOKlWasvIx6xyew8wv3JNIU6r53_IR4,32842
53
53
  trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
54
- trilogy/dialect/common.py,sha256=HVNPL8dGyQjT2REruV5C2YPpBbVR7KlD0akVM7GcuPI,3329
54
+ trilogy/dialect/common.py,sha256=LnOtsq4vUTTKB5QUk594QSoNDfOoOF08KQJZZamou84,3359
55
55
  trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
56
56
  trilogy/dialect/duckdb.py,sha256=u_gpL35kouWxoBLas1h0ABYY2QzlVtEh22hm5h0lCOM,3182
57
57
  trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
@@ -65,18 +65,18 @@ trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2
65
65
  trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
66
66
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- trilogy/parsing/common.py,sha256=zNd5buKxK4z9WSszOk4zOI0GexDFukzZtdNfQxg3kVw,6218
68
+ trilogy/parsing/common.py,sha256=fa3R0xHXQkU8BUN8quPe7qCzez6qaRbnctkYaLgHLxY,8863
69
69
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
70
70
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
71
71
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
72
- trilogy/parsing/parse_engine.py,sha256=riXOhMqYGvzyy1aeTZTjPIz3jOszue2VTEH7hn59zD4,63808
72
+ trilogy/parsing/parse_engine.py,sha256=yCHd6RVRijNctZJZ3iRJG2263UZL2n8EoFA_-Qfr88E,62966
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
- trilogy/parsing/trilogy.lark,sha256=QNJnExOdvJyKTrQA4ffh-SGIz7rYd93kf2Ccs0m3cn4,11498
74
+ trilogy/parsing/trilogy.lark,sha256=00j0D77gpTBsyet9WGN4Ir9Nc-YvNwXFr2toRiASb_M,11525
75
75
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
77
- pytrilogy-0.0.2.11.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
- pytrilogy-0.0.2.11.dist-info/METADATA,sha256=mZ6V2gwihh9nv-bIGkimK2-bKNvsY_3JIUSNzP_m2Wg,7907
79
- pytrilogy-0.0.2.11.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
80
- pytrilogy-0.0.2.11.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
- pytrilogy-0.0.2.11.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
- pytrilogy-0.0.2.11.dist-info/RECORD,,
77
+ pytrilogy-0.0.2.13.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.13.dist-info/METADATA,sha256=AJXWyDVSN9EhYPYJlrkXhMVULqg3HyscbZGDhgVd-FA,7907
79
+ pytrilogy-0.0.2.13.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
80
+ pytrilogy-0.0.2.13.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.13.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.13.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.2)
2
+ Generator: setuptools (75.1.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.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.11"
7
+ __version__ = "0.0.2.13"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/constants.py CHANGED
@@ -28,6 +28,11 @@ class Optimizations:
28
28
  direct_return: bool = True
29
29
 
30
30
 
31
+ @dataclass
32
+ class Comments:
33
+ pass
34
+
35
+
31
36
  # TODO: support loading from environments
32
37
  @dataclass
33
38
  class Config:
trilogy/core/enums.py CHANGED
@@ -62,6 +62,8 @@ class Modifier(Enum):
62
62
  strval = str(value)
63
63
  if strval == "~":
64
64
  return Modifier.PARTIAL
65
+ elif strval == "?":
66
+ return Modifier.NULLABLE
65
67
  return super()._missing_(value=strval.capitalize())
66
68
 
67
69
 
@@ -229,7 +231,6 @@ class ComparisonOperator(Enum):
229
231
  ILIKE = "ilike"
230
232
  CONTAINS = "contains"
231
233
  ELSE = "else"
232
- BETWEEN = "between"
233
234
 
234
235
  @classmethod
235
236
  def _missing_(cls, value):
@@ -274,6 +275,7 @@ class SourceType(Enum):
274
275
  CONSTANT = "constant"
275
276
  ROWSET = "rowset"
276
277
  MERGE = "merge"
278
+ BASIC = "basic"
277
279
 
278
280
 
279
281
  class ShowCategory(Enum):
@@ -1,6 +1,15 @@
1
- from trilogy.core.models import DataType, Concept, Environment, Function, Metadata
1
+ from trilogy.core.models import (
2
+ DataType,
3
+ Concept,
4
+ Environment,
5
+ Function,
6
+ Metadata,
7
+ StructType,
8
+ )
9
+ from trilogy.core.functions import AttrAccess
2
10
  from trilogy.core.enums import Purpose, FunctionType, ConceptSource
3
11
  from trilogy.constants import DEFAULT_NAMESPACE
12
+ from trilogy.parsing.common import process_function_args, arg_to_datatype, Meta
4
13
 
5
14
 
6
15
  def generate_date_concepts(concept: Concept, environment: Environment):
@@ -142,15 +151,44 @@ def generate_key_concepts(concept: Concept, environment: Environment):
142
151
  environment.add_concept(new_concept, add_derived=False)
143
152
 
144
153
 
145
- def generate_related_concepts(concept: Concept, environment: Environment):
154
+ def generate_related_concepts(
155
+ concept: Concept,
156
+ environment: Environment,
157
+ meta: Meta | None = None,
158
+ add_derived: bool = False,
159
+ ):
146
160
  """Auto populate common derived concepts on types"""
147
- if concept.purpose == Purpose.KEY:
161
+ if concept.purpose == Purpose.KEY and add_derived:
148
162
  generate_key_concepts(concept, environment)
149
- if concept.datatype == DataType.DATE:
163
+
164
+ # datatype types
165
+ if concept.datatype == DataType.DATE and add_derived:
150
166
  generate_date_concepts(concept, environment)
151
- elif concept.datatype == DataType.DATETIME:
167
+ elif concept.datatype == DataType.DATETIME and add_derived:
152
168
  generate_date_concepts(concept, environment)
153
169
  generate_datetime_concepts(concept, environment)
154
- elif concept.datatype == DataType.TIMESTAMP:
170
+ elif concept.datatype == DataType.TIMESTAMP and add_derived:
155
171
  generate_date_concepts(concept, environment)
156
172
  generate_datetime_concepts(concept, environment)
173
+
174
+ if isinstance(concept.datatype, StructType):
175
+ for key, value in concept.datatype.fields_map.items():
176
+ args = process_function_args(
177
+ [concept, key], meta=meta, environment=environment
178
+ )
179
+ auto = Concept(
180
+ name=key,
181
+ datatype=arg_to_datatype(value),
182
+ purpose=Purpose.PROPERTY,
183
+ namespace=(
184
+ environment.namespace + "." + concept.name
185
+ if environment.namespace
186
+ and environment.namespace != DEFAULT_NAMESPACE
187
+ else concept.name
188
+ ),
189
+ lineage=AttrAccess(args),
190
+ )
191
+ environment.add_concept(auto, meta=meta)
192
+ if isinstance(value, Concept):
193
+ environment.merge_concept(auto, value, modifiers=[])
194
+ assert value.pseudonyms is not None
trilogy/core/models.py CHANGED
@@ -300,7 +300,7 @@ class MapType(BaseModel):
300
300
 
301
301
  class StructType(BaseModel):
302
302
  fields: List[ALL_TYPES]
303
- fields_map: Dict[str, Concept | int | float | str] = Field(default_factory=dict)
303
+ fields_map: Dict[str, Concept | int | float | str]
304
304
 
305
305
  @property
306
306
  def data_type(self):
@@ -2119,16 +2119,19 @@ class Datasource(Namespaced, BaseModel):
2119
2119
 
2120
2120
 
2121
2121
  class UnnestJoin(BaseModel):
2122
- concept: Concept
2122
+ concepts: list[Concept]
2123
+ parent: Function
2123
2124
  alias: str = "unnest"
2124
2125
  rendering_required: bool = True
2125
2126
 
2126
2127
  def __hash__(self):
2127
- return (self.alias + self.concept.address).__hash__()
2128
+ return (
2129
+ self.alias + "".join([str(s.address) for s in self.concepts])
2130
+ ).__hash__()
2128
2131
 
2129
2132
 
2130
2133
  class InstantiatedUnnestJoin(BaseModel):
2131
- concept: Concept
2134
+ concept_to_unnest: Concept
2132
2135
  alias: str = "unnest"
2133
2136
 
2134
2137
 
@@ -2261,6 +2264,7 @@ class QueryDatasource(BaseModel):
2261
2264
  @field_validator("joins")
2262
2265
  @classmethod
2263
2266
  def validate_joins(cls, v):
2267
+ unique_pairs = set()
2264
2268
  for join in v:
2265
2269
  if not isinstance(join, BaseJoin):
2266
2270
  continue
@@ -2268,6 +2272,16 @@ class QueryDatasource(BaseModel):
2268
2272
  raise SyntaxError(
2269
2273
  f"Cannot join a datasource to itself, joining {join.left_datasource}"
2270
2274
  )
2275
+ pairing = "".join(
2276
+ sorted(
2277
+ [join.left_datasource.identifier, join.right_datasource.identifier]
2278
+ )
2279
+ )
2280
+ if pairing in unique_pairs:
2281
+ raise SyntaxError(
2282
+ f"Duplicate join {join.left_datasource.identifier} and {join.right_datasource.identifier}"
2283
+ )
2284
+ unique_pairs.add(pairing)
2271
2285
  return v
2272
2286
 
2273
2287
  @field_validator("input_concepts")
@@ -2287,8 +2301,13 @@ class QueryDatasource(BaseModel):
2287
2301
  for key in ("input_concepts", "output_concepts"):
2288
2302
  if not values.get(key):
2289
2303
  continue
2304
+ concept: Concept
2290
2305
  for concept in values[key]:
2291
- if concept.address not in v and CONFIG.validate_missing:
2306
+ if (
2307
+ concept.address not in v
2308
+ and not any(x in v for x in concept.pseudonyms)
2309
+ and CONFIG.validate_missing
2310
+ ):
2292
2311
  raise SyntaxError(
2293
2312
  f"Missing source map for {concept.address} on {key}, have {v}"
2294
2313
  )
@@ -2377,6 +2396,11 @@ class QueryDatasource(BaseModel):
2377
2396
  final_source_map[key] = other.source_map[key]
2378
2397
  for k, v in final_source_map.items():
2379
2398
  final_source_map[k] = set(merged_datasources[x.full_name] for x in list(v))
2399
+ self_hidden = self.hidden_concepts or []
2400
+ other_hidden = other.hidden_concepts or []
2401
+ hidden = [
2402
+ x for x in self_hidden if x.address in [y.address for y in other_hidden]
2403
+ ]
2380
2404
  qds = QueryDatasource(
2381
2405
  input_concepts=unique(
2382
2406
  self.input_concepts + other.input_concepts, "address"
@@ -2400,9 +2424,7 @@ class QueryDatasource(BaseModel):
2400
2424
  ),
2401
2425
  join_derived_concepts=self.join_derived_concepts,
2402
2426
  force_group=self.force_group,
2403
- hidden_concepts=unique(
2404
- self.hidden_concepts + other.hidden_concepts, "address"
2405
- ),
2427
+ hidden_concepts=hidden,
2406
2428
  )
2407
2429
 
2408
2430
  return qds
@@ -2533,7 +2555,7 @@ class CTE(BaseModel):
2533
2555
  )
2534
2556
  ]
2535
2557
  for join in self.joins:
2536
- if isinstance(join, UnnestJoin) and join.concept == concept:
2558
+ if isinstance(join, UnnestJoin) and concept in join.concepts:
2537
2559
  join.rendering_required = False
2538
2560
 
2539
2561
  self.parent_ctes = [
@@ -2548,6 +2570,7 @@ class CTE(BaseModel):
2548
2570
  @property
2549
2571
  def comment(self) -> str:
2550
2572
  base = f"Target: {str(self.grain)}."
2573
+ base += f" Source: {self.source.source_type}."
2551
2574
  if self.parent_ctes:
2552
2575
  base += f" References: {', '.join([x.name for x in self.parent_ctes])}."
2553
2576
  if self.joins:
@@ -2556,6 +2579,11 @@ class CTE(BaseModel):
2556
2579
  base += (
2557
2580
  f"\n-- Partials: {', '.join([str(x) for x in self.partial_concepts])}."
2558
2581
  )
2582
+ base += f"\n-- Source Map: {self.source_map}."
2583
+ base += f"\n-- Output: {', '.join([str(x) for x in self.output_columns])}."
2584
+ if self.hidden_concepts:
2585
+ base += f"\n-- Hidden: {', '.join([str(x) for x in self.hidden_concepts])}."
2586
+
2559
2587
  return base
2560
2588
 
2561
2589
  def inline_parent_datasource(self, parent: CTE, force_group: bool = False) -> bool:
@@ -2623,6 +2651,10 @@ class CTE(BaseModel):
2623
2651
  f" {self.name} {other.name} conditions {self.condition} {other.condition}"
2624
2652
  )
2625
2653
  raise ValueError(error)
2654
+ mutually_hidden = []
2655
+ for concept in self.hidden_concepts:
2656
+ if concept in other.hidden_concepts:
2657
+ mutually_hidden.append(concept)
2626
2658
  self.partial_concepts = unique(
2627
2659
  self.partial_concepts + other.partial_concepts, "address"
2628
2660
  )
@@ -2645,9 +2677,7 @@ class CTE(BaseModel):
2645
2677
  self.source.output_concepts = unique(
2646
2678
  self.source.output_concepts + other.source.output_concepts, "address"
2647
2679
  )
2648
- self.hidden_concepts = unique(
2649
- self.hidden_concepts + other.hidden_concepts, "address"
2650
- )
2680
+ self.hidden_concepts = mutually_hidden
2651
2681
  self.existence_source_map = {
2652
2682
  **self.existence_source_map,
2653
2683
  **other.existence_source_map,
@@ -2996,13 +3026,16 @@ class EnvironmentDatasourceDict(dict):
2996
3026
  except KeyError:
2997
3027
  if DEFAULT_NAMESPACE + "." + key in self:
2998
3028
  return self.__getitem__(DEFAULT_NAMESPACE + "." + key)
2999
- if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
3000
- return self.__getitem__(key.split(".")[1])
3029
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
3030
+ return self.__getitem__(key.split(".", 1)[1])
3001
3031
  raise
3002
3032
 
3003
3033
  def values(self) -> ValuesView[Datasource]: # type: ignore
3004
3034
  return super().values()
3005
3035
 
3036
+ def items(self) -> ItemsView[str, Datasource]: # type: ignore
3037
+ return super().items()
3038
+
3006
3039
 
3007
3040
  class EnvironmentConceptDict(dict):
3008
3041
  def __init__(self, *args, **kwargs) -> None:
@@ -3027,8 +3060,8 @@ class EnvironmentConceptDict(dict):
3027
3060
  return super(EnvironmentConceptDict, self).__getitem__(key)
3028
3061
 
3029
3062
  except KeyError:
3030
- if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
3031
- return self.__getitem__(key.split(".")[1], line_no)
3063
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
3064
+ return self.__getitem__(key.split(".", 1)[1], line_no)
3032
3065
  if DEFAULT_NAMESPACE + "." + key in self:
3033
3066
  return self.__getitem__(DEFAULT_NAMESPACE + "." + key, line_no)
3034
3067
  if not self.fail_on_missing:
@@ -3293,10 +3326,9 @@ class Environment(BaseModel):
3293
3326
  self.concepts[concept.name] = concept
3294
3327
  else:
3295
3328
  self.concepts[concept.address] = concept
3296
- if add_derived:
3297
- from trilogy.core.environment_helpers import generate_related_concepts
3329
+ from trilogy.core.environment_helpers import generate_related_concepts
3298
3330
 
3299
- generate_related_concepts(concept, self)
3331
+ generate_related_concepts(concept, self, meta=meta, add_derived=add_derived)
3300
3332
  self.gen_concept_list_caches()
3301
3333
  return concept
3302
3334
 
@@ -3423,14 +3455,6 @@ class Comparison(
3423
3455
  raise SyntaxError(
3424
3456
  f"Cannot compare {self.left} and {self.right} of different types"
3425
3457
  )
3426
- if self.operator == ComparisonOperator.BETWEEN:
3427
- if (
3428
- not isinstance(self.right, ComparisonOperator)
3429
- and self.right.operator == BooleanOperator.AND
3430
- ):
3431
- raise SyntaxError(
3432
- f"Between operator must have two operands with and, not {self.right}"
3433
- )
3434
3458
 
3435
3459
  def __add__(self, other):
3436
3460
  if other is None:
@@ -10,6 +10,7 @@ from trilogy.core.optimizations import (
10
10
  OptimizationRule,
11
11
  InlineConstant,
12
12
  PredicatePushdown,
13
+ PredicatePushdownRemove,
13
14
  InlineDatasource,
14
15
  )
15
16
 
@@ -34,6 +35,31 @@ MAX_OPTIMIZATION_LOOPS = 100
34
35
  # return parent
35
36
 
36
37
 
38
+ def reorder_ctes(
39
+ input: list[CTE],
40
+ ):
41
+ import networkx as nx
42
+
43
+ # Create a directed graph
44
+ G = nx.DiGraph()
45
+ mapping: dict[str, CTE] = {}
46
+ for cte in input:
47
+ mapping[cte.name] = cte
48
+ for parent in cte.parent_ctes:
49
+ G.add_edge(parent.name, cte.name)
50
+ # Perform topological sort (only works for DAGs)
51
+ try:
52
+ topological_order = list(nx.topological_sort(G))
53
+ if not topological_order:
54
+ return input
55
+ return [mapping[x] for x in topological_order]
56
+ except nx.NetworkXUnfeasible as e:
57
+ print(
58
+ "The graph is not a DAG (contains cycles) and cannot be topologically sorted."
59
+ )
60
+ raise e
61
+
62
+
37
63
  def filter_irrelevant_ctes(
38
64
  input: list[CTE],
39
65
  root_cte: CTE,
@@ -169,20 +195,22 @@ def optimize_ctes(
169
195
  REGISTERED_RULES.append(InlineDatasource())
170
196
  if CONFIG.optimizations.predicate_pushdown:
171
197
  REGISTERED_RULES.append(PredicatePushdown())
172
-
198
+ if CONFIG.optimizations.predicate_pushdown:
199
+ REGISTERED_RULES.append(PredicatePushdownRemove())
173
200
  for rule in REGISTERED_RULES:
174
201
  loops = 0
175
202
  complete = False
176
203
  while not complete and (loops <= MAX_OPTIMIZATION_LOOPS):
177
204
  actions_taken = False
178
205
  # assume we go through all CTEs once
179
- look_at = [root_cte, *input]
206
+ look_at = [root_cte, *reversed(input)]
180
207
  inverse_map = gen_inverse_map(look_at)
181
208
  for cte in look_at:
182
209
  opt = rule.optimize(cte, inverse_map)
183
210
  actions_taken = actions_taken or opt
184
211
  complete = not actions_taken
185
212
  loops += 1
213
+ input = reorder_ctes(filter_irrelevant_ctes(input, root_cte))
186
214
  logger.info(f"finished checking for {type(rule).__name__} in {loops} loops")
187
215
 
188
- return filter_irrelevant_ctes(input, root_cte)
216
+ return reorder_ctes(filter_irrelevant_ctes(input, root_cte))
@@ -1,6 +1,6 @@
1
1
  from .inline_constant import InlineConstant
2
2
  from .inline_datasource import InlineDatasource
3
- from .predicate_pushdown import PredicatePushdown
3
+ from .predicate_pushdown import PredicatePushdown, PredicatePushdownRemove
4
4
  from .base_optimization import OptimizationRule
5
5
 
6
6
  __all__ = [
@@ -8,4 +8,5 @@ __all__ = [
8
8
  "InlineConstant",
9
9
  "InlineDatasource",
10
10
  "PredicatePushdown",
11
+ "PredicatePushdownRemove",
11
12
  ]
@@ -114,48 +114,6 @@ class PredicatePushdown(OptimizationRule):
114
114
  if not cte.condition:
115
115
  self.debug(f"No CTE condition for {cte.name}")
116
116
  return False
117
-
118
- parent_filter_status = {
119
- parent.name: is_child_of(cte.condition, parent.condition)
120
- for parent in cte.parent_ctes
121
- }
122
- # flatten existnce argument tuples to a list
123
-
124
- flattened_existence = [
125
- x.address for y in cte.condition.existence_arguments for x in y
126
- ]
127
-
128
- existence_only = [
129
- parent.name
130
- for parent in cte.parent_ctes
131
- if all([x.address in flattened_existence for x in parent.output_columns])
132
- and len(flattened_existence) > 0
133
- ]
134
- if all(
135
- [
136
- value
137
- for key, value in parent_filter_status.items()
138
- if key not in existence_only
139
- ]
140
- ) and not any([isinstance(x, Datasource) for x in cte.source.datasources]):
141
- self.log(
142
- f"All parents of {cte.name} have same filter or are existence only inputs, removing filter from {cte.name}"
143
- )
144
- cte.condition = None
145
- # remove any "parent" CTEs that provided only existence inputs
146
- if existence_only:
147
- original = [y.name for y in cte.parent_ctes]
148
- cte.parent_ctes = [
149
- x for x in cte.parent_ctes if x.name not in existence_only
150
- ]
151
- self.log(
152
- f"new parents for {cte.name} are {[x.name for x in cte.parent_ctes]}, vs {original}"
153
- )
154
- return True
155
- else:
156
- self.log(
157
- f"Could not remove filter from {cte.name}, as not all parents have the same filter: {parent_filter_status}"
158
- )
159
117
  if self.complete.get(cte.name):
160
118
  self.debug("Have done this CTE before")
161
119
  return False
@@ -197,3 +155,63 @@ class PredicatePushdown(OptimizationRule):
197
155
 
198
156
  self.complete[cte.name] = True
199
157
  return optimized
158
+
159
+
160
+ class PredicatePushdownRemove(OptimizationRule):
161
+
162
+ def __init__(self, *args, **kwargs) -> None:
163
+ super().__init__(*args, **kwargs)
164
+ self.complete: dict[str, bool] = {}
165
+
166
+ def optimize(self, cte: CTE, inverse_map: dict[str, list[CTE]]) -> bool:
167
+ optimized = False
168
+
169
+ if not cte.parent_ctes:
170
+ self.debug(f"No parent CTEs for {cte.name}")
171
+
172
+ return False
173
+
174
+ if not cte.condition:
175
+ self.debug(f"No CTE condition for {cte.name}")
176
+ return False
177
+
178
+ parent_filter_status = {
179
+ parent.name: is_child_of(cte.condition, parent.condition)
180
+ for parent in cte.parent_ctes
181
+ }
182
+ # flatten existnce argument tuples to a list
183
+
184
+ flattened_existence = [
185
+ x.address for y in cte.condition.existence_arguments for x in y
186
+ ]
187
+
188
+ existence_only = [
189
+ parent.name
190
+ for parent in cte.parent_ctes
191
+ if all([x.address in flattened_existence for x in parent.output_columns])
192
+ and len(flattened_existence) > 0
193
+ ]
194
+ if all(
195
+ [
196
+ value
197
+ for key, value in parent_filter_status.items()
198
+ if key not in existence_only
199
+ ]
200
+ ) and not any([isinstance(x, Datasource) for x in cte.source.datasources]):
201
+ self.log(
202
+ f"All parents of {cte.name} have same filter or are existence only inputs, removing filter from {cte.name}"
203
+ )
204
+ cte.condition = None
205
+ # remove any "parent" CTEs that provided only existence inputs
206
+ if existence_only:
207
+ original = [y.name for y in cte.parent_ctes]
208
+ cte.parent_ctes = [
209
+ x for x in cte.parent_ctes if x.name not in existence_only
210
+ ]
211
+ self.log(
212
+ f"new parents for {cte.name} are {[x.name for x in cte.parent_ctes]}, vs {original}"
213
+ )
214
+ return True
215
+
216
+ self.complete[cte.name] = True
217
+ return optimized
@@ -180,7 +180,10 @@ def generate_candidates_restrictive(
180
180
  local_candidates = [
181
181
  x
182
182
  for x in list(candidates)
183
- if x.address not in exhausted and x.granularity != Granularity.SINGLE_ROW
183
+ if x.address not in exhausted
184
+ and x.granularity != Granularity.SINGLE_ROW
185
+ and x.address not in priority_concept.pseudonyms
186
+ and priority_concept.address not in x.pseudonyms
184
187
  ]
185
188
  combos: list[list[Concept]] = []
186
189
  grain_check = Grain(components=[*local_candidates]).components_copy
@@ -608,7 +611,7 @@ def _search_concepts(
608
611
  if len(stack) == 1:
609
612
  output = stack[0]
610
613
  logger.info(
611
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)} with output {[x.address for x in output.output_concepts]}"
614
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Source stack has single node, returning that {type(output)} with output {[x.address for x in output.output_concepts]} and {output.resolve().source_map}"
612
615
  )
613
616
  return output
614
617
 
@@ -658,8 +661,7 @@ def _search_concepts(
658
661
  if x.address not in [y.address for y in mandatory_list]
659
662
  and x not in ex_resolve.grain.components
660
663
  ]
661
- expanded.output_concepts = mandatory_list
662
- expanded.rebuild_cache()
664
+ expanded.set_output_concepts(mandatory_list)
663
665
 
664
666
  logger.info(
665
667
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Found connections for {[c.address for c in mandatory_list]} via concept addition; removing extra {[c.address for c in extra]}"