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
@@ -0,0 +1,271 @@
1
+ """Native DDL and drain operations for the public write-fence protocol.
2
+
3
+ These objects need dedicated provisioning connections. They do not run on the application's write
4
+ path. Metadata is checked as well as created: an identically named, unrelated table is not ours.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Any
11
+ from uuid import UUID
12
+
13
+ from ..errors import EngineError, MigrationRefused
14
+ from ..schema import QUOTE
15
+ from ..write_fence import DRAIN_TABLE, EPOCH_COLUMN, FenceMetadata, fence_state
16
+
17
+
18
+ class PostgresFences:
19
+ def __init__(self, connection: Any) -> None:
20
+ if not connection.autocommit:
21
+ raise MigrationRefused("write-fence DDL needs a dedicated autocommit connection")
22
+ self._cx = connection
23
+ self._quote = QUOTE["postgres"]
24
+
25
+ def _query(self, query: str, values: list[Any] | None = None) -> list[Any]:
26
+ try:
27
+ with self._cx.cursor() as cursor:
28
+ cursor.execute(query, values)
29
+ return list(cursor.fetchall()) if cursor.description else []
30
+ except Exception as exc:
31
+ raise EngineError(
32
+ f"write-fence operation failed; inspect or resume its state: {exc}"
33
+ ) from exc
34
+
35
+ def _idle(self) -> None:
36
+ if int(self._cx.info.transaction_status) != 0:
37
+ raise MigrationRefused("write-fence DDL cannot run inside an application transaction")
38
+
39
+ def metadata(self, table: str) -> FenceMetadata:
40
+ quoted = self._quote(table)
41
+ rows = self._query(
42
+ "SELECT c.oid::text, c.relkind, EXISTS (SELECT 1 FROM pg_inherits i "
43
+ "WHERE i.inhrelid=c.oid OR i.inhparent=c.oid) FROM pg_class c "
44
+ "WHERE c.oid=to_regclass(%s)",
45
+ [quoted],
46
+ )
47
+ if len(rows) != 1:
48
+ raise EngineError("write-fence table does not exist")
49
+ identity, kind, inherits = rows[0]
50
+ if kind != "r" or inherits:
51
+ raise MigrationRefused(
52
+ "write fences support ordinary PostgreSQL tables without inheritance"
53
+ )
54
+ columns = self._query(
55
+ "SELECT a.atttypid='bigint'::regtype, a.attnotnull, a.attgenerated, "
56
+ "pg_get_expr(d.adbin,d.adrelid) FROM pg_attribute a "
57
+ "LEFT JOIN pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum "
58
+ "WHERE a.attrelid=to_regclass(%s) AND a.attname=%s AND NOT a.attisdropped",
59
+ [quoted, EPOCH_COLUMN],
60
+ )
61
+ valid = (
62
+ bool(columns)
63
+ and columns[0][:3] == (True, True, "")
64
+ and columns[0][3]
65
+ in (
66
+ "0",
67
+ "'0'::bigint",
68
+ )
69
+ )
70
+ constraints = self._query(
71
+ "SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint "
72
+ "WHERE conrelid=to_regclass(%s) AND contype='c'",
73
+ [quoted],
74
+ )
75
+ return FenceMetadata(
76
+ str(identity),
77
+ "valid" if valid else "conflict" if columns else "absent",
78
+ {str(name): str(expression) for name, expression in constraints},
79
+ )
80
+
81
+ def add_column(self, table: str) -> None:
82
+ self._idle()
83
+ self._query(
84
+ f"ALTER TABLE {self._quote(table)} ADD COLUMN IF NOT EXISTS "
85
+ f"{self._quote(EPOCH_COLUMN)} bigint NOT NULL DEFAULT 0"
86
+ )
87
+
88
+ def add_constraint(self, table: str, name: str, expression: str) -> None:
89
+ self._idle()
90
+ if name in self.metadata(table).constraints:
91
+ return
92
+ expression = {"1": "true", "0": "false"}.get(expression, expression)
93
+ self._query(
94
+ f"ALTER TABLE {self._quote(table)} ADD CONSTRAINT {self._quote(name)} "
95
+ f"CHECK ({expression}) NOT VALID"
96
+ )
97
+
98
+ def drop_constraint(self, table: str, name: str) -> None:
99
+ self._idle()
100
+ self._query(
101
+ f"ALTER TABLE {self._quote(table)} DROP CONSTRAINT IF EXISTS {self._quote(name)}"
102
+ )
103
+
104
+ def drain(self, table: str, *, project_id: str, hold: str) -> None:
105
+ self._idle()
106
+ # Even when a hold already existed, returning from this transaction proves that every
107
+ # writer which held a conflicting table lock has completed. A retry repeats this proof.
108
+ with self._cx.transaction():
109
+ self._query(f"LOCK TABLE {self._quote(table)} IN SHARE ROW EXCLUSIVE MODE")
110
+ _held(self.metadata(table), project_id, hold)
111
+
112
+ def restore(self, table: str, *, project_id: str, hold: str) -> None:
113
+ _held(self.metadata(table), project_id, hold)
114
+
115
+
116
+ class ClickHouseFences:
117
+ def __init__(self, client: Any) -> None:
118
+ self._cx = client
119
+ self._quote = QUOTE["clickhouse"]
120
+
121
+ def _query(self, query: str, parameters: dict[str, Any] | None = None) -> list[Any]:
122
+ try:
123
+ return list(self._cx.query(query, parameters=parameters or {}).result_rows)
124
+ except Exception as exc:
125
+ raise EngineError(f"write-fence metadata query failed: {exc}") from exc
126
+
127
+ def _command(self, query: str) -> None:
128
+ try:
129
+ self._cx.command(query)
130
+ except Exception as exc:
131
+ raise EngineError(
132
+ f"write-fence DDL failed; the table may remain closed or detached; "
133
+ f"resume the same operation: {exc}"
134
+ ) from exc
135
+
136
+ def _table(self, table: str) -> list[Any]:
137
+ return self._query(
138
+ "SELECT toString(uuid), engine, formatQuery(create_table_query) FROM system.tables "
139
+ "WHERE database=currentDatabase() AND name={table:String}",
140
+ {"table": table},
141
+ )
142
+
143
+ def metadata(self, table: str) -> FenceMetadata:
144
+ database = self._query("SELECT engine FROM system.databases WHERE name=currentDatabase()")
145
+ if database != [("Atomic",)]:
146
+ raise MigrationRefused("write fences require a local Atomic ClickHouse database")
147
+ rows = self._table(table)
148
+ if len(rows) != 1:
149
+ raise EngineError(
150
+ "write-fence table does not exist or is detached; resume the same operation"
151
+ )
152
+ identity, kind, create = rows[0]
153
+ if kind not in ("MergeTree", "ReplacingMergeTree"):
154
+ raise MigrationRefused(
155
+ "write fences support local MergeTree and ReplacingMergeTree tables"
156
+ )
157
+ columns = self._query(
158
+ "SELECT type, default_kind, default_expression FROM system.columns "
159
+ "WHERE database=currentDatabase() AND table={table:String} AND name={column:String}",
160
+ {"table": table, "column": EPOCH_COLUMN},
161
+ )
162
+ valid = columns == [("Int64", "DEFAULT", "0")]
163
+ constraints = {
164
+ name: expression.rstrip().removesuffix(",")
165
+ for name, expression in re.findall(
166
+ r'^\s*CONSTRAINT\s+[`"]?(__sde_f_[A-Za-z0-9_]+)[`"]?\s+CHECK\s+([^\n]+)',
167
+ str(create),
168
+ re.MULTILINE,
169
+ )
170
+ }
171
+ return FenceMetadata(
172
+ str(identity),
173
+ "valid" if valid else "conflict" if columns else "absent",
174
+ constraints,
175
+ )
176
+
177
+ def add_column(self, table: str) -> None:
178
+ self._command(
179
+ f"ALTER TABLE {self._quote(table)} ADD COLUMN IF NOT EXISTS "
180
+ f"{self._quote(EPOCH_COLUMN)} Int64 DEFAULT 0"
181
+ )
182
+
183
+ def add_constraint(self, table: str, name: str, expression: str) -> None:
184
+ self._command(
185
+ f"ALTER TABLE {self._quote(table)} ADD CONSTRAINT IF NOT EXISTS "
186
+ f"{self._quote(name)} CHECK {expression}"
187
+ )
188
+
189
+ def drop_constraint(self, table: str, name: str) -> None:
190
+ self._command(
191
+ f"ALTER TABLE {self._quote(table)} DROP CONSTRAINT IF EXISTS {self._quote(name)}"
192
+ )
193
+
194
+ def _drain_log(self) -> None:
195
+ self._command(
196
+ f"CREATE TABLE IF NOT EXISTS {self._quote(DRAIN_TABLE)} ("
197
+ "table_name String, table_uuid UUID, project_id FixedString(32), hold String) "
198
+ "ENGINE=MergeTree ORDER BY (table_uuid, project_id, hold)"
199
+ )
200
+ columns = self._query(
201
+ "SELECT name,type FROM system.columns WHERE database=currentDatabase() "
202
+ "AND table={table:String} ORDER BY name",
203
+ {"table": DRAIN_TABLE},
204
+ )
205
+ if columns != [
206
+ ("hold", "String"),
207
+ ("project_id", "FixedString(32)"),
208
+ ("table_name", "String"),
209
+ ("table_uuid", "UUID"),
210
+ ]:
211
+ raise MigrationRefused("the reserved write-fence drain log has an incompatible schema")
212
+ kinds = self._query(
213
+ "SELECT engine FROM system.tables WHERE database=currentDatabase() "
214
+ "AND name={table:String}",
215
+ {"table": DRAIN_TABLE},
216
+ )
217
+ if kinds != [("MergeTree",)]:
218
+ raise MigrationRefused("the reserved write-fence drain log has an incompatible engine")
219
+
220
+ def drain(self, table: str, *, project_id: str, hold: str) -> None:
221
+ metadata = self.metadata(table)
222
+ _held(metadata, project_id, hold)
223
+ self._drain_log()
224
+ # Store the exact Atomic UUID before DETACH. A failed response can leave a permanently
225
+ # detached table; resumption must not attach an unrelated table just because its name fits.
226
+ try:
227
+ self._cx.insert(
228
+ DRAIN_TABLE,
229
+ [[table, UUID(metadata.identity), project_id, hold]],
230
+ column_names=["table_name", "table_uuid", "project_id", "hold"],
231
+ settings={"async_insert": 0, "wait_for_async_insert": 1},
232
+ )
233
+ except Exception as exc:
234
+ raise EngineError(f"write-fence drain intent was not confirmed: {exc}") from exc
235
+ self._command(f"DETACH TABLE {self._quote(table)} PERMANENTLY SYNC")
236
+ self._command(f"ATTACH TABLE {self._quote(table)}")
237
+ after = self.metadata(table)
238
+ if after.identity != metadata.identity:
239
+ raise EngineError("the write-fence table identity changed while draining it")
240
+ _held(after, project_id, hold)
241
+
242
+ def restore(self, table: str, *, project_id: str, hold: str) -> None:
243
+ if self._table(table):
244
+ _held(self.metadata(table), project_id, hold)
245
+ return
246
+ # Reading a missing log fails closed. This path does not create one and must not guess
247
+ # ownership from a detached table's name or from the supplied request alone.
248
+ records = self._query(
249
+ f"SELECT DISTINCT toString(table_uuid) FROM {self._quote(DRAIN_TABLE)} "
250
+ "WHERE table_name={table:String} AND project_id={project:String} "
251
+ "AND hold={hold:String}",
252
+ {"table": table, "project": project_id, "hold": hold},
253
+ )
254
+ detached = self._query(
255
+ "SELECT toString(uuid) FROM system.detached_tables "
256
+ "WHERE database=currentDatabase() AND table={table:String} AND is_permanently=1",
257
+ {"table": table},
258
+ )
259
+ if len(records) != 1 or detached != records:
260
+ raise MigrationRefused("no matching durable intent for this detached write-fence table")
261
+ self._command(f"ATTACH TABLE {self._quote(table)}")
262
+ metadata = self.metadata(table)
263
+ if metadata.identity != records[0][0]:
264
+ raise EngineError("the restored write-fence table has another identity")
265
+ _held(metadata, project_id, hold)
266
+
267
+
268
+ def _held(metadata: FenceMetadata, project_id: str, hold: str) -> None:
269
+ state = fence_state(metadata)
270
+ if state.project_id != project_id or hold not in state.holds:
271
+ raise MigrationRefused("the requested project's write barrier is not installed")