pytrilogy 0.0.3.88__py3-none-any.whl → 0.0.3.90__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.88
3
+ Version: 0.0.3.90
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,9 +1,9 @@
1
- pytrilogy-0.0.3.88.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=cl4nRewdhba3xQodaj4EqSuH_HCoF56PNkWEO79F_VY,303
1
+ pytrilogy-0.0.3.90.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=xsnAVhMdPDgMBudr3tOEYEMfxl0t6RWAK_231sFSxAU,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=eKb_EJvSqjN9tGbdVEViwdtwwh8fZ3-jpOEDqL71y70,1691
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
6
- trilogy/executor.py,sha256=iwrYs5hEaw2hTaNZOYW5Z0w6Va1RzdRpg5bn50tlslA,16731
6
+ trilogy/executor.py,sha256=tcowEz8I7zbwLnuTr7BlGJ5wnt1JKBNffXbm5ywkNv8,17032
7
7
  trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
8
8
  trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
@@ -16,17 +16,17 @@ trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0
16
16
  trilogy/core/environment_helpers.py,sha256=VvPIiFemqaLLpIpLIqprfu63K7muZ1YzNg7UZIUph8w,8267
17
17
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
18
18
  trilogy/core/exceptions.py,sha256=jYEduuMehcMkmCpf-OC_taELPZm7qNfeSNzIWkDYScs,707
19
- trilogy/core/functions.py,sha256=piMv91RiW1OAgDS5W8HIiO6fGleiNXIFW0YnNZgYOWU,32943
19
+ trilogy/core/functions.py,sha256=hnfcNjAD-XQ572vEwuUEAdBf8zKFWYwPeHIpESjUpZs,32928
20
20
  trilogy/core/graph_models.py,sha256=BYhJzHKSgnZHVLJs1CfsgrxTPHqKqPNeA64RlozGY0A,3498
21
21
  trilogy/core/internal.py,sha256=wFx4e1I0mtx159YFShSXeUBSQ82NINtAbOI-92RX4i8,2151
22
22
  trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
23
23
  trilogy/core/query_processor.py,sha256=5aFgv-2LVM1Uku9cR_tFuTRDwyLnxc95bCMAHeFy2AY,20332
24
24
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
25
25
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- trilogy/core/models/author.py,sha256=5o0x9d3U_J2b8GPdJnMsMruC2P-xiI55lZKWUwRIeGU,78936
26
+ trilogy/core/models/author.py,sha256=tcsr42hHiQ2PHh2_le9sr5IV5nv3twxUv2EXr5iDGxg,80201
27
27
  trilogy/core/models/build.py,sha256=CyrSo4xgU-uDKW3xUVYs5cTk3Z3Z2BMWdGQNHnHZOqU,66127
28
28
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
29
- trilogy/core/models/core.py,sha256=eSzrrscwTrlO_PIn9_UxKSPBgKgR9-xp90LY1e0lhN0,11538
29
+ trilogy/core/models/core.py,sha256=NOvonI4Ip4thpz5WoJZWbbBa44PFfpd2hXGx2Cbi4CE,12521
30
30
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
31
31
  trilogy/core/models/environment.py,sha256=0IHSCFf5e5b4LPQN3vmjumtfM1iD1tN4WMoUr0UqxZI,27855
32
32
  trilogy/core/models/execute.py,sha256=sVWhjwWull-T6pUJizhrYVGCWHY3eZivVN6KNlhcHig,41839
@@ -46,7 +46,7 @@ trilogy/core/processing/node_generators/__init__.py,sha256=iVJ-crowPxYeut-hFjyEj
46
46
  trilogy/core/processing/node_generators/basic_node.py,sha256=TLZCv4WS196a-0g5xgKuJGthnGP8Ugm46iz85_3NIY4,5626
47
47
  trilogy/core/processing/node_generators/common.py,sha256=PdysdroW9DUADP7f5Wv_GKPUyCTROZV1g3L45fawxi8,9443
48
48
  trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
49
- trilogy/core/processing/node_generators/filter_node.py,sha256=oRRq2-T3ufgn4D23uQsc58f20eFk-djs4QI3WKA75K8,10908
49
+ trilogy/core/processing/node_generators/filter_node.py,sha256=ArBsQJl-4fWBJWCE28CRQ7UT7ErnFfbcseoQQZrBodY,11220
50
50
  trilogy/core/processing/node_generators/group_node.py,sha256=1QJhRxsTklJ5xq8wHlAURZaN9gL9FPpeCa1OJ7IwXnY,6769
