pytrilogy 0.0.3.80__py3-none-any.whl → 0.0.3.82__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.80
3
+ Version: 0.0.3.82
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.80.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=huHsvMxTqEsRG3KLL_gETa2EIT-un4_yasKk03J5Mgs,303
1
+ pytrilogy-0.0.3.82.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=bYiPbjWf72hFcdkZbNsSKQsD0uR8NWYSSfzF26YTpfM,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
@@ -11,19 +11,19 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
11
11
  trilogy/authoring/__init__.py,sha256=e74k-Jep4DLYPQU_2m0aVsYlw5HKTOucAKtdTbd6f2g,2595
12
12
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
14
- trilogy/core/enums.py,sha256=tDUXVWxvWgnO1u7LLS0iIReeHYbyFLN2ZbpTdUyKdjE,8060
14
+ trilogy/core/enums.py,sha256=kuwTeEur5TZ07DX8nO3fSiRQ32bLZ5jRK-vIjHbBUnc,8118
15
15
  trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
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=IMMq2xE-HjD9gaez6-otgnHZkLSjnlROzfdgZyUxkpo,30960
19
+ trilogy/core/functions.py,sha256=9Q-c7UGxOR0SVwzcHt9mHX_aVroW9jEWwJr7BtHigw8,31766
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=9gQ2UBJ2qZ4ngFOXei4VbYQAp8UTrcAr_5GvWW2O2l8,78192
26
+ trilogy/core/models/author.py,sha256=Xt_TRVl4xHF6fe-KTKPSt4Z8Srmplnv2KQe2grZXX3w,78411
27
27
  trilogy/core/models/build.py,sha256=0TRcL__hvajLcpFwYeLjCV35cow5oVPbJHXA3oO09XA,65931
28
28
  trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
29
29
  trilogy/core/models/core.py,sha256=q-8ExAaSB3PoSDX2XOqfZ01HBBvT1pZFs7jGg1vZizc,11096
@@ -35,21 +35,22 @@ trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnb
35
35
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
36
36
  trilogy/core/optimizations/predicate_pushdown.py,sha256=g4AYE8Aw_iMlAh68TjNXGP754NTurrDduFECkUjoBnc,9399
37
37
  trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- trilogy/core/processing/concept_strategies_v3.py,sha256=3Hy8Lz5NOJt8B3cGv_B0LuOYrlcxM_WiPdsFaFWaMjE,23212
38
+ trilogy/core/processing/concept_strategies_v3.py,sha256=Izo7yfR6sGHTaD17lN7ZzGYCtXA5AkXAmIp_OBjHH58,23161
39
39
  trilogy/core/processing/discovery_loop.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- trilogy/core/processing/discovery_node_factory.py,sha256=I3JJxoF-u8OVvqXXAOhvMg2h-KdpHQwg6EpCeQtxGCI,15123
41
- trilogy/core/processing/discovery_utility.py,sha256=3xdd1ypKappSDm0SJs7WtW5YegL80SlYhDQlkNePp4E,4549
42
- trilogy/core/processing/discovery_validation.py,sha256=fGWJmKpgEd1f4RkK-fYOBUT1cwsJnahwXFAdRlou7MI,5365
40
+ trilogy/core/processing/discovery_node_factory.py,sha256=5QVYUsci_h6iYWhS0GCoDow2tSAipiBW1OyTRX-g_L8,15581
41
+ trilogy/core/processing/discovery_utility.py,sha256=eY4n7_r6_R-cx_Sm8FiouMXh78v2iO2SGhi0aI5jvDg,4549
42
+ trilogy/core/processing/discovery_validation.py,sha256=eZ4HfHMpqZLI8MGG2jez8arS8THs6ceuVrQFIY6gXrU,5364
43
43
  trilogy/core/processing/graph_utils.py,sha256=8QUVrkE9j-9C1AyrCb1nQEh8daCe0u1HuXl-Te85lag,1205
