pytrilogy 0.0.2.49__py3-none-any.whl → 0.0.2.51__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 (43) hide show
  1. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +43 -41
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/enums.py +11 -0
  5. trilogy/core/functions.py +4 -1
  6. trilogy/core/internal.py +5 -1
  7. trilogy/core/models.py +135 -263
  8. trilogy/core/processing/concept_strategies_v3.py +14 -7
  9. trilogy/core/processing/node_generators/basic_node.py +7 -3
  10. trilogy/core/processing/node_generators/common.py +8 -5
  11. trilogy/core/processing/node_generators/filter_node.py +5 -8
  12. trilogy/core/processing/node_generators/group_node.py +24 -9
  13. trilogy/core/processing/node_generators/group_to_node.py +0 -2
  14. trilogy/core/processing/node_generators/multiselect_node.py +4 -5
  15. trilogy/core/processing/node_generators/node_merge_node.py +14 -3
  16. trilogy/core/processing/node_generators/rowset_node.py +3 -5
  17. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  18. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
  19. trilogy/core/processing/node_generators/select_merge_node.py +153 -66
  20. trilogy/core/processing/node_generators/union_node.py +0 -1
  21. trilogy/core/processing/node_generators/unnest_node.py +0 -2
  22. trilogy/core/processing/node_generators/window_node.py +0 -2
  23. trilogy/core/processing/nodes/base_node.py +2 -36
  24. trilogy/core/processing/nodes/filter_node.py +0 -3
  25. trilogy/core/processing/nodes/group_node.py +19 -13
  26. trilogy/core/processing/nodes/merge_node.py +2 -5
  27. trilogy/core/processing/nodes/select_node_v2.py +0 -4
  28. trilogy/core/processing/nodes/union_node.py +0 -3
  29. trilogy/core/processing/nodes/unnest_node.py +0 -3
  30. trilogy/core/processing/nodes/window_node.py +0 -3
  31. trilogy/core/processing/utility.py +3 -0
  32. trilogy/core/query_processor.py +0 -1
  33. trilogy/dialect/base.py +14 -2
  34. trilogy/dialect/duckdb.py +7 -0
  35. trilogy/hooks/graph_hook.py +17 -1
  36. trilogy/parsing/common.py +68 -17
  37. trilogy/parsing/parse_engine.py +70 -20
  38. trilogy/parsing/render.py +8 -1
  39. trilogy/parsing/trilogy.lark +3 -1
  40. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
  41. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
  42. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
  43. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
trilogy/core/models.py CHANGED
@@ -5,6 +5,7 @@ import hashlib
5
5
  import os
6
6
  from abc import ABC
7
7
  from collections import UserDict, UserList, defaultdict
8
+ from datetime import date, datetime
8
9
  from enum import Enum
9
10
  from functools import cached_property
10
11
  from pathlib import Path
@@ -35,7 +36,6 @@ from pydantic import (
35
36
  ConfigDict,
36
37
  Field,
37
38
  ValidationInfo,
38
- ValidatorFunctionWrapHandler,
39
39
  computed_field,
40
40
  field_validator,
41
41
  )
@@ -408,31 +408,6 @@ class Metadata(BaseModel):
408
408
  concept_source: ConceptSource = ConceptSource.MANUAL
409
409
 
410
410
 
411
- def lineage_validator(
412
- v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
413
- ) -> Union[Function, WindowItem, FilterItem, AggregateWrapper]:
414
- if v and not isinstance(v, (Function, WindowItem, FilterItem, AggregateWrapper)):
415
- raise ValueError(v)
416
- return v
417
-
418
-
419
- def empty_grain() -> Grain:
420
- return Grain(components=[])
421
-
422
-
423
- class MultiLineage(BaseModel):
424
- lineages: list[
425
- Union[
426
- Function,
427
- WindowItem,
428
- FilterItem,
429
- AggregateWrapper,
430
- RowsetItem,
431
- MultiSelectStatement,
432
- ]
433
- ]
434
-
435
-
436
411
  class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
