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.
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/METADATA +184 -136
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/RECORD +35 -30
- trilogy/__init__.py +1 -1
- trilogy/authoring/__init__.py +61 -43
- trilogy/core/enums.py +13 -0
- trilogy/core/env_processor.py +19 -10
- trilogy/core/environment_helpers.py +111 -0
- trilogy/core/exceptions.py +21 -1
- trilogy/core/functions.py +6 -1
- trilogy/core/graph_models.py +11 -37
- trilogy/core/internal.py +18 -0
- trilogy/core/models/core.py +3 -0
- trilogy/core/models/environment.py +28 -0
- trilogy/core/models/execute.py +7 -0
- trilogy/core/processing/node_generators/select_merge_node.py +2 -2
- trilogy/core/query_processor.py +2 -1
- trilogy/core/statements/author.py +18 -3
- trilogy/core/statements/common.py +0 -10
- trilogy/core/statements/execute.py +73 -16
- trilogy/core/validation/common.py +110 -0
- trilogy/core/validation/concept.py +125 -0
- trilogy/core/validation/datasource.py +194 -0
- trilogy/core/validation/environment.py +71 -0
- trilogy/dialect/base.py +48 -21
- trilogy/dialect/metadata.py +233 -0
- trilogy/dialect/sql_server.py +3 -1
- trilogy/engine.py +25 -7
- trilogy/executor.py +94 -162
- trilogy/parsing/parse_engine.py +34 -3
- trilogy/parsing/trilogy.lark +11 -5
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/licenses/LICENSE.md +0 -0
- {pytrilogy-0.0.3.94.dist-info → pytrilogy-0.0.3.96.dist-info}/top_level.txt +0 -0
- /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
|
-
|
|
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(
|
|
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
|
|
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
|
trilogy/dialect/sql_server.py
CHANGED
|
@@ -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(
|
|
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
|
|
9
|
-
pass
|
|
8
|
+
class ResultProtocol(Protocol):
|
|
10
9
|
|
|
11
|
-
def fetchall(self) ->
|
|
12
|
-
|
|
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) ->
|
|
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
|
|
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):
|