smart-data-engine-sdk 0.1.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.
- sde/__init__.py +318 -0
- sde/_cutover_project.py +179 -0
- sde/_local_state.py +188 -0
- sde/_operator_deadline.py +50 -0
- sde/_usage.py +314 -0
- sde/bulk.py +79 -0
- sde/canonical.py +141 -0
- sde/capabilities.py +62 -0
- sde/cutover.py +286 -0
- sde/engines/__init__.py +0 -0
- sde/engines/_clickhouse_connection.py +224 -0
- sde/engines/_index_build.py +294 -0
- sde/engines/_operator.py +394 -0
- sde/engines/_staging.py +222 -0
- sde/engines/_storage.py +22 -0
- sde/engines/_write_fences.py +271 -0
- sde/engines/clickhouse.py +1115 -0
- sde/engines/orderbook.py +457 -0
- sde/engines/postgres.py +967 -0
- sde/entity.py +170 -0
- sde/errors.py +103 -0
- sde/explain.py +300 -0
- sde/frozen_verification.py +152 -0
- sde/generation.py +131 -0
- sde/groups.py +97 -0
- sde/hashing.py +242 -0
- sde/index_build.py +313 -0
- sde/index_operator.py +347 -0
- sde/infer.py +461 -0
- sde/inspection.py +62 -0
- sde/internal.py +90 -0
- sde/layout.py +669 -0
- sde/local_cutover.py +801 -0
- sde/logging.py +143 -0
- sde/migration.py +856 -0
- sde/model.py +482 -0
- sde/physical.py +531 -0
- sde/placement.py +1010 -0
- sde/provisioning.py +63 -0
- sde/py.typed +0 -0
- sde/query.py +521 -0
- sde/routing.py +85 -0
- sde/schema.py +466 -0
- sde/session.py +993 -0
- sde/shapes.py +153 -0
- sde/staging.py +264 -0
- sde/staging_operator.py +393 -0
- sde/telemetry.py +1087 -0
- sde/testing/__init__.py +14 -0
- sde/testing/loader.py +175 -0
- sde/testing/memory.py +331 -0
- sde/types.py +228 -0
- sde/verification.py +220 -0
- sde/watermark.py +222 -0
- sde/write_fence.py +283 -0
- sde_demo/__init__.py +1 -0
- sde_demo/__main__.py +183 -0
- sde_demo/diagnostics.py +92 -0
- sde_demo/model.py +75 -0
- sde_demo/project.py +312 -0
- sde_demo/py.typed +0 -0
- sde_demo/query_count.py +301 -0
- sde_demo/resources.py +969 -0
- sde_demo/runtime.py +419 -0
- sde_demo/verification.py +242 -0
- sde_operator/__init__.py +1 -0
- sde_operator/__main__.py +210 -0
- smart_data_engine_sdk-0.1.0.dist-info/METADATA +174 -0
- smart_data_engine_sdk-0.1.0.dist-info/RECORD +73 -0
- smart_data_engine_sdk-0.1.0.dist-info/WHEEL +4 -0
- smart_data_engine_sdk-0.1.0.dist-info/entry_points.txt +3 -0
- smart_data_engine_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- smart_data_engine_sdk-0.1.0.dist-info/licenses/NOTICE +13 -0
sde_demo/project.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Validated customer configuration and local Weather bootstrap orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import importlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import stat
|
|
11
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import sde
|
|
17
|
+
from sde._local_state import confirm_file, transaction, write_bytes
|
|
18
|
+
from sde.generation import GENERATIONS_SINCE, check_map_project
|
|
19
|
+
|
|
20
|
+
from .model import model
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DemoRefused(ValueError):
|
|
24
|
+
"""An actionable starter error whose message contains no driver or input values."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def payload(path: Path, *, private: bool = False) -> bytes:
|
|
28
|
+
if path.is_symlink() or not path.is_file():
|
|
29
|
+
raise DemoRefused("A required local file is missing or is a symlink.")
|
|
30
|
+
if private and stat.S_IMODE(path.stat().st_mode) != 0o600:
|
|
31
|
+
raise DemoRefused("Credential files must have mode 0600; fix their permissions.")
|
|
32
|
+
with path.open("rb") as stream:
|
|
33
|
+
data = stream.read(2 * 1024 * 1024 + 1)
|
|
34
|
+
if len(data) > 2 * 1024 * 1024:
|
|
35
|
+
raise DemoRefused("The local JSON input exceeds 2 MiB.")
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decode(data: bytes) -> dict[str, Any]:
|
|
40
|
+
def unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
41
|
+
result: dict[str, Any] = {}
|
|
42
|
+
for name, value in pairs:
|
|
43
|
+
if name in result:
|
|
44
|
+
raise DemoRefused("JSON contains duplicate keys; restore the original file.")
|
|
45
|
+
result[name] = value
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
value = json.loads(data, object_pairs_hook=unique)
|
|
49
|
+
if not isinstance(value, dict):
|
|
50
|
+
raise DemoRefused("The local JSON input must be an object.")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read(path: Path, *, private: bool = False) -> dict[str, Any]:
|
|
55
|
+
return decode(payload(path, private=private))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write(path: Path, value: Mapping[str, Any], *, once: bool = False) -> None:
|
|
59
|
+
payload = (
|
|
60
|
+
json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False) + "\n"
|
|
61
|
+
).encode()
|
|
62
|
+
if once and path.exists():
|
|
63
|
+
if path.is_symlink() or path.read_bytes() != payload:
|
|
64
|
+
raise DemoRefused("This directory belongs to another setup; use a fresh directory.")
|
|
65
|
+
confirm_file(path)
|
|
66
|
+
return
|
|
67
|
+
write_bytes(path, payload, replace=not once)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def public_keys(value: Any) -> dict[str, bytes]:
|
|
71
|
+
if not isinstance(value, dict) or not value:
|
|
72
|
+
raise DemoRefused("Bootstrap needs trusted public keys supplied by your operator.")
|
|
73
|
+
result = {}
|
|
74
|
+
for name, encoded in value.items():
|
|
75
|
+
if not isinstance(name, str) or not name or not isinstance(encoded, str):
|
|
76
|
+
raise DemoRefused("Invalid public key configuration.")
|
|
77
|
+
try:
|
|
78
|
+
decoded = base64.b64decode(encoded, validate=True)
|
|
79
|
+
except binascii.Error:
|
|
80
|
+
raise DemoRefused("Invalid public key configuration.") from None
|
|
81
|
+
if len(decoded) != 32:
|
|
82
|
+
raise DemoRefused("Ed25519 public keys must contain 32 bytes.")
|
|
83
|
+
result[name] = decoded
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def bootstrap(value: dict[str, Any]) -> tuple[dict[str, Any], sde.PlacementMap]:
|
|
88
|
+
if (
|
|
89
|
+
set(value)
|
|
90
|
+
!= {"kind", "protocol", "project_id", "model", "public_keys", "engines", "current_map"}
|
|
91
|
+
or value["kind"] != "sde-weather-bootstrap"
|
|
92
|
+
or type(value["protocol"]) is not int
|
|
93
|
+
or value["protocol"] != 1
|
|
94
|
+
):
|
|
95
|
+
raise DemoRefused("Use the complete protocol-1 Weather bootstrap from your operator.")
|
|
96
|
+
from sde.testing.loader import model_from_neutral
|
|
97
|
+
|
|
98
|
+
logical = model()
|
|
99
|
+
if model_from_neutral(value["model"]).version != logical.version:
|
|
100
|
+
raise DemoRefused("This starter requires the Weather model; request a matching bootstrap.")
|
|
101
|
+
engines = value["engines"]
|
|
102
|
+
if (
|
|
103
|
+
not isinstance(engines, dict)
|
|
104
|
+
or not 1 <= len(engines) <= 2
|
|
105
|
+
or any(not re.fullmatch(r"[a-z][a-z0-9_-]{0,47}", name) for name in engines)
|
|
106
|
+
or any(dialect not in ("postgres", "clickhouse") for dialect in engines.values())
|
|
107
|
+
or len(set(engines.values())) != len(engines)
|
|
108
|
+
):
|
|
109
|
+
raise DemoRefused(
|
|
110
|
+
"Weather demo needs one or two uniquely named PostgreSQL/ClickHouse bindings."
|
|
111
|
+
)
|
|
112
|
+
keys = public_keys(value["public_keys"])
|
|
113
|
+
try:
|
|
114
|
+
placement = sde.load_map(
|
|
115
|
+
value["current_map"], model=logical, public_key=keys, require_signature=True
|
|
116
|
+
)
|
|
117
|
+
except sde.MapError as exc:
|
|
118
|
+
# Refused before setup writes anything, and the reason is about the map - a missing
|
|
119
|
+
# 'signed' extra, a bad signature - never about a credential, so it can be said in full.
|
|
120
|
+
raise DemoRefused(f"The bootstrap's map does not load: {exc}") from None
|
|
121
|
+
check_map_project(placement, value["project_id"])
|
|
122
|
+
if placement.contract < GENERATIONS_SINCE or placement.map_version != 1:
|
|
123
|
+
raise DemoRefused(
|
|
124
|
+
"A new demo must start from a signed generation-bearing initial map (version 1)."
|
|
125
|
+
)
|
|
126
|
+
for group in placement.groups.values():
|
|
127
|
+
if len(group.all()) != 1 or group.source.engine not in engines:
|
|
128
|
+
raise DemoRefused("Bootstrap must contain only bound initial source materializations.")
|
|
129
|
+
config = {
|
|
130
|
+
"protocol": 1,
|
|
131
|
+
"project_id": value["project_id"],
|
|
132
|
+
"model": sde.neutral_declaration(logical),
|
|
133
|
+
"public_keys": value["public_keys"],
|
|
134
|
+
"engines": {
|
|
135
|
+
name: {
|
|
136
|
+
"dialect": dialect,
|
|
137
|
+
"operator_dsn_env": f"SDE_WEATHER_OPERATOR_{index}",
|
|
138
|
+
"runtime_dsn_envs": [f"SDE_WEATHER_RUNTIME_{index}"],
|
|
139
|
+
}
|
|
140
|
+
for index, (name, dialect) in enumerate(sorted(engines.items()))
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
return config, placement
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def config(root: Path, *, ready: bool = True) -> dict[str, Any]:
|
|
147
|
+
expected, _ = bootstrap(read(root / "bootstrap.json"))
|
|
148
|
+
if read(root / "config.json") != expected:
|
|
149
|
+
raise DemoRefused("Local config differs from the enrolled bootstrap; restore it.")
|
|
150
|
+
if ready:
|
|
151
|
+
if (root / "reset-request.json").exists():
|
|
152
|
+
raise DemoRefused("Reset was requested; finish reset and set up a fresh directory.")
|
|
153
|
+
marker = read(root / "setup-complete.json")
|
|
154
|
+
if marker != {"protocol": 1, "config_digest": sde.digest16(expected)}:
|
|
155
|
+
raise DemoRefused(
|
|
156
|
+
"Setup is incomplete or changed; rerun setup with its original input."
|
|
157
|
+
)
|
|
158
|
+
return expected
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def credentials(root: Path, purpose: str, bindings: Mapping[str, Any]) -> dict[str, str]:
|
|
162
|
+
import hashlib
|
|
163
|
+
|
|
164
|
+
filename = purpose + "-credentials.json"
|
|
165
|
+
data = payload(root / filename, private=True)
|
|
166
|
+
value = decode(data)
|
|
167
|
+
if set(value) != set(bindings) or any(
|
|
168
|
+
not isinstance(dsn, str) or not dsn for dsn in value.values()
|
|
169
|
+
):
|
|
170
|
+
raise DemoRefused("The local credential bindings are incomplete.")
|
|
171
|
+
# Verify the file this caller needs, without opening the other privilege level's secrets.
|
|
172
|
+
metadata = read(root / "resources.json")
|
|
173
|
+
hashes = metadata.get("credential_hashes")
|
|
174
|
+
if (
|
|
175
|
+
not isinstance(hashes, dict)
|
|
176
|
+
or metadata.get("status") != "ready"
|
|
177
|
+
or hashes.get(purpose) != hashlib.sha256(data).hexdigest()
|
|
178
|
+
):
|
|
179
|
+
raise DemoRefused("Local credentials differ from the resource allocation; restore them.")
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
DRIVERS = {"postgres": ("psycopg", "postgres"), "clickhouse": ("clickhouse_connect", "clickhouse")}
|
|
184
|
+
"""The driver module each binding's adapter imports, and the SDK extra that installs it."""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def require_drivers(dialects: Iterable[str]) -> None:
|
|
188
|
+
"""Refuse before anything is written when a binding's driver cannot be imported.
|
|
189
|
+
|
|
190
|
+
Without this the first session open failed inside a run's retry loop - ten seconds of retries,
|
|
191
|
+
a message that could only say "incomplete", and a report left `incomplete` for later fleet runs
|
|
192
|
+
to stop at. The import is the adapter's own, so a module that is present but cannot load counts
|
|
193
|
+
as missing too.
|
|
194
|
+
"""
|
|
195
|
+
missing = []
|
|
196
|
+
for dialect in sorted(set(dialects)):
|
|
197
|
+
module, extra = DRIVERS[dialect]
|
|
198
|
+
try:
|
|
199
|
+
importlib.import_module(module)
|
|
200
|
+
except ImportError:
|
|
201
|
+
missing.append(extra)
|
|
202
|
+
if missing:
|
|
203
|
+
raise DemoRefused(
|
|
204
|
+
"Install the drivers for this demo's engines: "
|
|
205
|
+
f"pip install 'smart-data-engine-sdk[{','.join(missing)}]'"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def engine(dialect: str, dsn: str) -> Any:
|
|
210
|
+
if dialect == "postgres":
|
|
211
|
+
from sde.engines.postgres import PostgresEngine
|
|
212
|
+
|
|
213
|
+
return PostgresEngine(dsn)
|
|
214
|
+
if dialect == "clickhouse":
|
|
215
|
+
from sde.engines.clickhouse import ClickHouseEngine
|
|
216
|
+
|
|
217
|
+
return ClickHouseEngine(dsn)
|
|
218
|
+
raise DemoRefused("Unsupported engine dialect.")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@contextmanager
|
|
222
|
+
def connections(
|
|
223
|
+
root: Path, settings: dict[str, Any]
|
|
224
|
+
) -> Iterator[tuple[dict[str, Any], dict[str, Any]]]:
|
|
225
|
+
opened: list[Any] = []
|
|
226
|
+
try:
|
|
227
|
+
operators: dict[str, Any] = {}
|
|
228
|
+
runtime: dict[str, Any] = {}
|
|
229
|
+
for purpose, target in (("operator", operators), ("runtime", runtime)):
|
|
230
|
+
dsns = credentials(root, purpose, settings["engines"])
|
|
231
|
+
for name, binding in sorted(settings["engines"].items()):
|
|
232
|
+
adapter = engine(binding["dialect"], dsns[name])
|
|
233
|
+
opened.append(adapter)
|
|
234
|
+
adapter.connect()
|
|
235
|
+
target[name] = adapter
|
|
236
|
+
yield operators, runtime
|
|
237
|
+
finally:
|
|
238
|
+
# Attempt every cleanup, even if an earlier adapter cannot close.
|
|
239
|
+
failures: list[Exception] = []
|
|
240
|
+
for adapter in reversed(opened):
|
|
241
|
+
try:
|
|
242
|
+
adapter.close()
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
failures.append(exc)
|
|
245
|
+
if failures:
|
|
246
|
+
raise DemoRefused("An engine connection could not close; stop this process.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def setup(root: Path, supplied: dict[str, Any], admin_dsns: Mapping[str, str]) -> dict[str, Any]:
|
|
250
|
+
from .resources import allocate, grant_tables
|
|
251
|
+
|
|
252
|
+
settings, placement = bootstrap(supplied)
|
|
253
|
+
require_drivers(supplied["engines"].values())
|
|
254
|
+
logical = model()
|
|
255
|
+
with transaction(root):
|
|
256
|
+
if (root / "reset-request.json").exists():
|
|
257
|
+
raise DemoRefused(
|
|
258
|
+
"This allocation is being reset; use a new directory for the next demo."
|
|
259
|
+
)
|
|
260
|
+
write(root / "bootstrap.json", supplied, once=True)
|
|
261
|
+
write(root / "config.json", settings, once=True)
|
|
262
|
+
allocate(root, supplied["engines"], admin_dsns)
|
|
263
|
+
if (root / "setup-complete.json").exists():
|
|
264
|
+
config(root)
|
|
265
|
+
from sde._cutover_project import ProjectState
|
|
266
|
+
|
|
267
|
+
store = ProjectState(root / "state", settings["project_id"], logical.version)
|
|
268
|
+
with store.lock():
|
|
269
|
+
store.read()
|
|
270
|
+
current = sde.load_local_map(
|
|
271
|
+
root / "state",
|
|
272
|
+
model=logical,
|
|
273
|
+
project_id=settings["project_id"],
|
|
274
|
+
public_key=public_keys(settings["public_keys"]),
|
|
275
|
+
)
|
|
276
|
+
store.confirm()
|
|
277
|
+
confirm_file(root / "setup-complete.json")
|
|
278
|
+
return {"status": "ready", "map_version": current.map_version}
|
|
279
|
+
if (root / "state" / "active-map.json").exists():
|
|
280
|
+
current = sde.load_local_map(
|
|
281
|
+
root / "state",
|
|
282
|
+
model=logical,
|
|
283
|
+
project_id=settings["project_id"],
|
|
284
|
+
public_key=public_keys(settings["public_keys"]),
|
|
285
|
+
)
|
|
286
|
+
if current.fingerprint != placement.fingerprint:
|
|
287
|
+
raise DemoRefused(
|
|
288
|
+
"An unfinished setup already advanced its map; inspect local state."
|
|
289
|
+
)
|
|
290
|
+
with connections(root, settings) as (operators, runtime):
|
|
291
|
+
sde.prepare_schema(logical, placement, operators, project_id=settings["project_id"])
|
|
292
|
+
tables: dict[str, list[str]] = {name: ["sde_map_state"] for name in operators}
|
|
293
|
+
for group in placement.groups.values():
|
|
294
|
+
tables[group.source.engine].extend(group.source.layout.tables.values())
|
|
295
|
+
grant_tables(root, tables, admin_dsns)
|
|
296
|
+
local = sde.LocalCutover(
|
|
297
|
+
root / "state",
|
|
298
|
+
model=logical,
|
|
299
|
+
project_id=settings["project_id"],
|
|
300
|
+
public_key=public_keys(settings["public_keys"]),
|
|
301
|
+
operators=operators,
|
|
302
|
+
runtime={name: [adapter] for name, adapter in runtime.items()},
|
|
303
|
+
)
|
|
304
|
+
local.enroll(supplied["current_map"])
|
|
305
|
+
with sde.Session(logical, placement, runtime, project_id=settings["project_id"]):
|
|
306
|
+
pass
|
|
307
|
+
write(
|
|
308
|
+
root / "setup-complete.json",
|
|
309
|
+
{"protocol": 1, "config_digest": sde.digest16(settings)},
|
|
310
|
+
once=True,
|
|
311
|
+
)
|
|
312
|
+
return {"status": "ready", "map_version": placement.map_version}
|
sde_demo/py.typed
ADDED
|
File without changes
|
sde_demo/query_count.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Execute the Weather demo's approved COUNT locally; business values never enter receipts.
|
|
2
|
+
|
|
3
|
+
This is deliberately a closed COUNT(*) grammar over the current source, not a general SQL runner.
|
|
4
|
+
The metadata constructor is pure and can be used by the controller without importing drivers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from contextlib import ExitStack
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
import sde
|
|
21
|
+
from sde.generation import GENERATIONS_SINCE, check_map_project
|
|
22
|
+
from sde.schema import QUOTE
|
|
23
|
+
|
|
24
|
+
from .model import model
|
|
25
|
+
from .project import DemoRefused, config, credentials, engine, public_keys, write
|
|
26
|
+
|
|
27
|
+
KIND = "sde-weather-count-query"
|
|
28
|
+
FIELDS = {
|
|
29
|
+
"kind",
|
|
30
|
+
"protocol",
|
|
31
|
+
"project_id",
|
|
32
|
+
"name",
|
|
33
|
+
"revision",
|
|
34
|
+
"map_fingerprint",
|
|
35
|
+
"query",
|
|
36
|
+
"digest",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _bytes(value: Any) -> bytes:
|
|
41
|
+
return (
|
|
42
|
+
json.dumps(
|
|
43
|
+
value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False
|
|
44
|
+
)
|
|
45
|
+
+ "\n"
|
|
46
|
+
).encode()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _digest(value: Mapping[str, Any]) -> str:
|
|
50
|
+
return hashlib.sha256(
|
|
51
|
+
_bytes({key: item for key, item in value.items() if key != "digest"})
|
|
52
|
+
).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _source(placement: sde.PlacementMap, project_id: str) -> sde.Materialization:
|
|
56
|
+
check_map_project(placement, project_id)
|
|
57
|
+
if (
|
|
58
|
+
placement.contract < GENERATIONS_SINCE
|
|
59
|
+
or not placement.signed
|
|
60
|
+
or placement.fingerprint is None
|
|
61
|
+
or placement.model_version != model().version
|
|
62
|
+
or set(placement.groups) != {"WeatherReading"}
|
|
63
|
+
):
|
|
64
|
+
raise DemoRefused("The count demo needs the loaded, signed Weather project map.")
|
|
65
|
+
# The source is the one authoritative copy, also while a staging maintains another: the count
|
|
66
|
+
# reads it and a query must name it. A query written for the copy stays refused until the
|
|
67
|
+
# cutover makes the copy the source.
|
|
68
|
+
return placement.groups["WeatherReading"].source
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def count_sql(sql: Any, *, table: str, dialect: str) -> str:
|
|
72
|
+
"""Permit exactly one count over the named source; no expressions, settings or comments."""
|
|
73
|
+
if (
|
|
74
|
+
not isinstance(sql, str)
|
|
75
|
+
or not 1 <= len(sql) <= 8192
|
|
76
|
+
or dialect not in ("postgres", "clickhouse")
|
|
77
|
+
):
|
|
78
|
+
raise DemoRefused("The demo accepts one bounded COUNT(*) statement on its current source.")
|
|
79
|
+
identifier = re.escape(QUOTE[dialect](table))
|
|
80
|
+
if re.fullmatch(r"[a-z_][a-z0-9_]*", table):
|
|
81
|
+
# PostgreSQL folds unquoted names; quoted names and ClickHouse names remain exact.
|
|
82
|
+
bare = "(?i:" + re.escape(table) + ")" if dialect == "postgres" else re.escape(table)
|
|
83
|
+
quoted_alternative = "|" + re.escape('"' + table + '"') if dialect == "clickhouse" else ""
|
|
84
|
+
identifier = "(?:" + identifier + "|" + bare + quoted_alternative + ")"
|
|
85
|
+
alias_name = r'[A-Za-z_][A-Za-z0-9_]*|"[A-Za-z_][A-Za-z0-9_]*"'
|
|
86
|
+
if dialect == "clickhouse":
|
|
87
|
+
alias_name += r"|`[A-Za-z_][A-Za-z0-9_]*`"
|
|
88
|
+
alias = r"(?:\s+(?i:AS)\s+(?:" + alias_name + r"))?"
|
|
89
|
+
final = r"\s+(?i:FINAL)" if dialect == "clickhouse" else ""
|
|
90
|
+
pattern = (
|
|
91
|
+
r"\s*(?i:SELECT)\s+(?i:COUNT)\s*\(\s*\*\s*\)"
|
|
92
|
+
+ alias
|
|
93
|
+
+ r"\s+(?i:FROM)\s+"
|
|
94
|
+
+ identifier
|
|
95
|
+
+ final
|
|
96
|
+
+ r"\s*;?\s*"
|
|
97
|
+
)
|
|
98
|
+
# SQL token folding is ASCII. Unicode IGNORECASE would also accept dotless i/long s/Kelvin
|
|
99
|
+
# as another identifier and could execute a count on a different accessible native table.
|
|
100
|
+
if re.fullmatch(pattern, sql, flags=re.ASCII) is None:
|
|
101
|
+
raise DemoRefused(
|
|
102
|
+
"Use SELECT COUNT(*) [AS alias] FROM the exact Weather source "
|
|
103
|
+
"(with FINAL on ClickHouse); other SQL is outside this demo."
|
|
104
|
+
)
|
|
105
|
+
return sql
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _query(value: Any, placement: sde.PlacementMap, project_id: str, dialect: str) -> str:
|
|
109
|
+
source = _source(placement, project_id)
|
|
110
|
+
if not isinstance(value, dict) or not isinstance(value.get("stamp"), dict):
|
|
111
|
+
raise DemoRefused("Supply the approved query record, including its stamp.")
|
|
112
|
+
stamp = value["stamp"]
|
|
113
|
+
if (
|
|
114
|
+
value.get("group") != "WeatherReading"
|
|
115
|
+
or value.get("engine") != source.engine
|
|
116
|
+
or value.get("materialization") != source.id
|
|
117
|
+
or value.get("dialect") != dialect
|
|
118
|
+
or stamp.get("model_version") != placement.model_version
|
|
119
|
+
or type(stamp.get("map_version")) is not int
|
|
120
|
+
or stamp["map_version"] != placement.map_version
|
|
121
|
+
or stamp.get("materialization") != source.id
|
|
122
|
+
):
|
|
123
|
+
raise DemoRefused("The query does not describe this project's current source and map.")
|
|
124
|
+
return count_sql(
|
|
125
|
+
value.get("sql"), table=source.layout.table_for("WeatherReading"), dialect=dialect
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def make_count_request(
|
|
130
|
+
*,
|
|
131
|
+
project_id: str,
|
|
132
|
+
name: str,
|
|
133
|
+
revision: int,
|
|
134
|
+
query: Mapping[str, Any],
|
|
135
|
+
placement: sde.PlacementMap,
|
|
136
|
+
dialect: str,
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Prepare metadata for an authenticated handoff; no connection or customer file is used."""
|
|
139
|
+
if (
|
|
140
|
+
not isinstance(name, str)
|
|
141
|
+
or not name
|
|
142
|
+
or len(name) > 128
|
|
143
|
+
or type(revision) is not int
|
|
144
|
+
or revision < 1
|
|
145
|
+
):
|
|
146
|
+
raise DemoRefused("The count handoff needs a query name and positive integer revision.")
|
|
147
|
+
copied = json.loads(_bytes(dict(query)))
|
|
148
|
+
_query(copied, placement, project_id, dialect)
|
|
149
|
+
document = {
|
|
150
|
+
"kind": KIND,
|
|
151
|
+
"protocol": 1,
|
|
152
|
+
"project_id": project_id,
|
|
153
|
+
"name": name,
|
|
154
|
+
"revision": revision,
|
|
155
|
+
"map_fingerprint": placement.fingerprint,
|
|
156
|
+
"query": copied,
|
|
157
|
+
}
|
|
158
|
+
document["digest"] = _digest(document)
|
|
159
|
+
return document
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True)
|
|
163
|
+
class CountRequest:
|
|
164
|
+
name: str
|
|
165
|
+
revision: int
|
|
166
|
+
sql: str
|
|
167
|
+
engine: str
|
|
168
|
+
dialect: str
|
|
169
|
+
digest: str
|
|
170
|
+
map_fingerprint: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_count_request(
|
|
174
|
+
value: Mapping[str, Any],
|
|
175
|
+
*,
|
|
176
|
+
project_id: str,
|
|
177
|
+
placement: sde.PlacementMap,
|
|
178
|
+
dialects: Mapping[str, str],
|
|
179
|
+
) -> CountRequest:
|
|
180
|
+
copied = json.loads(_bytes(dict(value)))
|
|
181
|
+
if (
|
|
182
|
+
set(copied) != FIELDS
|
|
183
|
+
or copied["kind"] != KIND
|
|
184
|
+
or type(copied["protocol"]) is not int
|
|
185
|
+
or copied["protocol"] != 1
|
|
186
|
+
or copied["project_id"] != project_id
|
|
187
|
+
or copied["map_fingerprint"] != placement.fingerprint
|
|
188
|
+
or copied["digest"] != _digest(copied)
|
|
189
|
+
):
|
|
190
|
+
raise DemoRefused(
|
|
191
|
+
"The count handoff is incomplete, changed or belongs to another map/project."
|
|
192
|
+
)
|
|
193
|
+
source = _source(placement, project_id)
|
|
194
|
+
if source.engine not in dialects or dialects[source.engine] not in ("postgres", "clickhouse"):
|
|
195
|
+
raise DemoRefused("The count source has no supported local runtime binding.")
|
|
196
|
+
rebuilt = make_count_request(
|
|
197
|
+
project_id=project_id,
|
|
198
|
+
name=copied["name"],
|
|
199
|
+
revision=copied["revision"],
|
|
200
|
+
query=copied["query"],
|
|
201
|
+
placement=placement,
|
|
202
|
+
dialect=dialects[source.engine],
|
|
203
|
+
)
|
|
204
|
+
if copied != rebuilt:
|
|
205
|
+
raise DemoRefused("The count handoff has inconsistent metadata.")
|
|
206
|
+
return CountRequest(
|
|
207
|
+
copied["name"],
|
|
208
|
+
copied["revision"],
|
|
209
|
+
copied["query"]["sql"],
|
|
210
|
+
source.engine,
|
|
211
|
+
dialects[source.engine],
|
|
212
|
+
copied["digest"],
|
|
213
|
+
str(placement.fingerprint),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _count(adapter: Any, query: str) -> int:
|
|
218
|
+
if adapter.dialect == "postgres":
|
|
219
|
+
with adapter._cx.transaction(), adapter._cx.cursor() as cursor:
|
|
220
|
+
cursor.execute("SET TRANSACTION READ ONLY")
|
|
221
|
+
cursor.execute("SET LOCAL statement_timeout = '10000ms'")
|
|
222
|
+
cursor.execute("SET LOCAL lock_timeout = '2000ms'")
|
|
223
|
+
sde.explain(adapter, query)
|
|
224
|
+
cursor.execute(query)
|
|
225
|
+
rows = cursor.fetchall()
|
|
226
|
+
else:
|
|
227
|
+
settings = {"readonly": 1, "max_execution_time": 10, "timeout_overflow_mode": "throw"}
|
|
228
|
+
# The SQL grammar cannot override these settings or call external table functions.
|
|
229
|
+
adapter._cx.query("EXPLAIN PLAN " + query, settings=settings)
|
|
230
|
+
rows = adapter._cx.query(query, settings=settings).result_rows
|
|
231
|
+
if len(rows) != 1 or len(rows[0]) != 1 or type(rows[0][0]) is not int or rows[0][0] < 0:
|
|
232
|
+
raise DemoRefused("The count result is not one exact nonnegative integer.")
|
|
233
|
+
return rows[0][0]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def run_count_query(root: Path, document: Mapping[str, Any]) -> dict[str, Any]:
|
|
237
|
+
from .verification import load_completed_runs, verify_runs
|
|
238
|
+
|
|
239
|
+
settings, logical = config(root), model()
|
|
240
|
+
keys = public_keys(settings["public_keys"])
|
|
241
|
+
|
|
242
|
+
def current() -> sde.PlacementMap:
|
|
243
|
+
return sde.load_local_map(
|
|
244
|
+
root / "state", model=logical, project_id=settings["project_id"], public_key=keys
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
placement = current()
|
|
248
|
+
request = load_count_request(
|
|
249
|
+
document,
|
|
250
|
+
project_id=settings["project_id"],
|
|
251
|
+
placement=placement,
|
|
252
|
+
dialects={name: item["dialect"] for name, item in settings["engines"].items()},
|
|
253
|
+
)
|
|
254
|
+
ids = sorted(path.name for path in (root / "runs").iterdir())
|
|
255
|
+
runs = load_completed_runs(root, ids)
|
|
256
|
+
if not runs:
|
|
257
|
+
raise DemoRefused("Finish a demo workload before running its count oracle.")
|
|
258
|
+
verify_runs(root, ids)
|
|
259
|
+
if current().fingerprint != placement.fingerprint:
|
|
260
|
+
raise DemoRefused("The local map changed during data verification; retry after cutover.")
|
|
261
|
+
# A staging's map also names the engine of the copy it maintains, and the session checks the
|
|
262
|
+
# map against every engine it names; the count itself runs on the source's adapter. Each has a
|
|
263
|
+
# local binding: the run verification above refused this map otherwise.
|
|
264
|
+
named = sorted({item.engine for group in placement.groups.values() for item in group.all()})
|
|
265
|
+
dsns = credentials(root, "runtime", settings["engines"])
|
|
266
|
+
with ExitStack() as cleanup: # every adapter is closed, even if an earlier one cannot close
|
|
267
|
+
adapters: dict[str, Any] = {}
|
|
268
|
+
for name in named:
|
|
269
|
+
adapters[name] = engine(settings["engines"][name]["dialect"], dsns[name])
|
|
270
|
+
cleanup.callback(adapters[name].close)
|
|
271
|
+
adapters[name].connect()
|
|
272
|
+
with sde.Session(logical, placement, adapters, project_id=settings["project_id"]):
|
|
273
|
+
total = _count(adapters[request.engine], request.sql)
|
|
274
|
+
if total != sum(run.expected_rows for run in runs):
|
|
275
|
+
raise DemoRefused("The local SQL result differs from the completed synthetic runs.")
|
|
276
|
+
if (
|
|
277
|
+
current().fingerprint != placement.fingerprint
|
|
278
|
+
or ids != sorted(path.name for path in (root / "runs").iterdir())
|
|
279
|
+
or load_completed_runs(root, ids) != runs
|
|
280
|
+
):
|
|
281
|
+
raise DemoRefused(
|
|
282
|
+
"The map or run catalog changed during the query; retry after it settles."
|
|
283
|
+
)
|
|
284
|
+
identity = uuid4().hex
|
|
285
|
+
receipt = {
|
|
286
|
+
"protocol": 1,
|
|
287
|
+
"kind": "sde-weather-count-observation",
|
|
288
|
+
"execution_id": identity,
|
|
289
|
+
"project_id": settings["project_id"],
|
|
290
|
+
"map_fingerprint": request.map_fingerprint,
|
|
291
|
+
"query_name": request.name,
|
|
292
|
+
"query_revision": request.revision,
|
|
293
|
+
"query_digest": request.digest,
|
|
294
|
+
"verified": True,
|
|
295
|
+
"run_ids": ids,
|
|
296
|
+
"observed_at": datetime.now(UTC).isoformat(),
|
|
297
|
+
}
|
|
298
|
+
# Business values exist only in this private local result file, never in the exported receipt.
|
|
299
|
+
write(root / "query-results" / identity / "result.json", {"count": total, "receipt": receipt})
|
|
300
|
+
write(root / "query-results" / identity / "receipt.json", receipt)
|
|
301
|
+
return receipt
|