pytrilogy 0.0.2.50__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 (27) hide show
  1. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +27 -25
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/internal.py +5 -1
  5. trilogy/core/models.py +124 -263
  6. trilogy/core/processing/concept_strategies_v3.py +14 -4
  7. trilogy/core/processing/node_generators/basic_node.py +7 -3
  8. trilogy/core/processing/node_generators/common.py +8 -3
  9. trilogy/core/processing/node_generators/filter_node.py +5 -5
  10. trilogy/core/processing/node_generators/group_node.py +24 -8
  11. trilogy/core/processing/node_generators/multiselect_node.py +4 -3
  12. trilogy/core/processing/node_generators/node_merge_node.py +14 -2
  13. trilogy/core/processing/node_generators/rowset_node.py +3 -4
  14. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  15. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
  16. trilogy/core/processing/node_generators/select_merge_node.py +17 -9
  17. trilogy/core/processing/nodes/base_node.py +2 -33
  18. trilogy/core/processing/nodes/group_node.py +19 -10
  19. trilogy/core/processing/nodes/merge_node.py +2 -2
  20. trilogy/hooks/graph_hook.py +3 -1
  21. trilogy/parsing/common.py +54 -12
  22. trilogy/parsing/parse_engine.py +39 -20
  23. trilogy/parsing/render.py +8 -1
  24. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
  26. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
  27. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
trilogy/core/models.py CHANGED
@@ -36,7 +36,6 @@ from pydantic import (
36
36
  ConfigDict,
37
37
  Field,
38
38
  ValidationInfo,
39
- ValidatorFunctionWrapHandler,
40
39
  computed_field,
41
40
  field_validator,
42
41
  )
@@ -409,31 +408,6 @@ class Metadata(BaseModel):
409
408
  concept_source: ConceptSource = ConceptSource.MANUAL
410
409
 
411
410
 
412
- def lineage_validator(
413
- v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
414
- ) -> Union[Function, WindowItem, FilterItem, AggregateWrapper]:
415
- if v and not isinstance(v, (Function, WindowItem, FilterItem, AggregateWrapper)):
416
- raise ValueError(v)
417
- return v
418
-
419
-
420
- def empty_grain() -> Grain:
421
- return Grain(components=[])
422
-
423
-
424
- class MultiLineage(BaseModel):
425
- lineages: list[
426
- Union[
427
- Function,
428
- WindowItem,
429
- FilterItem,
430
- AggregateWrapper,
431
- RowsetItem,
432
- MultiSelectStatement,
433
- ]
434
- ]
435
-
436
-
437
411
  class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
438
412
  name: str
439
413
  datatype: DataType | ListType | StructType | MapType | NumericType
@@ -550,28 +524,22 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
550
524
  values = info.data
551
525
  if not v and values.get("purpose", None) == Purpose.KEY:
552
526
  v = Grain(
553
- components=[
554
- Concept(
555
- namespace=values.get("namespace", DEFAULT_NAMESPACE),
556
- name=values["name"],
557
- datatype=values["datatype"],
558
- purpose=values["purpose"],
559
- grain=Grain(),
560
- )
561
- ]
527
+ components={
528
+ f'{values.get("namespace", DEFAULT_NAMESPACE)}.{values["name"]}'
529
+ }
562
530
  )
563
531
  elif (
564
532
  "lineage" in values
565
533
  and isinstance(values["lineage"], AggregateWrapper)
566
534
  and values["lineage"].by
567
535
  ):
568
- v = Grain(components=values["lineage"].by)
536
+ v = Grain(components={c.address for c in values["lineage"].by})
569
537
  elif not v:
570
- v = Grain(components=[])
538
+ v = Grain(components=set())
571
539
  elif isinstance(v, Grain):
572
- return v
540
+ pass
573
541
  elif isinstance(v, Concept):
574
- v = Grain(components=[v])
542
+ v = Grain(components={v.address})
575
543
  elif isinstance(v, dict):
576
544
  v = Grain.model_validate(v)
577
545
  else:
@@ -594,8 +562,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
594
562
  )
595
563
 
596
564
  def __str__(self):
