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.
Files changed (73) hide show
  1. sde/__init__.py +318 -0
  2. sde/_cutover_project.py +179 -0
  3. sde/_local_state.py +188 -0
  4. sde/_operator_deadline.py +50 -0
  5. sde/_usage.py +314 -0
  6. sde/bulk.py +79 -0
  7. sde/canonical.py +141 -0
  8. sde/capabilities.py +62 -0
  9. sde/cutover.py +286 -0
  10. sde/engines/__init__.py +0 -0
  11. sde/engines/_clickhouse_connection.py +224 -0
  12. sde/engines/_index_build.py +294 -0
  13. sde/engines/_operator.py +394 -0
  14. sde/engines/_staging.py +222 -0
  15. sde/engines/_storage.py +22 -0
  16. sde/engines/_write_fences.py +271 -0
  17. sde/engines/clickhouse.py +1115 -0
  18. sde/engines/orderbook.py +457 -0
  19. sde/engines/postgres.py +967 -0
  20. sde/entity.py +170 -0
  21. sde/errors.py +103 -0
  22. sde/explain.py +300 -0
  23. sde/frozen_verification.py +152 -0
  24. sde/generation.py +131 -0
  25. sde/groups.py +97 -0
  26. sde/hashing.py +242 -0
  27. sde/index_build.py +313 -0
  28. sde/index_operator.py +347 -0
  29. sde/infer.py +461 -0
  30. sde/inspection.py +62 -0
  31. sde/internal.py +90 -0
  32. sde/layout.py +669 -0
  33. sde/local_cutover.py +801 -0
  34. sde/logging.py +143 -0
  35. sde/migration.py +856 -0
  36. sde/model.py +482 -0
  37. sde/physical.py +531 -0
  38. sde/placement.py +1010 -0
  39. sde/provisioning.py +63 -0
  40. sde/py.typed +0 -0
  41. sde/query.py +521 -0
  42. sde/routing.py +85 -0
  43. sde/schema.py +466 -0
  44. sde/session.py +993 -0
  45. sde/shapes.py +153 -0
  46. sde/staging.py +264 -0
  47. sde/staging_operator.py +393 -0
  48. sde/telemetry.py +1087 -0
  49. sde/testing/__init__.py +14 -0
  50. sde/testing/loader.py +175 -0
  51. sde/testing/memory.py +331 -0
  52. sde/types.py +228 -0
  53. sde/verification.py +220 -0
  54. sde/watermark.py +222 -0
  55. sde/write_fence.py +283 -0
  56. sde_demo/__init__.py +1 -0
  57. sde_demo/__main__.py +183 -0
  58. sde_demo/diagnostics.py +92 -0
  59. sde_demo/model.py +75 -0
  60. sde_demo/project.py +312 -0
  61. sde_demo/py.typed +0 -0
  62. sde_demo/query_count.py +301 -0
  63. sde_demo/resources.py +969 -0
  64. sde_demo/runtime.py +419 -0
  65. sde_demo/verification.py +242 -0
  66. sde_operator/__init__.py +1 -0
  67. sde_operator/__main__.py +210 -0
  68. smart_data_engine_sdk-0.1.0.dist-info/METADATA +174 -0
  69. smart_data_engine_sdk-0.1.0.dist-info/RECORD +73 -0
  70. smart_data_engine_sdk-0.1.0.dist-info/WHEEL +4 -0
  71. smart_data_engine_sdk-0.1.0.dist-info/entry_points.txt +3 -0
  72. smart_data_engine_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
  73. smart_data_engine_sdk-0.1.0.dist-info/licenses/NOTICE +13 -0