44
- trilogy/core/processing/utility.py,sha256=xsuVMRFi2nY2So0yohhweI2D92wZVsTpHezS0giM4ck,22583
45
- trilogy/core/processing/node_generators/__init__.py,sha256=w8TQQgNhyAra6JQHdg1_Ags4BGyxjXYruu6UeC5yOkI,873
46
- trilogy/core/processing/node_generators/basic_node.py,sha256=n65myXmg6iOrv1LFvDEfBIcBBIhTBFZ7fOmkzxrR-gs,5462
44
+ trilogy/core/processing/utility.py,sha256=VjKhDFeItB642riF8KHwyk4YEc58nVDiPLIydwdLPcQ,22716
45
+ trilogy/core/processing/node_generators/__init__.py,sha256=iVJ-crowPxYeut-hFjyEjfibKIDq7PfB4LEuDAUCjGY,943
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
+ trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
48
49
  trilogy/core/processing/node_generators/filter_node.py,sha256=oRRq2-T3ufgn4D23uQsc58f20eFk-djs4QI3WKA75K8,10908
49
50
  trilogy/core/processing/node_generators/group_node.py,sha256=1QJhRxsTklJ5xq8wHlAURZaN9gL9FPpeCa1OJ7IwXnY,6769
50
51
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
51
52
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
52
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=KtTuvL9-xaTV_ZUO1DUyz2Rd595QfD9SvOv5filCDy0,17428
53
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=bYhXGJqJDgT4JWlN4u761V3bkP96o6J25LFs6YBOiKQ,18304
53
54
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
54
55
  trilogy/core/processing/node_generators/rowset_node.py,sha256=5L5u6xz1In8EaHQdcYgR2si-tz9WB9YLXURo4AkUT9A,6630
55
56
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=Cv2GwNiYSmwewjuK8T3JB3pbgrLZFPsB75DCP153BMA,22818
@@ -76,12 +77,12 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
76
77
  trilogy/core/statements/common.py,sha256=KxEmz2ySySyZ6CTPzn0fJl5NX2KOk1RPyuUSwWhnK1g,759
77
78
  trilogy/core/statements/execute.py,sha256=pfr1CZ_Cx1qQ-7LDyRI0JUfvtxBr_GGv-VeqiAjr43g,1406
78
79
  trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- trilogy/dialect/base.py,sha256=aLJ7-Lq7TTe7I_FjMzhkC5LfrqyjN55AFD8T0lqsiRs,47202
80
+ trilogy/dialect/base.py,sha256=0nVDvsnDS8iMTIlCUsBs_ymoZMW6uh04U4MMZxOuHUw,47428
80
81
  trilogy/dialect/bigquery.py,sha256=8xhEu0z_lKANjbvzvBbC7CeKrJf1iP8YyrHqNale-ug,4351
81
82
  trilogy/dialect/common.py,sha256=tSthIZOXXRPQ4KeMKnDDsH7KlTmf2EVqigVtLyoc4zc,6071
82
83
  trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
83
84
  trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
84
- trilogy/dialect/duckdb.py,sha256=0qWQkyijz1_MnsVFyY_Iqqx0ba6E8o50s8pvLLpZH6g,5138
85
+ trilogy/dialect/duckdb.py,sha256=uqnZEd1IH4tO8FSbbUXe3Jl9EjoyrDde406k0VDuMXw,5544
85
86
  trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
86
87
  trilogy/dialect/postgres.py,sha256=el2PKwfyvWGk5EZtLudqAH5ewLitY1sFHJiocBSyxyM,3393
87
88
  trilogy/dialect/presto.py,sha256=yzSF8SZ6o1dizj1UueAa7S3lR0qNYJdSXbF78EHyhY0,3668
@@ -97,9 +98,9 @@ trilogy/parsing/common.py,sha256=yV1AckK0h8u1OFeGQBTMu-wuW5m63c5CcZuPicsTH_w,306
97
98
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
98
99
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
99
100
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
100
- trilogy/parsing/parse_engine.py,sha256=xtwWwe7EfYcmt9_lwJ0JH0AhKVcPHty97VJCpyM5beA,78068
101
+ trilogy/parsing/parse_engine.py,sha256=ll3uBwpJ2iAxT-KbbXy7OuCqUvKfLPzB1gHr8gApv_s,78591
101
102
  trilogy/parsing/render.py,sha256=HSNntD82GiiwHT-TWPLXAaIMWLYVV5B5zQEsgwrHIBE,19605
102
- trilogy/parsing/trilogy.lark,sha256=wiGXJdKfPTG7E_XdkN1rf9g9Yy1-UMVAXyTxtrBPm9w,15037
103
+ trilogy/parsing/trilogy.lark,sha256=Uhy6A00-GsC-L8fRCN0JZC6fL87ujOOd9_4LbQWApY4,15248
103
104
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
105
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
105
106
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -110,8 +111,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
110
111
  trilogy/std/net.preql,sha256=7l7MqIjs6TDCpO6dBAoNJU81Ex255jZRK36kBgE1GDs,158