597
- grain = ",".join([str(c.address) for c in self.grain.components])
598
- 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}"
599
567
 
600
568
  @cached_property
601
569
  def address(self) -> str:
@@ -620,10 +588,6 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
620
588
  return f"{self.namespace.replace('.','_')}_{self.name.replace('.','_')}"
621
589
  return self.name.replace(".", "_")
622
590
 
623
- @property
624
- def grain_components(self) -> List["Concept"]:
625
- return self.grain.components_copy if self.grain else []
626
-
627
591
  def with_namespace(self, namespace: str) -> Self:
628
592
  if namespace == self.namespace:
629
593
  return self
@@ -636,7 +600,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
636
600
  grain=(
637
601
  self.grain.with_namespace(namespace)
638
602
  if self.grain
639
- else Grain(components=[])
603
+ else Grain(components=set())
640
604
  ),
641
605
  namespace=(
642
606
  namespace + "." + self.namespace
@@ -662,7 +626,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
662
626
  new_lineage = self.lineage.with_select_context(
663
627
  local_concepts=local_concepts, grain=grain, environment=environment
664
628
  )
665
- final_grain = self.grain
629
+ final_grain = self.grain or grain
666
630
  keys = (
667
631
  tuple(
668
632
  [
@@ -674,9 +638,10 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
674
638
  else None
675
639
  )
676
640
  if self.is_aggregate and isinstance(new_lineage, Function):
677
- 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)
678
643
  final_grain = grain
679
- keys = tuple(grain.components)
644
+ keys = tuple(grain_components)
680
645
  elif (
681
646
  self.is_aggregate and not keys and isinstance(new_lineage, AggregateWrapper)
682
647
  ):
@@ -697,15 +662,13 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
697
662
  )
698
663
 
699
664
  def with_grain(self, grain: Optional["Grain"] = None) -> Self:
700
- if not all([isinstance(x, Concept) for x in self.keys or []]):
701
- raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
702
665
  return self.__class__(
703
666
  name=self.name,
704
667
  datatype=self.datatype,
705
668
  purpose=self.purpose,
706
669
  metadata=self.metadata,
707
670
  lineage=self.lineage,
708
- grain=grain if grain else Grain(components=[]),
671
+ grain=grain if grain else Grain(components=set()),
709
672
  namespace=self.namespace,
710
673
  keys=self.keys,
711
674
  modifiers=self.modifiers,
@@ -716,7 +679,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
716
679
  def _with_default_grain(self) -> Self:
717
680
  if self.purpose == Purpose.KEY:
718
681
  # we need to make this abstract
719
- grain = Grain(components=[self.with_grain(Grain())], nested=True)
682
+ grain = Grain(components={self.address})
720
683
  elif self.purpose == Purpose.PROPERTY:
721
684
  components = []
722
685
  if self.keys:
@@ -728,12 +691,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
728
691
  components += item.sources
729
692
  else:
730
693
  components += item.sources
731
- 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))
732
698
  elif self.purpose == Purpose.METRIC:
733
699
  grain = Grain()
734
700
  elif self.purpose == Purpose.CONSTANT:
735
701
  if self.derivation != PurposeLineage.CONSTANT:
736
- grain = Grain(components=[self.with_grain(Grain())], nested=True)
702
+ grain = Grain(components={self.address})
737
703
  else:
738
704
  grain = self.grain
739
705
  else:
@@ -855,7 +821,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
855
821
  elif self.derivation == PurposeLineage.AGGREGATE:
856
822
  # if it's an aggregate grouped over all rows
857
823
  # there is only one row left and it's fine to cross_join
858
- 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]):
859
825
  return Granularity.SINGLE_ROW
860
826
  elif self.namespace == INTERNAL_NAMESPACE and self.name == ALL_ROWS_CONCEPT:
861
827
  return Granularity.SINGLE_ROW
@@ -893,7 +859,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
893
859
  metadata=self.metadata,
894
860
  lineage=FilterItem(content=self, where=WhereClause(conditional=condition)),
895
861
  keys=(self.keys if self.purpose == Purpose.PROPERTY else None),