51
51
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
52
52
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
@@ -77,8 +77,8 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
77
77
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
78
78
  trilogy/core/statements/execute.py,sha256=pfr1CZ_Cx1qQ-7LDyRI0JUfvtxBr_GGv-VeqiAjr43g,1406
79
79
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- trilogy/dialect/base.py,sha256=bkCJsJvUQLlrKzHCTbWlLikas5o-JluCVI12RSSbYuE,48228
81
- trilogy/dialect/bigquery.py,sha256=8xhEu0z_lKANjbvzvBbC7CeKrJf1iP8YyrHqNale-ug,4351
80
+ trilogy/dialect/base.py,sha256=_L5wHBHz8v4Us3tu4QIupKuaObmyhWhyDuroT95wUbo,48228
81
+ trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
82
82
  trilogy/dialect/common.py,sha256=tSthIZOXXRPQ4KeMKnDDsH7KlTmf2EVqigVtLyoc4zc,6071
83
83
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
84
84
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
@@ -98,7 +98,7 @@ trilogy/parsing/common.py,sha256=yV1AckK0h8u1OFeGQBTMu-wuW5m63c5CcZuPicsTH_w,306
98
98
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
99
99
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
100
100
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
101
- trilogy/parsing/parse_engine.py,sha256=snT7m7LWrOHcSZlqdJ5HXWmH1wMMPDaqrsf6AQsMx70,79800
101
+ trilogy/parsing/parse_engine.py,sha256=fgqCtV6sf9HrkViEjf6XXdRpPf4hJ1gSyzLXZ9sLBHs,80148
102
102
  trilogy/parsing/render.py,sha256=HSNntD82GiiwHT-TWPLXAaIMWLYVV5B5zQEsgwrHIBE,19605
103
103
  trilogy/parsing/trilogy.lark,sha256=ySzMMLxyPjn74MjFHZxXPTW-jHW68KLPJpiszPvZaO0,15780
