pytrilogy 0.0.3.94__py3-none-any.whl → 0.0.3.96__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 (35) hide show
  1. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/METADATA +184 -136
  2. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/RECORD +35 -30
  3. trilogy/__init__.py +1 -1
  4. trilogy/authoring/__init__.py +61 -43
  5. trilogy/core/enums.py +13 -0
  6. trilogy/core/env_processor.py +19 -10
  7. trilogy/core/environment_helpers.py +111 -0
  8. trilogy/core/exceptions.py +21 -1
  9. trilogy/core/functions.py +6 -1
  10. trilogy/core/graph_models.py +11 -37
  11. trilogy/core/internal.py +18 -0
  12. trilogy/core/models/core.py +3 -0
  13. trilogy/core/models/environment.py +28 -0
  14. trilogy/core/models/execute.py +7 -0
  15. trilogy/core/processing/node_generators/select_merge_node.py +2 -2
  16. trilogy/core/query_processor.py +2 -1
  17. trilogy/core/statements/author.py +18 -3
  18. trilogy/core/statements/common.py +0 -10
  19. trilogy/core/statements/execute.py +73 -16
  20. trilogy/core/validation/common.py +110 -0
  21. trilogy/core/validation/concept.py +125 -0
  22. trilogy/core/validation/datasource.py +194 -0
  23. trilogy/core/validation/environment.py +71 -0
  24. trilogy/dialect/base.py +48 -21
  25. trilogy/dialect/metadata.py +233 -0
  26. trilogy/dialect/sql_server.py +3 -1
  27. trilogy/engine.py +25 -7
  28. trilogy/executor.py +94 -162
  29. trilogy/parsing/parse_engine.py +34 -3
  30. trilogy/parsing/trilogy.lark +11 -5
  31. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/WHEEL +0 -0
  32. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/entry_points.txt +0 -0
  33. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/licenses/LICENSE.md +0 -0
  34. {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/top_level.txt +0 -0
  35. /trilogy/{compiler.py → core/validation/__init__.py} +0 -0
trilogy/dialect/base.py CHANGED
@@ -72,14 +72,17 @@ from trilogy.core.statements.author import (
72
72
  RowsetDerivationStatement,
73
73
  SelectStatement,
74
74
  ShowStatement,
75
+ ValidateStatement,
75
76
  )
76
77
  from trilogy.core.statements.execute import (
78
+ PROCESSED_STATEMENT_TYPES,
77
79
  ProcessedCopyStatement,
78
80
  ProcessedQuery,
79
81
  ProcessedQueryPersist,
80
82
  ProcessedRawSQLStatement,
81
83
  ProcessedShowStatement,
82
84
  ProcessedStaticValueOutput,
85
+ ProcessedValidateStatement,
83
86
  )
84
87
  from trilogy.core.utility import safe_quote
85
88
  from trilogy.dialect.common import render_join, render_unnest
@@ -343,6 +346,7 @@ class BaseDialect:
343
346
  COMPLEX_DATATYPE_MAP = COMPLEX_DATATYPE_MAP
344
347
  UNNEST_MODE = UnnestMode.CROSS_APPLY
345
348
  GROUP_MODE = GroupMode.AUTO
349
+ EXPLAIN_KEYWORD = "EXPLAIN"
346
350
 
347
351
  def __init__(self, rendering: Rendering | None = None):
348
352
  self.rendering = rendering or CONFIG.rendering
@@ -1025,21 +1029,11 @@ class BaseDialect:
1025
1029
  | RawSQLStatement
1026
1030
  | MergeStatementV2
1027
1031
  | CopyStatement
1032
+ | ValidateStatement
1028
1033
  ],
1029
1034
  hooks: Optional[List[BaseHook]] = None,
1030
- ) -> List[
1031
- ProcessedQuery
1032
- | ProcessedQueryPersist
1033
- | ProcessedShowStatement
1034
- | ProcessedRawSQLStatement
1035
- ]:
1036
- output: List[
1037
- ProcessedQuery
1038
- | ProcessedQueryPersist
1039
- | ProcessedShowStatement
1040
- | ProcessedRawSQLStatement
1041
- | ProcessedCopyStatement
1042
- ] = []
1035
+ ) -> List[PROCESSED_STATEMENT_TYPES]:
1036
+ output: List[PROCESSED_STATEMENT_TYPES] = []
1043
1037
  for statement in statements:
1044
1038
  if isinstance(statement, PersistStatement):
1045
1039
  if hooks:
@@ -1089,10 +1083,39 @@ class BaseDialect:
1089
1083
  output.append(
1090
1084
  self.create_show_output(environment, statement.content)
1091
1085
  )