896
- grain=self.grain if self.grain else Grain(components=[]),
862
+ grain=self.grain if self.grain else Grain(components=set()),
897
863
  namespace=self.namespace,
898
864
  modifiers=self.modifiers,
899
865
  pseudonyms=self.pseudonyms,
@@ -911,161 +877,123 @@ class ConceptRef(BaseModel):
911
877
  return environment.concepts.__getitem__(self.address, self.line_no)
912
878
 
913
879
 
914
- class Grain(Mergeable, BaseModel, SelectContext):
915
- nested: bool = False
916
- components: List[Concept] = Field(default_factory=list, validate_default=True)
917
- 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
918
883
 
919
- @field_validator("components")
920
- def component_validator(cls, v, info: ValidationInfo):
921
- values = info.data
922
- if not values.get("nested", False):
923
- v2: List[Concept] = unique(
924
- [safe_concept(c).with_default_grain() for c in v], "address"
925
- )
926
- else:
927
- v2 = unique(v, "address")
928
- final: List[Concept] = []
929
- for sub in v2:
930
- if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
931
- if all([c in v2 for c in sub.keys]):
932
- continue
933
- final.append(sub)
934
- v2 = sorted(final, key=lambda x: x.name)
935
- return v2
936
-
937
- def with_select_context(
938
- self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
939
- ):
940
- if self.nested:
941
- return self
942
- return Grain(
943
- components=[
944
- x.with_select_context(local_concepts, grain, environment)
945
- for x in self.components
946
- ],
947
- where_clause=self.where_clause,
948
- nested=self.nested,
949
- )
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)
950
892
 
951
- def with_filter(
952
- self,
953
- condition: "Conditional | Comparison | Parenthetical",
893
+ @classmethod
894
+ def from_concepts(
895
+ cls,
896
+ concepts: List[Concept],
954
897
  environment: Environment | None = None,
898
+ where_clause: WhereClause | None = None,
955
899
  ) -> "Grain":
900
+ from trilogy.parsing.common import concepts_to_grain_concepts
901
+
956
902
  return Grain(
957
- components=[c.with_filter(condition, environment) for c in self.components],
958
- 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,
959
908
  )
960
909
 
961
- @property
962
- def components_copy(self) -> List[Concept]:
963
- 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
+ )
964
919
 
965
- def __str__(self):
966
- if self.abstract:
967
- 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)
968
931
  else:
969
- base = "Grain<" + ",".join([c.address for c in self.components]) + ">"
970
- if self.where_clause:
971
- base += f"|{str(self.where_clause)}"
972
- 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
973
938
 
974
- 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
+ )
975
948
  return Grain(
976
- components=[c.with_namespace(namespace) for c in self.components],
977
- nested=self.nested,
949
+ components=self.components.union(other.components), where_clause=where
978
950
  )
979
951
 
980
- def with_merge(
981
- self, source: Concept, target: Concept, modifiers: List[Modifier]
982
- ) -> "Grain":
952
+ def __sub__(self, other: "Grain") -> "Grain":
983
953
  return Grain(
984
- components=[
985
- x.with_merge(source, target, modifiers) for x in self.components
986
- ],
987
- nested=self.nested,
954
+ components=self.components.difference(other.components),
955
+ where_clause=self.where_clause,
988
956
  )
989
957
 
990
958
  @property
991
959
  def abstract(self):
992
960
  return not self.components or all(
993
- [c.name == ALL_ROWS_CONCEPT for c in self.components]
961
+ [c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
994
962
  )
995
963
 
996
- @property
997
- def synonym_set(self) -> set[str]:
998
- base = []
999
- for x in self.components_copy:
1000
- if isinstance(x.lineage, RowsetItem):
1001
- base.append(x.lineage.content.address)
1002
- for c in x.lineage.content.pseudonyms:
1003
- base.append(c)
1004
- else:
1005
- base.append(x.address)
1006
- for c in x.pseudonyms:
1007
- base.append(c)
1008
- return set(base)
1009
-
1010
- @property
1011
- def set(self) -> set[str]:
1012
- base = []
1013
- for x in self.components_copy:
1014
- if isinstance(x.lineage, RowsetItem):
1015
- base.append(x.lineage.content.address)
1016
- else:
1017
- base.append(x.address)
1018
- return set(base)
1019
-
1020
964
  def __eq__(self, other: object):
1021
965
  if isinstance(other, list):
1022
- 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])
1023
969
  if not isinstance(other, Grain):
