pytrilogy 0.0.2.11__py3-none-any.whl → 0.0.2.12__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.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.11
3
+ Version: 0.0.2.12
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=rSlG7hlvpZjtLdU_7j6ldvO5bmiafhgPB40sdcOu3j0,291
1
+ trilogy/__init__.py,sha256=qXNp3R3OFRd_QUIgMqKJ4RGQoBEMVcm6s2DHSehlVWU,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=HRQq4i3cpSEJCywt61QKEzRO1jd4tEPZNSBuxUA_7yg,922
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -8,50 +8,50 @@ 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=WwQPOLfSdL27qhqXw4JTkMyUNvj57T3Xz9M3JUZzhZ8,5940
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=kJcSNv6JX79KBEDTZlIyc1nAsn34fWUQLWZN3y7oTVs,143710
20
+ trilogy/core/optimization.py,sha256=7E-Ol51u6ZAxF56F_bzLxgRO-Hu6Yl1ZbPopZJB2tqk,7533
21
+ trilogy/core/query_processor.py,sha256=Y8C03J9PSyXQoARiMmFomYhnP13L61XjRKNOD7nIops,19520
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
32
+ trilogy/core/processing/node_generators/basic_node.py,sha256=EfCCYleCXVWeoCOUih1VtfUXewg1oyG7EdUMRQOyyMk,3135
33
33
  trilogy/core/processing/node_generators/common.py,sha256=lDBRq9X6dQ_xSwXxLLNDq2pW8D-XwAY-ylTJLMugkLw,9525
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=szquAzrIkCTXlVhAVSHt6HSJ7rw3b8lfjeO5eFIcEU8,13067
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=34eEH-denk9kkzD8FcZvxgDSMUB9K5e4lSeNpbqSt7I,14456
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=rLF7SFj_qtLh92ox-cHrtVSyjgl1aTaa7qZJdR1RDuA,8182
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=upL2pmq34vwNqzOtmb0EB22tbEZ4TTiK46J5qPq-_yw,62841
73
73
  trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
74
74
  trilogy/parsing/trilogy.lark,sha256=QNJnExOdvJyKTrQA4ffh-SGIz7rYd93kf2Ccs0m3cn4,11498
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.12.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
78
+ pytrilogy-0.0.2.12.dist-info/METADATA,sha256=i7Cd69-1p3XsssoHNy1IqbvtZaNeaKpQErvJp940JMw,7907
79
+ pytrilogy-0.0.2.12.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
80
+ pytrilogy-0.0.2.12.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
81
+ pytrilogy-0.0.2.12.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
82
+ pytrilogy-0.0.2.12.dist-info/RECORD,,
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.12"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -229,7 +229,6 @@ class ComparisonOperator(Enum):
229
229
  ILIKE = "ilike"
230
230
  CONTAINS = "contains"
231
231
  ELSE = "else"
232
- BETWEEN = "between"
233
232
 
234
233
  @classmethod
235
234
  def _missing_(cls, value):
@@ -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
 
@@ -2268,6 +2271,7 @@ class QueryDatasource(BaseModel):
2268
2271
  raise SyntaxError(
2269
2272
  f"Cannot join a datasource to itself, joining {join.left_datasource}"
2270
2273
  )
2274
+
2271
2275
  return v
2272
2276
 
2273
2277
  @field_validator("input_concepts")
@@ -2287,8 +2291,13 @@ class QueryDatasource(BaseModel):
2287
2291
  for key in ("input_concepts", "output_concepts"):
2288
2292
  if not values.get(key):
2289
2293
  continue
2294
+ concept: Concept
2290
2295
  for concept in values[key]:
2291
- if concept.address not in v and CONFIG.validate_missing:
2296
+ if (
2297
+ concept.address not in v
2298
+ and not any(x in v for x in concept.pseudonyms)
2299
+ and CONFIG.validate_missing
2300
+ ):
2292
2301
  raise SyntaxError(
2293
2302
  f"Missing source map for {concept.address} on {key}, have {v}"
2294
2303
  )
@@ -2533,7 +2542,7 @@ class CTE(BaseModel):
2533
2542
  )
2534
2543
  ]
2535
2544
  for join in self.joins:
2536
- if isinstance(join, UnnestJoin) and join.concept == concept:
2545
+ if isinstance(join, UnnestJoin) and concept in join.concepts:
2537
2546
  join.rendering_required = False
2538
2547
 