111
112
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
112
113
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
113
- pytrilogy-0.0.3.80.dist-info/METADATA,sha256=7b0Krod3hNbcoOugGJ5MnklYMzmEU3Q7sViBVPxsK3I,9589
114
- pytrilogy-0.0.3.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- pytrilogy-0.0.3.80.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
- pytrilogy-0.0.3.80.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
- pytrilogy-0.0.3.80.dist-info/RECORD,,
114
+ pytrilogy-0.0.3.82.dist-info/METADATA,sha256=eFCjlA6cbwL_BeJhwzGHYJEVqKLaWB49VJSkG71s0ls,9589
115
+ pytrilogy-0.0.3.82.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
116
+ pytrilogy-0.0.3.82.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
117
+ pytrilogy-0.0.3.82.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
118
+ pytrilogy-0.0.3.82.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.80"
7
+ __version__ = "0.0.3.82"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -171,6 +171,7 @@ class FunctionType(Enum):
171
171
  RANDOM = "random"
172
172
  FLOOR = "floor"
173
173
  CEIL = "ceil"
174
+ LOG = "log"
174
175
 
175
176
  # Aggregates
176
177
  ## group is not a real aggregate - it just means group by this + some other set of fields
@@ -193,6 +194,8 @@ class FunctionType(Enum):
193
194
  SUBSTRING = "substring"
194
195
  STRPOS = "strpos"
195
196
  CONTAINS = "contains"
197
+ TRIM = "trim"
198
+ REPLACE = "replace"
196
199
 
197
200
  # STRING REGEX
198
201
  REGEXP_CONTAINS = "regexp_contains"
trilogy/core/functions.py CHANGED
@@ -358,6 +358,16 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
358
358
  output_type=DataType.INTEGER,
359
359
  arg_count=2,
360
360
  ),