1024
970
  return False
1025
- if self.set == other.set:
1026
- return True
1027
- elif self.synonym_set == other.synonym_set:
971
+ if self.components == other.components:
1028
972
  return True
1029
973
  return False
1030
974
 
1031
975
  def issubset(self, other: "Grain"):
1032
- return self.set.issubset(other.set)
976
+ return self.components.issubset(other.components)
1033
977
 
1034
978
  def union(self, other: "Grain"):
1035
- addresses = self.set.union(other.set)
1036
-
1037
- return Grain(
1038
- components=[c for c in self.components if c.address in addresses]
1039
- + [c for c in other.components if c.address in addresses]
1040
- )
979
+ addresses = self.components.union(other.components)
980
+ return Grain(components=addresses, where_clause=self.where_clause)
1041
981
 
1042
982
  def isdisjoint(self, other: "Grain"):
1043
- return self.set.isdisjoint(other.set)
983
+ return self.components.isdisjoint(other.components)
1044
984
 
1045
985
  def intersection(self, other: "Grain") -> "Grain":
1046
- intersection = self.set.intersection(other.set)
1047
- components = [i for i in self.components if i.address in intersection]
1048
- return Grain(components=components)
986
+ intersection = self.components.intersection(other.components)
987
+ return Grain(components=intersection)
1049
988
 
1050
- def __add__(self, other: "Grain") -> "Grain":
1051
- components: List[Concept] = []
1052
- for clist in [self.components_copy, other.components_copy]:
1053
- for component in clist:
1054
- if component.with_default_grain() in components:
1055
- continue
1056
- components.append(component.with_default_grain())
1057
- base_components = [c for c in components if c.purpose == Purpose.KEY]
1058
- for c in components:
1059
- if c.purpose == Purpose.PROPERTY and not any(
1060
- [key in base_components for key in (c.keys or [])]
1061
- ):
1062
- base_components.append(c)
1063
- elif (
1064
- c.purpose == Purpose.CONSTANT
1065
- and not c.derivation == PurposeLineage.CONSTANT
1066
- ):
1067
- base_components.append(c)
1068
- 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
1069
997
 
1070
998
  def __radd__(self, other) -> "Grain":
1071
999
  if other == 0:
@@ -1755,6 +1683,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1755
1683
  local_concepts: Annotated[
1756
1684
  EnvironmentConceptDict, PlainValidator(validate_concepts)
1757
1685
  ] = Field(default_factory=EnvironmentConceptDict)
1686
+ grain: Grain = Field(default_factory=Grain)
1758
1687
 
1759
1688
  def validate_syntax(self, environment: Environment):
1760
1689
  if self.where_clause:
@@ -1806,15 +1735,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1806
1735
 
1807
1736
  return render_query(self)
1808
1737
 
1809
- def __init__(self, *args, **kwargs) -> None:
1810
- super().__init__(*args, **kwargs)
1811
- for nitem in self.selection:
1812
- if not isinstance(nitem.content, Concept):
1813
- continue
1814
- if nitem.content.grain == Grain():
1815
- if nitem.content.derivation == PurposeLineage.AGGREGATE:
1816
- nitem.content = nitem.content.with_grain(self.grain)
1817
-
1818
1738
  @field_validator("selection", mode="before")
1819
1739
  @classmethod
1820
1740
  def selection_validation(cls, v):
@@ -1886,9 +1806,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1886
1806
 
1887
1807
  @property
1888
1808
  def all_components(self) -> List[Concept]:
1889
- return (
1890
- self.input_components + self.output_components + self.grain.components_copy
1891
- )
1809
+ return self.input_components + self.output_components
1892
1810
 