104
104
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -111,8 +111,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
111
111
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
112
112
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
113
113
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
114
- pytrilogy-0.0.3.88.dist-info/METADATA,sha256=MJ92RHQZuHjfV6eUuy9Z_F7U3BBt4ilDIzrfkS_kzSg,9589
115
- pytrilogy-0.0.3.88.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
116
- pytrilogy-0.0.3.88.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
117
- pytrilogy-0.0.3.88.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
118
- pytrilogy-0.0.3.88.dist-info/RECORD,,
114
+ pytrilogy-0.0.3.90.dist-info/METADATA,sha256=X046-UiVgiZTCb6DQXtQuzAe9Qm4DNTefqbNXixNx5g,9589
115
+ pytrilogy-0.0.3.90.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
116
+ pytrilogy-0.0.3.90.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
117
+ pytrilogy-0.0.3.90.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
118
+ pytrilogy-0.0.3.90.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.88"
7
+ __version__ = "0.0.3.90"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/functions.py CHANGED
@@ -92,7 +92,7 @@ def get_attr_datatype(
92
92
  lookup = args[1]
93
93
  datatype = arg_to_datatype(arg)
94
94
  if isinstance(datatype, StructType):
95
- return arg_to_datatype(datatype.fields_map[lookup])
95
+ return datatype.field_types[lookup]
96
96
  return datatype
97
97
 
98
98
 
@@ -935,6 +935,7 @@ class FunctionFactory:
935
935
  output_purpose = Purpose.METRIC
936
936
  else:
937
937
  output_purpose = Purpose.PROPERTY
938
+
938
939
  return Function(
939
940
  operator=operator,
940
941
  arguments=full_args, # type: ignore
@@ -172,11 +172,16 @@ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
172
172
  for candidate in candidates:
173
173
  if not candidate.startswith(f"{source}."):
174
174
  continue
175
+ attribute = self.address.rsplit(".", 1)[1]
176
+ dtype = arg_to_datatype(target)
177
+ if not isinstance(dtype, StructType):
178
+ continue
179
+ output_type = dtype.field_types.get(attribute, DataType.UNKNOWN)
175
180
  return Function(
176
181
  arguments=[target, self.address.rsplit(".", 1)[1]],
177
182
  operator=FunctionType.ATTR_ACCESS,
178
183
  arg_count=2,
179
- output_datatype=arg_to_datatype(target),
184
+ output_datatype=output_type,
180
185
  output_purpose=Purpose.PROPERTY,
181
186
  )
182
187
  return self
@@ -654,6 +659,12 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
654
659
  def validate_comparison(self):
655
660
  left_type = arg_to_datatype(self.left)
656
661
  right_type = arg_to_datatype(self.right)
662
+ left_name = (
663
+ left_type.name if isinstance(left_type, DataType) else str(left_type)
664
+ )
665
+ right_name = (
666
+ right_type.name if isinstance(right_type, DataType) else str(right_type)
667
+ )
657
668
  if self.operator in (ComparisonOperator.IS, ComparisonOperator.IS_NOT):
658
669
  if self.right != MagicConstants.NULL and DataType.BOOL != right_type:
659
670
  raise SyntaxError(
@@ -671,12 +682,12 @@ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
671
682
  left_type, right_type
672
683
  ):
673
684
  raise SyntaxError(
674
- f"Cannot compare {left_type.name} and {right_type.name} with operator {self.operator} in {str(self)}"
685
+ f"Cannot compare {left_name} and {right_name} with operator {self.operator} in {str(self)}"
675
686
  )
676
687
  else:
677
688
  if not is_compatible_datatype(left_type, right_type):
678
689
  raise SyntaxError(
679
- f"Cannot compare {left_type.name} ({self.left}) and {right_type.name} ({self.right}) of different types with operator {self.operator.value} in {str(self)}"
690
+ f"Cannot compare {left_name} ({self.left}) and {right_name} ({self.right}) of different types with operator {self.operator.value} in {str(self)}"
680
691
  )
681
692
 
682
693
  return self
@@ -1637,6 +1648,9 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1637
1648
  ] = None
1638
1649
  arguments: Sequence[FuncArgs]
1639
1650
 
1651
+ class Config:
1652
+ frozen = True
1653
+
1640
1654
  def __repr__(self):
1641
1655
  return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
1642
1656
 
@@ -1647,6 +1661,16 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1647
1661
  def datatype(self):
1648
1662
  return self.output_datatype
1649
1663
 
1664
+ @field_validator("output_datatype")
1665
+ @classmethod
1666
+ def parse_output_datatype(cls, v, info: ValidationInfo):
1667
+ values = info.data
1668
+ if values.get("operator") == FunctionType.ATTR_ACCESS:
1669
+ print(v)
1670
+ if isinstance(v, StructType):
1671
+ raise SyntaxError
1672
+ return v
1673
+
1650
1674
  @field_validator("arguments", mode="before")
1651
1675
  @classmethod
1652
1676
  def parse_arguments(cls, v, info: ValidationInfo):
@@ -1745,8 +1769,16 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1745
1769
  ]
1746
1770
  if self.output_datatype == DataType.UNKNOWN:
1747
1771
  new_output = merge_datatypes([arg_to_datatype(x) for x in nargs])
1772
+
1773
+ if self.operator == FunctionType.ATTR_ACCESS:
1774
+ if isinstance(new_output, StructType):
1775
+ new_output = new_output.field_types[str(nargs[1])]
1748
1776
  else:
1749
1777
  new_output = self.output_datatype