437
412
  name: str
438
413
  datatype: DataType | ListType | StructType | MapType | NumericType
@@ -549,28 +524,22 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
549
524
  values = info.data
550
525
  if not v and values.get("purpose", None) == Purpose.KEY:
551
526
  v = Grain(
552
- components=[
553
- Concept(
554
- namespace=values.get("namespace", DEFAULT_NAMESPACE),
555
- name=values["name"],
556
- datatype=values["datatype"],
557
- purpose=values["purpose"],
558
- grain=Grain(),
559
- )
560
- ]
527
+ components={
528
+ f'{values.get("namespace", DEFAULT_NAMESPACE)}.{values["name"]}'
529
+ }
561
530
  )
562
531
  elif (
563
532
  "lineage" in values
564
533
  and isinstance(values["lineage"], AggregateWrapper)
565
534
  and values["lineage"].by
566
535
  ):
567
- v = Grain(components=values["lineage"].by)
536
+ v = Grain(components={c.address for c in values["lineage"].by})
568
537
  elif not v:
569
- v = Grain(components=[])
538
+ v = Grain(components=set())
570
539
  elif isinstance(v, Grain):
571
- return v
540
+ pass
572
541
  elif isinstance(v, Concept):
573
- v = Grain(components=[v])
542
+ v = Grain(components={v.address})
574
543
  elif isinstance(v, dict):
575
544
  v = Grain.model_validate(v)
576
545
  else:
@@ -593,8 +562,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
593
562
  )
594
563
 
595
564
  def __str__(self):
596
- grain = ",".join([str(c.address) for c in self.grain.components])
597
- return f"{self.namespace}.{self.name}<{grain}>"
565
+ grain = str(self.grain) if self.grain else "Grain<>"
566
+ return f"{self.namespace}.{self.name}@{grain}"
598
567
 
599
568
  @cached_property
600
569
  def address(self) -> str:
@@ -619,10 +588,6 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
619
588
  return f"{self.namespace.replace('.','_')}_{self.name.replace('.','_')}"
620
589
  return self.name.replace(".", "_")
621
590
 
622
- @property
623
- def grain_components(self) -> List["Concept"]:
624
- return self.grain.components_copy if self.grain else []
625
-
626
591
  def with_namespace(self, namespace: str) -> Self:
627
592
  if namespace == self.namespace:
628
593
  return self
@@ -635,7 +600,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
635
600
  grain=(
636
601
  self.grain.with_namespace(namespace)
637
602
  if self.grain
638
- else Grain(components=[])
603
+ else Grain(components=set())
639
604
  ),
