meridian-storage-postgresql 1.0.0__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.
- meridian_storage/adapters/postgresql/__init__.py +27 -0
- meridian_storage/adapters/postgresql/_errors.py +88 -0
- meridian_storage/adapters/postgresql/_operation.py +354 -0
- meridian_storage/adapters/postgresql/_runtime.py +273 -0
- meridian_storage/adapters/postgresql/_settings.py +472 -0
- meridian_storage/adapters/postgresql/_version.py +4 -0
- meridian_storage/adapters/postgresql/compatibility.json +22 -0
- meridian_storage/adapters/postgresql/descriptor/__init__.py +229 -0
- meridian_storage/adapters/postgresql/migration/__init__.py +281 -0
- meridian_storage/adapters/postgresql/probe/__init__.py +309 -0
- meridian_storage/adapters/postgresql/py.typed +1 -0
- meridian_storage/adapters/postgresql/query/__init__.py +824 -0
- meridian_storage/adapters/postgresql/query/_sql.py +85 -0
- meridian_storage/adapters/postgresql/query/_values.py +122 -0
- meridian_storage/adapters/postgresql/query/dml.py +440 -0
- meridian_storage/adapters/postgresql/schema/__init__.py +272 -0
- meridian_storage/adapters/postgresql/semantics/__init__.py +338 -0
- meridian_storage/adapters/postgresql/transactions/__init__.py +299 -0
- meridian_storage_postgresql-1.0.0.dist-info/METADATA +158 -0
- meridian_storage_postgresql-1.0.0.dist-info/RECORD +24 -0
- meridian_storage_postgresql-1.0.0.dist-info/WHEEL +4 -0
- meridian_storage_postgresql-1.0.0.dist-info/entry_points.txt +2 -0
- meridian_storage_postgresql-1.0.0.dist-info/licenses/LICENSE +201 -0
- meridian_storage_postgresql-1.0.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Meridian V1 PostgreSQL/PostGIS Adapter."""
|
|
3
|
+
|
|
4
|
+
from ._runtime import PostgreSQLAdapterFactory, PostgreSQLAdapterRuntime
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .descriptor import DESCRIPTOR, QUERY_CAPABILITIES
|
|
7
|
+
from .migration import LogicalTransfer, MigrationExecutor, RecoveryHook
|
|
8
|
+
from .query import PostgreSQLQueryTranslator
|
|
9
|
+
from .query.dml import DMLCompiler
|
|
10
|
+
from .schema import MigrationPlan, SchemaCompiler
|
|
11
|
+
from .semantics import PostgreSQLSemanticsAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DESCRIPTOR",
|
|
15
|
+
"QUERY_CAPABILITIES",
|
|
16
|
+
"DMLCompiler",
|
|
17
|
+
"LogicalTransfer",
|
|
18
|
+
"MigrationExecutor",
|
|
19
|
+
"MigrationPlan",
|
|
20
|
+
"PostgreSQLAdapterFactory",
|
|
21
|
+
"PostgreSQLAdapterRuntime",
|
|
22
|
+
"PostgreSQLQueryTranslator",
|
|
23
|
+
"PostgreSQLSemanticsAdapter",
|
|
24
|
+
"RecoveryHook",
|
|
25
|
+
"SchemaCompiler",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Credential-free PostgreSQL error mapping."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from meridian_storage.errors import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
AuthorizationError,
|
|
9
|
+
ConflictError,
|
|
10
|
+
ConstraintError,
|
|
11
|
+
ErrorCode,
|
|
12
|
+
MeridianError,
|
|
13
|
+
MeridianTimeoutError,
|
|
14
|
+
SafeCause,
|
|
15
|
+
TransactionError,
|
|
16
|
+
TransientError,
|
|
17
|
+
UnavailableError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def map_postgresql_error(
|
|
23
|
+
exc: BaseException,
|
|
24
|
+
*,
|
|
25
|
+
operation_contract: str | None = None,
|
|
26
|
+
request_id: str | None = None,
|
|
27
|
+
execution_id: str | None = None,
|
|
28
|
+
) -> MeridianError:
|
|
29
|
+
"""Map SQLSTATE class without exposing SQL, values, hosts, or credentials."""
|
|
30
|
+
|
|
31
|
+
state = getattr(exc, "sqlstate", None)
|
|
32
|
+
details = {
|
|
33
|
+
"operation_contract": operation_contract,
|
|
34
|
+
"request_id": request_id,
|
|
35
|
+
"execution_id": execution_id,
|
|
36
|
+
"adapter_provenance": {
|
|
37
|
+
"adapter": "postgresql",
|
|
38
|
+
**({"sqlstate": state} if isinstance(state, str) else {}),
|
|
39
|
+
},
|
|
40
|
+
"cause": SafeCause(type=type(exc).__name__, code=state),
|
|
41
|
+
}
|
|
42
|
+
if state in {"28P01", "28000"}:
|
|
43
|
+
return AuthenticationError(
|
|
44
|
+
ErrorCode.ADAPTER_FAILURE, "PostgreSQL authentication failed", **details
|
|
45
|
+
)
|
|
46
|
+
if state == "42501":
|
|
47
|
+
return AuthorizationError(
|
|
48
|
+
ErrorCode.ADAPTER_FAILURE, "PostgreSQL authorization failed", **details
|
|
49
|
+
)
|
|
50
|
+
if state == "23505":
|
|
51
|
+
return ConflictError(
|
|
52
|
+
ErrorCode.IDEMPOTENCY_CONFLICT, "a unique value already exists", **details
|
|
53
|
+
)
|
|
54
|
+
if isinstance(state, str) and state.startswith("23"):
|
|
55
|
+
return ConstraintError(
|
|
56
|
+
ErrorCode.OPERATION_INVALID, "a PostgreSQL constraint rejected the operation", **details
|
|
57
|
+
)
|
|
58
|
+
if state in {"40001", "40P01", "55P03"}:
|
|
59
|
+
return TransientError(
|
|
60
|
+
ErrorCode.ADAPTER_FAILURE, "the PostgreSQL transaction must be retried", **details
|
|
61
|
+
)
|
|
62
|
+
if state == "57014":
|
|
63
|
+
return MeridianTimeoutError(
|
|
64
|
+
ErrorCode.DEADLINE_EXCEEDED, "the PostgreSQL statement deadline expired", **details
|
|
65
|
+
)
|
|
66
|
+
if isinstance(state, str) and state.startswith("08"):
|
|
67
|
+
return UnavailableError(
|
|
68
|
+
ErrorCode.ADAPTER_FAILURE,
|
|
69
|
+
"the PostgreSQL service is unavailable",
|
|
70
|
+
retryable=True,
|
|
71
|
+
**details,
|
|
72
|
+
)
|
|
73
|
+
if isinstance(state, str) and state.startswith("25"):
|
|
74
|
+
return TransactionError(
|
|
75
|
+
ErrorCode.TRANSACTION_STATE, "PostgreSQL transaction state is invalid", **details
|
|
76
|
+
)
|
|
77
|
+
if state in {"42P01", "42703", "42883"}:
|
|
78
|
+
return ValidationError(
|
|
79
|
+
ErrorCode.PHYSICAL_FINGERPRINT,
|
|
80
|
+
"the pinned PostgreSQL physical schema is absent or incompatible",
|
|
81
|
+
**details,
|
|
82
|
+
)
|
|
83
|
+
return UnavailableError(
|
|
84
|
+
ErrorCode.ADAPTER_FAILURE, "PostgreSQL rejected the adapter operation", **details
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = ["map_postgresql_error"]
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Core Operation dispatch without exposing engine concepts to consumers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
from meridian_storage.query.adapter import CompiledQuery, TranslationContext
|
|
11
|
+
from meridian_storage.query.ast import (
|
|
12
|
+
Aggregate,
|
|
13
|
+
Field,
|
|
14
|
+
NamedAggregate,
|
|
15
|
+
Projection,
|
|
16
|
+
Sort,
|
|
17
|
+
full_text,
|
|
18
|
+
parse_filter,
|
|
19
|
+
)
|
|
20
|
+
from meridian_storage.query.wire import (
|
|
21
|
+
PageSpec,
|
|
22
|
+
QueryOperation,
|
|
23
|
+
QueryTarget,
|
|
24
|
+
ResultSpec,
|
|
25
|
+
SafetyBudget,
|
|
26
|
+
TraversalSpec,
|
|
27
|
+
)
|
|
28
|
+
from meridian_storage.semantics import RecordReference, sha256_fingerprint
|
|
29
|
+
from meridian_storage.spi.adapters import ExecutionRequest
|
|
30
|
+
|
|
31
|
+
from ._settings import PostgreSQLSettings
|
|
32
|
+
from .query import PostgreSQLQueryTranslator
|
|
33
|
+
from .query.dml import DMLCommand, DMLCompiler
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class QueryCommand:
|
|
38
|
+
compiled: CompiledQuery
|
|
39
|
+
translator: PostgreSQLQueryTranslator
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
type AdapterCommand = DMLCommand | QueryCommand
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OperationCompiler:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
settings: PostgreSQLSettings,
|
|
49
|
+
translator: PostgreSQLQueryTranslator,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.settings = settings
|
|
52
|
+
self.translator = translator
|
|
53
|
+
self.dml = DMLCompiler(settings)
|
|
54
|
+
|
|
55
|
+
def compile(self, request: ExecutionRequest) -> AdapterCommand:
|
|
56
|
+
operation = request.operation
|
|
57
|
+
if operation.catalog not in {"structured", "evidence"}:
|
|
58
|
+
raise ValueError("PostgreSQL V1 implements structured and evidence operations")
|
|
59
|
+
if operation.operation_version != "1.0.0":
|
|
60
|
+
raise ValueError("PostgreSQL V1 accepts only Operation version 1.0.0")
|
|
61
|
+
prefix = f"meridian.{operation.catalog}."
|
|
62
|
+
if not operation.operation_contract.startswith(prefix):
|
|
63
|
+
raise ValueError("Operation contract does not match its Catalog")
|
|
64
|
+
method = operation.operation_contract.removeprefix(prefix)
|
|
65
|
+
allowed = {
|
|
66
|
+
"structured": {
|
|
67
|
+
"aggregate",
|
|
68
|
+
"create_resource",
|
|
69
|
+
"delete",
|
|
70
|
+
"get",
|
|
71
|
+
"patch",
|
|
72
|
+
"publish_schema",
|
|
73
|
+
"put",
|
|
74
|
+
"query",
|
|
75
|
+
"search",
|
|
76
|
+
"traverse",
|
|
77
|
+
},
|
|
78
|
+
"evidence": {"append", "query"},
|
|
79
|
+
}
|
|
80
|
+
if method not in allowed[operation.catalog]:
|
|
81
|
+
raise ValueError(f"unsupported {operation.catalog} Operation contract: {method!r}")
|
|
82
|
+
if method in {"create_resource", "publish_schema"}:
|
|
83
|
+
raise ValueError("physical DDL is only available through the Platform migration hook")
|
|
84
|
+
if len(operation.resources) < 1:
|
|
85
|
+
raise ValueError("PostgreSQL Operation requires a Resource")
|
|
86
|
+
if any(resource.catalog != operation.catalog for resource in operation.resources):
|
|
87
|
+
raise ValueError("Operation Resources must belong to its Catalog")
|
|
88
|
+
if (
|
|
89
|
+
method != "traverse"
|
|
90
|
+
and "queryPlan" not in operation.input
|
|
91
|
+
and len(operation.resources) != 1
|
|
92
|
+
):
|
|
93
|
+
raise ValueError("non-traversal Operations require exactly one Resource")
|
|
94
|
+
if (
|
|
95
|
+
method in {"put", "get", "patch", "delete", "append"}
|
|
96
|
+
and "queryPlan" not in operation.input
|
|
97
|
+
):
|
|
98
|
+
return self.dml.compile(
|
|
99
|
+
method,
|
|
100
|
+
operation.resources[0],
|
|
101
|
+
cast(Mapping[str, object], operation.input),
|
|
102
|
+
request.context,
|
|
103
|
+
)
|
|
104
|
+
query_operation = self._query_operation(request, method)
|
|
105
|
+
if set(query_operation.resources) != set(operation.resources):
|
|
106
|
+
raise ValueError("query plan Resources differ from the enclosing Operation")
|
|
107
|
+
context = TranslationContext(
|
|
108
|
+
binding_id=request.binding_id,
|
|
109
|
+
plan_fingerprint=query_operation.fingerprint,
|
|
110
|
+
registry_fingerprint=request.registry_fingerprint,
|
|
111
|
+
schema_fingerprints={
|
|
112
|
+
ref.canonical: self.settings.layout(ref).schema_fingerprint
|
|
113
|
+
for ref in query_operation.resources
|
|
114
|
+
},
|
|
115
|
+
scope_fingerprint=self._scope_fingerprint(request),
|
|
116
|
+
deadline_ms=self._deadline_ms(request, query_operation.budget.deadline_ms),
|
|
117
|
+
)
|
|
118
|
+
return QueryCommand(self.translator.compile(query_operation, context), self.translator)
|
|
119
|
+
|
|
120
|
+
def _query_operation(self, request: ExecutionRequest, method: str) -> QueryOperation:
|
|
121
|
+
raw_plan = request.operation.input.get("queryPlan")
|
|
122
|
+
if raw_plan is not None:
|
|
123
|
+
if not isinstance(raw_plan, Mapping):
|
|
124
|
+
raise TypeError("queryPlan must be a released QueryOperation mapping")
|
|
125
|
+
return QueryOperation.from_mapping(cast(Mapping[str, object], raw_plan))
|
|
126
|
+
values = cast(Mapping[str, object], request.operation.input)
|
|
127
|
+
resource = request.operation.resources[0]
|
|
128
|
+
layout = self.settings.layout(resource)
|
|
129
|
+
target = QueryTarget(resource)
|
|
130
|
+
where = values.get("where", {})
|
|
131
|
+
if not isinstance(where, Mapping):
|
|
132
|
+
raise TypeError("structured query where must be an object")
|
|
133
|
+
predicate = parse_filter(where)
|
|
134
|
+
configured_result_limit = request.operation.input.get("resultByteLimit", 16 * 1024 * 1024)
|
|
135
|
+
result_limit = (
|
|
136
|
+
configured_result_limit
|
|
137
|
+
if isinstance(configured_result_limit, int)
|
|
138
|
+
and not isinstance(configured_result_limit, bool)
|
|
139
|
+
else 16 * 1024 * 1024
|
|
140
|
+
)
|
|
141
|
+
budget = SafetyBudget(
|
|
142
|
+
deadline_ms=self._deadline_ms(request, 30_000),
|
|
143
|
+
max_result_values=10_000,
|
|
144
|
+
max_normalized_bytes=min(result_limit, 16 * 1024 * 1024),
|
|
145
|
+
)
|
|
146
|
+
if method == "query":
|
|
147
|
+
return self._structured_query(values, target, predicate, budget, layout)
|
|
148
|
+
if method == "search":
|
|
149
|
+
return self._structured_search(values, target, predicate, budget, layout)
|
|
150
|
+
if method == "aggregate":
|
|
151
|
+
return self._structured_aggregate(values, target, predicate, budget)
|
|
152
|
+
if method == "traverse":
|
|
153
|
+
return self._structured_traverse(request, values, target, budget)
|
|
154
|
+
raise ValueError(f"unsupported {request.operation.catalog} Operation contract: {method!r}")
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _structured_query(
|
|
158
|
+
values: Mapping[str, object],
|
|
159
|
+
target: QueryTarget,
|
|
160
|
+
predicate: Any,
|
|
161
|
+
budget: SafetyBudget,
|
|
162
|
+
layout: Any,
|
|
163
|
+
) -> QueryOperation:
|
|
164
|
+
select = values.get("select", ())
|
|
165
|
+
order_by = values.get("orderBy", ())
|
|
166
|
+
limit = values.get("limit", 50)
|
|
167
|
+
if (
|
|
168
|
+
not isinstance(select, Sequence)
|
|
169
|
+
or isinstance(select, (str, bytes))
|
|
170
|
+
or not isinstance(order_by, Sequence)
|
|
171
|
+
or isinstance(order_by, (str, bytes))
|
|
172
|
+
or isinstance(limit, bool)
|
|
173
|
+
or not isinstance(limit, int)
|
|
174
|
+
):
|
|
175
|
+
raise TypeError("structured query select, orderBy, or limit has an invalid type")
|
|
176
|
+
projection = tuple(Projection(Field(cast(str, name)), cast(str, name)) for name in select)
|
|
177
|
+
sorts: list[Sort] = []
|
|
178
|
+
for item in order_by:
|
|
179
|
+
if not isinstance(item, Mapping) or set(item) - {"field", "direction", "nulls"}:
|
|
180
|
+
raise ValueError("orderBy entries require field/direction/nulls")
|
|
181
|
+
field_name = item.get("field")
|
|
182
|
+
if not isinstance(field_name, str) or field_name not in layout.field_map:
|
|
183
|
+
raise ValueError("orderBy references an unknown field")
|
|
184
|
+
sorts.append(
|
|
185
|
+
Sort(
|
|
186
|
+
Field(field_name),
|
|
187
|
+
cast(str, item.get("direction", "asc")),
|
|
188
|
+
cast(str, item.get("nulls", "last")),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
return QueryOperation(
|
|
192
|
+
catalog=target.resource.catalog,
|
|
193
|
+
targets=(target,),
|
|
194
|
+
operation="scan",
|
|
195
|
+
result=ResultSpec("records", projection),
|
|
196
|
+
filter=predicate,
|
|
197
|
+
order=tuple(sorts),
|
|
198
|
+
page=PageSpec(limit, cast(str | None, values.get("cursor"))),
|
|
199
|
+
budget=budget,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _structured_search(
|
|
204
|
+
values: Mapping[str, object],
|
|
205
|
+
target: QueryTarget,
|
|
206
|
+
predicate: Any,
|
|
207
|
+
budget: SafetyBudget,
|
|
208
|
+
layout: Any,
|
|
209
|
+
) -> QueryOperation:
|
|
210
|
+
if values.get("facets") or values.get("highlights"):
|
|
211
|
+
raise ValueError("this V1 profile does not advertise facets or highlights")
|
|
212
|
+
full_text_fields = tuple(
|
|
213
|
+
field for index in layout.indexes if index.kind == "full-text" for field in index.fields
|
|
214
|
+
)
|
|
215
|
+
if not full_text_fields:
|
|
216
|
+
raise ValueError("search requires a pinned full-text index")
|
|
217
|
+
query = values.get("query")
|
|
218
|
+
if isinstance(query, Mapping):
|
|
219
|
+
text = query.get("text")
|
|
220
|
+
selected = query.get("fields", full_text_fields)
|
|
221
|
+
if not isinstance(selected, Sequence) or isinstance(selected, (str, bytes)):
|
|
222
|
+
raise TypeError("search fields must be an array")
|
|
223
|
+
fields = tuple(cast(str, item) for item in selected)
|
|
224
|
+
else:
|
|
225
|
+
text = query
|
|
226
|
+
fields = full_text_fields
|
|
227
|
+
if not isinstance(text, str):
|
|
228
|
+
raise TypeError("search query text must be a string")
|
|
229
|
+
if (
|
|
230
|
+
not fields
|
|
231
|
+
or len(set(fields)) != len(fields)
|
|
232
|
+
or any(field not in full_text_fields for field in fields)
|
|
233
|
+
):
|
|
234
|
+
raise ValueError("search fields must be unique and backed by pinned full-text indexes")
|
|
235
|
+
match = full_text(text, fields=fields)
|
|
236
|
+
combined = match if predicate is None else predicate.and_(match)
|
|
237
|
+
limit = values.get("limit", 50)
|
|
238
|
+
if isinstance(limit, bool) or not isinstance(limit, int):
|
|
239
|
+
raise TypeError("search limit must be an integer")
|
|
240
|
+
return QueryOperation(
|
|
241
|
+
catalog=target.resource.catalog,
|
|
242
|
+
targets=(target,),
|
|
243
|
+
operation="search",
|
|
244
|
+
result=ResultSpec("search"),
|
|
245
|
+
filter=combined,
|
|
246
|
+
page=PageSpec(limit, cast(str | None, values.get("cursor"))),
|
|
247
|
+
budget=budget,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _structured_aggregate(
|
|
252
|
+
values: Mapping[str, object],
|
|
253
|
+
target: QueryTarget,
|
|
254
|
+
predicate: Any,
|
|
255
|
+
budget: SafetyBudget,
|
|
256
|
+
) -> QueryOperation:
|
|
257
|
+
raw_grouping = values.get("groupBy", ())
|
|
258
|
+
raw_metrics = values.get("metrics", ())
|
|
259
|
+
if (
|
|
260
|
+
not isinstance(raw_grouping, Sequence)
|
|
261
|
+
or isinstance(raw_grouping, (str, bytes))
|
|
262
|
+
or not isinstance(raw_metrics, Sequence)
|
|
263
|
+
or isinstance(raw_metrics, (str, bytes))
|
|
264
|
+
):
|
|
265
|
+
raise TypeError("aggregate groupBy and metrics must be arrays")
|
|
266
|
+
grouping = tuple(Field(cast(str, name)) for name in raw_grouping)
|
|
267
|
+
aggregates: list[NamedAggregate] = []
|
|
268
|
+
for metric in raw_metrics:
|
|
269
|
+
if not isinstance(metric, Mapping) or set(metric) - {
|
|
270
|
+
"name",
|
|
271
|
+
"function",
|
|
272
|
+
"field",
|
|
273
|
+
"distinct",
|
|
274
|
+
}:
|
|
275
|
+
raise ValueError("aggregate metric is not a closed V1 metric")
|
|
276
|
+
name = metric.get("name")
|
|
277
|
+
function = metric.get("function")
|
|
278
|
+
field_name = metric.get("field")
|
|
279
|
+
if not isinstance(name, str) or not isinstance(function, str):
|
|
280
|
+
raise TypeError("aggregate name and function must be strings")
|
|
281
|
+
distinct = metric.get("distinct", False)
|
|
282
|
+
if not isinstance(distinct, bool):
|
|
283
|
+
raise TypeError("aggregate distinct must be a boolean")
|
|
284
|
+
operand = None if field_name is None else Field(cast(str, field_name))
|
|
285
|
+
aggregates.append(
|
|
286
|
+
NamedAggregate(
|
|
287
|
+
name,
|
|
288
|
+
Aggregate(function, operand, distinct),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
return QueryOperation(
|
|
292
|
+
catalog="structured",
|
|
293
|
+
targets=(target,),
|
|
294
|
+
operation="aggregate",
|
|
295
|
+
result=ResultSpec("aggregate"),
|
|
296
|
+
filter=predicate,
|
|
297
|
+
grouping=grouping,
|
|
298
|
+
aggregates=tuple(aggregates),
|
|
299
|
+
budget=budget,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _structured_traverse(
|
|
304
|
+
request: ExecutionRequest,
|
|
305
|
+
values: Mapping[str, object],
|
|
306
|
+
target: QueryTarget,
|
|
307
|
+
budget: SafetyBudget,
|
|
308
|
+
) -> QueryOperation:
|
|
309
|
+
start = values.get("start")
|
|
310
|
+
if not isinstance(start, Mapping):
|
|
311
|
+
raise TypeError("traversal start must be a RecordReference")
|
|
312
|
+
relations = tuple(request.operation.resources[1:])
|
|
313
|
+
all_neighbors = values.get("allNeighbors") is True
|
|
314
|
+
registry_fingerprint = (
|
|
315
|
+
cast(str, values.get("registryFingerprint")) if all_neighbors else None
|
|
316
|
+
)
|
|
317
|
+
max_depth = values.get("maxDepth", 1)
|
|
318
|
+
if isinstance(max_depth, bool) or not isinstance(max_depth, int):
|
|
319
|
+
raise TypeError("traversal maxDepth must be an integer")
|
|
320
|
+
traversal = TraversalSpec(
|
|
321
|
+
start=RecordReference.from_mapping(cast(Mapping[str, object], start)),
|
|
322
|
+
relation_collections=relations,
|
|
323
|
+
all_neighbors=all_neighbors,
|
|
324
|
+
max_depth=max_depth,
|
|
325
|
+
result_shape="records",
|
|
326
|
+
registry_fingerprint=registry_fingerprint,
|
|
327
|
+
)
|
|
328
|
+
return QueryOperation(
|
|
329
|
+
catalog="structured",
|
|
330
|
+
targets=(target,),
|
|
331
|
+
operation="traverse",
|
|
332
|
+
result=ResultSpec("records"),
|
|
333
|
+
traversal=traversal,
|
|
334
|
+
page=PageSpec(min(budget.max_returned_paths, 500)),
|
|
335
|
+
budget=budget,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _scope_fingerprint(self, request: ExecutionRequest) -> str:
|
|
339
|
+
return sha256_fingerprint(
|
|
340
|
+
{
|
|
341
|
+
"tenant": request.context.tenant,
|
|
342
|
+
"scope": dict(sorted(request.context.scope.items())),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _deadline_ms(request: ExecutionRequest, default: int) -> int:
|
|
348
|
+
remaining = request.context.remaining_seconds()
|
|
349
|
+
if remaining is None:
|
|
350
|
+
return default
|
|
351
|
+
return max(1, min(default, int(remaining * 1000)))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
__all__ = ["AdapterCommand", "OperationCompiler", "QueryCommand"]
|