1778
+ # this is not ideal - see hacky logic for datatypes above
1779
+ # we need to figure out how to patch properly
1780
+ # should use function factory, but does not have environment access
1781
+ # probably move all datatype resolution to build?
1750
1782
  return Function.model_construct(
1751
1783
  operator=self.operator,
1752
1784
  arguments=nargs,
@@ -2444,6 +2476,7 @@ FuncArgs = (
2444
2476
  | CaseElse
2445
2477
  | WindowItem
2446
2478
  | FilterItem
2479
+ | bool
2447
2480
  | int
2448
2481
  | float
2449
2482
  | DatePart
@@ -244,6 +244,19 @@ class StructType(BaseModel):
244
244
  def value(self):
245
245
  return self.data_type.value
246
246
 
247
+ @property
248
+ def field_types(self) -> Dict[str, CONCRETE_TYPES]:
249
+ out: Dict[str, CONCRETE_TYPES] = {}
250
+ keys = list(self.fields_map.keys())
251
+ for idx, field in enumerate(self.fields):
252
+ if isinstance(field, StructComponent):
253
+ out[field.name] = arg_to_datatype(field.type)
254
+ elif isinstance(field, DataTyped):
255
+ out[keys[idx]] = field.output_datatype
256
+ else:
257
+ out[keys[idx]] = field
258
+ return out
259
+
247
260
  def __hash__(self):
248
261
  return hash(str(self))
249
262
 
@@ -251,9 +264,10 @@ class StructType(BaseModel):
251
264
  class ListWrapper(Generic[VT], UserList):
252
265
  """Used to distinguish parsed list objects from other lists"""
253
266
 
254
- def __init__(self, *args, type: DataType, **kwargs):
267
+ def __init__(self, *args, type: DataType, nullable: bool = False, **kwargs):
255
268
  super().__init__(*args, **kwargs)
256
269
  self.type = type
270
+ self.nullable = nullable
257
271
 
258
272
  @classmethod
259
273
  def __get_pydantic_core_schema__(
@@ -302,10 +316,11 @@ class MapWrapper(Generic[KT, VT], UserDict):
302
316
  class TupleWrapper(Generic[VT], tuple):
303
317
  """Used to distinguish parsed tuple objects from other tuples"""
304
318
 
305
- def __init__(self, val, type: DataType, **kwargs):
319
+ def __init__(self, val, type: DataType, nullable: bool = False, **kwargs):
306
320
  super().__init__()
307
321
  self.type = type
308
322
  self.val = val
323
+ self.nullable = nullable
309
324
 
310
325
  def __getnewargs__(self):
311
326
  return (self.val, self.type)
@@ -331,15 +346,19 @@ class TupleWrapper(Generic[VT], tuple):
331
346
 
332
347
 
333
348
  def list_to_wrapper(args):
334
- types = [arg_to_datatype(arg) for arg in args]
335
- assert len(set(types)) == 1
336
- return ListWrapper(args, type=types[0])
349
+ rtypes = [arg_to_datatype(arg) for arg in args]
350
+ types = [arg for arg in rtypes if arg != DataType.NULL]
351
+ if not len(set(types)) == 1:
352
+ raise SyntaxError(f"Cannot create a list with this set of types: {set(types)}")
353
+ return ListWrapper(args, type=types[0], nullable=DataType.NULL in rtypes)
337
354
 
338
355
 
339
356
  def tuple_to_wrapper(args):
340
- types = [arg_to_datatype(arg) for arg in args]
341
- assert len(set(types)) == 1
342
- return TupleWrapper(args, type=types[0])
357
+ rtypes = [arg_to_datatype(arg) for arg in args]
358
+ types = [arg for arg in rtypes if arg != DataType.NULL]
359
+ if not len(set(types)) == 1:
360
+ raise SyntaxError(f"Cannot create a tuple with this set of types: {set(types)}")
361
+ return TupleWrapper(args, type=types[0], nullable=DataType.NULL in rtypes)
343
362
 
344
363
 
345
364
  def dict_to_map_wrapper(arg):
@@ -77,11 +77,17 @@ def build_parent_concepts(
77
77
  True if (conditions and conditions == filter_where) else False
78
78
  )
79
79
 
80
+ exact_partial_matches = True
80
81
  for x in local_optional:
81
82
  if isinstance(x.lineage, FILTER_TYPES):
82
- if concept.lineage.where == filter_where:
83
+ if set([x.address for x in x.lineage.where.concept_arguments]) == set(
84
+ [x.address for x in filter_where.concept_arguments]
85
+ ):
86
+ exact_partial_matches = (
87
+ exact_partial_matches and x.lineage.where == filter_where
88
+ )
83
89
  logger.info(
84
- f"{padding(depth)}{LOGGER_PREFIX} fetching parents for peer {x} with same filter conditions"
90
+ f"{padding(depth)}{LOGGER_PREFIX} fetching parents for peer {x.address} (of {concept.address})"
85
91
  )
86
92
 
87
93
  for arg in x.lineage.content_concept_arguments:
@@ -100,7 +106,7 @@ def build_parent_concepts(
100
106
  if x.address in same_filter_optional:
101
107
  continue
102
108
  extra_row_level_optional.append(x)
103
- is_optimized_pushdown = pushdown_filter_to_parent(
109
+ is_optimized_pushdown = exact_partial_matches and pushdown_filter_to_parent(
104
110
  local_optional, conditions, filter_where, same_filter_optional, depth
105
111
  )
106
112
  if not is_optimized_pushdown:
trilogy/dialect/base.py CHANGED
@@ -174,7 +174,7 @@ FUNCTION_MAP = {
174
174
  FunctionType.CAST: lambda x: f"cast({x[0]} as {x[1]})",
175
175
  FunctionType.CASE: lambda x: render_case(x),
176
176
  FunctionType.SPLIT: lambda x: f"split({x[0]}, {x[1]})",
177
- FunctionType.IS_NULL: lambda x: f"isnull({x[0]})",
177
+ FunctionType.IS_NULL: lambda x: f"{x[0]} is null",
178
178
  FunctionType.BOOL: lambda x: f"CASE WHEN {x[0]} THEN TRUE ELSE FALSE END",
179
179
  FunctionType.PARENTHETICAL: lambda x: f"({x[0]})",
180
180
  # Complex
@@ -24,7 +24,7 @@ FUNCTION_MAP = {
24
24
  FunctionType.LIKE: lambda x: (
25
25
  f" CASE WHEN {x[0]} like {x[1]} THEN True ELSE False END"
26
26
  ),
27
- FunctionType.IS_NULL: lambda x: f"CASE WHEN {x[0]} IS NULL THEN True ELSE False END",
27
+ FunctionType.IS_NULL: lambda x: f"{x[0]} IS NULL",
28
28
  FunctionType.MINUTE: lambda x: f"EXTRACT(MINUTE from {x[0]})",
29
29
  FunctionType.SECOND: lambda x: f"EXTRACT(SECOND from {x[0]})",
30
30
  FunctionType.HOUR: lambda x: f"EXTRACT(HOUR from {x[0]})",
trilogy/executor.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, Generator, List, Optional, Protocol
6
6
  from sqlalchemy import text
7
7
  from sqlalchemy.engine import CursorResult
8
8
 
9
- from trilogy.constants import Rendering, logger
9
+ from trilogy.constants import MagicConstants, Rendering, logger
10
10
  from trilogy.core.enums import FunctionType, Granularity, IOType
11
11
  from trilogy.core.models.author import Concept, ConceptRef, Function
12
12
  from trilogy.core.models.build import BuildFunction
@@ -388,6 +388,11 @@ class Executor(object):
388
388
  if persist and isinstance(x, ProcessedQueryPersist):
389
389
  self.environment.add_datasource(x.datasource)
390
390
 
391
+ def _atom_to_value(self, val: Any) -> Any:
392
+ if val == MagicConstants.NULL:
393
+ return None
394
+ return val
395
+
391
396
  def _concept_to_value(
392
397
  self,
393
398
  concept: Concept,
@@ -402,12 +407,15 @@ class Executor(object):
402
407
  ):
403
408
  rval = concept.lineage.arguments[0]
404
409
  if isinstance(rval, ListWrapper):
405
- return [x for x in rval]
410
+ return [self._atom_to_value(x) for x in rval]
406
411
  if isinstance(rval, MapWrapper):
407
412
  # duckdb expects maps in this format as variables
408
413
  if self.dialect == Dialects.DUCK_DB:
409
- return {"key": [x for x in rval], "value": [rval[x] for x in rval]}
410
- return {k: v for k, v in rval.items()}
414
+ return {
415
+ "key": [self._atom_to_value(x) for x in rval],
416
+ "value": [self._atom_to_value(rval[x]) for x in rval],
417
+ }
418
+ return {k: self._atom_to_value(v) for k, v in rval.items()}
411
419
  # if isinstance(rval, ConceptRef):
412
420
  # return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
413
421
  return rval
@@ -701,6 +701,7 @@ class ParseToObjects(Transformer):
701
701
  environment=self.environment,
702
702
  metadata=metadata,
703
703
  )
704
+
704
705
  # let constant purposes exist to support round-tripping
705
706
  # as a build concept may end up with a constant based on constant inlining happening recursively
706
707
  if purpose == Purpose.KEY and concept.purpose != Purpose.KEY:
@@ -922,7 +923,6 @@ class ParseToObjects(Transformer):
922
923
  name=output,
923
924
  metadata=metadata,
924
925
  )
925
-
926
926
  return ConceptTransform(function=transformation, output=concept)
927
927
 
928
928
  @v_args(meta=True)
@@ -2040,6 +2040,15 @@ class ParseToObjects(Transformer):
2040
2040
 
2041
2041
  @v_args(meta=True)
2042
2042
  def fnot(self, meta, args):
2043
+ if arg_to_datatype(args[0]) == DataType.BOOL:
2044
+ return Comparison(
2045
+ left=self.function_factory.create_function(
2046
+ [args[0], False], FunctionType.COALESCE, meta
2047
+ ),
2048
+ operator=ComparisonOperator.EQ,
2049
+ right=False,
2050
+ meta=meta,
2051
+ )
2043
2052
  return self.function_factory.create_function(args, FunctionType.IS_NULL, meta)
2044
2053
 
2045
2054
  @v_args(meta=True)