361
+ FunctionType.REPLACE: FunctionConfig(
362
+ valid_inputs=[
363
+ {DataType.STRING},
364
+ {DataType.STRING},
365
+ {DataType.STRING},
366
+ ],
367
+ output_purpose=Purpose.PROPERTY,
368
+ output_type=DataType.STRING,
369
+ arg_count=3,
370
+ ),
361
371
  FunctionType.CONTAINS: FunctionConfig(
362
372
  valid_inputs=[
363
373
  {DataType.STRING},
@@ -367,6 +377,15 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
367
377
  output_type=DataType.BOOL,
368
378
  arg_count=2,
369
379
  ),
380
+ FunctionType.TRIM: FunctionConfig(
381
+ valid_inputs=[
382
+ {DataType.STRING},
383
+ {DataType.STRING},
384
+ ],
385
+ output_purpose=Purpose.PROPERTY,
386
+ output_type=DataType.BOOL,
387
+ arg_count=2,
388
+ ),
370
389
  FunctionType.SUBSTRING: FunctionConfig(
371
390
  valid_inputs=[{DataType.STRING}, {DataType.INTEGER}, {DataType.INTEGER}],
372
391
  output_purpose=Purpose.PROPERTY,
@@ -688,6 +707,15 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
688
707
  output_type=DataType.INTEGER,
689
708
  arg_count=1,
690
709
  ),
710
+ FunctionType.LOG: FunctionConfig(
711
+ valid_inputs=[
712
+ {DataType.INTEGER, DataType.FLOAT, DataType.NUMBER, DataType.NUMERIC},
713
+ {DataType.INTEGER},
714
+ ],
715
+ output_purpose=Purpose.PROPERTY,
716
+ output_type=DataType.FLOAT,
717
+ arg_count=2,
718
+ ),
691
719
  FunctionType.RANDOM: FunctionConfig(
692
720
  valid_inputs=[],
693
721
  output_purpose=Purpose.PROPERTY,
@@ -1605,6 +1605,10 @@ def get_concept_arguments(expr) -> List["ConceptRef"]:
1605
1605
  return output
1606
1606
 
1607
1607
 
1608
+ def args_to_pretty(input: set[DataType]) -> str:
1609
+ return ", ".join(sorted([f"'{x.value}'" for x in input if x != DataType.UNKNOWN]))
1610
+
1611
+
1608
1612
  class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1609
1613
  operator: FunctionType
1610
1614
  arg_count: int = Field(default=1)
@@ -1669,9 +1673,10 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1669
1673
  and get_basic_type(arg.datatype.data_type) not in valid_inputs[idx]
1670
1674
  ):
1671
1675
  if arg.datatype != DataType.UNKNOWN:
1676
+
1672
1677
  raise TypeError(
1673
- f"Invalid input datatype {arg.datatype.data_type} passed into position {idx}"
1674
- f" for {operator_name} from concept {arg.name}, valid is {valid_inputs[idx]}"
1678
+ f"Invalid argument type '{arg.datatype.data_type.value}' passed into {operator_name} function in position {idx+1}"
1679
+ f" from concept: {arg.name}. Valid: {args_to_pretty(valid_inputs[idx])}."
1675
1680
  )
1676
1681
  if (
1677
1682
  isinstance(arg, Function)
@@ -1679,8 +1684,8 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1679
1684
  ):
1680
1685
  if arg.output_datatype != DataType.UNKNOWN:
1681
1686
  raise TypeError(
1682
- f"Invalid input datatype {arg.output_datatype} passed into"
1683
- f" {operator_name} from function {arg.operator.name}, need {valid_inputs[idx]}"
1687
+ f"Invalid argument type {arg.output_datatype}' passed into"
1688
+ f" {operator_name} function from function {arg.operator.name} in position {idx+1}. Valid: {args_to_pretty(valid_inputs[idx])}"
1684
1689
  )
1685
1690
  # check constants
1686
1691
  comparisons: List[Tuple[Type, DataType]] = [
@@ -63,6 +63,11 @@ def generate_candidates_restrictive(
63
63
  and x.address not in priority_concept.pseudonyms
64
64
  and priority_concept.address not in x.pseudonyms
65
65
  ]
66
+
67
+ # if it's single row, joins are irrelevant. Fetch without keys.
68
+ if priority_concept.granularity == Granularity.SINGLE_ROW:
69
+ return [], conditions
70
+
66
71
  if conditions and priority_concept.derivation in ROOT_DERIVATIONS:
67
72
  logger.info(
68
73
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Injecting additional conditional row arguments as all remaining concepts are roots or constant"
@@ -72,9 +77,6 @@ def generate_candidates_restrictive(
72
77
  unique(list(conditions.row_arguments) + local_candidates, "address"),
73
78
  None,
74
79
  )
75
- # if it's single row, joins are irrelevant. Fetch without keys.
76
- if priority_concept.granularity == Granularity.SINGLE_ROW:
77
- return [], conditions
78
80
 
79
81
  return local_candidates, conditions
80
82
 
@@ -324,7 +326,6 @@ def check_for_early_exit(
324
326
  f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Not complete (missing {missing}), continuing search"
325
327
  )
326
328
  # if we have attempted on root node, we've tried them all.
327
- # inject in another search with filter concepts
328
329
  if priority_concept.derivation == Derivation.ROOT:
329
330
  logger.info(
330
331
  f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as attempted root with no results"
@@ -364,7 +365,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
364
365
  for x in context.stack
365
366
  }
366
367
  logger.info(
367
- f"Condition {context.conditions} not required, parents included filtering! {parent_map }"
368
+ f"Condition {context.conditions} not required, parents included filtering! {parent_map}"
368
369
  )
369
370
  if len(context.stack) == 1:
370
371
  output: StrategyNode = context.stack[0]
@@ -12,6 +12,7 @@ from trilogy.core.models.build_environment import BuildEnvironment
12
12
  from trilogy.core.processing.discovery_utility import LOGGER_PREFIX, depth_to_prefix
13
13
  from trilogy.core.processing.node_generators import (
14
14
  gen_basic_node,
15
+ gen_constant_node,
15
16
  gen_filter_node,
16
17
  gen_group_node,
17
18
  gen_group_to_node,
@@ -252,6 +253,21 @@ def _generate_basic_node(ctx: NodeGenerationContext) -> StrategyNode | None:
252
253
  )
253
254
 
254
255
 
256
+ def _generate_constant_node(ctx: NodeGenerationContext) -> StrategyNode | None:
257
+ ctx.log_generation("constant")
258
+ return gen_constant_node(
259
+ ctx.concept,
260
+ ctx.local_optional,
261
+ history=ctx.history,
262
+ environment=ctx.environment,
263
+ g=ctx.g,
264
+ depth=ctx.next_depth,
265
+ source_concepts=ctx.source_concepts,
266
+ conditions=ctx.conditions,
267
+ accept_partial=ctx.accept_partial,
268
+ )
269
+
270
+
255
271
  class RootNodeHandler:
256
272
  """Handles complex root node generation logic."""
257
273
 
@@ -469,7 +485,7 @@ def generate_node(
469
485
  Derivation.GROUP_TO: lambda: _generate_group_to_node(context),
470
486
  Derivation.BASIC: lambda: _generate_basic_node(context),
471
487
  Derivation.ROOT: lambda: RootNodeHandler(context).generate(),
472
- Derivation.CONSTANT: lambda: RootNodeHandler(context).generate(),
488
+ Derivation.CONSTANT: lambda: _generate_constant_node(context),
473
489
  }
474
490
 
475
491
  handler = derivation_handlers.get(concept.derivation)
@@ -73,11 +73,11 @@ def get_priority_concept(
73
73
  + [c for c in remaining_concept if c.derivation == Derivation.RECURSIVE]
74
74
  + [c for c in remaining_concept if c.derivation == Derivation.BASIC]
75
75
  + [c for c in remaining_concept if c.derivation == Derivation.GROUP_TO]
76
+ + [c for c in remaining_concept if c.derivation == Derivation.CONSTANT]
76
77
  # finally our plain selects
77
78
  + [
78
79
  c for c in remaining_concept if c.derivation == Derivation.ROOT
79
80
  ] # and any non-single row constants
80
- + [c for c in remaining_concept if c.derivation == Derivation.CONSTANT]
81
81
  )
82
82
 
83
83
  priority += [
@@ -105,7 +105,6 @@ def validate_stack(
105
105
 
106
106
  for concept in resolved.output_concepts:
107
107
  if concept.address in resolved.hidden_concepts:
108
-
109
108
  continue
110
109
 
111
110
  validate_concept(
@@ -1,4 +1,5 @@
1
1
  from .basic_node import gen_basic_node
2
+ from .constant_node import gen_constant_node
2
3
  from .filter_node import gen_filter_node
3
4
  from .group_node import gen_group_node
4
5
  from .group_to_node import gen_group_to_node
@@ -26,4 +27,5 @@ __all__ = [
26
27
  "gen_multiselect_node",
27
28
  "gen_synonym_node",
28
29
  "gen_recursive_node",
30
+ "gen_constant_node",
29
31
  ]
@@ -97,6 +97,9 @@ def gen_basic_node(
97
97
  and not any(x.address in y.pseudonyms for y in equivalent_optional)
98
98
  and x.address not in ignored_optional
99
99
  ]
100
+ logger.info(
101
+ f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has non-equivalent optional {[x.address for x in non_equivalent_optional]}"
102
+ )
100
103
  all_parents: list[BuildConcept] = unique(
101
104
  parent_concepts + non_equivalent_optional, "address"
102
105
  )
@@ -0,0 +1,38 @@
1
+ from typing import List
2
+
3
+ from trilogy.core.models.build import BuildConcept, BuildWhereClause
4
+ from trilogy.core.models.build_environment import BuildEnvironment
5
+ from trilogy.core.processing.nodes import History, StrategyNode
6
+
7
+ LOGGER_PREFIX = "[GEN_CONSTANT_NODE]"
8
+
9
+
10
+ def gen_constant_node(
11
+ concept: BuildConcept,
12
+ local_optional: List[BuildConcept],
13
+ environment: BuildEnvironment,
14
+ g,
15
+ depth: int,
16
+ source_concepts,
17
+ history: History | None = None,
18
+ conditions: BuildWhereClause | None = None,
19
+ accept_partial: bool = False,
20
+ ):
21
+ """our only goal here is to generate a row if conditions exist, or none if they do not"""
22
+
23
+ targets = [concept] + local_optional
24
+ if conditions:
25
+ targets += conditions.row_arguments
26
+ parent_node: StrategyNode | None = source_concepts(
27
+ mandatory_list=targets,
28
+ environment=environment,
29
+ g=g,
30
+ depth=depth + 1,
31
+ history=history,
32
+ conditions=conditions,
33
+ accept_partial=accept_partial,
34
+ )
35
+ if not parent_node:
36
+ return None
37
+ parent_node.set_output_concepts([concept] + local_optional)
38
+ return parent_node
@@ -94,31 +94,45 @@ def determine_induced_minimal_nodes(
94
94
  # inclusion of aggregates can create ambiguous node relation chains
95
95
  # there may be a better way to handle this
96
96
  # can be revisited if we need to connect a derived synonym based on an aggregate
97
- if lookup.derivation in (Derivation.CONSTANT, Derivation.AGGREGATE):
97
+ if lookup.derivation in (
98
+ Derivation.CONSTANT,
99
+ Derivation.AGGREGATE,
100
+ Derivation.FILTER,
101
+ ):
98
102
  nodes_to_remove.append(node)
99
103
  # purge a node if we're already looking for all it's parents
100
104
  if filter_downstream and lookup.derivation not in (Derivation.ROOT,):
101
105
  nodes_to_remove.append(node)
102
-
103
- H.remove_nodes_from(nodes_to_remove)
104
-
105
- H.remove_nodes_from(list(nx.isolates(H)))
106
+ if nodes_to_remove:
107
+ logger.debug(f"Removing nodes {nodes_to_remove} from graph")
108
+ H.remove_nodes_from(nodes_to_remove)
109
+ isolates = list(nx.isolates(H))
110
+ if isolates:
111
+ logger.debug(f"Removing isolates {isolates} from graph")
112
+ H.remove_nodes_from(isolates)
106
113
 
107
114
  zero_out = list(x for x in H.nodes if G.out_degree(x) == 0 and x not in nodelist)
108
115
  while zero_out:
116
+ logger.debug(f"Removing zero out nodes {zero_out} from graph")
109
117
  H.remove_nodes_from(zero_out)
110
118
  zero_out = list(
111
119
  x for x in H.nodes if G.out_degree(x) == 0 and x not in nodelist
112
120
  )
113
-
114
121
  try:
115
122
  paths = nx.multi_source_dijkstra_path(H, nodelist)
116
123
  # logger.debug(f"Paths found for {nodelist}")
117
124
  except nx.exception.NodeNotFound:
118
125
  # logger.debug(f"Unable to find paths for {nodelist}- {str(e)}")
119
126
  return None
120
- H.remove_nodes_from(list(x for x in H.nodes if x not in paths))
127
+ path_removals = list(x for x in H.nodes if x not in paths)
128
+ if path_removals:
129
+ logger.debug(f"Removing paths {path_removals} from graph")
130
+ H.remove_nodes_from(path_removals)
131
+ logger.debug(f"Graph after path removal {H.nodes}")
121
132
  sG: nx.Graph = ax.steinertree.steiner_tree(H, nodelist).copy()
133
+ if not sG.nodes:
134
+ logger.debug(f"No Steiner tree found for nodes {nodelist}")
135
+ return None
122
136
  # logger.debug(f"Steiner tree found for nodes {nodelist} {sG.nodes}")
123
137
  final: nx.DiGraph = nx.subgraph(G, sG.nodes).copy()
124
138
 
@@ -293,7 +307,8 @@ def resolve_weak_components(
293
307
  if not nx.is_weakly_connected(g):
294
308
  break_flag = True
295
309
  continue
296
-
310
+ # from trilogy.hooks.graph_hook import GraphHook
311
+ # GraphHook().query_graph_built(g, highlight_nodes=[concept_to_node(c.with_default_grain()) for c in all_concepts if "__preql_internal" not in c.address])
297
312
  all_graph_concepts = [
298
313
  extract_concept(extract_address(node), environment)
299
314
  for node in g.nodes
@@ -10,6 +10,7 @@ from trilogy.constants import MagicConstants
10
10
  from trilogy.core.enums import (
11
11
  BooleanOperator,
12
12
  DatePart,
13
+ Derivation,
13
14
  FunctionClass,
14
15
  Granularity,
15
16
  JoinType,
@@ -262,16 +263,19 @@ def calculate_graph_relevance(
262
263
  """
263
264
  relevance = 0
264
265
  for node in g.nodes:
266
+
265
267
  if node not in subset_nodes:
266
268
  continue
267
269
  if not g.nodes[node]["type"] == NodeType.CONCEPT:
268
270
  continue
269
271
  concept = [x for x in concepts if x.address == node].pop()
270
-
272
+ # debug granularity and derivation
271
273
  # a single row concept can always be crossjoined
272
274
  # therefore a graph with only single row concepts is always relevant
273
275
  if concept.granularity == Granularity.SINGLE_ROW:
274
276
  continue
277
+ if concept.derivation == Derivation.CONSTANT:
278
+ continue
275
279
  # if it's an aggregate up to an arbitrary grain, it can be joined in later
276
280
  # and can be ignored in subgraph
277
281
  if concept.purpose == Purpose.METRIC:
@@ -284,7 +288,6 @@ def calculate_graph_relevance(
284
288
  continue
285
289
  # Added 2023-10-18 since we seemed to be strangely dropping things
286
290
  relevance += 1
287
-
288
291
  return relevance
289
292
 
290
293
 
trilogy/dialect/base.py CHANGED
@@ -205,6 +205,9 @@ FUNCTION_MAP = {
205
205
  FunctionType.MOD: lambda x: f"({x[0]} % {x[1]})",
206
206
  FunctionType.SQRT: lambda x: f"sqrt({x[0]})",
207
207
  FunctionType.RANDOM: lambda x: "random()",
208
+ FunctionType.LOG: lambda x: (
209
+ f"log({x[0]})" if x[1] == 10 else f"log({x[0]}, {x[1]})"
210
+ ),
208
211
  # aggregate types
209
212
  FunctionType.COUNT_DISTINCT: lambda x: f"count(distinct {x[0]})",
210
213
  FunctionType.COUNT: lambda x: f"count({x[0]})",
@@ -224,6 +227,8 @@ FUNCTION_MAP = {
224
227
  FunctionType.REGEXP_CONTAINS: lambda x: f"REGEXP_CONTAINS({x[0]},{x[1]})",
225
228
  FunctionType.REGEXP_EXTRACT: lambda x: f"REGEXP_EXTRACT({x[0]},{x[1]})",
226
229
  FunctionType.REGEXP_REPLACE: lambda x: f"REGEXP_REPLACE({x[0]},{x[1]}, {x[2]})",
230
+ FunctionType.TRIM: lambda x: f"TRIM({x[0]})",
231
+ FunctionType.REPLACE: lambda x: f"REPLACE({x[0]},{x[1]},{x[2]})",
227
232
  # FunctionType.NOT_LIKE: lambda x: f" CASE WHEN {x[0]} like {x[1]} THEN 0 ELSE 1 END",
228
233
  # date types
229
234
  FunctionType.DATE_TRUNCATE: lambda x: f"date_trunc({x[0]},{x[1]})",
trilogy/dialect/duckdb.py CHANGED
@@ -33,11 +33,25 @@ def render_sort(args):
33
33
  return f"list_sort({args[0]}, '{order[0]}', '{order[1]}')"
34
34
 
35
35
 
36
+ def render_log(args):
37
+ if len(args) == 1:
38
+ return f"log({args[0]})"
39
+ elif len(args) == 2:
40
+ if int(args[1]) == 10:
41
+ return f"log({args[0]})"
42
+ else:
43
+ # change of base formula
44
+ return f"log({args[0]})/log({args[1]})"
45
+ else:
46
+ raise ValueError("log function requires 1 or 2 arguments")
47
+
48
+
36
49
  FUNCTION_MAP = {
37
50
  FunctionType.COUNT: lambda args: f"count({args[0]})",
38
51
  FunctionType.SUM: lambda args: f"sum({args[0]})",
39
52
  FunctionType.AVG: lambda args: f"avg({args[0]})",
40
53
  FunctionType.LENGTH: lambda args: f"length({args[0]})",
54
+ FunctionType.LOG: lambda args: render_log(args),
41
55
  FunctionType.LIKE: lambda args: (
42
56
  f" CASE WHEN {args[0]} like {args[1]} THEN True ELSE False END"
43
57
  ),
@@ -1793,10 +1793,18 @@ class ParseToObjects(Transformer):
1793
1793
  def fstrpos(self, meta, args):
1794
1794
  return self.function_factory.create_function(args, FunctionType.STRPOS, meta)
1795
1795
 
1796
+ @v_args(meta=True)
1797
+ def freplace(self, meta, args):
1798
+ return self.function_factory.create_function(args, FunctionType.REPLACE, meta)
1799
+
1796
1800
  @v_args(meta=True)
1797
1801
  def fcontains(self, meta, args):
1798
1802
  return self.function_factory.create_function(args, FunctionType.CONTAINS, meta)
1799
1803
 
1804
+ @v_args(meta=True)
1805
+ def ftrim(self, meta, args):
1806
+ return self.function_factory.create_function(args, FunctionType.TRIM, meta)
1807
+
1800
1808
  @v_args(meta=True)
1801
1809
  def fsubstring(self, meta, args):
1802
1810
  return self.function_factory.create_function(args, FunctionType.SUBSTRING, meta)
@@ -1971,6 +1979,12 @@ class ParseToObjects(Transformer):
1971
1979
  args.append(0)
1972
1980
  return self.function_factory.create_function(args, FunctionType.ROUND, meta)
1973
1981
 
1982
+ @v_args(meta=True)
1983
+ def flog(self, meta, args) -> Function:
1984
+ if len(args) == 1:
1985
+ args.append(10)
1986
+ return self.function_factory.create_function(args, FunctionType.LOG, meta)
1987
+
1974
1988
  @v_args(meta=True)
1975
1989
  def ffloor(self, meta, args) -> Function:
1976
1990
  return self.function_factory.create_function(args, FunctionType.FLOOR, meta)
@@ -2074,9 +2088,9 @@ def unpack_visit_error(e: VisitError, text: str | None = None):
2074
2088
  extract = text[e.obj.meta.start_pos - 5 : e.obj.meta.end_pos + 5]
2075
2089
  raise InvalidSyntaxException(
2076
2090
  str(e.orig_exc)
2077
- + " in "
2091
+ + " Raised when parsing rule: "
2078
2092
  + str(e.rule)
2079
- + f" Line: {e.obj.meta.line} ({extract})"
2093
+ + f' Line: {e.obj.meta.line} "...{extract}..."'
2080
2094
  )
2081
2095
  InvalidSyntaxException(
2082
2096
  str(e.orig_exc) + " in " + str(e.rule) + f" Line: {e.obj.meta.line}"
@@ -222,6 +222,8 @@
222
222
  fmul: ("multiply"i "(" expr "," expr ")" )
223
223
  fdiv: ( "divide"i "(" expr "," expr ")")
224
224
  fmod: ( "mod"i "(" expr "," (int_lit | concept_lit ) ")")
225
+ _LOG.1: "log"i "("
226
+ flog: _LOG expr ("," expr)? ")"
225
227
  _ROUND.1: "round"i "("
226
228
  fround: _ROUND expr ("," expr)? ")"
227
229
  _FLOOR.1: "floor"i "("
@@ -234,7 +236,7 @@
234
236
  _RANDOM.1: "random("i
235
237
  frandom: _RANDOM expr ")"
236
238
 
237
- _math_functions: fmul | fdiv | fadd | fsub | fround | ffloor | fceil | fmod | fabs | fsqrt | frandom
239
+ _math_functions: fmul | fdiv | fadd | fsub | fround | ffloor | fceil | fmod | flog | fabs | fsqrt | frandom
238
240
 
239
241
  //generic
240
242
  _fcast_primary: "cast"i "(" expr "as"i data_type ")"
@@ -278,6 +280,10 @@
278
280
  fstrpos: _STRPOS expr "," expr ")"
279
281
  _CONTAINS.1: "contains("i
280
282
  fcontains: _CONTAINS expr "," expr ")"
283
+ _TRIM.1: "trim("i
284
+ ftrim: _TRIM expr ")"
285
+ _REPLACE.1: "replace("i
286
+ freplace: _REPLACE expr "," expr "," expr ")"
281
287
  _SUBSTRING.1: "substring("i
282
288
  fsubstring: _SUBSTRING expr "," expr "," expr ")"
283
289
  _REGEXP_EXTRACT.1: "regexp_extract("
@@ -287,7 +293,7 @@
287
293
  _REGEXP_REPLACE.1: "regexp_replace("
288
294
  fregexp_replace: _REGEXP_REPLACE expr "," expr "," expr ")"
289
295
 
290
- _string_functions: like | ilike | upper | flower | fsplit | fstrpos | fsubstring | fcontains | fregexp_extract | fregexp_contains | fregexp_replace
296
+ _string_functions: like | ilike | upper | flower | fsplit | fstrpos | fsubstring | fcontains | ftrim | freplace | fregexp_extract | fregexp_contains | fregexp_replace
291
297
 
292
298
  //array_functions
293
299
  _ARRAY_SUM.1: "array_sum("i