2539
2548
  self.parent_ctes = [
@@ -2996,8 +3005,8 @@ class EnvironmentDatasourceDict(dict):
2996
3005
  except KeyError:
2997
3006
  if DEFAULT_NAMESPACE + "." + key in self:
2998
3007
  return self.__getitem__(DEFAULT_NAMESPACE + "." + key)
2999
- if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
3000
- return self.__getitem__(key.split(".")[1])
3008
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
3009
+ return self.__getitem__(key.split(".", 1)[1])
3001
3010
  raise
3002
3011
 
3003
3012
  def values(self) -> ValuesView[Datasource]: # type: ignore
@@ -3027,8 +3036,8 @@ class EnvironmentConceptDict(dict):
3027
3036
  return super(EnvironmentConceptDict, self).__getitem__(key)
3028
3037
 
3029
3038
  except KeyError:
3030
- if "." in key and key.split(".")[0] == DEFAULT_NAMESPACE:
3031
- return self.__getitem__(key.split(".")[1], line_no)
3039
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
3040
+ return self.__getitem__(key.split(".", 1)[1], line_no)
3032
3041
  if DEFAULT_NAMESPACE + "." + key in self:
3033
3042
  return self.__getitem__(DEFAULT_NAMESPACE + "." + key, line_no)
3034
3043
  if not self.fail_on_missing:
@@ -3293,10 +3302,9 @@ class Environment(BaseModel):
3293
3302
  self.concepts[concept.name] = concept
3294
3303
  else:
3295
3304
  self.concepts[concept.address] = concept
3296
- if add_derived:
3297
- from trilogy.core.environment_helpers import generate_related_concepts
3305
+ from trilogy.core.environment_helpers import generate_related_concepts
3298
3306
 
3299
- generate_related_concepts(concept, self)
3307
+ generate_related_concepts(concept, self, meta=meta, add_derived=add_derived)
3300
3308
  self.gen_concept_list_caches()
3301
3309
  return concept
3302
3310
 
@@ -3423,14 +3431,6 @@ class Comparison(
3423
3431
  raise SyntaxError(
3424
3432
  f"Cannot compare {self.left} and {self.right} of different types"
3425
3433
  )
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
3434
 
3435
3435
  def __add__(self, other):
3436
3436
  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]}"
@@ -10,6 +10,7 @@ from trilogy.core.processing.node_generators.common import (
10
10
  )
11
11
  from trilogy.utility import unique
12
12
  from trilogy.constants import logger
13
+ from itertools import combinations
13
14
 
14
15
  LOGGER_PREFIX = "[GEN_BASIC_NODE]"
15
16
 
@@ -31,12 +32,17 @@ def gen_basic_node(
31
32
  )
32
33
 
33
34
  local_optional_redundant = [x for x in local_optional if x in parent_concepts]
34
- attempts = [(parent_concepts, [concept] + local_optional_redundant)]
35
- from itertools import combinations
35
+ attempts: List[tuple[list[Concept], list[Concept]]] = [
36
+ (parent_concepts, [concept] + local_optional_redundant)
37
+ ]
38
+ equivalent_optional = [x for x in local_optional if x.lineage == concept.lineage]
39
+ non_equivalent_optional = [
40
+ x for x in local_optional if x not in equivalent_optional
41
+ ]
36
42
 
37
43
  if local_optional:
38
- for combo in range(1, len(local_optional) + 1):
39
- combos = combinations(local_optional, combo)
44
+ for combo in range(1, len(non_equivalent_optional) + 1):
45
+ combos = combinations(non_equivalent_optional, combo)
40
46
  for optional_set in combos:
41
47
  attempts.append(
42
48
  (
@@ -64,13 +70,10 @@ def gen_basic_node(
64
70
  continue
65
71
  if all(x in source.partial_concepts for source in sources):
66
72
  partials.append(x)
67
- outputs = parent_node.output_concepts + [concept]
68
- logger.info(
69
- f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}, output {[x.address for x in outputs]}"
70
- )
71
- # parents.resolve()
72
73
 
73
74
  parent_node.add_output_concept(concept)
75
+ for x in equivalent_optional:
76
+ parent_node.add_output_concept(x)
74
77
 
75
78
  parent_node.remove_output_concepts(
76
79
  [
@@ -79,6 +82,9 @@ def gen_basic_node(
79
82
  if x.address not in [y.address for y in basic_output]
80
83
  ]
81
84
  )
85
+ logger.info(
86
+ f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}, output {[x.address for x in parent_node.output_concepts]}"
87
+ )
82
88
  return parent_node
83
89
  logger.info(
84
90
  f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
@@ -86,7 +86,7 @@ def determine_induced_minimal_nodes(
86
86
 
87
87
  for node in G.nodes:
88
88
  if concepts.get(node):
89
- lookup = concepts[node]
89
+ lookup: Concept = concepts[node]
90
90
  if lookup.derivation not in (PurposeLineage.BASIC, PurposeLineage.ROOT):
91
91
  nodes_to_remove.append(node)
92
92
  elif lookup.derivation == PurposeLineage.BASIC and G.out_degree(node) == 0:
@@ -155,6 +155,26 @@ def detect_ambiguity_and_raise(all_concepts, reduced_concept_sets) -> None:
155
155
  )
156
156
 
157
157
 
158
+ def has_synonym(concept: Concept, others: list[list[Concept]]) -> bool:
159
+ return any(
160
+ c.address in concept.pseudonyms or concept.address in c.pseudonyms
161
+ for sublist in others
162
+ for c in sublist
163
+ )
164
+
165
+
166
+ def filter_relevant_subgraphs(subgraphs: list[list[Concept]]) -> list[list[Concept]]:
167
+ return [
168
+ subgraph
169
+ for subgraph in subgraphs
170
+ if len(subgraph) > 1
171
+ or (
172
+ len(subgraph) == 1
173
+ and not has_synonym(subgraph[0], [x for x in subgraphs if x != subgraph])
174
+ )
175
+ ]
176
+
177
+
158
178
  def resolve_weak_components(
159
179
  all_concepts: List[Concept],
160
180
  environment: Environment,
@@ -249,6 +269,7 @@ def resolve_weak_components(
249
269
  continue
250
270
  subgraphs.append(sub_component)
251
271
  return subgraphs
272
+ # return filter_relevant_subgraphs(subgraphs)
252
273
 
253
274
 
254
275
  def subgraphs_to_merge_node(