1893
1811
  def to_datasource(
1894
1812
  self,
@@ -1938,55 +1856,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1938
1856
  column.concept = column.concept.with_grain(new_datasource.grain)
1939
1857
  return new_datasource
1940
1858
 
1941
- @property
1942
- def grain(self) -> "Grain":
1943
- output = []
1944
- for item in self.output_components:
1945
- if item.purpose == Purpose.KEY:
1946
- output.append(item)
1947
- # if self.where_clause:
1948
- # for item in self.where_clause.concept_arguments:
1949
- # if item.purpose == Purpose.KEY:
1950
- # output.append(item)
1951
- # elif item.purpose == Purpose.PROPERTY and item.grain:
1952
- # output += item.grain.components
1953
- # TODO: handle other grain cases
1954
- # new if block by design
1955
- # add back any purpose that is not at the grain
1956
- # if a query already has the key of the property in the grain
1957
- # we want to group to that grain and ignore the property, which is a derivation
1958
- # otherwise, we need to include property as the group by
1959
- for item in self.output_components:
1960
- if (
1961
- item.purpose == Purpose.PROPERTY
1962
- and item.grain
1963
- and (
1964
- not item.grain.components
1965
- or not item.grain.issubset(
1966
- Grain(components=unique(output, "address"))
1967
- )
1968
- )
1969
- ):
1970
- output.append(item)
1971
- if (
1972
- item.purpose == Purpose.CONSTANT
1973
- and item.derivation != PurposeLineage.CONSTANT
1974
- and item.grain
1975
- and (
1976
- not item.grain.components
1977
- or not item.grain.issubset(
1978
- Grain(components=unique(output, "address"))
1979
- )
1980
- )
1981
- ):
1982
- output.append(item)
1983
- # TODO: explore implicit filtering more
1984
- # if self.where_clause.conditional and self.where_clause_category == SelectFiltering.IMPLICIT:
1985
- # output =[x.with_filter(self.where_clause.conditional) for x in output]
1986
- return Grain(
1987
- components=unique(output, "address"), where_clause=self.where_clause
1988
- )
1989
-
1990
1859
  def with_namespace(self, namespace: str) -> "SelectStatement":
1991
1860
  return SelectStatement(
1992
1861
  selection=[c.with_namespace(namespace) for c in self.selection],
@@ -2203,7 +2072,7 @@ def safe_grain(v) -> Grain:
2203
2072
  elif isinstance(v, Grain):
2204
2073
  return v
2205
2074
  elif not v:
2206
- return Grain(components=[])
2075
+ return Grain(components=set())
2207
2076
  else:
2208
2077
  raise ValueError(f"Invalid input type to safe_grain {type(v)}")
2209
2078
 
@@ -2235,7 +2104,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2235
2104
  columns: List[ColumnAssignment]
2236
2105
  address: Union[Address, str]
2237
2106
  grain: Grain = Field(
2238
- default_factory=lambda: Grain(components=[]), validate_default=True
2107
+ default_factory=lambda: Grain(components=set()), validate_default=True
2239
2108
  )
2240
2109
  namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
2241
2110
  metadata: DatasourceMetadata = Field(
@@ -2323,8 +2192,8 @@ class Datasource(HasUUID, Namespaced, BaseModel):
2323
2192
  grain: Grain = safe_grain(v)
2324
2193
  if not grain.components:
2325
2194
  columns: List[ColumnAssignment] = values.get("columns", [])
2326
- grain = Grain(
2327
- components=[
2195
+ grain = Grain.from_concepts(
2196
+ [
2328
2197
  c.concept.with_grain(Grain())
2329
2198
  for c in columns
2330
2199
  if c.concept.purpose == Purpose.KEY
@@ -2758,15 +2627,11 @@ class QueryDatasource(BaseModel):
2758
2627
  @property
2759
2628
  def identifier(self) -> str:
2760
2629
  filters = abs(hash(str(self.condition))) if self.condition else ""
2761
- grain = "_".join(
2762
- [str(c.address).replace(".", "_") for c in self.grain.components]
2763
- )
2764
- # partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
2630
+ grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
2765
2631
  return (
2766
2632
  "_join_".join([d.identifier for d in self.datasources])
2767
2633
  + (f"_at_{grain}" if grain else "_at_abstract")
2768
2634
  + (f"_filtered_by_{filters}" if filters else "")
2769
- # + (f"_partial_{partial}" if partial else "")
2770
2635
  )
2771
2636
 
2772
2637
  def get_alias(
@@ -3105,12 +2970,16 @@ class CTE(BaseModel):
3105
2970
  for cte in self.parent_ctes:
3106
2971
  if address in cte.output_columns:
3107
2972
  match = [x for x in cte.output_columns if x.address == address].pop()
3108
- return match
2973
+ if match:
2974
+ return match
3109
2975
 
3110
2976
  for array in [self.source.input_concepts, self.source.output_concepts]:
3111
2977
  match_list = [x for x in array if x.address == address]
3112
2978
  if match_list:
3113
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()
3114
2983
  return None
3115
2984
 
3116
2985
  def get_alias(self, concept: Concept, source: str | None = None) -> str:
@@ -3119,8 +2988,10 @@ class CTE(BaseModel):
3119
2988
  if source and source != cte.name:
3120
2989
  continue
3121
2990
  return concept.safe_address
2991
+
3122
2992
  try:
3123
2993
  source = self.source.get_alias(concept, source=source)
2994
+
3124
2995
  if not source:
3125
2996
  raise ValueError("No source found")
3126
2997
  return source
@@ -3133,7 +3004,8 @@ class CTE(BaseModel):
3133
3004
  if len(self.source_map.get(c.address, [])) > 0:
3134
3005
  return False
3135
3006
  if c.derivation == PurposeLineage.ROWSET:
3136
- return False
3007
+ assert isinstance(c.lineage, RowsetItem)
3008
+ return check_is_not_in_group(c.lineage.content)
3137
3009
  if c.derivation == PurposeLineage.CONSTANT:
3138
3010
  return False
3139
3011
  if c.purpose == Purpose.METRIC:
@@ -3330,7 +3202,6 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
3330
3202
  if self.address in local_concepts:
3331
3203
  rval = local_concepts[self.address]
3332
3204
  rval = rval.with_select_context(local_concepts, grain, environment)
3333
-
3334
3205
  return rval
3335
3206
  environment.concepts.raise_undefined(self.address, line_no=self.line_no)
3336
3207
 
@@ -4462,7 +4333,7 @@ class AggregateWrapper(Mergeable, Namespaced, SelectContext, BaseModel):
4462
4333
  self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
4463
4334
  ) -> AggregateWrapper:
4464
4335
  if not self.by:
4465
- by = grain.components_copy
4336
+ by = [environment.concepts[c] for c in grain.components]
4466
4337
  else:
4467
4338
  by = [
4468
4339
  x.with_select_context(local_concepts, grain, environment)
@@ -4511,16 +4382,6 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
4511
4382
  )
4512
4383
  )
4513
4384
 
4514
- @property
4515
- def grain(self) -> Grain:
4516
- output = []
4517
- for item in self.input:
4518
- if item.purpose == Purpose.KEY:
4519
- output.append(item)
4520
- elif item.purpose == Purpose.PROPERTY:
4521
- output += item.grain.components if item.grain else []
4522
- return Grain(components=list(set(output)))
4523
-
4524
4385
  @property
4525
4386
  def components(self):
4526
4387
  from trilogy.core.processing.utility import decompose_condition
@@ -4656,7 +4517,7 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
4656
4517
  )
4657
4518
  orig[orig_concept.address] = new_concept
4658
4519
  output.append(new_concept)
4659
- default_grain = Grain(components=[*output])
4520
+ default_grain = Grain.from_concepts([*output])
4660
4521
  # remap everything to the properties of the rowset
4661
4522
  for x in output:
4662
4523
  if x.keys:
@@ -4668,9 +4529,9 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
4668
4529
  # TODO: fix this up
4669
4530
  x.keys = tuple()
4670
4531
  for x in output:
4671
- 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]):
4672
4533
  x.grain = Grain(
4673
- components=[orig[c.address] for c in x.grain.components_copy]
4534
+ components={orig[c].address for c in x.grain.components}
4674
4535
  )
4675
4536
  else:
4676
4537
  x.grain = default_grain