1086
+ elif isinstance(statement.content, ValidateStatement):
1087
+ output.append(
1088
+ ProcessedShowStatement(
1089
+ output_columns=[
1090
+ environment.concepts[
1091
+ DEFAULT_CONCEPTS["label"].address
1092
+ ].reference,
1093
+ environment.concepts[
1094
+ DEFAULT_CONCEPTS["query_text"].address
1095
+ ].reference,
1096
+ environment.concepts[
1097
+ DEFAULT_CONCEPTS["expected"].address
1098
+ ].reference,
1099
+ ],
1100
+ output_values=[
1101
+ ProcessedValidateStatement(
1102
+ scope=statement.content.scope,
1103
+ targets=statement.content.targets,
1104
+ )
1105
+ ],
1106
+ )
1107
+ )
1092
1108
  else:
1093
1109
  raise NotImplementedError(type(statement.content))
1094
1110
  elif isinstance(statement, RawSQLStatement):
1095
1111
  output.append(ProcessedRawSQLStatement(text=statement.text))
1112
+ elif isinstance(statement, ValidateStatement):
1113
+ output.append(
1114
+ ProcessedValidateStatement(
1115
+ scope=statement.scope,
1116
+ targets=statement.targets,
1117
+ )
1118
+ )
1096
1119
  elif isinstance(
1097
1120
  statement,
1098
1121
  (
@@ -1111,18 +1134,22 @@ class BaseDialect:
1111
1134
 
1112
1135
  def compile_statement(
1113
1136
  self,
1114
- query: (
1115
- ProcessedQuery
1116
- | ProcessedQueryPersist
1117
- | ProcessedShowStatement
1118
- | ProcessedRawSQLStatement
1119
- ),
1137
+ query: PROCESSED_STATEMENT_TYPES,
1120
1138
  ) -> str:
1121
1139
  if isinstance(query, ProcessedShowStatement):
1122
- return ";\n".join([str(x) for x in query.output_values])
1140
+ return ";\n".join(
1141
+ [
1142
+ f'{self.EXPLAIN_KEYWORD} {self.compile_statement(x)}'
1143
+ for x in query.output_values
1144
+ if isinstance(x, (ProcessedQuery, ProcessedCopyStatement))
1145
+ ]
1146
+ )
1123
1147
  elif isinstance(query, ProcessedRawSQLStatement):
1124
1148
  return query.text
1125
1149
 
1150
+ elif isinstance(query, ProcessedValidateStatement):
1151
+ return "select 1;"
1152
+
1126
1153
  recursive = any(isinstance(x, RecursiveCTE) for x in query.ctes)
1127
1154
 
1128
1155
  compiled_ctes = self.generate_ctes(query)
@@ -1139,7 +1166,7 @@ class BaseDialect:
1139
1166
  if CONFIG.strict_mode and INVALID_REFERENCE_STRING(1) in final:
1140
1167
  raise ValueError(
1141
1168
  f"Invalid reference string found in query: {final}, this should never"
1142
- " occur. Please create a GitHub issue to report this."
1169
+ " occur. Please create an issue to report this."
1143
1170
  )
1144
1171
  logger.info(f"{LOGGER_PREFIX} Compiled query: {final}")
1145
1172
  return final
@@ -0,0 +1,233 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, Optional
3
+
4
+ from trilogy.core.models.author import ConceptRef
5
+ from trilogy.core.models.datasource import Datasource
6
+ from trilogy.core.models.environment import Environment
7
+ from trilogy.core.statements.author import (
8
+ ConceptDeclarationStatement,
9
+ ImportStatement,
10
+ MergeStatementV2,
11
+ )
12
+ from trilogy.core.statements.execute import (
13
+ ProcessedShowStatement,
14
+ ProcessedStaticValueOutput,
15
+ ProcessedValidateStatement,
16
+ )
17
+ from trilogy.core.validation.common import ValidationTest
18
+ from trilogy.dialect.base import BaseDialect
19
+ from trilogy.engine import ResultProtocol
20
+
21
+
22
+ @dataclass
23
+ class MockResult(ResultProtocol):
24
+ values: list["MockResultRow"]
25
+ columns: list[str]
26
+
27
+ def __init__(self, values: list[Any], columns: list[str]):
28
+ processed: list[MockResultRow] = []
29
+ for x in values:
30
+ if isinstance(x, dict):
31
+ processed.append(MockResultRow(x))
32
+ elif isinstance(x, MockResultRow):
33
+ processed.append(x)
34
+ else:
35
+ raise ValueError(
36
+ f"Cannot process value of type {type(x)} in MockResult"
37
+ )
38
+ self.columns = columns
39
+ self.values = processed
40
+
41
+ def __iter__(self):
42
+ while self.values:
43
+ yield self.values.pop(0)
44
+
45
+ def fetchall(self):
46
+ return self.values
47
+
48
+ def fetchone(self):
49
+ if self.values:
50
+ return self.values.pop(0)
51
+ return None
52
+
53
+ def fetchmany(self, size: int):
54
+ rval = self.values[:size]
55
+ self.values = self.values[size:]
56
+ return rval
57
+
58
+ def keys(self):
59
+ return self.columns
60
+
61
+ def as_dict(self):
62
+ return [x.as_dict() if isinstance(x, MockResultRow) else x for x in self.values]
63
+
64
+
65
+ @dataclass
66
+ class MockResultRow:
67
+ _values: dict[str, Any]
68
+
69
+ def as_dict(self):
70
+ return self._values
71
+
72
+ def __str__(self) -> str:
73
+ return str(self._values)
74
+
75
+ def __repr__(self) -> str:
76
+ return repr(self._values)
77
+
78
+ def __getattr__(self, name: str) -> Any:
79
+ if name in self._values:
80
+ return self._values[name]
81
+ return super().__getattribute__(name)
82
+
83
+ def __getitem__(self, key: str) -> Any:
84
+ return self._values[key]
85
+
86
+ def values(self):
87
+ return self._values.values()
88
+
89
+ def keys(self):
90
+ return self._values.keys()
91
+
92
+
93
+ def generate_result_set(
94
+ columns: List[ConceptRef], output_data: list[Any]
95
+ ) -> MockResult:
96
+ """Generate a mock result set from columns and output data."""
97
+ names = [x.address.replace(".", "_") for x in columns]
98
+ return MockResult(
99
+ values=[dict(zip(names, [row])) for row in output_data], columns=names
100
+ )
101
+
102
+
103
+ def handle_concept_declaration(query: ConceptDeclarationStatement) -> MockResult:
104
+ """Handle concept declaration statements without execution."""
105
+ concept = query.concept
106
+ return MockResult(
107
+ [
108
+ {
109
+ "address": concept.address,
110
+ "type": concept.datatype.value,
111
+ "purpose": concept.purpose.value,
112
+ "derivation": concept.derivation.value,
113
+ }
114
+ ],
115
+ ["address", "type", "purpose", "derivation"],
116
+ )
117
+
118
+
119
+ def handle_datasource(query: Datasource) -> MockResult:
120
+ """Handle datasource queries without execution."""
121
+ return MockResult(
122
+ [
123
+ {
124
+ "name": query.name,
125
+ }
126
+ ],
127
+ ["name"],
128
+ )
129
+
130
+
131
+ def handle_import_statement(query: ImportStatement) -> MockResult:
132
+ """Handle import statements without execution."""
133
+ return MockResult(
134
+ [
135
+ {
136
+ "path": query.path,
137
+ "alias": query.alias,
138
+ }
139
+ ],
140
+ ["path", "alias"],
141
+ )
142
+
143
+
144
+ def handle_merge_statement(
145
+ query: MergeStatementV2, environment: Environment
146
+ ) -> MockResult:
147
+ """Handle merge statements by updating environment and returning result."""
148
+ for concept in query.sources:
149
+ environment.merge_concept(
150
+ concept, query.targets[concept.address], modifiers=query.modifiers
151
+ )
152
+
153
+ return MockResult(
154
+ [
155
+ {
156
+ "sources": ",".join([x.address for x in query.sources]),
157
+ "targets": ",".join([x.address for _, x in query.targets.items()]),
158
+ }
159
+ ],
160
+ ["source", "target"],
161
+ )
162
+
163
+
164
+ def handle_processed_show_statement(
165
+ query: ProcessedShowStatement, compiled_statements: list[str]
166
+ ) -> MockResult:
167
+ """Handle processed show statements without execution."""
168
+
169
+ return generate_result_set(query.output_columns, compiled_statements)
170
+
171
+
172
+ def raw_validation_to_result(
173
+ raw: list[ValidationTest], generator: Optional[BaseDialect] = None
174
+ ) -> Optional[MockResult]:
175
+ """Convert raw validation tests to mock result."""
176
+ if not raw:
177
+ return None
178
+ output = []
179
+ for row in raw:
180
+ if row.raw_query and generator and not row.generated_query:
181
+ try:
182
+ row.generated_query = generator.compile_statement(row.raw_query)
183
+ except Exception as e:
184
+ row.generated_query = f"Error generating query: {e}"
185
+ output.append(
186
+ {
187
+ "check_type": row.check_type.value,
188
+ "expected": row.expected,
189
+ "result": str(row.result) if row.result else None,
190
+ "ran": row.ran,
191
+ "query": row.generated_query if row.generated_query else "",
192
+ }
193
+ )
194
+ return MockResult(output, ["check_type", "expected", "result", "ran", "query"])
195
+
196
+
197
+ def handle_processed_validate_statement(
198
+ query: ProcessedValidateStatement, dialect: BaseDialect, validate_environment_func
199
+ ) -> Optional[MockResult]:
200
+ """Handle processed validate statements."""
201
+ results = validate_environment_func(query.scope, query.targets)
202
+ return raw_validation_to_result(results, dialect)
203
+
204
+
205
+ def handle_show_statement_outputs(
206
+ statement: ProcessedShowStatement,
207
+ compiled_statements: list[str],
208
+ environment: Environment,
209
+ dialect: BaseDialect,
210
+ ) -> list[MockResult]:
211
+ """Handle show statement outputs without execution."""
212
+ output = []
213
+ for x in statement.output_values:
214
+ if isinstance(x, ProcessedStaticValueOutput):
215
+ output.append(generate_result_set(statement.output_columns, x.values))
216
+ elif compiled_statements:
217
+
218
+ output.append(
219
+ generate_result_set(
220
+ statement.output_columns,
221
+ compiled_statements,
222
+ )
223
+ )
224
+ elif isinstance(x, ProcessedValidateStatement):
225
+ from trilogy.core.validation.environment import validate_environment
226
+
227
+ raw = validate_environment(environment, x.scope, x.targets)
228
+ results = raw_validation_to_result(raw, dialect)
229
+ if results:
230
+ output.append(results)
231
+ else:
232
+ raise NotImplementedError(f"Cannot show type {type(x)} in show statement")
233
+ return output
@@ -8,6 +8,7 @@ from trilogy.core.statements.execute import (
8
8
  ProcessedQueryPersist,
9
9
  ProcessedRawSQLStatement,
10
10
  ProcessedShowStatement,
11
+ ProcessedValidateStatement,
11
12
  )
12
13
  from trilogy.dialect.base import BaseDialect
13
14
  from trilogy.utility import string_to_hash
@@ -90,10 +91,11 @@ class SqlServerDialect(BaseDialect):
90
91
  | ProcessedQueryPersist
91
92
  | ProcessedShowStatement
92
93
  | ProcessedRawSQLStatement
94
+ | ProcessedValidateStatement
93
95
  ),
94
96
  ) -> str:
95
97
  base = super().compile_statement(query)
96
- if isinstance(base, (ProcessedQuery, ProcessedQueryPersist)):
98
+ if isinstance(query, (ProcessedQuery, ProcessedQueryPersist)):
97
99
  for cte in query.ctes:
98
100
  if len(cte.name) > MAX_IDENTIFIER_LENGTH:
99
101
  new_name = f"rhash_{string_to_hash(cte.name)}"
trilogy/engine.py CHANGED
@@ -1,21 +1,27 @@
1
- from typing import Any, Protocol
1
+ from typing import Any, Generator, List, Optional, Protocol
2
2
 
3
3
  from sqlalchemy.engine import Connection, CursorResult, Engine
4
4
 
5
5
  from trilogy.core.models.environment import Environment
6
6
 
7
7
 
8
- class EngineResult(Protocol):
9
- pass
8
+ class ResultProtocol(Protocol):
10
9
 
11
- def fetchall(self) -> list[tuple]:
12
- pass
10
+ def fetchall(self) -> List[Any]: ...
11
+
12
+ def keys(self) -> List[str]: ...
13
+
14
+ def fetchone(self) -> Optional[Any]: ...
15
+
16
+ def fetchmany(self, size: int) -> List[Any]: ...
17
+
18
+ def __iter__(self) -> Generator[Any, None, None]: ...
13
19
 
14
20
 
15
21
  class EngineConnection(Protocol):
16
22
  pass
17
23
 
18
- def execute(self, statement: str, parameters: Any | None = None) -> EngineResult:
24
+ def execute(self, statement: str, parameters: Any | None = None) -> ResultProtocol:
19
25
  pass
20
26
 
21
27
  def commit(self):
@@ -39,13 +45,25 @@ class ExecutionEngine(Protocol):
39
45
 
40
46
 
41
47
  ### Begin default SQLAlchemy implementation
42
- class SqlAlchemyResult(EngineResult):
48
+ class SqlAlchemyResult:
43
49
  def __init__(self, result: CursorResult):
44
50
  self.result = result
45
51
 
46
52
  def fetchall(self):
47
53
  return self.result.fetchall()
48
54
 
55
+ def keys(self):
56
+ return self.result.keys()
57
+
58
+ def fetchone(self):
59
+ return self.result.fetchone()
60
+
61
+ def fetchmany(self, size: int):
62
+ return self.result.fetchmany(size)
63
+
64
+ def __iter__(self):
65
+ return iter(self.result)
66
+
49
67
 
50
68
  class SqlAlchemyConnection(EngineConnection):
51
69
  def __init__(self, connection: Connection):