pytrilogy 0.0.3.96__py3-none-any.whl → 0.0.3.98__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.
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/METADATA +16 -2
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/RECORD +26 -24
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -0
- trilogy/core/enums.py +9 -0
- trilogy/core/exceptions.py +56 -2
- trilogy/core/functions.py +1 -1
- trilogy/core/models/execute.py +0 -1
- trilogy/core/processing/node_generators/node_merge_node.py +109 -5
- trilogy/core/validation/common.py +53 -2
- trilogy/core/validation/concept.py +25 -4
- trilogy/core/validation/datasource.py +22 -20
- trilogy/core/validation/environment.py +3 -1
- trilogy/core/validation/fix.py +106 -0
- trilogy/dialect/base.py +2 -1
- trilogy/dialect/common.py +2 -10
- trilogy/dialect/metadata.py +1 -1
- trilogy/executor.py +7 -0
- trilogy/parsing/parse_engine.py +10 -8
- trilogy/parsing/render.py +32 -5
- trilogy/parsing/trilogy.lark +7 -4
- trilogy/std/metric.preql +15 -0
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.96.dist-info → pytrilogy-0.0.3.98.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytrilogy
|
|
3
|
-
Version: 0.0.3.
|
|
3
|
+
Version: 0.0.3.98
|
|
4
4
|
Summary: Declarative, typed query language that compiles to SQL.
|
|
5
5
|
Home-page:
|
|
6
6
|
Author:
|
|
@@ -334,7 +334,21 @@ from pytrilogy.authoring import Concept, Function, ...
|
|
|
334
334
|
|
|
335
335
|
Are likely to be unstable. Open an issue if you need to take dependencies on other modules outside those two paths.
|
|
336
336
|
|
|
337
|
-
##
|
|
337
|
+
## MCP/Server
|
|
338
|
+
|
|
339
|
+
Trilogy is straightforward to run as a server/MCP server; the former to generate SQL on demand and integrate into other tools, and MCP
|
|
340
|
+
for full interactive query loops.
|
|
341
|
+
|
|
342
|
+
This makes it easy to integrate Trilogy into existing tools or workflows.
|
|
343
|
+
|
|
344
|
+
You can see examples of both use cases in the trilogy-studio codebase [here](https://github.com/trilogy-data/trilogy-studio-core)
|
|
345
|
+
and install and run an MCP server directly with that codebase.
|
|
346
|
+
|
|
347
|
+
If you're interested in a more fleshed out standalone server or MCP server, please open an issue and we'll prioritize it!
|
|
348
|
+
|
|
349
|
+
## Trilogy Syntax Reference
|
|
350
|
+
|
|
351
|
+
Not exhaustive - see [documentation](https://trilogydata.dev/) for more details.
|
|
338
352
|
|
|
339
353
|
### Import
|
|
340
354
|
```sql
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
pytrilogy-0.0.3.
|
|
2
|
-
trilogy/__init__.py,sha256=
|
|
3
|
-
trilogy/constants.py,sha256=
|
|
1
|
+
pytrilogy-0.0.3.98.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
2
|
+
trilogy/__init__.py,sha256=NSBY37z8wVkVSFPfHPo0BTvi3hO7KMw7ENbaoX9PqBY,303
|
|
3
|
+
trilogy/constants.py,sha256=SSsRMg9HTou259nMKAw-rJNBgzkWjQ3QIQXcrq9i5Kk,1717
|
|
4
4
|
trilogy/engine.py,sha256=3MiADf5MKcmxqiHBuRqiYdsXiLj7oitDfVvXvHrfjkA,2178
|
|
5
|
-
trilogy/executor.py,sha256=
|
|
5
|
+
trilogy/executor.py,sha256=0yggm9Ejl1DFELUtRaPxbaU5mpKqYMHZlzSMXOFmODE,16111
|
|
6
6
|
trilogy/parser.py,sha256=o4cfk3j3yhUFoiDKq9ZX_GjBF3dKhDjXEwb63rcBkBM,293
|
|
7
7
|
trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
trilogy/render.py,sha256=qQWwduymauOlB517UtM-VGbVe8Cswa4UJub5aGbSO6c,1512
|
|
@@ -10,12 +10,12 @@ trilogy/utility.py,sha256=euQccZLKoYBz0LNg5tzLlvv2YHvXh9HArnYp1V3uXsM,763
|
|
|
10
10
|
trilogy/authoring/__init__.py,sha256=TABMOETSMERrWuyDLR0nK4ISlqR0yaqeXrmuOdrSvAY,3060
|
|
11
11
|
trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
|
|
13
|
-
trilogy/core/enums.py,sha256=
|
|
13
|
+
trilogy/core/enums.py,sha256=H8I2Dz4POHZ4ixYCGzNs4c3KDqxLQklGLVfmje1DSMo,8877
|
|
14
14
|
trilogy/core/env_processor.py,sha256=H-rr2ALj31l5oh3FqeI47Qju6OOfiXBacXNJGNZ92zQ,4521
|
|
15
15
|
trilogy/core/environment_helpers.py,sha256=TRlqVctqIRBxzfjRBmpQsAVoiCcsEKBhG1B6PUE0l1M,12743
|
|
16
16
|
trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
|
|
17
|
-
trilogy/core/exceptions.py,sha256=
|
|
18
|
-
trilogy/core/functions.py,sha256=
|
|
17
|
+
trilogy/core/exceptions.py,sha256=fI16oTNCVMMAJFSn2AFzZVapzsF5M9WbdN5e5UixwXc,2807
|
|
18
|
+
trilogy/core/functions.py,sha256=oY-F0hsA9vp1ZipGTyx4QVtz_x83Ekk-lkHv6mMkHVQ,33095
|
|
19
19
|
trilogy/core/graph_models.py,sha256=4EWFTHGfYd72zvS2HYoV6hm7nMC_VEd7vWr6txY-ig0,3400
|
|
20
20
|
trilogy/core/internal.py,sha256=r9QagDB2GvpqlyD_I7VrsfbVfIk5mnok2znEbv72Aa4,2681
|
|
21
21
|
trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
|
|
@@ -28,7 +28,7 @@ trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYy
|
|
|
28
28
|
trilogy/core/models/core.py,sha256=EofJ8-kltNr_7oFhyCPqauVX1bSJzJI5xOp0eMP_vlA,12892
|
|
29
29
|
trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
|
|
30
30
|
trilogy/core/models/environment.py,sha256=hwTIRnJgaHUdCYof7U5A9NPitGZ2s9yxqiW5O2SaJ9Y,28759
|
|
31
|
-
trilogy/core/models/execute.py,sha256=
|
|
31
|
+
trilogy/core/models/execute.py,sha256=lQTpiuNhBT4In-oQ76ImgIoTdUbs4mmyd0J0iTOZOdw,42105
|
|
32
32
|
trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
|
|
33
33
|
trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
|
|
34
34
|
trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
|
|
@@ -49,7 +49,7 @@ trilogy/core/processing/node_generators/filter_node.py,sha256=ArBsQJl-4fWBJWCE28
|
|
|
49
49
|
trilogy/core/processing/node_generators/group_node.py,sha256=8HJ1lkOvIXfX3xoS2IMbM_wCu_mT0J_hQ7xnTaxsVlo,6611
|
|
50
50
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
|
|
51
51
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
|
|
52
|
-
trilogy/core/processing/node_generators/node_merge_node.py,sha256=
|
|
52
|
+
trilogy/core/processing/node_generators/node_merge_node.py,sha256=cBcZm3AUfx4K70MIhU0T9iKMxw_qZRVFhQ0i6qMQeoQ,21999
|
|
53
53
|
trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
|
|
54
54
|
trilogy/core/processing/node_generators/rowset_node.py,sha256=5L5u6xz1In8EaHQdcYgR2si-tz9WB9YLXURo4AkUT9A,6630
|
|
55
55
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=KQvGoNT5ZBWQ_caEomRTtG1PKZC7OPT4PKfY0QmwMGE,22270
|
|
@@ -76,19 +76,20 @@ trilogy/core/statements/build.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
76
76
|
trilogy/core/statements/common.py,sha256=VnVLULQg1TJLNUFzJaROT1tsf2ewk3IpuhvZaP36R6A,535
|
|
77
77
|
trilogy/core/statements/execute.py,sha256=kiwJcVeMa4wZR-xLfM2oYOJ9DeyJkP8An38WFyJxktM,2413
|
|
78
78
|
trilogy/core/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
|
-
trilogy/core/validation/common.py,sha256=
|
|
80
|
-
trilogy/core/validation/concept.py,sha256=
|
|
81
|
-
trilogy/core/validation/datasource.py,sha256=
|
|
82
|
-
trilogy/core/validation/environment.py,sha256=
|
|
79
|
+
trilogy/core/validation/common.py,sha256=Sd-towAX1uSDe3dK51FcVtIwVrMhayEwdHqhzeJHro0,4776
|
|
80
|
+
trilogy/core/validation/concept.py,sha256=PM2BxBxLvuBScSWZMPsDZVcOblDil5pNT0pHLcLhdPA,5242
|
|
81
|
+
trilogy/core/validation/datasource.py,sha256=d9AQNcukIRgN2spItPsXFiNtlZva-lDnfei3i06yQCE,6489
|
|
82
|
+
trilogy/core/validation/environment.py,sha256=ymvhQyt7jLK641JAAIQkqjQaAmr9C5022ILzYvDgPP0,2835
|
|
83
|
+
trilogy/core/validation/fix.py,sha256=Z818UFNLxndMTLiyhB3doLxIfnOZ-16QGvVFWuD7UsA,3750
|
|
83
84
|
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
84
|
-
trilogy/dialect/base.py,sha256=
|
|
85
|
+
trilogy/dialect/base.py,sha256=0QVHv4F0t3_gRQrZ0woFoUNKu7vaXGo-BG1l47CZUKc,49698
|
|
85
86
|
trilogy/dialect/bigquery.py,sha256=XS3hpybeowgfrOrkycAigAF3NX2YUzTzfgE6f__2fT4,4316
|
|
86
|
-
trilogy/dialect/common.py,sha256=
|
|
87
|
+
trilogy/dialect/common.py,sha256=_MarnMWRBn3VcNt3k5VUdFrwH6oHzGdNQquSpHNLq4o,5644
|
|
87
88
|
trilogy/dialect/config.py,sha256=olnyeVU5W5T6b9-dMeNAnvxuPlyc2uefb7FRME094Ec,3834
|
|
88
89
|
trilogy/dialect/dataframe.py,sha256=RUbNgReEa9g3pL6H7fP9lPTrAij5pkqedpZ99D8_5AE,1522
|
|
89
90
|
trilogy/dialect/duckdb.py,sha256=JoUvQ19WvgxoaJkGLM7DPXOd1H0394k3vBiblksQzOI,5631
|
|
90
91
|
trilogy/dialect/enums.py,sha256=FRNYQ5-w-B6-X0yXKNU5g9GowsMlERFogTC5u2nxL_s,4740
|
|
91
|
-
trilogy/dialect/metadata.py,sha256=
|
|
92
|
+
trilogy/dialect/metadata.py,sha256=p_V-MYPQ2iR6fcTjagnptCIWtsZe4fTfoS_iXpavPzY,7098
|
|
92
93
|
trilogy/dialect/postgres.py,sha256=el2PKwfyvWGk5EZtLudqAH5ewLitY1sFHJiocBSyxyM,3393
|
|
93
94
|
trilogy/dialect/presto.py,sha256=k1IaeilR3nzPC9Hp7jlAdzJ7TsuxB3LQTBQ28MYE7O8,3715
|
|
94
95
|
trilogy/dialect/snowflake.py,sha256=T6_mKfhpDazB1xQxqFLS2AJwzwzBcPYY6_qxRnAtFBs,3326
|
|
@@ -103,21 +104,22 @@ trilogy/parsing/common.py,sha256=550-L0444GUuBFdiDWkOg_DxnMXtcJFUMES2R5zlwik,310
|
|
|
103
104
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
104
105
|
trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
|
|
105
106
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
106
|
-
trilogy/parsing/parse_engine.py,sha256
|
|
107
|
-
trilogy/parsing/render.py,sha256=
|
|
108
|
-
trilogy/parsing/trilogy.lark,sha256=
|
|
107
|
+
trilogy/parsing/parse_engine.py,sha256=-3_-EoiciWGOlylKurSPlG7gYSbScjneBup7ZvjDz-c,81800
|
|
108
|
+
trilogy/parsing/render.py,sha256=tqB3GlGk3bX6AbkJjvADad2QH6n63nw1kgrpjzLX2tI,20520
|
|
109
|
+
trilogy/parsing/trilogy.lark,sha256=rM4WleeyGhoRgU-FOGcaeHOzZcYVxN4f13e_3B4OeLQ,16389
|
|
109
110
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
110
111
|
trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
|
|
111
112
|
trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
113
|
trilogy/std/date.preql,sha256=HWZm4t4HWyxr5geWRsY05RnHBVDMci8z8YA2cu0-OOw,188
|
|
113
114
|
trilogy/std/display.preql,sha256=nm7lox87Xf6lBvXCVCS6x2HskguMKzndEBucJ5pktzk,175
|
|
114
115
|
trilogy/std/geography.preql,sha256=1A9Sq5PPMBnEPPf7f-rPVYxJfsnWpQ8oV_k4Fm3H2dU,675
|
|
116
|
+
trilogy/std/metric.preql,sha256=DRECGhkMyqfit5Fl4Ut9zbWrJuSMI1iO9HikuyoBpE0,421
|
|
115
117
|
trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
|
|
116
118
|
trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
|
|
117
119
|
trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
|
|
118
120
|
trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
|
|
119
|
-
pytrilogy-0.0.3.
|
|
120
|
-
pytrilogy-0.0.3.
|
|
121
|
-
pytrilogy-0.0.3.
|
|
122
|
-
pytrilogy-0.0.3.
|
|
123
|
-
pytrilogy-0.0.3.
|
|
121
|
+
pytrilogy-0.0.3.98.dist-info/METADATA,sha256=8FfhicjKv1X47j0nL557PM_b1TisXXmAz3DG2B-PBdI,11683
|
|
122
|
+
pytrilogy-0.0.3.98.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
123
|
+
pytrilogy-0.0.3.98.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
|
|
124
|
+
pytrilogy-0.0.3.98.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
125
|
+
pytrilogy-0.0.3.98.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
trilogy/constants.py
CHANGED
trilogy/core/enums.py
CHANGED
|
@@ -82,6 +82,15 @@ class Modifier(Enum):
|
|
|
82
82
|
return Modifier.NULLABLE
|
|
83
83
|
return super()._missing_(value=strval.capitalize())
|
|
84
84
|
|
|
85
|
+
def __lt__(self, other):
|
|
86
|
+
order = [
|
|
87
|
+
Modifier.HIDDEN,
|
|
88
|
+
Modifier.PARTIAL,
|
|
89
|
+
Modifier.NULLABLE,
|
|
90
|
+
Modifier.OPTIONAL,
|
|
91
|
+
]
|
|
92
|
+
return order.index(self) < order.index(other)
|
|
93
|
+
|
|
85
94
|
|
|
86
95
|
class JoinType(Enum):
|
|
87
96
|
INNER = "inner"
|
trilogy/core/exceptions.py
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, List, Sequence
|
|
3
|
+
|
|
4
|
+
from trilogy.core.enums import Modifier
|
|
5
|
+
from trilogy.core.models.core import (
|
|
6
|
+
ArrayType,
|
|
7
|
+
DataType,
|
|
8
|
+
MapType,
|
|
9
|
+
NumericType,
|
|
10
|
+
StructType,
|
|
11
|
+
TraitDataType,
|
|
12
|
+
)
|
|
2
13
|
|
|
3
14
|
|
|
4
15
|
class UndefinedConceptException(Exception):
|
|
@@ -29,7 +40,7 @@ class ModelValidationError(Exception):
|
|
|
29
40
|
self,
|
|
30
41
|
message,
|
|
31
42
|
children: Sequence["ModelValidationError"] | None = None,
|
|
32
|
-
**kwargs
|
|
43
|
+
**kwargs,
|
|
33
44
|
):
|
|
34
45
|
super().__init__(self, message, **kwargs)
|
|
35
46
|
self.message = message
|
|
@@ -40,6 +51,49 @@ class DatasourceModelValidationError(ModelValidationError):
|
|
|
40
51
|
pass
|
|
41
52
|
|
|
42
53
|
|
|
54
|
+
class DatasourceGrainValidationError(DatasourceModelValidationError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class DatasourceColumnBindingData:
|
|
60
|
+
address: str
|
|
61
|
+
value: Any
|
|
62
|
+
value_type: (
|
|
63
|
+
DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
|
|
64
|
+
)
|
|
65
|
+
value_modifiers: List[Modifier]
|
|
66
|
+
actual_type: (
|
|
67
|
+
DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
|
|
68
|
+
)
|
|
69
|
+
actual_modifiers: List[Modifier]
|
|
70
|
+
|
|
71
|
+
def format_failure(self):
|
|
72
|
+
return f"Concept {self.address} value '{self.value}' with type {self.value_modifiers} does not conform to expected type {str(self.actual_type)} with modifiers {self.actual_modifiers}"
|
|
73
|
+
|
|
74
|
+
def is_modifier_issue(self) -> bool:
|
|
75
|
+
return len(self.value_modifiers) > 0 and any(
|
|
76
|
+
[x not in self.actual_modifiers for x in self.value_modifiers]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def is_type_issue(self) -> bool:
|
|
80
|
+
return self.value_type != self.actual_type
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DatasourceColumnBindingError(DatasourceModelValidationError):
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
address: str,
|
|
87
|
+
errors: list[DatasourceColumnBindingData],
|
|
88
|
+
message: str | None = None,
|
|
89
|
+
):
|
|
90
|
+
if not message:
|
|
91
|
+
message = f"Datasource {address} failed validation. Found rows that do not conform to types: {[failure.format_failure() for failure in errors]}"
|
|
92
|
+
super().__init__(message)
|
|
93
|
+
self.errors = errors
|
|
94
|
+
self.dataset_address = address
|
|
95
|
+
|
|
96
|
+
|
|
43
97
|
class ConceptModelValidationError(ModelValidationError):
|
|
44
98
|
pass
|
|
45
99
|
|
trilogy/core/functions.py
CHANGED
|
@@ -427,7 +427,7 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
|
|
|
427
427
|
{DataType.STRING},
|
|
428
428
|
],
|
|
429
429
|
output_purpose=Purpose.PROPERTY,
|
|
430
|
-
output_type=DataType.
|
|
430
|
+
output_type=DataType.STRING,
|
|
431
431
|
arg_count=2,
|
|
432
432
|
),
|
|
433
433
|
FunctionType.SUBSTRING: FunctionConfig(
|
trilogy/core/models/execute.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from itertools import combinations
|
|
2
|
+
from typing import Callable, List, Optional
|
|
2
3
|
|
|
3
4
|
import networkx as nx
|
|
4
5
|
from networkx.algorithms import approximation as ax
|
|
@@ -11,7 +12,12 @@ from trilogy.core.graph_models import (
|
|
|
11
12
|
concept_to_node,
|
|
12
13
|
prune_sources_for_conditions,
|
|
13
14
|
)
|
|
14
|
-
from trilogy.core.models.build import
|
|
15
|
+
from trilogy.core.models.build import (
|
|
16
|
+
BuildConcept,
|
|
17
|
+
BuildConditional,
|
|
18
|
+
BuildGrain,
|
|
19
|
+
BuildWhereClause,
|
|
20
|
+
)
|
|
15
21
|
from trilogy.core.models.build_environment import BuildEnvironment
|
|
16
22
|
from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
|
|
17
23
|
from trilogy.core.processing.utility import padding
|
|
@@ -29,7 +35,10 @@ def filter_pseudonyms_for_source(
|
|
|
29
35
|
if edge in pseudonyms:
|
|
30
36
|
lengths = {}
|
|
31
37
|
for n in edge:
|
|
32
|
-
|
|
38
|
+
try:
|
|
39
|
+
lengths[n] = nx.shortest_path_length(ds_graph, node, n)
|
|
40
|
+
except nx.NetworkXNoPath:
|
|
41
|
+
lengths[n] = 999
|
|
33
42
|
to_remove.add(max(lengths, key=lambda x: lengths.get(x, 0)))
|
|
34
43
|
for node in to_remove:
|
|
35
44
|
ds_graph.remove_node(node)
|
|
@@ -84,12 +93,104 @@ def extract_ds_components(
|
|
|
84
93
|
return graphs
|
|
85
94
|
|
|
86
95
|
|
|
96
|
+
def prune_and_merge(
|
|
97
|
+
G: nx.DiGraph,
|
|
98
|
+
keep_node_lambda: Callable[[str], bool],
|
|
99
|
+
) -> nx.DiGraph:
|
|
100
|
+
"""
|
|
101
|
+
Prune nodes of one type and create direct connections between remaining nodes.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
G: NetworkX graph
|
|
105
|
+
keep_node_type: The node type to keep
|
|
106
|
+
node_type_attr: Attribute name that stores node type
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
New graph with only keep_node_type nodes and merged connections
|
|
110
|
+
"""
|
|
111
|
+
# Get nodes to keep
|
|
112
|
+
nodes_to_keep = [n for n in G.nodes if keep_node_lambda(n)]
|
|
113
|
+
# Create new graph
|
|
114
|
+
new_graph = G.subgraph(nodes_to_keep).copy()
|
|
115
|
+
|
|
116
|
+
# Find paths between nodes to keep through removed nodes
|
|
117
|
+
nodes_to_remove = [n for n in G.nodes() if n not in nodes_to_keep]
|
|
118
|
+
|
|
119
|
+
for node_pair in combinations(nodes_to_keep, 2):
|
|
120
|
+
n1, n2 = node_pair
|
|
121
|
+
|
|
122
|
+
# Check if there's a path through removed nodes
|
|
123
|
+
try:
|
|
124
|
+
path = nx.shortest_path(G, n1, n2)
|
|
125
|
+
# If path exists and goes through nodes we're removing
|
|
126
|
+
if len(path) > 2 or any(node in nodes_to_remove for node in path[1:-1]):
|
|
127
|
+
new_graph.add_edge(n1, n2)
|
|
128
|
+
except nx.NetworkXNoPath:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
return new_graph
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def reinject_common_join_keys_v2(
|
|
135
|
+
G: ReferenceGraph,
|
|
136
|
+
final: nx.DiGraph,
|
|
137
|
+
nodelist: list[str],
|
|
138
|
+
synonyms: set[str] = set(),
|
|
139
|
+
) -> bool:
|
|
140
|
+
# when we've discovered a concept join, for each pair of ds nodes
|
|
141
|
+
# check if they have more keys in common
|
|
142
|
+
# and inject those to discovery as join conditions
|
|
143
|
+
def is_ds_node(n: str) -> bool:
|
|
144
|
+
return n.startswith("ds~")
|
|
145
|
+
|
|
146
|
+
ds_graph = prune_and_merge(final, is_ds_node)
|
|
147
|
+
injected = False
|
|
148
|
+
for datasource in ds_graph.nodes:
|
|
149
|
+
node1 = G.datasources[datasource]
|
|
150
|
+
neighbors = nx.all_neighbors(ds_graph, datasource)
|
|
151
|
+
for neighbor in neighbors:
|
|
152
|
+
node2 = G.datasources[neighbor]
|
|
153
|
+
common_concepts = set(
|
|
154
|
+
x.concept.address for x in node1.columns
|
|
155
|
+
).intersection(set(x.concept.address for x in node2.columns))
|
|
156
|
+
concrete_concepts = [
|
|
157
|
+
x.concept for x in node1.columns if x.concept.address in common_concepts
|
|
158
|
+
]
|
|
159
|
+
reduced = BuildGrain.from_concepts(concrete_concepts).components
|
|
160
|
+
existing_addresses = set()
|
|
161
|
+
for concrete in concrete_concepts:
|
|
162
|
+
logger.info(
|
|
163
|
+
f"looking at column {concrete.address} with pseudonyms {concrete.pseudonyms}"
|
|
164
|
+
)
|
|
165
|
+
cnode = concept_to_node(concrete.with_default_grain())
|
|
166
|
+
if cnode in final.nodes:
|
|
167
|
+
existing_addresses.add(concrete.address)
|
|
168
|
+
continue
|
|
169
|
+
for concrete in concrete_concepts:
|
|
170
|
+
if concrete.address in synonyms:
|
|
171
|
+
continue
|
|
172
|
+
if concrete.address not in reduced:
|
|
173
|
+
continue
|
|
174
|
+
# skip anything that is already in the graph pseudonyms
|
|
175
|
+
if any(x in concrete.pseudonyms for x in existing_addresses):
|
|
176
|
+
continue
|
|
177
|
+
cnode = concept_to_node(concrete.with_default_grain())
|
|
178
|
+
final.add_edge(datasource, cnode)
|
|
179
|
+
final.add_edge(neighbor, cnode)
|
|
180
|
+
logger.info(
|
|
181
|
+
f"{LOGGER_PREFIX} reinjecting common join key {cnode} between {datasource} and {neighbor}"
|
|
182
|
+
)
|
|
183
|
+
injected = True
|
|
184
|
+
return injected
|
|
185
|
+
|
|
186
|
+
|
|
87
187
|
def determine_induced_minimal_nodes(
|
|
88
188
|
G: ReferenceGraph,
|
|
89
189
|
nodelist: list[str],
|
|
90
190
|
environment: BuildEnvironment,
|
|
91
191
|
filter_downstream: bool,
|
|
92
192
|
accept_partial: bool = False,
|
|
193
|
+
synonyms: set[str] = set(),
|
|
93
194
|
) -> nx.DiGraph | None:
|
|
94
195
|
H: nx.Graph = nx.to_undirected(G).copy()
|
|
95
196
|
nodes_to_remove = []
|
|
@@ -129,7 +230,7 @@ def determine_induced_minimal_nodes(
|
|
|
129
230
|
return None
|
|
130
231
|
path_removals = list(x for x in H.nodes if x not in paths)
|
|
131
232
|
if path_removals:
|
|
132
|
-
logger.debug(f"Removing paths {path_removals} from graph")
|
|
233
|
+
# logger.debug(f"Removing paths {path_removals} from graph")
|
|
133
234
|
H.remove_nodes_from(path_removals)
|
|
134
235
|
# logger.debug(f"Graph after path removal {H.nodes}")
|
|
135
236
|
sG: nx.Graph = ax.steinertree.steiner_tree(H, nodelist).copy()
|
|
@@ -148,8 +249,10 @@ def determine_induced_minimal_nodes(
|
|
|
148
249
|
if not accept_partial:
|
|
149
250
|
continue
|
|
150
251
|
final.add_edge(*edge)
|
|
151
|
-
# all concept nodes must have a parent
|
|
152
252
|
|
|
253
|
+
reinject_common_join_keys_v2(G, final, nodelist, synonyms)
|
|
254
|
+
|
|
255
|
+
# all concept nodes must have a parent
|
|
153
256
|
if not all(
|
|
154
257
|
[
|
|
155
258
|
final.in_degree(node) > 0
|
|
@@ -302,6 +405,7 @@ def resolve_weak_components(
|
|
|
302
405
|
filter_downstream=filter_downstream,
|
|
303
406
|
accept_partial=accept_partial,
|
|
304
407
|
environment=environment,
|
|
408
|
+
synonyms=synonyms,
|
|
305
409
|
)
|
|
306
410
|
|
|
307
411
|
if not g or not g.nodes:
|
|
@@ -2,13 +2,25 @@ from dataclasses import dataclass
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
4
|
from trilogy import Environment
|
|
5
|
-
from trilogy.authoring import
|
|
5
|
+
from trilogy.authoring import (
|
|
6
|
+
ConceptRef,
|
|
7
|
+
DataType,
|
|
8
|
+
Ordering,
|
|
9
|
+
Purpose,
|
|
10
|
+
)
|
|
11
|
+
from trilogy.constants import MagicConstants
|
|
12
|
+
from trilogy.core.enums import ComparisonOperator, FunctionType
|
|
6
13
|
from trilogy.core.exceptions import ModelValidationError
|
|
7
14
|
from trilogy.core.models.build import (
|
|
15
|
+
BuildCaseElse,
|
|
16
|
+
BuildCaseWhen,
|
|
8
17
|
BuildComparison,
|
|
9
18
|
BuildConcept,
|
|
10
19
|
BuildConditional,
|
|
11
20
|
BuildDatasource,
|
|
21
|
+
BuildFunction,
|
|
22
|
+
BuildOrderBy,
|
|
23
|
+
BuildOrderItem,
|
|
12
24
|
)
|
|
13
25
|
from trilogy.core.models.environment import EnvironmentConceptDict
|
|
14
26
|
from trilogy.core.models.execute import (
|
|
@@ -39,6 +51,32 @@ class ValidationType(Enum):
|
|
|
39
51
|
CONCEPTS = "concepts"
|
|
40
52
|
|
|
41
53
|
|
|
54
|
+
def build_order_args(concepts: list[BuildConcept]) -> list[BuildFunction]:
|
|
55
|
+
order_args = []
|
|
56
|
+
for concept in concepts:
|
|
57
|
+
order_args.append(
|
|
58
|
+
BuildFunction(
|
|
59
|
+
operator=FunctionType.CASE,
|
|
60
|
+
arguments=[
|
|
61
|
+
BuildCaseWhen(
|
|
62
|
+
comparison=BuildComparison(
|
|
63
|
+
left=concept,
|
|
64
|
+
operator=ComparisonOperator.IS,
|
|
65
|
+
right=MagicConstants.NULL,
|
|
66
|
+
),
|
|
67
|
+
expr=1,
|
|
68
|
+
),
|
|
69
|
+
BuildCaseElse(expr=0),
|
|
70
|
+
],
|
|
71
|
+
output_data_type=DataType.INTEGER,
|
|
72
|
+
output_purpose=Purpose.PROPERTY,
|
|
73
|
+
arg_count=2,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return order_args
|
|
78
|
+
|
|
79
|
+
|
|
42
80
|
def easy_query(
|
|
43
81
|
concepts: list[BuildConcept],
|
|
44
82
|
datasource: BuildDatasource,
|
|
@@ -81,7 +119,6 @@ def easy_query(
|
|
|
81
119
|
group_to_grain=True,
|
|
82
120
|
base_alias_override=datasource.safe_identifier,
|
|
83
121
|
)
|
|
84
|
-
|
|
85
122
|
filter_cte = CTE(
|
|
86
123
|
name=f"datasource_{datasource.name}_filter",
|
|
87
124
|
source=QueryDatasource(
|
|
@@ -100,6 +137,20 @@ def easy_query(
|
|
|
100
137
|
grain=cte.grain,
|
|
101
138
|
condition=condition,
|
|
102
139
|
limit=limit,
|
|
140
|
+
order_by=BuildOrderBy(
|
|
141
|
+
items=[
|
|
142
|
+
BuildOrderItem(
|
|
143
|
+
expr=BuildFunction(
|
|
144
|
+
operator=FunctionType.SUM,
|
|
145
|
+
arguments=build_order_args(concepts),
|
|
146
|
+
output_data_type=DataType.INTEGER,
|
|
147
|
+
output_purpose=Purpose.PROPERTY,
|
|
148
|
+
arg_count=len(concepts),
|
|
149
|
+
),
|
|
150
|
+
order=Ordering.DESCENDING,
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
),
|
|
103
154
|
)
|
|
104
155
|
|
|
105
156
|
return ProcessedQuery(
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from trilogy import Environment, Executor
|
|
2
|
-
from trilogy.core.enums import Derivation, Purpose
|
|
2
|
+
from trilogy.core.enums import Derivation, Modifier, Purpose
|
|
3
3
|
from trilogy.core.exceptions import (
|
|
4
4
|
ConceptModelValidationError,
|
|
5
|
-
|
|
5
|
+
DatasourceColumnBindingData,
|
|
6
|
+
DatasourceColumnBindingError,
|
|
6
7
|
)
|
|
7
8
|
from trilogy.core.models.build import (
|
|
8
9
|
BuildConcept,
|
|
@@ -25,6 +26,15 @@ def validate_key_concept(
|
|
|
25
26
|
):
|
|
26
27
|
results: list[ValidationTest] = []
|
|
27
28
|
seen: dict[str, int] = {}
|
|
29
|
+
|
|
30
|
+
count = 0
|
|
31
|
+
for datasource in build_env.datasources.values():
|
|
32
|
+
if concept.address in [c.address for c in datasource.concepts]:
|
|
33
|
+
count += 1
|
|
34
|
+
# if it only has one source, it's a key
|
|
35
|
+
if count <= 1:
|
|
36
|
+
return results
|
|
37
|
+
|
|
28
38
|
for datasource in build_env.datasources.values():
|
|
29
39
|
if concept.address in [c.address for c in datasource.concepts]:
|
|
30
40
|
assignment = [
|
|
@@ -69,8 +79,19 @@ def validate_key_concept(
|
|
|
69
79
|
err = None
|
|
70
80
|
datasource_count: int = seen.get(datasource.name, 0)
|
|
71
81
|
if datasource_count < max_seen and assignment.is_complete:
|
|
72
|
-
err =
|
|
73
|
-
|
|
82
|
+
err = DatasourceColumnBindingError(
|
|
83
|
+
address=datasource.identifier,
|
|
84
|
+
errors=[
|
|
85
|
+
DatasourceColumnBindingData(
|
|
86
|
+
address=concept.address,
|
|
87
|
+
value=None,
|
|
88
|
+
value_type=concept.datatype,
|
|
89
|
+
value_modifiers=[Modifier.PARTIAL],
|
|
90
|
+
actual_type=concept.datatype,
|
|
91
|
+
actual_modifiers=concept.modifiers,
|
|
92
|
+
)
|
|
93
|
+
],
|
|
94
|
+
message=f"Key concept {concept.address} is missing values in datasource {datasource.name} (max cardinality in data {max_seen}, datasource has {seen[datasource.name]} values) but is not marked as partial.",
|
|
74
95
|
)
|
|
75
96
|
results.append(
|
|
76
97
|
ValidationTest(
|
|
@@ -10,9 +10,14 @@ from trilogy.authoring import (
|
|
|
10
10
|
NumericType,
|
|
11
11
|
StructType,
|
|
12
12
|
TraitDataType,
|
|
13
|
+
arg_to_datatype,
|
|
14
|
+
)
|
|
15
|
+
from trilogy.core.enums import ComparisonOperator, Modifier
|
|
16
|
+
from trilogy.core.exceptions import (
|
|
17
|
+
DatasourceColumnBindingData,
|
|
18
|
+
DatasourceColumnBindingError,
|
|
19
|
+
DatasourceModelValidationError,
|
|
13
20
|
)
|
|
14
|
-
from trilogy.core.enums import ComparisonOperator
|
|
15
|
-
from trilogy.core.exceptions import DatasourceModelValidationError
|
|
16
21
|
from trilogy.core.models.build import (
|
|
17
22
|
BuildComparison,
|
|
18
23
|
BuildDatasource,
|
|
@@ -64,6 +69,7 @@ def validate_datasource(
|
|
|
64
69
|
env: Environment,
|
|
65
70
|
build_env: BuildEnvironment,
|
|
66
71
|
exec: Executor | None = None,
|
|
72
|
+
fix: bool = False,
|
|
67
73
|
) -> list[ValidationTest]:
|
|
68
74
|
results: list[ValidationTest] = []
|
|
69
75
|
# we might have merged concepts, where both will map out to the same
|
|
@@ -109,14 +115,7 @@ def validate_datasource(
|
|
|
109
115
|
)
|
|
110
116
|
)
|
|
111
117
|
return results
|
|
112
|
-
failures: list[
|
|
113
|
-
tuple[
|
|
114
|
-
str,
|
|
115
|
-
Any,
|
|
116
|
-
DataType | ArrayType | StructType | MapType | NumericType | TraitDataType,
|
|
117
|
-
bool,
|
|
118
|
-
]
|
|
119
|
-
] = []
|
|
118
|
+
failures: list[DatasourceColumnBindingData] = []
|
|
120
119
|
cols_with_error = set()
|
|
121
120
|
for row in rows:
|
|
122
121
|
for col in datasource.columns:
|
|
@@ -127,26 +126,29 @@ def validate_datasource(
|
|
|
127
126
|
passed = type_check(rval, col.concept.datatype, col.is_nullable)
|
|
128
127
|
if not passed:
|
|
129
128
|
failures.append(
|
|
130
|
-
(
|
|
131
|
-
col.concept.address,
|
|
132
|
-
rval,
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
DatasourceColumnBindingData(
|
|
130
|
+
address=col.concept.address,
|
|
131
|
+
value=rval,
|
|
132
|
+
value_type=(
|
|
133
|
+
arg_to_datatype(rval)
|
|
134
|
+
if rval is not None
|
|
135
|
+
else col.concept.datatype
|
|
136
|
+
),
|
|
137
|
+
value_modifiers=[Modifier.NULLABLE] if rval is None else [],
|
|
138
|
+
actual_type=col.concept.datatype,
|
|
139
|
+
actual_modifiers=col.concept.modifiers,
|
|
135
140
|
)
|
|
136
141
|
)
|
|
137
142
|
cols_with_error.add(actual_address)
|
|
138
143
|
|
|
139
|
-
def format_failure(failure):
|
|
140
|
-
return f"Concept {failure[0]} value '{failure[1]}' does not conform to expected type {str(failure[2])} (nullable={failure[3]})"
|
|
141
|
-
|
|
142
144
|
if failures:
|
|
143
145
|
results.append(
|
|
144
146
|
ValidationTest(
|
|
145
147
|
check_type=ExpectationType.LOGICAL,
|
|
146
148
|
expected="datatype_match",
|
|
147
149
|
ran=True,
|
|
148
|
-
result=
|
|
149
|
-
|
|
150
|
+
result=DatasourceColumnBindingError(
|
|
151
|
+
address=datasource.identifier, errors=failures
|
|
150
152
|
),
|
|
151
153
|
)
|
|
152
154
|
)
|
|
@@ -15,9 +15,10 @@ def validate_environment(
|
|
|
15
15
|
scope: ValidationScope = ValidationScope.ALL,
|
|
16
16
|
targets: list[str] | None = None,
|
|
17
17
|
exec: Executor | None = None,
|
|
18
|
+
generate_only: bool = False,
|
|
18
19
|
) -> list[ValidationTest]:
|
|
19
20
|
# avoid mutating the environment for validation
|
|
20
|
-
generate_only = exec is None
|
|
21
|
+
generate_only = exec is None or generate_only
|
|
21
22
|
env = env.duplicate()
|
|
22
23
|
grain_check = function_to_concept(
|
|
23
24
|
parent=Function(
|
|
@@ -68,4 +69,5 @@ def validate_environment(
|
|
|
68
69
|
f"Environment validation failed with the following errors:\n{messages}",
|
|
69
70
|
children=exceptions,
|
|
70
71
|
)
|
|
72
|
+
|
|
71
73
|
return results
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from trilogy import Environment, Executor
|
|
6
|
+
from trilogy.authoring import ConceptDeclarationStatement, Datasource
|
|
7
|
+
from trilogy.core.exceptions import (
|
|
8
|
+
DatasourceColumnBindingData,
|
|
9
|
+
DatasourceColumnBindingError,
|
|
10
|
+
)
|
|
11
|
+
from trilogy.core.validation.environment import validate_environment
|
|
12
|
+
from trilogy.parsing.render import Renderer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rewrite_file_with_errors(
|
|
16
|
+
statements: list[Any], errors: list[DatasourceColumnBindingError]
|
|
17
|
+
):
|
|
18
|
+
renderer = Renderer()
|
|
19
|
+
output = []
|
|
20
|
+
ds_error_map: dict[str, list[DatasourceColumnBindingData]] = defaultdict(list)
|
|
21
|
+
concept_error_map: dict[str, list[DatasourceColumnBindingData]] = defaultdict(list)
|
|
22
|
+
for error in errors:
|
|
23
|
+
if isinstance(error, DatasourceColumnBindingError):
|
|
24
|
+
for x in error.errors:
|
|
25
|
+
if error.dataset_address not in ds_error_map:
|
|
26
|
+
ds_error_map[error.dataset_address] = []
|
|
27
|
+
# this is by dataset address
|
|
28
|
+
if x.is_modifier_issue():
|
|
29
|
+
ds_error_map[error.dataset_address].append(x)
|
|
30
|
+
# this is by column
|
|
31
|
+
if x.is_type_issue():
|
|
32
|
+
concept_error_map[x.address].append(x)
|
|
33
|
+
for statement in statements:
|
|
34
|
+
if isinstance(statement, Datasource):
|
|
35
|
+
if statement.identifier in ds_error_map:
|
|
36
|
+
error_cols = ds_error_map[statement.identifier]
|
|
37
|
+
for col in statement.columns:
|
|
38
|
+
if col.concept.address in [x.address for x in error_cols]:
|
|
39
|
+
error_col = [
|
|
40
|
+
x for x in error_cols if x.address == col.concept.address
|
|
41
|
+
][0]
|
|
42
|
+
col.modifiers = list(
|
|
43
|
+
set(col.modifiers + error_col.value_modifiers)
|
|
44
|
+
)
|
|
45
|
+
elif isinstance(statement, ConceptDeclarationStatement):
|
|
46
|
+
if statement.concept.address in concept_error_map:
|
|
47
|
+
error_cols = concept_error_map[statement.concept.address]
|
|
48
|
+
statement.concept.datatype = error_cols[0].value_type
|
|
49
|
+
output.append(statement)
|
|
50
|
+
|
|
51
|
+
return renderer.render_statement_string(output)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
DEPTH_CUTOFF = 3
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_and_rewrite(
|
|
58
|
+
input: Path | str, exec: Executor | None = None, depth: int = 0
|
|
59
|
+
) -> str | None:
|
|
60
|
+
if depth > DEPTH_CUTOFF:
|
|
61
|
+
print(f"Reached depth cutoff of {DEPTH_CUTOFF}, stopping.")
|
|
62
|
+
return None
|
|
63
|
+
if isinstance(input, str):
|
|
64
|
+
raw = input
|
|
65
|
+
env = Environment()
|
|
66
|
+
else:
|
|
67
|
+
with open(input, "r") as f:
|
|
68
|
+
raw = f.read()
|
|
69
|
+
env = Environment(working_path=input.parent)
|
|
70
|
+
if exec:
|
|
71
|
+
env = exec.environment
|
|
72
|
+
env, statements = env.parse(raw)
|
|
73
|
+
|
|
74
|
+
validation_results = validate_environment(env, exec=exec, generate_only=True)
|
|
75
|
+
|
|
76
|
+
errors = [
|
|
77
|
+
x.result
|
|
78
|
+
for x in validation_results
|
|
79
|
+
if isinstance(x.result, DatasourceColumnBindingError)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
if not errors:
|
|
83
|
+
print("No validation errors found")
|
|
84
|
+
return None
|
|
85
|
+
print(
|
|
86
|
+
f"Found {len(errors)} validation errors, attempting to fix, current depth: {depth}..."
|
|
87
|
+
)
|
|
88
|
+
for error in errors:
|
|
89
|
+
for item in error.errors:
|
|
90
|
+
print(f"- {item.format_failure()}")
|
|
91
|
+
|
|
92
|
+
new_text = rewrite_file_with_errors(statements, errors)
|
|
93
|
+
|
|
94
|
+
while iteration := validate_and_rewrite(new_text, exec=exec, depth=depth + 1):
|
|
95
|
+
depth = depth + 1
|
|
96
|
+
if depth >= DEPTH_CUTOFF:
|
|
97
|
+
break
|
|
98
|
+
if iteration:
|
|
99
|
+
new_text = iteration
|
|
100
|
+
depth += 1
|
|
101
|
+
if isinstance(input, Path):
|
|
102
|
+
with open(input, "w") as f:
|
|
103
|
+
f.write(new_text)
|
|
104
|
+
return None
|
|
105
|
+
else:
|
|
106
|
+
return new_text
|
trilogy/dialect/base.py
CHANGED
|
@@ -761,6 +761,7 @@ class BaseDialect:
|
|
|
761
761
|
elif isinstance(e, MagicConstants):
|
|
762
762
|
if e == MagicConstants.NULL:
|
|
763
763
|
return "null"
|
|
764
|
+
return str(e.value)
|
|
764
765
|
elif isinstance(e, date):
|
|
765
766
|
return self.FUNCTION_MAP[FunctionType.DATE_LITERAL](e)
|
|
766
767
|
elif isinstance(e, datetime):
|
|
@@ -1139,7 +1140,7 @@ class BaseDialect:
|
|
|
1139
1140
|
if isinstance(query, ProcessedShowStatement):
|
|
1140
1141
|
return ";\n".join(
|
|
1141
1142
|
[
|
|
1142
|
-
f
|
|
1143
|
+
f"{self.EXPLAIN_KEYWORD} {self.compile_statement(x)}"
|
|
1143
1144
|
for x in query.output_values
|
|
1144
1145
|
if isinstance(x, (ProcessedQuery, ProcessedCopyStatement))
|
|
1145
1146
|
]
|
trilogy/dialect/common.py
CHANGED
|
@@ -10,7 +10,6 @@ from trilogy.core.models.build import (
|
|
|
10
10
|
BuildParamaterizedConceptReference,
|
|
11
11
|
BuildParenthetical,
|
|
12
12
|
)
|
|
13
|
-
from trilogy.core.models.datasource import RawColumnExpr
|
|
14
13
|
from trilogy.core.models.execute import (
|
|
15
14
|
CTE,
|
|
16
15
|
InstantiatedUnnestJoin,
|
|
@@ -65,15 +64,8 @@ def render_join_concept(
|
|
|
65
64
|
inlined_ctes: set[str],
|
|
66
65
|
):
|
|
67
66
|
if cte.name in inlined_ctes:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if isinstance(raw_content, RawColumnExpr):
|
|
71
|
-
rval = raw_content.text
|
|
72
|
-
return rval
|
|
73
|
-
elif isinstance(raw_content, BuildFunction):
|
|
74
|
-
rval = render_expr(raw_content, cte=cte)
|
|
75
|
-
return rval
|
|
76
|
-
return f"{quote_character}{name}{quote_character}.{quote_character}{raw_content}{quote_character}"
|
|
67
|
+
base = render_expr(concept, cte)
|
|
68
|
+
return base
|
|
77
69
|
return f"{quote_character}{name}{quote_character}.{quote_character}{concept.safe_address}{quote_character}"
|
|
78
70
|
|
|
79
71
|
|
trilogy/dialect/metadata.py
CHANGED
|
@@ -174,7 +174,7 @@ def raw_validation_to_result(
|
|
|
174
174
|
) -> Optional[MockResult]:
|
|
175
175
|
"""Convert raw validation tests to mock result."""
|
|
176
176
|
if not raw:
|
|
177
|
-
return
|
|
177
|
+
return MockResult([], ["check_type", "expected", "result", "ran", "query"])
|
|
178
178
|
output = []
|
|
179
179
|
for row in raw:
|
|
180
180
|
if row.raw_query and generator and not row.generated_query:
|
trilogy/executor.py
CHANGED
|
@@ -410,6 +410,13 @@ class Executor(object):
|
|
|
410
410
|
)
|
|
411
411
|
output.extend(results)
|
|
412
412
|
continue
|
|
413
|
+
elif isinstance(statement, ProcessedValidateStatement):
|
|
414
|
+
validate_result = handle_processed_validate_statement(
|
|
415
|
+
statement, self.generator, self.validate_environment
|
|
416
|
+
)
|
|
417
|
+
if validate_result:
|
|
418
|
+
output.append(validate_result)
|
|
419
|
+
continue
|
|
413
420
|
if non_interactive:
|
|
414
421
|
if not isinstance(
|
|
415
422
|
statement, (ProcessedCopyStatement, ProcessedQueryPersist)
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -379,14 +379,16 @@ class ParseToObjects(Transformer):
|
|
|
379
379
|
def start(self, args):
|
|
380
380
|
return args
|
|
381
381
|
|
|
382
|
+
def LINE_SEPARATOR(self, args):
|
|
383
|
+
return MagicConstants.LINE_SEPARATOR
|
|
384
|
+
|
|
382
385
|
def block(self, args):
|
|
383
386
|
output = args[0]
|
|
384
387
|
if isinstance(output, ConceptDeclarationStatement):
|
|
385
|
-
if len(args) > 1 and
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
388
|
+
if len(args) > 1 and args[1] != MagicConstants.LINE_SEPARATOR:
|
|
389
|
+
comments = [x for x in args[1:] if isinstance(x, Comment)]
|
|
390
|
+
merged = "\n".join([x.text.split("#")[1].rstrip() for x in comments])
|
|
391
|
+
output.concept.metadata.description = merged
|
|
390
392
|
# this is a bad plan for now;
|
|
391
393
|
# because a comment after an import statement is very common
|
|
392
394
|
# and it's not intuitive that it modifies the import description
|
|
@@ -913,7 +915,7 @@ class ParseToObjects(Transformer):
|
|
|
913
915
|
return Comment(text=args[0].value)
|
|
914
916
|
|
|
915
917
|
def PARSE_COMMENT(self, args):
|
|
916
|
-
return Comment(text=args.value)
|
|
918
|
+
return Comment(text=args.value.rstrip())
|
|
917
919
|
|
|
918
920
|
@v_args(meta=True)
|
|
919
921
|
def select_transform(self, meta: Meta, args) -> ConceptTransform:
|
|
@@ -1000,7 +1002,7 @@ class ParseToObjects(Transformer):
|
|
|
1000
1002
|
def validate_statement(self, meta: Meta, args) -> ValidateStatement:
|
|
1001
1003
|
if len(args) == 2:
|
|
1002
1004
|
scope = args[0]
|
|
1003
|
-
targets = args[1]
|
|
1005
|
+
targets = args[1].split(",")
|
|
1004
1006
|
elif len(args) == 0:
|
|
1005
1007
|
scope = ValidationScope.ALL
|
|
1006
1008
|
targets = None
|
|
@@ -2293,7 +2295,7 @@ def parse_text(
|
|
|
2293
2295
|
raise _create_syntax_error(210, pos, text)
|
|
2294
2296
|
|
|
2295
2297
|
# Handle FROM token error
|
|
2296
|
-
parsed_tokens = [x.value for x in e.token_history] if e.token_history else []
|
|
2298
|
+
parsed_tokens = [x.value for x in e.token_history if x] if e.token_history else []
|
|
2297
2299
|
if parsed_tokens == ["FROM"]:
|
|
2298
2300
|
raise _create_syntax_error(101, pos, text)
|
|
2299
2301
|
|
trilogy/parsing/render.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
from functools import singledispatchmethod
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from jinja2 import Template
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ from trilogy.core.models.author import (
|
|
|
12
13
|
AlignItem,
|
|
13
14
|
CaseElse,
|
|
14
15
|
CaseWhen,
|
|
16
|
+
Comment,
|
|
15
17
|
Comparison,
|
|
16
18
|
Concept,
|
|
17
19
|
ConceptRef,
|
|
@@ -83,6 +85,23 @@ class Renderer:
|
|
|
83
85
|
def __init__(self, environment: Environment | None = None):
|
|
84
86
|
self.environment = environment
|
|
85
87
|
|
|
88
|
+
def render_statement_string(self, list_of_statements: list[Any]) -> str:
|
|
89
|
+
new = []
|
|
90
|
+
last_statement_type = None
|
|
91
|
+
for stmt in list_of_statements:
|
|
92
|
+
stmt_type = type(stmt)
|
|
93
|
+
if last_statement_type is None:
|
|
94
|
+
pass
|
|
95
|
+
elif last_statement_type == Comment:
|
|
96
|
+
new.append("\n")
|
|
97
|
+
elif stmt_type != last_statement_type:
|
|
98
|
+
new.append("\n\n")
|
|
99
|
+
else:
|
|
100
|
+
new.append("\n")
|
|
101
|
+
new.append(Renderer().to_string(stmt))
|
|
102
|
+
last_statement_type = stmt_type
|
|
103
|
+
return "".join(new)
|
|
104
|
+
|
|
86
105
|
@singledispatchmethod
|
|
87
106
|
def to_string(self, arg):
|
|
88
107
|
raise NotImplementedError("Cannot render type {}".format(type(arg)))
|
|
@@ -269,6 +288,8 @@ class Renderer:
|
|
|
269
288
|
@to_string.register
|
|
270
289
|
def _(self, arg: "Address"):
|
|
271
290
|
if arg.is_query:
|
|
291
|
+
if arg.location.startswith("("):
|
|
292
|
+
return f"query '''{arg.location[1:-1]}'''"
|
|
272
293
|
return f"query '''{arg.location}'''"
|
|
273
294
|
return f"address {arg.location}"
|
|
274
295
|
|
|
@@ -286,7 +307,7 @@ class Renderer:
|
|
|
286
307
|
def _(self, arg: "ColumnAssignment"):
|
|
287
308
|
if arg.modifiers:
|
|
288
309
|
modifiers = "".join(
|
|
289
|
-
[self.to_string(modifier) for modifier in arg.modifiers]
|
|
310
|
+
[self.to_string(modifier) for modifier in sorted(arg.modifiers)]
|
|
290
311
|
)
|
|
291
312
|
else:
|
|
292
313
|
modifiers = ""
|
|
@@ -328,7 +349,7 @@ class Renderer:
|
|
|
328
349
|
else:
|
|
329
350
|
output = f"{concept.purpose.value} {namespace}{concept.name} <- {self.to_string(concept.lineage)};"
|
|
330
351
|
if base_description:
|
|
331
|
-
output += f" #
|
|
352
|
+
output += f" #{base_description}"
|
|
332
353
|
return output
|
|
333
354
|
|
|
334
355
|
@to_string.register
|
|
@@ -428,6 +449,10 @@ class Renderer:
|
|
|
428
449
|
def _(self, arg: "Comparison"):
|
|
429
450
|
return f"{self.to_string(arg.left)} {arg.operator.value} {self.to_string(arg.right)}"
|
|
430
451
|
|
|
452
|
+
@to_string.register
|
|
453
|
+
def _(self, arg: "Comment"):
|
|
454
|
+
return f"{arg.text}"
|
|
455
|
+
|
|
431
456
|
@to_string.register
|
|
432
457
|
def _(self, arg: "WindowItem"):
|
|
433
458
|
over = ",".join(self.to_string(c) for c in arg.over)
|
|
@@ -521,8 +546,8 @@ class Renderer:
|
|
|
521
546
|
return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
|
|
522
547
|
|
|
523
548
|
if arg.operator == FunctionType.CASE:
|
|
524
|
-
inputs = "\n".join(args)
|
|
525
|
-
return f"CASE
|
|
549
|
+
inputs = "\n\t".join(args)
|
|
550
|
+
return f"CASE\n\t{inputs}\nEND"
|
|
526
551
|
return f"{arg.operator.value}({inputs})"
|
|
527
552
|
|
|
528
553
|
@to_string.register
|
|
@@ -551,8 +576,10 @@ class Renderer:
|
|
|
551
576
|
def _(self, arg: Modifier):
|
|
552
577
|
if arg == Modifier.PARTIAL:
|
|
553
578
|
return "~"
|
|
554
|
-
|
|
579
|
+
elif arg == Modifier.HIDDEN:
|
|
555
580
|
return "--"
|
|
581
|
+
elif arg == Modifier.NULLABLE:
|
|
582
|
+
return "?"
|
|
556
583
|
return arg.value
|
|
557
584
|
|
|
558
585
|
@to_string.register
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
!start: ( block | show_statement )*
|
|
2
|
-
block: statement _TERMINATOR PARSE_COMMENT
|
|
1
|
+
!start: ( block | show_statement | PARSE_COMMENT )*
|
|
2
|
+
block: statement _TERMINATOR LINE_SEPARATOR? PARSE_COMMENT*
|
|
3
3
|
?statement: concept
|
|
4
4
|
| datasource
|
|
5
5
|
| function
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
| rawsql_statement
|
|
15
15
|
| validate_statement
|
|
16
16
|
|
|
17
|
-
_TERMINATOR: ";"i
|
|
17
|
+
_TERMINATOR: ";"i
|
|
18
18
|
|
|
19
|
-
PARSE_COMMENT.1: /#.*(\n|$)/ |
|
|
19
|
+
PARSE_COMMENT.1: /#.*(\n|$)/ | /\/\/.*(\n|$)/
|
|
20
|
+
|
|
21
|
+
// when whitespace matters - comment placement
|
|
22
|
+
LINE_SEPARATOR.1: /[ \t\r\f\v]*\n+/
|
|
20
23
|
|
|
21
24
|
// property display_name string
|
|
22
25
|
concept_declaration: PURPOSE IDENTIFIER data_type concept_nullable_modifier? metadata?
|
trilogy/std/metric.preql
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Length and distance units
|
|
2
|
+
type m numeric; # meters
|
|
3
|
+
type km numeric; # kilometers
|
|
4
|
+
type cm numeric; # centimeters
|
|
5
|
+
type mm numeric; # millimeters
|
|
6
|
+
|
|
7
|
+
# Mass units
|
|
8
|
+
type kg numeric; # kilograms
|
|
9
|
+
type g numeric; # grams
|
|
10
|
+
type tonne numeric; # metric tons (1000 kg)
|
|
11
|
+
|
|
12
|
+
# Force units
|
|
13
|
+
type n numeric; # newtons
|
|
14
|
+
type kn numeric; # kilonewtons (1000 N)
|
|
15
|
+
type mn numeric; # meganewtons (1,000,000 N)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|