sde_demo/resources.py ADDED
@@ -0,0 +1,969 @@
1
+ """Durable ownership of disposable loopback demo namespaces and runtime logins.
2
+
3
+ This is a local demo facility, not a general database administrator. Its directory lock is shared
4
+ with the starter. External schema/access administration must not race these local operations.
5
+ Passwords and DSNs live only in the two private credential files, never in the manifest or errors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import hashlib
12
+ import http.client
13
+ import ipaddress
14
+ import json
15
+ import re
16
+ import secrets
17
+ import stat
18
+ import uuid
19
+ from collections.abc import Iterator, Mapping, Sequence
20
+ from contextlib import ExitStack, contextmanager
21
+ from copy import deepcopy
22
+ from pathlib import Path
23
+ from typing import Any
24
+ from urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit
25
+
26
+ from sde import _local_state
27
+ from sde.engines._storage import STORAGE_COLUMNS
28
+
29
+ FILES = {"operator": "operator-credentials.json", "runtime": "runtime-credentials.json"}
30
+ _FIELDS = {
31
+ "protocol",
32
+ "allocation_id",
33
+ "status",
34
+ "credential_files",
35
+ "credential_hashes",
36
+ "engines",
37
+ "digest",
38
+ }
39
+ _ENTRY_FIELDS = {
40
+ "dialect",
41
+ "namespace",
42
+ "runtime_user",
43
+ "owner_marker",
44
+ "endpoint",
45
+ "expected_uuid",
46
+ "namespace_identity",
47
+ "runtime_identity",
48
+ "phase",
49
+ "tables",
50
+ }
51
+ _PHASES = {
52
+ "planned",
53
+ "namespace_created",
54
+ "created",
55
+ "ready",
56
+ "drop_namespace",
57
+ "namespace_dropped",
58
+ "user_dropped",
59
+ "drop_user",
60
+ "reset",
61
+ }
62
+
63
+
64
+ class ResourceRefused(RuntimeError):
65
+ """The demo cannot prove that a native resource or local file still belongs to this run."""
66
+
67
+
68
+ def _after_step(_step: str) -> None:
69
+ """Fault-injection seam, including after native CREATE/DROP and before identity publication."""
70
+
71
+
72
+ def _encode(value: Any) -> bytes:
73
+ return (
74
+ json.dumps(
75
+ value, sort_keys=True, ensure_ascii=False, allow_nan=False, separators=(",", ":")
76
+ )
77
+ + "\n"
78
+ ).encode("utf-8")
79
+
80
+
81
+ def _digest(value: Any) -> str:
82
+ return hashlib.sha256(_encode(value)).hexdigest()
83
+
84
+
85
+ def _pairs(items: list[tuple[str, Any]]) -> dict[str, Any]:
86
+ result: dict[str, Any] = {}
87
+ for name, value in items:
88
+ if name in result:
89
+ raise ResourceRefused("demo state contains duplicate JSON keys")
90
+ result[name] = value
91
+ return result
92
+
93
+
94
+ def _read(path: Path) -> dict[str, Any]:
95
+ if path.is_symlink() or not path.is_file():
96
+ raise ResourceRefused("demo state or credentials are missing; restore verified state")
97
+ if stat.S_IMODE(path.stat().st_mode) != 0o600:
98
+ raise ResourceRefused("demo state and credential files must have mode 0600")
99
+ value = json.loads(path.read_bytes(), object_pairs_hook=_pairs)
100
+ if not isinstance(value, dict):
101
+ raise ResourceRefused("demo state must be an object")
102
+ return value
103
+
104
+
105
+ def _write(root: Path, record: dict[str, Any], *, replace: bool = True) -> None:
106
+ record["digest"] = _digest({key: value for key, value in record.items() if key != "digest"})
107
+ _local_state.write_bytes(root / "resources.json", _encode(record), replace=replace)
108
+
109
+
110
+ def _engines(engines: Mapping[str, str]) -> dict[str, str]:
111
+ if not isinstance(engines, Mapping) or not 1 <= len(engines) <= 2:
112
+ raise ResourceRefused("demo needs one or two named engines")
113
+ if any(
114
+ not isinstance(name, str)
115
+ or re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,63}", name) is None
116
+ or dialect not in ("postgres", "clickhouse")
117
+ for name, dialect in engines.items()
118
+ ):
119
+ raise ResourceRefused("demo engine names must be ASCII and dialects PostgreSQL/ClickHouse")
120
+ if len(set(engines.values())) != len(engines):
121
+ raise ResourceRefused("demo permits only one binding per dialect")
122
+ return dict(sorted(engines.items()))
123
+
124
+
125
+ def _load(root: Path) -> dict[str, Any] | None:
126
+ path = root / "resources.json"
127
+ if not path.exists() and not path.is_symlink():
128
+ if any((root / name).exists() or (root / name).is_symlink() for name in FILES.values()):
129
+ raise ResourceRefused("demo credential files have lost their ownership manifest")
130
+ return None
131
+ record = _read(path)
132
+ if (
133
+ set(record) != _FIELDS
134
+ or type(record["protocol"]) is not int
135
+ or record["protocol"] != 1
136
+ or record["digest"] != _digest({k: v for k, v in record.items() if k != "digest"})
137
+ or record["credential_files"] != FILES
138
+ or record["status"] not in ("allocating", "ready", "resetting", "reset")
139
+ or re.fullmatch(r"[0-9a-f]{32}", record["allocation_id"]) is None
140
+ ):
141
+ raise ResourceRefused("demo resource manifest is corrupt or unsupported")
142
+ entries = record["engines"]
143
+ _engines({name: entry["dialect"] for name, entry in entries.items()})
144
+ for entry in entries.values():
145
+ if (
146
+ set(entry) != _ENTRY_FIELDS
147
+ or entry["phase"] not in _PHASES
148
+ or re.fullmatch(r"sde_demo_[0-9a-f]{32}_[0-9]+", entry["namespace"]) is None
149
+ or entry["runtime_user"] != entry["namespace"] + "_app"
150
+ or entry["owner_marker"] != "sde-demo-v1:" + record["allocation_id"]
151
+ or not isinstance(entry["tables"], dict)
152
+ ):
153
+ raise ResourceRefused("demo resource entry has invalid ownership metadata")
154
+ if entry["phase"] in {"created", "ready"} and (
155
+ entry["namespace_identity"] is None or entry["runtime_identity"] is None
156
+ ):
157
+ raise ResourceRefused("demo resource history has lost a native identity")
158
+ if record["status"] == "ready" and entry["phase"] != "ready":
159
+ raise ResourceRefused("ready demo has incomplete resource history")
160
+ if record["status"] == "reset" and entry["phase"] != "reset":
161
+ raise ResourceRefused("reset demo has incomplete resource history")
162
+ _local_state.confirm_file(path)
163
+ return record
164
+
165
+
166
+ def _host(value: str | None) -> str:
167
+ if value == "localhost":
168
+ return "127.0.0.1"
169
+ try:
170
+ address = ipaddress.ip_address(value or "")
171
+ except ValueError:
172
+ raise ResourceRefused("demo bindings must use an explicit loopback address") from None
173
+ if not address.is_loopback:
174
+ raise ResourceRefused("demo refuses non-loopback bindings")
175
+ return str(address)
176
+
177
+
178
+ def _pg_uri(parts: Mapping[str, str | int | None]) -> str:
179
+ values = {name: str(value) for name, value in parts.items() if value is not None}
180
+ host = _host(values.pop("host", None))
181
+ authority = "[" + host + "]" if ":" in host else host
182
+ port = values.pop("port", "5432")
183
+ if not port.isdigit() or not 1 <= int(port) <= 65535:
184
+ raise ResourceRefused("demo PostgreSQL port must be one TCP port")
185
+ user, password, database = (values.pop(name, "") for name in ("user", "password", "dbname"))
186
+ if not user or not database:
187
+ raise ResourceRefused("demo PostgreSQL binding requires explicit user and database")
188
+ return (
189
+ f"postgresql://{quote(user, safe='')}:{quote(password, safe='')}@{authority}:{port}/"
190
+ f"{quote(database, safe='')}?{urlencode(sorted(values.items()))}"
191
+ )
192
+
193
+
194
+ def _normalize(dialect: str, dsn: str) -> str:
195
+ if not isinstance(dsn, str) or not dsn:
196
+ raise ResourceRefused("demo administrator binding is absent")
197
+ if dialect == "postgres":
198
+ from psycopg.conninfo import conninfo_to_dict
199
+
200
+ parts = conninfo_to_dict(dsn)
201
+ if set(parts) - {
202
+ "host",
203
+ "port",
204
+ "user",
205
+ "password",
206
+ "dbname",
207
+ "options",
208
+ "sslmode",
209
+ "connect_timeout",
210
+ "application_name",
211
+ }:
212
+ raise ResourceRefused("demo PostgreSQL binding contains unsupported routing options")
213
+ return _pg_uri(parts)
214
+ url = urlsplit(dsn)
215
+ if url.scheme not in ("clickhouse", "clickhouses", "http", "https") or url.fragment:
216
+ raise ResourceRefused("demo ClickHouse binding must be an explicit HTTP(S) DSN")
217
+ host = _host(url.hostname)
218
+ secure = url.scheme in ("clickhouses", "https")
219
+ port = url.port or (8443 if secure else 8123)
220
+ query = dict(parse_qsl(url.query, strict_parsing=True))
221
+ if set(query) - {"connect_timeout", "send_receive_timeout"}:
222
+ raise ResourceRefused("demo ClickHouse binding contains unsupported routing options")
223
+ if not url.username or not url.path.removeprefix("/"):
224
+ raise ResourceRefused("demo ClickHouse binding requires explicit user and database")
225
+ authority = "[" + host + "]" if ":" in host else host
226
+ authority = (
227
+ f"{quote(unquote(url.username), safe='')}:"
228
+ f"{quote(unquote(url.password or ''), safe='')}@{authority}:{port}"
229
+ )
230
+ return urlunsplit(
231
+ (
232
+ "clickhouses" if secure else "clickhouse",
233
+ authority,
234
+ url.path,
235
+ urlencode(sorted(query.items())),
236
+ "",
237
+ )
238
+ )
239
+
240
+
241
+ def _scoped(
242
+ dialect: str,
243
+ admin: str,
244
+ namespace: str,
245
+ *,
246
+ user: str | None = None,
247
+ password: str | None = None,
248
+ ) -> str:
249
+ if dialect == "postgres":
250
+ from psycopg.conninfo import conninfo_to_dict
251
+
252
+ parts = conninfo_to_dict(admin)
253
+ parts["options"] = "-csearch_path=" + namespace
254
+ if user is not None:
255
+ parts.update(user=user, password=password or "")
256
+ return _pg_uri(parts)
257
+ url = urlsplit(admin)
258
+ authority = url.netloc
259
+ if user is not None:
260
+ host = "[" + str(url.hostname) + "]" if ":" in str(url.hostname) else str(url.hostname)
261
+ authority = f"{quote(user, safe='')}:{quote(password or '', safe='')}@{host}:{url.port}"
262
+ return urlunsplit((url.scheme, authority, "/" + quote(namespace, safe=""), url.query, ""))
263
+
264
+
265
+ @contextmanager
266
+ def _errors() -> Iterator[None]:
267
+ try:
268
+ yield
269
+ except (ResourceRefused, _local_state.DurabilityUncertain):
270
+ raise
271
+ except Exception:
272
+ # Driver errors can contain the complete DSN or a truncated CREATE USER password.
273
+ raise ResourceRefused(
274
+ "local demo operation failed; preserve its files and retry or restore"
275
+ ) from None
276
+
277
+
278
+ @contextmanager
279
+ def _connections(
280
+ bindings: Mapping[str, str], admin_dsns: Mapping[str, str]
281
+ ) -> Iterator[dict[str, Any]]:
282
+ from sde.engines.clickhouse import ClickHouseEngine
283
+ from sde.engines.postgres import PostgresEngine
284
+
285
+ if not isinstance(admin_dsns, Mapping) or set(bindings.values()) - set(admin_dsns):
286
+ raise ResourceRefused("demo needs administrator bindings for its dialects")
287
+ normalized = {
288
+ dialect: _normalize(dialect, admin_dsns[dialect]) for dialect in set(bindings.values())
289
+ }
290
+ with ExitStack() as stack:
291
+ result = {}
292
+ for name, dialect in sorted(bindings.items()):
293
+ engine = (PostgresEngine if dialect == "postgres" else ClickHouseEngine)(
294
+ normalized[dialect]
295
+ )
296
+ stack.callback(engine.close)
297
+ engine.connect()
298
+ result[name] = engine
299
+ yield result
300
+
301
+
302
+ def _endpoint(engine: Any, *, database_name: str | None = None) -> dict[str, str]:
303
+ if engine.dialect == "postgres":
304
+ row = engine._cx.execute(
305
+ "SELECT c.system_identifier::text,d.oid::text,pg_catalog.current_database() "
306
+ "FROM pg_catalog.pg_control_system() c CROSS JOIN pg_catalog.pg_database d "
307
+ "WHERE d.datname=pg_catalog.current_database()"
308
+ ).fetchone()
309
+ else:
310
+ condition = "name=currentDatabase()" if database_name is None else "name={database:String}"
311
+ rows = engine._cx.query(
312
+ "SELECT toString(serverUUID()),toString(uuid),name FROM system.databases WHERE "
313
+ + condition,
314
+ parameters={} if database_name is None else {"database": database_name},
315
+ ).result_rows
316
+ row = rows[0] if len(rows) == 1 else None
317
+ if row is None or len(row) != 3 or any(not str(value) for value in row):
318
+ raise ResourceRefused("demo native endpoint identity is unavailable")
319
+ return dict(zip(("server", "database", "database_name"), map(str, row), strict=True))
320
+
321
+
322
+ def _namespace(engine: Any, entry: Mapping[str, Any]) -> dict[str, str] | None:
323
+ if engine.dialect == "postgres":
324
+ row = engine._cx.execute(
325
+ "SELECT oid::text,nspowner::text,pg_catalog.obj_description(oid,'pg_namespace') "
326
+ "FROM pg_catalog.pg_namespace WHERE nspname=%s",
327
+ (entry["namespace"],),
328
+ ).fetchone()
329
+ return None if row is None else {"id": row[0], "owner": row[1], "marker": row[2]}
330
+ rows = engine._cx.query(
331
+ "SELECT toString(uuid),engine,comment FROM system.databases WHERE name={name:String}",
332
+ parameters={"name": entry["namespace"]},
333
+ ).result_rows
334
+ return None if not rows else {"id": rows[0][0], "engine": rows[0][1], "marker": rows[0][2]}
335
+
336
+
337
+ def _principal(engine: Any, entry: Mapping[str, Any]) -> dict[str, str] | None:
338
+ if engine.dialect == "postgres":
339
+ row = engine._cx.execute(
340
+ "SELECT oid::text,pg_catalog.shobj_description(oid,'pg_authid') "
341
+ "FROM pg_catalog.pg_roles WHERE rolname=%s",
342
+ (entry["runtime_user"],),
343
+ ).fetchone()
344
+ return None if row is None else {"id": row[0], "marker": row[1]}
345
+ rows = engine._cx.query(
346
+ "SELECT toString(id),storage,default_database FROM system.users WHERE name={name:String}",
347
+ parameters={"name": entry["runtime_user"]},
348
+ ).result_rows
349
+ return (
350
+ None
351
+ if not rows
352
+ else {"id": rows[0][0], "storage": rows[0][1], "default_database": rows[0][2]}
353
+ )
354
+
355
+
356
+ def _authentication(admin: str, user: str, password: str) -> tuple[int, str | None, list[str]]:
357
+ url = urlsplit(admin)
358
+ connection = (
359
+ http.client.HTTPSConnection if url.scheme == "clickhouses" else http.client.HTTPConnection
360
+ )(
361
+ str(url.hostname),
362
+ url.port,
363
+ timeout=10,
364
+ )
365
+ try:
366
+ authorization = base64.b64encode((user + ":" + password).encode()).decode()
367
+ connection.request(
368
+ "POST",
369
+ "/?" + urlencode({"database": unquote(url.path.removeprefix("/"))}),
370
+ body="SELECT currentUser(), toString(serverUUID()) FORMAT TabSeparated",
371
+ headers={"Authorization": "Basic " + authorization},
372
+ )
373
+ response = connection.getresponse()
374
+ data = response.read(65_537)
375
+ identity = data.decode("utf-8").strip().split("\t") if response.status == 200 else []
376
+ return response.status, response.getheader("X-ClickHouse-Exception-Code"), identity
377
+ finally:
378
+ connection.close()
379
+
380
+
381
+ def _prove_ch_user(engine: Any, entry: Mapping[str, Any], runtime_dsn: str) -> None:
382
+ password = unquote(urlsplit(runtime_dsn).password or "")
383
+ correct = _authentication(engine._dsn, entry["runtime_user"], password)
384
+ wrong = _authentication(engine._dsn, entry["runtime_user"], password + "-wrong")
385
+ if (
386
+ correct[0] != 200
387
+ or correct[2] != [entry["runtime_user"], entry["endpoint"]["server"]]
388
+ or wrong[0] != 403
389
+ or wrong[1] != "516"
390
+ ):
391
+ raise ResourceRefused("demo runtime login does not prove ownership of its saved credential")
392
+
393
+
394
+ def _table(engine: Any, entry: Mapping[str, Any], name: str) -> dict[str, str] | None:
395
+ if engine.dialect == "postgres":
396
+ row = engine._cx.execute(
397
+ "SELECT c.oid::text,c.relowner::text,c.relkind FROM pg_catalog.pg_class c "
398
+ "JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace "
399
+ "WHERE n.nspname=%s AND c.relname=%s",
400
+ (entry["namespace"], name),
401
+ ).fetchone()
402
+ if row is None:
403
+ return None
404
+ if row[2] != "r" or row[1] != entry["namespace_identity"]["owner"]:
405
+ raise ResourceRefused("demo can grant/reset only its ordinary owned tables")
406
+ return {"id": row[0], "owner": row[1]}
407
+ rows = engine._cx.query(
408
+ "SELECT toString(uuid),engine FROM system.tables "
409
+ "WHERE database={database:String} AND name={table:String}",
410
+ parameters={"database": entry["namespace"], "table": name},
411
+ ).result_rows
412
+ if not rows:
413
+ return None
414
+ if rows[0][1] not in ("MergeTree", "ReplacingMergeTree"):
415
+ raise ResourceRefused("demo can grant only local MergeTree tables")
416
+ return {"id": rows[0][0], "engine": rows[0][1]}
417
+
418
+
419
+ def _observe(engine: Any, entry: Mapping[str, Any], runtime_dsn: str | None) -> tuple[Any, Any]:
420
+ if _endpoint(engine) != entry["endpoint"]:
421
+ raise ResourceRefused("demo administrator binding now resolves to another native endpoint")
422
+ namespace, principal = _namespace(engine, entry), _principal(engine, entry)
423
+ phase = entry["phase"]
424
+ resetting = {"drop_namespace", "namespace_dropped", "drop_user", "user_dropped", "reset"}
425
+ namespace_gone = {"namespace_dropped", "reset"}
426
+ user_gone = {"user_dropped", "reset"}
427
+ if engine.dialect == "postgres":
428
+ namespace_gone.update({"drop_user", "user_dropped"})
429
+ else:
430
+ user_gone.update({"drop_namespace", "namespace_dropped"})
431
+ if namespace is not None:
432
+ if phase in namespace_gone:
433
+ raise ResourceRefused("a dropped demo namespace has reappeared")
434
+ if phase in resetting and entry["namespace_identity"] is None:
435
+ raise ResourceRefused("an absent demo namespace appeared after reset intent")
436
+ if namespace["marker"] != entry["owner_marker"] or (
437
+ engine.dialect == "clickhouse"
438
+ and (namespace["id"] != entry["expected_uuid"] or namespace["engine"] != "Atomic")
439
+ ):
440
+ raise ResourceRefused("demo namespace ownership marker or UUID differs")
441
+ if entry["namespace_identity"] is not None and namespace != entry["namespace_identity"]:
442
+ raise ResourceRefused("demo namespace native identity changed")
443
+ elif entry["namespace_identity"] is not None and phase not in namespace_gone | {
444
+ "drop_namespace"
445
+ }:
446
+ raise ResourceRefused("a recorded demo namespace is missing")
447
+ if principal is not None:
448
+ if phase in user_gone:
449
+ raise ResourceRefused("a dropped demo runtime has reappeared")
450
+ if phase in resetting and entry["runtime_identity"] is None:
451
+ raise ResourceRefused("an absent demo runtime appeared after reset intent")
452
+ if entry["runtime_identity"] is not None and principal != entry["runtime_identity"]:
453
+ raise ResourceRefused("demo runtime native identity changed")
454
+ if engine.dialect == "postgres":
455
+ if principal["marker"] != entry["owner_marker"]:
456
+ raise ResourceRefused("demo runtime ownership marker differs")
457
+ else:
458
+ if (
459
+ principal["storage"] != "local_directory"
460
+ or principal["default_database"] != entry["namespace"]
461
+ or runtime_dsn is None
462
+ ):
463
+ raise ResourceRefused("demo runtime ownership metadata differs")
464
+ _prove_ch_user(engine, entry, runtime_dsn)
465
+ elif entry["runtime_identity"] is not None and phase not in user_gone | {"drop_user"}:
466
+ raise ResourceRefused("a recorded demo runtime login is missing")
467
+ if phase == "planned" and (
468
+ (engine.dialect == "postgres" and (namespace is None) != (principal is None))
469
+ or (engine.dialect == "clickhouse" and principal is not None)
470
+ ):
471
+ raise ResourceRefused("demo creation history contradicts native resources")
472
+ if namespace is not None:
473
+ for name, granted in entry["tables"].items():
474
+ actual = _table(engine, entry, name)
475
+ if actual != granted["identity"] and not (actual is None and phase == "drop_namespace"):
476
+ raise ResourceRefused("a recorded demo table was removed or replaced")
477
+ return namespace, principal
478
+
479
+
480
+ def _credential_values(
481
+ root: Path, record: dict[str, Any], connections: Mapping[str, Any]
482
+ ) -> dict[str, dict[str, str]]:
483
+ hashes = record["credential_hashes"]
484
+ if hashes is None:
485
+ if any(
486
+ _namespace(engine, record["engines"][name]) is not None
487
+ or _principal(engine, record["engines"][name]) is not None
488
+ for name, engine in connections.items()
489
+ ):
490
+ raise ResourceRefused("native demo resources exist without durable credential history")
491
+ values: dict[str, dict[str, str]] = {"operator": {}, "runtime": {}}
492
+ for name, engine in connections.items():
493
+ entry = record["engines"][name]
494
+ values["operator"][name] = _scoped(engine.dialect, engine._dsn, entry["namespace"])
495
+ for kind in FILES:
496
+ path = root / FILES[kind]
497
+ if path.exists() or path.is_symlink():
498
+ stored = _read(path)
499
+ if kind == "operator" and stored != values[kind]:
500
+ raise ResourceRefused("unfinished demo credentials conflict with its bindings")
501
+ values[kind] = stored
502
+ elif kind == "runtime":
503
+ values[kind] = {
504
+ name: _scoped(
505
+ engine.dialect,
506
+ engine._dsn,
507
+ record["engines"][name]["namespace"],
508
+ user=record["engines"][name]["runtime_user"],
509
+ password=secrets.token_hex(32),
510
+ )
511
+ for name, engine in connections.items()
512
+ }
513
+ if not path.exists():
514
+ _local_state.write_bytes(path, _encode(values[kind]), replace=False)
515
+ _after_step("credential_written:" + kind)
516
+ _validate_credentials(values, record, connections)
517
+ record["credential_hashes"] = {
518
+ kind: hashlib.sha256((root / file).read_bytes()).hexdigest()
519
+ for kind, file in FILES.items()
520
+ }
521
+ for file in FILES.values():
522
+ _local_state.confirm_file(root / file)
523
+ _write(root, record)
524
+ _after_step("credentials_ready")
525
+ return values
526
+ if not isinstance(hashes, dict) or set(hashes) != set(FILES):
527
+ raise ResourceRefused("demo credential fingerprint history is incomplete")
528
+ values = {}
529
+ for kind, file in FILES.items():
530
+ path = root / file
531
+ values[kind] = _read(path)
532
+ if hashlib.sha256(path.read_bytes()).hexdigest() != hashes[kind]:
533
+ raise ResourceRefused("demo credentials changed; restore the original files")
534
+ _local_state.confirm_file(path)
535
+ _validate_credentials(values, record, connections)
536
+ return values
537
+
538
+
539
+ def _validate_credentials(
540
+ values: Mapping[str, Any], record: Mapping[str, Any], connections: Mapping[str, Any]
541
+ ) -> None:
542
+ for kind in FILES:
543
+ if set(values[kind]) != set(record["engines"]):
544
+ raise ResourceRefused("demo credentials do not cover the exact bindings")
545
+ for name, engine in connections.items():
546
+ entry = record["engines"][name]
547
+ dsn = values[kind][name]
548
+ normalized = _normalize(engine.dialect, dsn)
549
+ url, admin = urlsplit(normalized), urlsplit(engine._dsn)
550
+ if (url.hostname, url.port) != (admin.hostname, admin.port):
551
+ raise ResourceRefused("demo credential binding points to another endpoint")
552
+ if engine.dialect == "postgres":
553
+ from psycopg.conninfo import conninfo_to_dict
554
+
555
+ options = conninfo_to_dict(normalized)
556
+ if (
557
+ options["dbname"] != entry["endpoint"]["database_name"]
558
+ or options.get("options") != "-csearch_path=" + entry["namespace"]
559
+ ):
560
+ raise ResourceRefused("demo PostgreSQL credentials name another namespace")
561
+ elif unquote(url.path.removeprefix("/")) != entry["namespace"]:
562
+ raise ResourceRefused("demo ClickHouse credentials name another namespace")
563
+ if kind == "runtime" and (
564
+ unquote(url.username or "") != entry["runtime_user"]
565
+ or re.fullmatch(r"[0-9a-f]{64}", unquote(url.password or "")) is None
566
+ ):
567
+ raise ResourceRefused("demo runtime credential ownership is invalid")
568
+
569
+
570
+ def _new_record(bindings: Mapping[str, str], connections: Mapping[str, Any]) -> dict[str, Any]:
571
+ identity = uuid.uuid4().hex
572
+ entries = {}
573
+ for index, (name, dialect) in enumerate(bindings.items()):
574
+ namespace = f"sde_demo_{identity}_{index}"
575
+ entries[name] = {
576
+ "dialect": dialect,
577
+ "namespace": namespace,
578
+ "runtime_user": namespace + "_app",
579
+ "owner_marker": "sde-demo-v1:" + identity,
580
+ "endpoint": _endpoint(connections[name]),
581
+ "expected_uuid": str(uuid.uuid4()) if dialect == "clickhouse" else None,
582
+ "namespace_identity": None,
583
+ "runtime_identity": None,
584
+ "phase": "planned",
585
+ "tables": {},
586
+ }
587
+ return {
588
+ "protocol": 1,
589
+ "allocation_id": identity,
590
+ "status": "allocating",
591
+ "credential_files": dict(FILES),
592
+ "credential_hashes": None,
593
+ "engines": entries,
594
+ }
595
+
596
+
597
+ def _create_pg(engine: Any, entry: Mapping[str, Any], password: str) -> None:
598
+ from psycopg import sql
599
+
600
+ with engine._cx.transaction():
601
+ engine._cx.execute(
602
+ sql.SQL("CREATE ROLE {} LOGIN NOINHERIT PASSWORD {}").format(
603
+ sql.Identifier(entry["runtime_user"]), sql.Literal(password)
604
+ )
605
+ )
606
+ engine._cx.execute(
607
+ sql.SQL("COMMENT ON ROLE {} IS {}").format(
608
+ sql.Identifier(entry["runtime_user"]), sql.Literal(entry["owner_marker"])
609
+ )
610
+ )
611
+ engine._cx.execute(sql.SQL("CREATE SCHEMA {}").format(sql.Identifier(entry["namespace"])))
612
+ engine._cx.execute(
613
+ sql.SQL("COMMENT ON SCHEMA {} IS {}").format(
614
+ sql.Identifier(entry["namespace"]), sql.Literal(entry["owner_marker"])
615
+ )
616
+ )
617
+ engine._cx.execute(
618
+ sql.SQL("GRANT USAGE ON SCHEMA {} TO {}").format(
619
+ sql.Identifier(entry["namespace"]), sql.Identifier(entry["runtime_user"])
620
+ )
621
+ )
622
+
623
+
624
+ def allocate(
625
+ root: Path, engines: Mapping[str, str], admin_dsns: Mapping[str, str]
626
+ ) -> dict[str, Any]:
627
+ """Allocate or verify this run's resources; ready allocations never restore grants."""
628
+ bindings = _engines(engines)
629
+ with (
630
+ _errors(),
631
+ _connections(bindings, admin_dsns) as connections,
632
+ _local_state.transaction(root),
633
+ ):
634
+ record = _load(root)
635
+ if record is not None:
636
+ if {name: entry["dialect"] for name, entry in record["engines"].items()} != bindings:
637
+ raise ResourceRefused("demo engine bindings changed; use the original allocation")
638
+ if record["status"] == "resetting":
639
+ raise ResourceRefused("finish the demo reset before allocating again")
640
+ if record["status"] == "reset":
641
+ for name, entry in record["engines"].items():
642
+ _observe(connections[name], entry, None)
643
+ _remove_credentials(root, record)
644
+ record = None
645
+ if record is None:
646
+ record = _new_record(bindings, connections)
647
+ _write(root, record, replace=(root / "resources.json").exists())
648
+ _after_step("intent_written")
649
+ for name, entry in record["engines"].items():
650
+ if _endpoint(connections[name]) != entry["endpoint"]:
651
+ raise ResourceRefused("demo native endpoint changed")
652
+ credentials = _credential_values(root, record, connections)
653
+ for name, entry in record["engines"].items():
654
+ _observe(connections[name], entry, credentials["runtime"][name])
655
+ if record["status"] == "ready":
656
+ return deepcopy(record)
657
+ for name, entry in record["engines"].items():
658
+ engine = connections[name]
659
+ runtime = credentials["runtime"][name]
660
+ namespace, principal = _observe(engine, entry, runtime)
661
+ password = unquote(urlsplit(runtime).password or "")
662
+ if entry["phase"] == "planned":
663
+ if namespace is None:
664
+ if engine.dialect == "postgres":
665
+ _create_pg(engine, entry, password)
666
+ else:
667
+ engine._cx.command(
668
+ f"CREATE DATABASE `{entry['namespace']}` UUID "
669
+ f"'{entry['expected_uuid']}' ENGINE=Atomic "
670
+ f"COMMENT '{entry['owner_marker']}'"
671
+ )
672
+ _after_step("namespace_created:" + name)
673
+ namespace, principal = _observe(engine, entry, runtime)
674
+ entry["namespace_identity"] = namespace
675
+ entry["runtime_identity"] = principal
676
+ entry["phase"] = "created" if engine.dialect == "postgres" else "namespace_created"
677
+ _write(root, record)
678
+ _after_step("namespace_recorded:" + name)
679
+ if entry["phase"] == "namespace_created":
680
+ if principal is None:
681
+ engine._cx.command(
682
+ f"CREATE USER `{entry['runtime_user']}` IDENTIFIED WITH "
683
+ f"sha256_password BY '{password}' DEFAULT DATABASE `{entry['namespace']}`"
684
+ )
685
+ _after_step("runtime_created:" + name)
686
+ _, principal = _observe(engine, entry, runtime)
687
+ entry["runtime_identity"] = principal
688
+ entry["phase"] = "created"
689
+ _write(root, record)
690
+ _after_step("runtime_recorded:" + name)
691
+ if entry["phase"] == "created":
692
+ _observe(engine, entry, runtime)
693
+ if engine.dialect == "clickhouse":
694
+ engine._cx.command(
695
+ f"GRANT SELECT ON system.settings TO `{entry['runtime_user']}`"
696
+ )
697
+ # What a storage measurement reads, and nothing more: ClickHouse refuses
698
+ # system.parts to a login with table grants alone, and this column grant shows
699
+ # it the parts of its own tables only. The operator's qualification admits it.
700
+ engine._cx.command(
701
+ f"GRANT SELECT({', '.join(STORAGE_COLUMNS)}) ON system.parts "
702
+ f"TO `{entry['runtime_user']}`"
703
+ )
704
+ entry["phase"] = "ready"
705
+ _write(root, record)
706
+ _after_step("engine_ready:" + name)
707
+ record["status"] = "ready"
708
+ _write(root, record)
709
+ _after_step("ready")
710
+ return deepcopy(record)
711
+
712
+
713
+ def verify(root: Path) -> dict[str, Any]:
714
+ """Verify a ready allocation using its saved local operator credentials, without provisioning.
715
+
716
+ This proves the saved native incarnations and ownership markers. It deliberately does not
717
+ restore or qualify access grants: cutover can have revoked them since initial provisioning.
718
+ """
719
+ with _errors(), _local_state.transaction(root):
720
+ record = _load(root)
721
+ if record is None or record["status"] != "ready":
722
+ raise ResourceRefused("demo must be fully allocated before verifying its resources")
723
+ hashes = record["credential_hashes"]
724
+ if not isinstance(hashes, dict) or set(hashes) != set(FILES):
725
+ raise ResourceRefused("demo credential fingerprint history is incomplete")
726
+ operator_path = root / FILES["operator"]
727
+ operators = _read(operator_path)
728
+ if (
729
+ set(operators) != set(record["engines"])
730
+ or hashlib.sha256(operator_path.read_bytes()).hexdigest() != hashes["operator"]
731
+ ):
732
+ raise ResourceRefused("demo operator credentials changed; restore the original file")
733
+ bindings = {name: entry["dialect"] for name, entry in record["engines"].items()}
734
+ admin = {dialect: operators[name] for name, dialect in bindings.items()}
735
+ with _connections(bindings, admin) as connections:
736
+ credentials = _credential_values(root, record, connections)
737
+ for name, entry in record["engines"].items():
738
+ observed = entry
739
+ if entry["dialect"] == "clickhouse":
740
+ # Allocation began through the administrator's base database. The saved
741
+ # operator URI now opens our own database; check both native identities.
742
+ if (
743
+ _endpoint(
744
+ connections[name], database_name=entry["endpoint"]["database_name"]
745
+ )
746
+ != entry["endpoint"]
747
+ ):
748
+ raise ResourceRefused("demo native endpoint changed")
749
+ observed = {
750
+ **entry,
751
+ "endpoint": {
752
+ "server": entry["endpoint"]["server"],
753
+ "database": entry["namespace_identity"]["id"],
754
+ "database_name": entry["namespace"],
755
+ },
756
+ }
757
+ _observe(connections[name], observed, credentials["runtime"][name])
758
+ return deepcopy(record)
759
+
760
+
761
+ def _table_name(name: Any) -> str:
762
+ if (
763
+ not isinstance(name, str)
764
+ or not name
765
+ or len(name.encode("utf-8")) > 255
766
+ or any(ord(char) < 32 for char in name)
767
+ or name in ("*", "ALL TABLES")
768
+ ):
769
+ raise ResourceRefused("demo grants require explicit table names")
770
+ return name
771
+
772
+
773
+ def _grant(engine: Any, entry: Mapping[str, Any], table: str) -> None:
774
+ if engine.dialect == "postgres":
775
+ from psycopg import sql
776
+
777
+ engine._cx.execute(
778
+ sql.SQL("GRANT SELECT, INSERT ON {}.{} TO {}").format(
779
+ sql.Identifier(entry["namespace"]),
780
+ sql.Identifier(table),
781
+ sql.Identifier(entry["runtime_user"]),
782
+ )
783
+ )
784
+ else:
785
+ from sde.schema import QUOTE
786
+
787
+ engine._cx.command(
788
+ f"GRANT SELECT, INSERT ON `{entry['namespace']}`.{QUOTE['clickhouse'](table)} "
789
+ f"TO `{entry['runtime_user']}`"
790
+ )
791
+
792
+
793
+ def grant_tables(
794
+ root: Path, tables: Mapping[str, Sequence[str]], admin_dsns: Mapping[str, str]
795
+ ) -> None:
796
+ """Grant only listed existing tables, binding each grant to its native table identity."""
797
+ with _errors(), _local_state.transaction(root):
798
+ record = _load(root)
799
+ if record is None or record["status"] != "ready":
800
+ raise ResourceRefused("demo must be fully allocated before granting tables")
801
+ if not isinstance(tables, Mapping) or set(tables) - set(record["engines"]):
802
+ raise ResourceRefused("demo grants refer to unknown engines")
803
+ requested = {}
804
+ for name, names in sorted(tables.items()):
805
+ if isinstance(names, (str, bytes)) or not isinstance(names, Sequence):
806
+ raise ResourceRefused("demo grants require a sequence of table names")
807
+ requested[name] = sorted({_table_name(table) for table in names})
808
+ bindings = {name: entry["dialect"] for name, entry in record["engines"].items()}
809
+ with _connections(bindings, admin_dsns) as connections:
810
+ credentials = _credential_values(root, record, connections)
811
+ for name, entry in record["engines"].items():
812
+ _observe(connections[name], entry, credentials["runtime"][name])
813
+ identities = {}
814
+ for name, names in requested.items():
815
+ for table in names:
816
+ identity = _table(connections[name], record["engines"][name], table)
817
+ if identity is None:
818
+ raise ResourceRefused("a requested demo table does not exist")
819
+ identities[name, table] = identity
820
+ for (name, table), identity in identities.items():
821
+ entry = record["engines"][name]
822
+ prior = entry["tables"].get(table)
823
+ if prior is not None and prior["identity"] != identity:
824
+ raise ResourceRefused("a demo grant names a replacement table")
825
+ entry["tables"][table] = {"identity": identity, "phase": "granting"}
826
+ _write(root, record)
827
+ _after_step("grant_intent:" + name + ":" + table)
828
+ _observe(connections[name], entry, credentials["runtime"][name])
829
+ _grant(connections[name], entry, table)
830
+ _after_step("grant_applied:" + name + ":" + table)
831
+ _observe(connections[name], entry, credentials["runtime"][name])
832
+ entry["tables"][table]["phase"] = "granted"
833
+ _write(root, record)
834
+
835
+
836
+ def _drop_namespace(engine: Any, entry: Mapping[str, Any]) -> None:
837
+ if engine.dialect == "postgres":
838
+ from psycopg import sql
839
+
840
+ with engine._cx.transaction():
841
+ rows = engine._cx.execute(
842
+ "SELECT c.relname,c.relkind,c.relowner::text FROM pg_catalog.pg_class c "
843
+ "JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace "
844
+ "WHERE n.nspname=%s AND c.relkind NOT IN ('i','I') ORDER BY c.relname",
845
+ (entry["namespace"],),
846
+ ).fetchall()
847
+ if any(
848
+ kind != "r" or owner != entry["namespace_identity"]["owner"]
849
+ for _, kind, owner in rows
850
+ ):
851
+ raise ResourceRefused(
852
+ "demo namespace contains unsupported or foreign-owned objects"
853
+ )
854
+ for table, _, _ in rows:
855
+ engine._cx.execute(
856
+ sql.SQL("DROP TABLE {}.{}").format(
857
+ sql.Identifier(entry["namespace"]), sql.Identifier(table)
858
+ )
859
+ )
860
+ engine._cx.execute(sql.SQL("DROP SCHEMA {}").format(sql.Identifier(entry["namespace"])))
861
+ else:
862
+ dependencies = engine._cx.query(
863
+ "SELECT dependencies_database FROM system.tables WHERE database={database:String}",
864
+ parameters={"database": entry["namespace"]},
865
+ ).result_rows
866
+ if any(database != entry["namespace"] for row in dependencies for database in row[0]):
867
+ raise ResourceRefused("demo tables have dependents outside their namespace")
868
+ engine._cx.command(f"DROP DATABASE `{entry['namespace']}` SYNC")
869
+
870
+
871
+ def _drop_user(engine: Any, entry: Mapping[str, Any]) -> None:
872
+ if engine.dialect == "postgres":
873
+ from psycopg import sql
874
+
875
+ engine._cx.execute(sql.SQL("DROP ROLE {}").format(sql.Identifier(entry["runtime_user"])))
876
+ else:
877
+ engine._cx.command(f"DROP USER `{entry['runtime_user']}`")
878
+
879
+
880
+ def _remove_credentials(root: Path, record: Mapping[str, Any]) -> None:
881
+ for kind, file in FILES.items():
882
+ path = root / file
883
+ if path.exists() or path.is_symlink():
884
+ _read(path)
885
+ if (
886
+ record["credential_hashes"] is not None
887
+ and hashlib.sha256(path.read_bytes()).hexdigest()
888
+ != record["credential_hashes"][kind]
889
+ ):
890
+ raise ResourceRefused("reset refuses changed credential files")
891
+ path.unlink()
892
+ _local_state.sync_directory(root)
893
+
894
+
895
+ def reset(root: Path, admin_dsns: Mapping[str, str]) -> dict[str, Any]:
896
+ """Remove only this manifest's native incarnations; retain an idempotent reset tombstone.
897
+
898
+ PostgreSQL removes its namespace before its role, because grants depend on the role. In
899
+ ClickHouse the user goes first: dropping its DEFAULT DATABASE prevents the password proof.
900
+ """
901
+ with _errors(), _local_state.transaction(root):
902
+ record = _load(root)
903
+ if record is None:
904
+ raise ResourceRefused("no demo resource manifest exists")
905
+ bindings = {name: entry["dialect"] for name, entry in record["engines"].items()}
906
+ with _connections(bindings, admin_dsns) as connections:
907
+ if record["status"] == "reset":
908
+ for name, entry in record["engines"].items():
909
+ _observe(connections[name], entry, None)
910
+ _remove_credentials(root, record)
911
+ return deepcopy(record)
912
+ credentials = _credential_values(root, record, connections)
913
+ for name, entry in record["engines"].items():
914
+ _observe(connections[name], entry, credentials["runtime"][name])
915
+ record["status"] = "resetting"
916
+ _write(root, record)
917
+ _after_step("reset_intent")
918
+ for name, entry in record["engines"].items():
919
+ engine, runtime = connections[name], credentials["runtime"][name]
920
+ order = (
921
+ ("namespace", "user") if engine.dialect == "postgres" else ("user", "namespace")
922
+ )
923
+ namespace, principal = _observe(engine, entry, runtime)
924
+ if entry["phase"] == "reset":
925
+ continue
926
+ if entry["phase"] not in {
927
+ "drop_namespace",
928
+ "namespace_dropped",
929
+ "drop_user",
930
+ "user_dropped",
931
+ }:
932
+ entry["namespace_identity"], entry["runtime_identity"] = namespace, principal
933
+ entry["phase"] = "drop_" + order[0]
934
+ _write(root, record)
935
+ if order[0] == "user":
936
+ _after_step("user_drop_intent:" + name)
937
+ for index, resource in enumerate(order):
938
+ intended, finished = "drop_" + resource, resource + "_dropped"
939
+ if entry["phase"] == intended:
940
+ namespace, principal = _observe(engine, entry, runtime)
941
+ exists = namespace if resource == "namespace" else principal
942
+ if exists is not None:
943
+ if resource == "namespace":
944
+ _drop_namespace(engine, entry)
945
+ _after_step("namespace_dropped:" + name)
946
+ else:
947
+ _drop_user(engine, entry)
948
+ _after_step("runtime_dropped:" + name)
949
+ remaining = (
950
+ _namespace(engine, entry)
951
+ if resource == "namespace"
952
+ else _principal(engine, entry)
953
+ )
954
+ if remaining is not None:
955
+ raise ResourceRefused("demo resource remains after DROP")
956
+ entry["phase"] = finished
957
+ _write(root, record)
958
+ if entry["phase"] == finished:
959
+ entry["phase"] = (
960
+ "drop_" + order[index + 1] if index + 1 < len(order) else "reset"
961
+ )
962
+ _write(root, record)
963
+ if entry["phase"] == "drop_user":
964
+ _after_step("user_drop_intent:" + name)
965
+ record["status"] = "reset"
966
+ _write(root, record)
967
+ _after_step("reset_complete")
968
+ _remove_credentials(root, record)
969
+ return deepcopy(record)