640
605
  namespace=(
641
606
  namespace + "." + self.namespace
@@ -661,7 +626,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
661
626
  new_lineage = self.lineage.with_select_context(
662
627
  local_concepts=local_concepts, grain=grain, environment=environment
663
628
  )
664
- final_grain = self.grain
629
+ final_grain = self.grain or grain
665
630
  keys = (
666
631
  tuple(
667
632
  [
@@ -673,9 +638,10 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
673
638
  else None
674
639
  )
675
640
  if self.is_aggregate and isinstance(new_lineage, Function):
676
- new_lineage = AggregateWrapper(function=new_lineage, by=grain.components)
641
+ grain_components = [environment.concepts[c] for c in grain.components]
642
+ new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
677
643
  final_grain = grain
678
- keys = tuple(grain.components)
644
+ keys = tuple(grain_components)
679
645
  elif (
680
646
  self.is_aggregate and not keys and isinstance(new_lineage, AggregateWrapper)
681
647
  ):
@@ -696,15 +662,13 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
696
662
  )
697
663
 
698
664
  def with_grain(self, grain: Optional["Grain"] = None) -> Self:
699
- if not all([isinstance(x, Concept) for x in self.keys or []]):
700
- raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
701
665
  return self.__class__(
702
666
  name=self.name,
703
667
  datatype=self.datatype,
704
668
  purpose=self.purpose,
705
669
  metadata=self.metadata,
706
670
  lineage=self.lineage,
707
- grain=grain if grain else Grain(components=[]),
671
+ grain=grain if grain else Grain(components=set()),
708
672
  namespace=self.namespace,
709
673
  keys=self.keys,
710
674
  modifiers=self.modifiers,
@@ -715,7 +679,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
715
679
  def _with_default_grain(self) -> Self:
716
680
  if self.purpose == Purpose.KEY:
717
681
  # we need to make this abstract
718
- grain = Grain(components=[self.with_grain(Grain())], nested=True)
682
+ grain = Grain(components={self.address})
719
683
  elif self.purpose == Purpose.PROPERTY:
720
684
  components = []
721
685
  if self.keys:
@@ -727,12 +691,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
727
691
  components += item.sources
728
692
  else:
729
693
  components += item.sources
730
- grain = Grain(components=components)
694
+ # TODO: set synonyms
695
+ grain = Grain(
696
+ components=set([x.address for x in components]),
697
+ ) # synonym_set=generate_concept_synonyms(components))
731
698
  elif self.purpose == Purpose.METRIC:
732
699
  grain = Grain()
733
700
  elif self.purpose == Purpose.CONSTANT:
734
701
  if self.derivation != PurposeLineage.CONSTANT:
735
- grain = Grain(components=[self.with_grain(Grain())], nested=True)
702
+ grain = Grain(components={self.address})
736
703
  else:
737
704
  grain = self.grain
738
705
  else:
@@ -854,7 +821,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
854
821
  elif self.derivation == PurposeLineage.AGGREGATE:
855
822
  # if it's an aggregate grouped over all rows
856
823
  # there is only one row left and it's fine to cross_join
857
- if all([x.name == ALL_ROWS_CONCEPT for x in self.grain.components]):
824
+ if all([x.endswith(ALL_ROWS_CONCEPT) for x in self.grain.components]):
858
825
  return Granularity.SINGLE_ROW
859
826
  elif self.namespace == INTERNAL_NAMESPACE and self.name == ALL_ROWS_CONCEPT:
860
827
  return Granularity.SINGLE_ROW
@@ -892,7 +859,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
892
859
  metadata=self.metadata,
893
860
  lineage=FilterItem(content=self, where=WhereClause(conditional=condition)),
894
861
  keys=(self.keys if self.purpose == Purpose.PROPERTY else None),
895
- grain=self.grain if self.grain else Grain(components=[]),
862
+ grain=self.grain if self.grain else Grain(components=set()),
896
863
  namespace=self.namespace,
897
864
  modifiers=self.modifiers,
898
865
  pseudonyms=self.pseudonyms,
@@ -910,161 +877,123 @@ class ConceptRef(BaseModel):
910
877
  return environment.concepts.__getitem__(self.address, self.line_no)
911
878
 
912
879
 
913
- class Grain(Mergeable, BaseModel, SelectContext):
914
- nested: bool = False
915
- components: List[Concept] = Field(default_factory=list, validate_default=True)
916
- where_clause: Optional[WhereClause] = Field(default=None)
880
+ class Grain(Namespaced, BaseModel):
881
+ components: set[str] = Field(default_factory=set)
882
+ where_clause: Optional["WhereClause"] = None
917
883
 
918
- @field_validator("components")
919
- def component_validator(cls, v, info: ValidationInfo):
920
- values = info.data
921
- if not values.get("nested", False):
922
- v2: List[Concept] = unique(
923
- [safe_concept(c).with_default_grain() for c in v], "address"
924
- )
925
- else:
926
- v2 = unique(v, "address")
927
- final: List[Concept] = []
928
- for sub in v2:
929
- if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
930
- if all([c in v2 for c in sub.keys]):
931
- continue
932
- final.append(sub)
933
- v2 = sorted(final, key=lambda x: x.name)
934
- return v2
935
-
936
- def with_select_context(
937
- self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
938
- ):
939
- if self.nested:
940
- return self
941
- return Grain(
942
- components=[
943
- x.with_select_context(local_concepts, grain, environment)
944
- for x in self.components
945
- ],
946
- where_clause=self.where_clause,
947
- nested=self.nested,
948
- )
884
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
885
+ new_components = set()
886
+ for c in self.components:
887
+ if c == source.address:
888
+ new_components.add(target.address)
889
+ else:
890
+ new_components.add(c)
891
+ return Grain(components=new_components)
949
892
 
950
- def with_filter(
951
- self,
952
- condition: "Conditional | Comparison | Parenthetical",
893
+ @classmethod
894
+ def from_concepts(
895
+ cls,
896
+ concepts: List[Concept],
953
897
  environment: Environment | None = None,
898
+ where_clause: WhereClause | None = None,
954
899
  ) -> "Grain":
900
+ from trilogy.parsing.common import concepts_to_grain_concepts
901
+
955
902
  return Grain(
956
- components=[c.with_filter(condition, environment) for c in self.components],
957
- nested=self.nested,
903
+ components={
904
+ c.address
905
+ for c in concepts_to_grain_concepts(concepts, environment=environment)
906
+ },
907
+ where_clause=where_clause,
958
908
  )
959
909
 
960
- @property
961
- def components_copy(self) -> List[Concept]:
962
- return [*self.components]
910
+ def with_namespace(self, namespace: str) -> "Grain":
911
+ return Grain(
912
+ components={address_with_namespace(c, namespace) for c in self.components},
913
+ where_clause=(
914
+ self.where_clause.with_namespace(namespace)
915
+ if self.where_clause
916
+ else None
917
+ ),
918
+ )
963
919
 
964
- def __str__(self):
965
- if self.abstract:
966
- base = "Grain<Abstract>"
920
+ @field_validator("components", mode="before")
921
+ def component_validator(cls, v, info: ValidationInfo):
922
+ output = set()
923
+ if isinstance(v, list):
924
+ for vc in v:
925
+ if isinstance(vc, Concept):
926
+ output.add(vc.address)
927
+ elif isinstance(vc, ConceptRef):
928
+ output.add(vc.address)
929
+ else:
930
+ output.add(vc)
967
931
  else:
968
- base = "Grain<" + ",".join([c.address for c in self.components]) + ">"
969
- if self.where_clause:
970
- base += f"|{str(self.where_clause)}"
971
- return base
932
+ output = v
933
+ if not isinstance(output, set):
934
+ raise ValueError(f"Invalid grain component {output}, is not set")
935
+ if not all(isinstance(x, str) for x in output):
936
+ raise ValueError(f"Invalid component {output}")
937
+ return output
972
938
 
973
- def with_namespace(self, namespace: str) -> "Grain":
939
+ def __add__(self, other: "Grain") -> "Grain":
940
+ where = self.where_clause
941
+ if other.where_clause:
942
+ if not self.where_clause:
943
+ where = other.where_clause
944
+ elif not other.where_clause == self.where_clause:
945
+ raise NotImplementedError(
946
+ f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
947
+ )
974
948
  return Grain(
975
- components=[c.with_namespace(namespace) for c in self.components],
976
- nested=self.nested,
949
+ components=self.components.union(other.components), where_clause=where
977
950
  )
978
951
 
979
- def with_merge(
980
- self, source: Concept, target: Concept, modifiers: List[Modifier]
981
- ) -> "Grain":
952
+ def __sub__(self, other: "Grain") -> "Grain":
982
953
  return Grain(
983
- components=[
984
- x.with_merge(source, target, modifiers) for x in self.components
985
- ],
986
- nested=self.nested,
954
+ components=self.components.difference(other.components),
955
+ where_clause=self.where_clause,
987
956
  )
988
957
 
989
958
  @property
990
959
  def abstract(self):
991
960
  return not self.components or all(
992
- [c.name == ALL_ROWS_CONCEPT for c in self.components]
961
+ [c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
993
962
  )
994
963
 
995
- @property
996
- def synonym_set(self) -> set[str]:
997
- base = []
998
- for x in self.components_copy:
999
- if isinstance(x.lineage, RowsetItem):
1000
- base.append(x.lineage.content.address)
1001
- for c in x.lineage.content.pseudonyms:
1002
- base.append(c)
1003
- else:
1004
- base.append(x.address)
1005
- for c in x.pseudonyms:
1006
- base.append(c)
1007
- return set(base)
1008
-
1009
- @property
1010
- def set(self) -> set[str]:
1011
- base = []
1012
- for x in self.components_copy:
1013
- if isinstance(x.lineage, RowsetItem):
1014
- base.append(x.lineage.content.address)
1015
- else:
1016
- base.append(x.address)
1017
- return set(base)
1018
-
1019
964
  def __eq__(self, other: object):
1020
965
  if isinstance(other, list):
1021
- return self.set == set([c.address for c in other])
966
+ if not all([isinstance(c, Concept) for c in other]):
967
+ return False
968
+ return self.components == set([c.address for c in other])
1022
969
  if not isinstance(other, Grain):
1023
970
  return False
1024
- if self.set == other.set:
1025
- return True
1026
- elif self.synonym_set == other.synonym_set:
971
+ if self.components == other.components:
1027
972
  return True
1028
973
  return False
1029
974
 
1030
975
  def issubset(self, other: "Grain"):
1031
- return self.set.issubset(other.set)
976
+ return self.components.issubset(other.components)
1032
977
 
1033
978
  def union(self, other: "Grain"):
1034
- addresses = self.set.union(other.set)
1035
-
1036
- return Grain(
1037
- components=[c for c in self.components if c.address in addresses]
1038
- + [c for c in other.components if c.address in addresses]
1039
- )
979
+ addresses = self.components.union(other.components)
980
+ return Grain(components=addresses, where_clause=self.where_clause)
1040
981
 
1041
982
  def isdisjoint(self, other: "Grain"):
1042
- return self.set.isdisjoint(other.set)
983
+ return self.components.isdisjoint(other.components)
1043
984
 
1044
985
  def intersection(self, other: "Grain") -> "Grain":
1045
- intersection = self.set.intersection(other.set)
1046
- components = [i for i in self.components if i.address in intersection]
1047
- return Grain(components=components)
986
+ intersection = self.components.intersection(other.components)
987
+ return Grain(components=intersection)
1048
988
 
1049
- def __add__(self, other: "Grain") -> "Grain":
1050
- components: List[Concept] = []
1051
- for clist in [self.components_copy, other.components_copy]:
1052
- for component in clist:
1053
- if component.with_default_grain() in components:
1054
- continue
1055
- components.append(component.with_default_grain())
1056
- base_components = [c for c in components if c.purpose == Purpose.KEY]
1057
- for c in components:
1058
- if c.purpose == Purpose.PROPERTY and not any(
1059
- [key in base_components for key in (c.keys or [])]
1060
- ):
1061
- base_components.append(c)
1062
- elif (
1063
- c.purpose == Purpose.CONSTANT
1064
- and not c.derivation == PurposeLineage.CONSTANT
1065
- ):
1066
- base_components.append(c)
1067
- return Grain(components=base_components)
989
+ def __str__(self):
990
+ if self.abstract:
991
+ base = "Grain<Abstract>"
992
+ else:
993
+ base = "Grain<" + ",".join([c for c in sorted(list(self.components))]) + ">"
994
+ if self.where_clause:
995
+ base += f"|{str(self.where_clause)}"
996
+ return base
1068
997
 
1069
998
  def __radd__(self, other) -> "Grain":
1070
999
  if other == 0:
@@ -1264,6 +1193,8 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
1264
1193
  int,
1265
1194
  float,
1266
1195
  str,
1196
+ date,
1197
+ datetime,
1267
1198
  MapWrapper[Any, Any],
1268
1199
  DataType,
1269
1200
  ListType,
@@ -1752,6 +1683,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1752
1683
  local_concepts: Annotated[
1753
1684
  EnvironmentConceptDict, PlainValidator(validate_concepts)
1754
1685
  ] = Field(default_factory=EnvironmentConceptDict)
1686
+ grain: Grain = Field(default_factory=Grain)
1755
1687
 
1756
1688
  def validate_syntax(self, environment: Environment):
1757
1689
  if self.where_clause:
@@ -1803,15 +1735,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1803
1735
 
1804
1736
  return render_query(self)
1805
1737
 
1806
- def __init__(self, *args, **kwargs) -> None:
1807
- super().__init__(*args, **kwargs)
1808
- for nitem in self.selection:
1809
- if not isinstance(nitem.content, Concept):
1810
- continue
1811
- if nitem.content.grain == Grain():
1812
- if nitem.content.derivation == PurposeLineage.AGGREGATE:
1813
- nitem.content = nitem.content.with_grain(self.grain)
1814
-
1815
1738
  @field_validator("selection", mode="before")
1816
1739
  @classmethod
1817
1740
  def selection_validation(cls, v):
@@ -1883,9 +1806,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1883
1806
 
1884
1807
  @property
1885
1808
  def all_components(self) -> List[Concept]:
1886
- return (
1887
- self.input_components + self.output_components + self.grain.components_copy
1888
- )
1809
+ return self.input_components + self.output_components
1889
1810
 
1890
1811
  def to_datasource(
1891
1812
  self,
@@ -1935,55 +1856,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1935
1856
  column.concept = column.concept.with_grain(new_datasource.grain)
1936
1857
  return new_datasource
1937
1858
 
1938
- @property
1939
- def grain(self) -> "Grain":
1940
- output = []
1941
- for item in self.output_components:
1942
- if item.purpose == Purpose.KEY:
1943
- output.append(item)
1944
- # if self.where_clause:
1945
- # for item in self.where_clause.concept_arguments:
1946
- # if item.purpose == Purpose.KEY:
1947
- # output.append(item)
1948
- # elif item.purpose == Purpose.PROPERTY and item.grain:
1949
- # output += item.grain.components
1950
- # TODO: handle other grain cases
1951
- # new if block by design
1952
- # add back any purpose that is not at the grain
1953
- # if a query already has the key of the property in the grain
1954
- # we want to group to that grain and ignore the property, which is a derivation
1955
- # otherwise, we need to include property as the group by
1956
- for item in self.output_components:
1957
- if (
1958
- item.purpose == Purpose.PROPERTY
1959
- and item.grain
1960
- and (
1961
- not item.grain.components
1962
- or not item.grain.issubset(
1963
- Grain(components=unique(output, "address"))
1964
- )
1965
- )
1966
- ):
1967
- output.append(item)
1968
- if (
1969
- item.purpose == Purpose.CONSTANT
1970
- and item.derivation != PurposeLineage.CONSTANT
1971
- and item.grain
1972
- and (
1973
- not item.grain.components
1974
- or not item.grain.issubset(
1975
- Grain(components=unique(output, "address"))
1976
- )
1977
- )
1978
- ):
1979
- output.append(item)
1980
- # TODO: explore implicit filtering more
1981
- # if self.where_clause.conditional and self.where_clause_category == SelectFiltering.IMPLICIT:
1982
- # output =[x.with_filter(self.where_clause.conditional) for x in output]
1983
- return Grain(
1984
- components=unique(output, "address"), where_clause=self.where_clause
1985
- )
1986
-
1987
1859
  def with_namespace(self, namespace: str) -> "SelectStatement":
1988
1860
  return SelectStatement(
1989
1861
  selection=[c.with_namespace(namespace) for c in self.selection],
@@ -2200,7 +2072,7 @@ def safe_grain(v) -> Grain:
2200
2072
  elif isinstance(v, Grain):
2201
2073
  return v
2202
2074
  elif not v:
2203
- return Grain(components=[])
2075
+ return Grain(components=set())
2204
2076
  else:
2205
2077
  raise ValueError(f"Invalid input type to safe_grain {type(v)}")
2206
2078
 
@@ -2232,7 +2104,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2232
2104
  columns: List[ColumnAssignment]
2233
2105
  address: Union[Address, str]
2234
2106
  grain: Grain = Field(
2235
- default_factory=lambda: Grain(components=[]), validate_default=True
2107
+ default_factory=lambda: Grain(components=set()), validate_default=True
2236
2108
  )
2237
2109
  namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
2238
2110
  metadata: DatasourceMetadata = Field(
@@ -2320,8 +2192,8 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2320
2192
  grain: Grain = safe_grain(v)
2321
2193
  if not grain.components:
2322
2194
  columns: List[ColumnAssignment] = values.get("columns", [])
2323
- grain = Grain(
2324
- components=[
2195
+ grain = Grain.from_concepts(
2196
+ [
2325
2197
  c.concept.with_grain(Grain())
2326
2198
  for c in columns
2327
2199
  if c.concept.purpose == Purpose.KEY
@@ -2755,15 +2627,11 @@ class QueryDatasource(BaseModel):
2755
2627
  @property
2756
2628
  def identifier(self) -> str:
2757
2629
  filters = abs(hash(str(self.condition))) if self.condition else ""
2758
- grain = "_".join(
2759
- [str(c.address).replace(".", "_") for c in self.grain.components]
2760
- )
2761
- # partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
2630
+ grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
2762
2631
  return (
2763
2632
  "_join_".join([d.identifier for d in self.datasources])
2764
2633
  + (f"_at_{grain}" if grain else "_at_abstract")
2765
2634
  + (f"_filtered_by_{filters}" if filters else "")
2766
- # + (f"_partial_{partial}" if partial else "")
2767
2635
  )
2768
2636
 
2769
2637
  def get_alias(
@@ -3102,12 +2970,16 @@ class CTE(BaseModel):
3102
2970
  for cte in self.parent_ctes:
3103
2971
  if address in cte.output_columns:
3104
2972
  match = [x for x in cte.output_columns if x.address == address].pop()
3105
- return match
2973
+ if match:
2974
+ return match
3106
2975
 
3107
2976
  for array in [self.source.input_concepts, self.source.output_concepts]:
3108
2977
  match_list = [x for x in array if x.address == address]
3109
2978
  if match_list:
3110
2979
  return match_list.pop()
2980
+ match_list = [x for x in self.output_columns if x.address == address]
2981
+ if match_list:
2982
+ return match_list.pop()
3111
2983
  return None
3112
2984
 
3113
2985
  def get_alias(self, concept: Concept, source: str | None = None) -> str:
@@ -3116,8 +2988,10 @@ class CTE(BaseModel):
3116
2988
  if source and source != cte.name:
3117
2989
  continue
3118
2990
  return concept.safe_address
2991
+
3119
2992
  try:
3120
2993
  source = self.source.get_alias(concept, source=source)
2994
+
3121
2995
  if not source:
3122
2996
  raise ValueError("No source found")
3123
2997
  return source
@@ -3130,7 +3004,8 @@ class CTE(BaseModel):
3130
3004
  if len(self.source_map.get(c.address, [])) > 0:
3131
3005
  return False
3132
3006
  if c.derivation == PurposeLineage.ROWSET:
3133
- return False
3007
+ assert isinstance(c.lineage, RowsetItem)
3008
+ return check_is_not_in_group(c.lineage.content)
3134
3009
  if c.derivation == PurposeLineage.CONSTANT:
3135
3010
  return False
3136
3011
  if c.purpose == Purpose.METRIC:
@@ -3327,7 +3202,6 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
3327
3202
  if self.address in local_concepts:
3328
3203
  rval = local_concepts[self.address]
3329
3204
  rval = rval.with_select_context(local_concepts, grain, environment)
3330
-
3331
3205
  return rval
3332
3206
  environment.concepts.raise_undefined(self.address, line_no=self.line_no)
3333
3207
 
@@ -3868,6 +3742,8 @@ class Comparison(
3868
3742
  float,
3869
3743
  list,
3870
3744
  bool,
3745
+ datetime,
3746
+ date,
3871
3747
  Function,
3872
3748
  Concept,
3873
3749
  "Conditional",
@@ -3884,6 +3760,8 @@ class Comparison(
3884
3760
  float,
3885
3761
  list,
3886
3762
  bool,
3763
+ date,
3764
+ datetime,
3887
3765
  Concept,
3888
3766
  Function,
3889
3767
  "Conditional",
@@ -4455,7 +4333,7 @@ class AggregateWrapper(Mergeable, Namespaced, SelectContext, BaseModel):
4455
4333
  self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
4456
4334
  ) -> AggregateWrapper:
4457
4335
  if not self.by:
4458
- by = grain.components_copy
4336
+ by = [environment.concepts[c] for c in grain.components]
4459
4337
  else:
4460
4338
  by = [
4461
4339
  x.with_select_context(local_concepts, grain, environment)
@@ -4504,16 +4382,6 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
4504
4382
  )
4505
4383
  )
4506
4384
 
4507
- @property
4508
- def grain(self) -> Grain:
4509
- output = []
4510
- for item in self.input:
4511
- if item.purpose == Purpose.KEY:
4512
- output.append(item)
4513
- elif item.purpose == Purpose.PROPERTY:
4514
- output += item.grain.components if item.grain else []
4515
- return Grain(components=list(set(output)))
4516
-
4517
4385
  @property
4518
4386
  def components(self):
4519
4387
  from trilogy.core.processing.utility import decompose_condition
@@ -4649,7 +4517,7 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
4649
4517
  )
4650
4518
  orig[orig_concept.address] = new_concept
4651
4519
  output.append(new_concept)
4652
- default_grain = Grain(components=[*output])
4520
+ default_grain = Grain.from_concepts([*output])
4653
4521
  # remap everything to the properties of the rowset
4654
4522
  for x in output:
4655
4523
  if x.keys:
@@ -4661,9 +4529,9 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
4661
4529
  # TODO: fix this up
4662
4530
  x.keys = tuple()
4663
4531
  for x in output:
4664
- if all([c.address in orig for c in x.grain.components_copy]):
4532
+ if all([c in orig for c in x.grain.components]):
4665
4533
  x.grain = Grain(
4666
- components=[orig[c.address] for c in x.grain.components_copy]
4534
+ components={orig[c].address for c in x.grain.components}
4667
4535
  )
4668
4536
  else:
4669
4537
  x.grain = default_grain
@@ -5008,5 +4876,9 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
5008
4876
  return ListType(type=wrapper.type)
5009
4877
  elif isinstance(arg, MapWrapper):
5010
4878
  return MapType(key_type=arg.key_type, value_type=arg.value_type)
4879
+ elif isinstance(arg, datetime):
4880
+ return DataType.DATETIME
4881
+ elif isinstance(arg, date):
4882
+ return DataType.DATE
5011
4883
  else:
5012
4884
  raise ValueError(f"Cannot parse arg datatype for arg of raw type {type(arg)}")