vgi-python 0.8.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 (124) hide show
  1. vgi/__init__.py +152 -0
  2. vgi/_duckdb.py +62 -0
  3. vgi/_storage_profile.py +132 -0
  4. vgi/_test_fixtures/__init__.py +20 -0
  5. vgi/_test_fixtures/accumulate/__init__.py +19 -0
  6. vgi/_test_fixtures/accumulate/worker.py +762 -0
  7. vgi/_test_fixtures/aggregate/__init__.py +62 -0
  8. vgi/_test_fixtures/aggregate/_common.py +21 -0
  9. vgi/_test_fixtures/aggregate/basic.py +232 -0
  10. vgi/_test_fixtures/aggregate/dynamic.py +409 -0
  11. vgi/_test_fixtures/aggregate/generic.py +86 -0
  12. vgi/_test_fixtures/aggregate/listagg.py +71 -0
  13. vgi/_test_fixtures/aggregate/percentile.py +107 -0
  14. vgi/_test_fixtures/aggregate/streaming.py +192 -0
  15. vgi/_test_fixtures/aggregate/varargs.py +75 -0
  16. vgi/_test_fixtures/aggregate/window.py +380 -0
  17. vgi/_test_fixtures/attach_options.py +308 -0
  18. vgi/_test_fixtures/bad_protocol.py +62 -0
  19. vgi/_test_fixtures/cancellable.py +336 -0
  20. vgi/_test_fixtures/catalog.py +813 -0
  21. vgi/_test_fixtures/http_server.py +394 -0
  22. vgi/_test_fixtures/nest_tensor.py +614 -0
  23. vgi/_test_fixtures/orchard_catalog.py +47 -0
  24. vgi/_test_fixtures/projection_repro/__init__.py +6 -0
  25. vgi/_test_fixtures/projection_repro/worker.py +454 -0
  26. vgi/_test_fixtures/scalar/__init__.py +116 -0
  27. vgi/_test_fixtures/scalar/_common.py +69 -0
  28. vgi/_test_fixtures/scalar/arithmetic.py +321 -0
  29. vgi/_test_fixtures/scalar/binary.py +120 -0
  30. vgi/_test_fixtures/scalar/formatting.py +176 -0
  31. vgi/_test_fixtures/scalar/geo.py +300 -0
  32. vgi/_test_fixtures/scalar/null_handling.py +107 -0
  33. vgi/_test_fixtures/scalar/random_demo.py +171 -0
  34. vgi/_test_fixtures/scalar/settings_secrets.py +102 -0
  35. vgi/_test_fixtures/scalar/type_info.py +219 -0
  36. vgi/_test_fixtures/schema_reconcile/__init__.py +29 -0
  37. vgi/_test_fixtures/schema_reconcile/worker.py +653 -0
  38. vgi/_test_fixtures/simple_writable.py +793 -0
  39. vgi/_test_fixtures/table/__init__.py +221 -0
  40. vgi/_test_fixtures/table/_common.py +162 -0
  41. vgi/_test_fixtures/table/batch_index.py +283 -0
  42. vgi/_test_fixtures/table/batch_index_broken.py +200 -0
  43. vgi/_test_fixtures/table/catalog_scans.py +162 -0
  44. vgi/_test_fixtures/table/filters.py +1005 -0
  45. vgi/_test_fixtures/table/late_materialization.py +249 -0
  46. vgi/_test_fixtures/table/make_series.py +273 -0
  47. vgi/_test_fixtures/table/misc.py +499 -0
  48. vgi/_test_fixtures/table/order_modes.py +164 -0
  49. vgi/_test_fixtures/table/pairs.py +437 -0
  50. vgi/_test_fixtures/table/partition_columns.py +472 -0
  51. vgi/_test_fixtures/table/partition_columns_broken.py +304 -0
  52. vgi/_test_fixtures/table/profiling_example.py +195 -0
  53. vgi/_test_fixtures/table/required_filters.py +234 -0
  54. vgi/_test_fixtures/table/sequence.py +710 -0
  55. vgi/_test_fixtures/table/settings.py +426 -0
  56. vgi/_test_fixtures/table/transaction_storage.py +162 -0
  57. vgi/_test_fixtures/table/tt_pushdown.py +191 -0
  58. vgi/_test_fixtures/table/versioned.py +230 -0
  59. vgi/_test_fixtures/table_in_out.py +1392 -0
  60. vgi/_test_fixtures/versioned.py +155 -0
  61. vgi/_test_fixtures/versioned_tables.py +595 -0
  62. vgi/_test_fixtures/worker.py +1631 -0
  63. vgi/_test_fixtures/writable/__init__.py +8 -0
  64. vgi/_test_fixtures/writable/generic.py +236 -0
  65. vgi/_test_fixtures/writable/table.py +149 -0
  66. vgi/_test_fixtures/writable/worker.py +1148 -0
  67. vgi/aggregate_function.py +607 -0
  68. vgi/argument_spec.py +472 -0
  69. vgi/arguments.py +1747 -0
  70. vgi/auth.py +55 -0
  71. vgi/catalog/__init__.py +88 -0
  72. vgi/catalog/attach_option.py +206 -0
  73. vgi/catalog/catalog_interface.py +2767 -0
  74. vgi/catalog/descriptors.py +870 -0
  75. vgi/catalog/duckdb_statistics.py +377 -0
  76. vgi/catalog/secret_type.py +96 -0
  77. vgi/catalog/setting.py +253 -0
  78. vgi/catalog/storage.py +372 -0
  79. vgi/client/__init__.py +67 -0
  80. vgi/client/catalog_mixin.py +1251 -0
  81. vgi/client/cli.py +582 -0
  82. vgi/client/cli_catalog.py +182 -0
  83. vgi/client/cli_schema.py +270 -0
  84. vgi/client/cli_table.py +907 -0
  85. vgi/client/cli_transaction.py +97 -0
  86. vgi/client/cli_utils.py +441 -0
  87. vgi/client/cli_view.py +303 -0
  88. vgi/client/client.py +2183 -0
  89. vgi/exceptions.py +205 -0
  90. vgi/function.py +245 -0
  91. vgi/function_storage.py +1636 -0
  92. vgi/function_storage_azure_sql.py +922 -0
  93. vgi/function_storage_cf_do.py +740 -0
  94. vgi/http/__init__.py +25 -0
  95. vgi/http/demo_storage.py +212 -0
  96. vgi/http/worker_page.py +1252 -0
  97. vgi/invocation.py +154 -0
  98. vgi/logging_config.py +93 -0
  99. vgi/meta_worker.py +661 -0
  100. vgi/metadata.py +1403 -0
  101. vgi/otel.py +406 -0
  102. vgi/protocol.py +2418 -0
  103. vgi/protocol_version.txt +1 -0
  104. vgi/py.typed +0 -0
  105. vgi/scalar_function.py +1211 -0
  106. vgi/schema_utils.py +234 -0
  107. vgi/secret_protocol.py +124 -0
  108. vgi/secret_service.py +238 -0
  109. vgi/serve.py +769 -0
  110. vgi/table_buffering_function.py +443 -0
  111. vgi/table_filter_pushdown.py +1528 -0
  112. vgi/table_function.py +1130 -0
  113. vgi/table_in_out_function.py +383 -0
  114. vgi/transactor/__init__.py +24 -0
  115. vgi/transactor/_duckdb_compat.py +27 -0
  116. vgi/transactor/client.py +137 -0
  117. vgi/transactor/protocol.py +149 -0
  118. vgi/transactor/server.py +740 -0
  119. vgi/worker.py +4761 -0
  120. vgi_python-0.8.0.dist-info/METADATA +735 -0
  121. vgi_python-0.8.0.dist-info/RECORD +124 -0
  122. vgi_python-0.8.0.dist-info/WHEEL +4 -0
  123. vgi_python-0.8.0.dist-info/entry_points.txt +5 -0
  124. vgi_python-0.8.0.dist-info/licenses/LICENSE +134 -0
@@ -0,0 +1,813 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """In-memory catalog implementation for testing and examples.
4
+
5
+ This module provides an in-memory implementation of CatalogInterface that can
6
+ be used for testing and as a reference implementation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import uuid
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Any, Literal, overload
15
+
16
+ if TYPE_CHECKING:
17
+ import pyarrow as pa
18
+ from vgi_rpc.rpc import CallContext
19
+
20
+ from vgi.catalog import (
21
+ AttachOpaqueData,
22
+ CatalogAttachResult,
23
+ CatalogInfo,
24
+ CatalogInterface,
25
+ FunctionInfo,
26
+ IndexInfo,
27
+ MacroInfo,
28
+ MacroType,
29
+ OnConflict,
30
+ SchemaInfo,
31
+ SchemaObjectType,
32
+ SerializedSchema,
33
+ TableInfo,
34
+ TransactionOpaqueData,
35
+ ViewInfo,
36
+ )
37
+ from vgi.worker import Worker
38
+
39
+
40
+ @dataclass
41
+ class TableData:
42
+ """In-memory storage for table metadata."""
43
+
44
+ info: TableInfo
45
+
46
+
47
+ @dataclass
48
+ class ViewData:
49
+ """In-memory storage for view metadata."""
50
+
51
+ info: ViewInfo
52
+
53
+
54
+ @dataclass
55
+ class MacroData:
56
+ """In-memory storage for macro metadata."""
57
+
58
+ info: MacroInfo
59
+
60
+
61
+ @dataclass
62
+ class SchemaData:
63
+ """In-memory storage for schema metadata."""
64
+
65
+ info: SchemaInfo
66
+ tables: dict[str, TableData] = field(default_factory=dict)
67
+ views: dict[str, ViewData] = field(default_factory=dict)
68
+ macros: dict[str, MacroData] = field(default_factory=dict)
69
+
70
+
71
+ @dataclass
72
+ class CatalogData:
73
+ """In-memory storage for catalog metadata."""
74
+
75
+ name: str
76
+ schemas: dict[str, SchemaData] = field(default_factory=dict)
77
+ version: int = 1
78
+ comment: str | None = None
79
+ tags: dict[str, str] = field(default_factory=dict)
80
+
81
+
82
+ class InMemoryCatalog(CatalogInterface):
83
+ """In-memory catalog implementation for testing.
84
+
85
+ This implementation stores all catalog, schema, table, and view data
86
+ in memory using Python dictionaries. It supports basic DDL operations
87
+ but does not support transactions.
88
+
89
+ Attach IDs are generated as random UUIDs.
90
+ """
91
+
92
+ def __init__(self) -> None:
93
+ """Initialize the in-memory catalog."""
94
+ # Maps catalog name -> CatalogData
95
+ self._catalogs: dict[str, CatalogData] = {}
96
+ # Maps attach_opaque_data -> catalog_name
97
+ self._attachments: dict[AttachOpaqueData, str] = {}
98
+ # Create default "memory" catalog with "main" schema
99
+ self._create_default_catalog()
100
+
101
+ def _create_default_catalog(self) -> None:
102
+ """Create the default memory catalog with main schema."""
103
+ catalog = CatalogData(name="memory")
104
+ # Create a placeholder attach_opaque_data for internal use
105
+ placeholder_attach_opaque_data = AttachOpaqueData(b"\x00" * 16)
106
+ catalog.schemas["main"] = SchemaData(
107
+ info=SchemaInfo(
108
+ attach_opaque_data=placeholder_attach_opaque_data,
109
+ name="main",
110
+ comment=None,
111
+ tags={},
112
+ )
113
+ )
114
+ self._catalogs["memory"] = catalog
115
+
116
+ def _get_catalog(self, attach_opaque_data: AttachOpaqueData) -> CatalogData:
117
+ """Get the catalog for the given attach_opaque_data."""
118
+ catalog_name = self._attachments.get(attach_opaque_data)
119
+ if catalog_name is None:
120
+ msg = f"No catalog attached with id {attach_opaque_data!r}"
121
+ raise ValueError(msg)
122
+ catalog = self._catalogs.get(catalog_name)
123
+ if catalog is None:
124
+ msg = f"Catalog {catalog_name!r} not found"
125
+ raise ValueError(msg)
126
+ return catalog
127
+
128
+ def _get_schema(self, attach_opaque_data: AttachOpaqueData, schema_name: str) -> SchemaData:
129
+ """Get the schema for the given attach_opaque_data and schema name."""
130
+ catalog = self._get_catalog(attach_opaque_data)
131
+ schema = catalog.schemas.get(schema_name)
132
+ if schema is None:
133
+ msg = f"Schema {schema_name!r} not found in catalog"
134
+ raise ValueError(msg)
135
+ return schema
136
+
137
+ def _increment_version(self, attach_opaque_data: AttachOpaqueData) -> None:
138
+ """Increment the catalog version after a modification."""
139
+ catalog = self._get_catalog(attach_opaque_data)
140
+ catalog.version += 1
141
+
142
+ # Required abstract methods
143
+
144
+ def catalogs(self) -> list[CatalogInfo]:
145
+ """Get a list of catalog discovery records."""
146
+ return [CatalogInfo(name=name, implementation_version=None, data_version_spec=None) for name in self._catalogs]
147
+
148
+ def catalog_attach(
149
+ self,
150
+ *,
151
+ name: str,
152
+ options: dict[str, Any],
153
+ data_version_spec: str | None,
154
+ implementation_version: str | None,
155
+ ctx: CallContext | None = None,
156
+ ) -> CatalogAttachResult:
157
+ """Attach to a catalog with the given name.
158
+
159
+ This example has no version opinion: requested versions are ignored and
160
+ resolved_* fields echo back empty strings.
161
+ """
162
+ del data_version_spec, implementation_version, ctx
163
+ if name not in self._catalogs:
164
+ msg = f"Catalog {name!r} not found"
165
+ raise ValueError(msg)
166
+
167
+ # Generate a unique attach_opaque_data
168
+ attach_opaque_data = AttachOpaqueData(uuid.uuid4().bytes)
169
+ self._attachments[attach_opaque_data] = name
170
+
171
+ catalog = self._catalogs[name]
172
+ return CatalogAttachResult(
173
+ attach_opaque_data=attach_opaque_data,
174
+ supports_transactions=False,
175
+ supports_time_travel=False,
176
+ catalog_version_frozen=False,
177
+ catalog_version=catalog.version,
178
+ comment=catalog.comment,
179
+ tags=dict(catalog.tags),
180
+ resolved_data_version=None,
181
+ resolved_implementation_version=None,
182
+ )
183
+
184
+ def catalog_detach(self, *, attach_opaque_data: AttachOpaqueData) -> None:
185
+ """Detach from the catalog."""
186
+ self._attachments.pop(attach_opaque_data, None)
187
+
188
+ def schema_get(
189
+ self,
190
+ *,
191
+ attach_opaque_data: AttachOpaqueData,
192
+ transaction_opaque_data: TransactionOpaqueData | None,
193
+ name: str,
194
+ ) -> SchemaInfo | None:
195
+ """Get information about a schema."""
196
+ catalog = self._get_catalog(attach_opaque_data)
197
+ schema_data = catalog.schemas.get(name)
198
+ if schema_data is None:
199
+ return None
200
+ # Update the attach_opaque_data in the returned info
201
+ return SchemaInfo(
202
+ attach_opaque_data=attach_opaque_data,
203
+ name=schema_data.info.name,
204
+ comment=schema_data.info.comment,
205
+ tags=schema_data.info.tags,
206
+ )
207
+
208
+ def table_get(
209
+ self,
210
+ *,
211
+ attach_opaque_data: AttachOpaqueData,
212
+ transaction_opaque_data: TransactionOpaqueData | None,
213
+ schema_name: str,
214
+ name: str,
215
+ at_unit: str | None = None,
216
+ at_value: str | None = None,
217
+ ) -> TableInfo | None:
218
+ """Get information about a table."""
219
+ catalog = self._get_catalog(attach_opaque_data)
220
+ schema_data = catalog.schemas.get(schema_name)
221
+ if schema_data is None:
222
+ return None
223
+ table_data = schema_data.tables.get(name)
224
+ if table_data is None:
225
+ return None
226
+ return table_data.info
227
+
228
+ def view_get(
229
+ self,
230
+ *,
231
+ attach_opaque_data: AttachOpaqueData,
232
+ transaction_opaque_data: TransactionOpaqueData | None,
233
+ schema_name: str,
234
+ name: str,
235
+ ) -> ViewInfo | None:
236
+ """Get information about a view."""
237
+ catalog = self._get_catalog(attach_opaque_data)
238
+ schema_data = catalog.schemas.get(schema_name)
239
+ if schema_data is None:
240
+ return None
241
+ view_data = schema_data.views.get(name)
242
+ if view_data is None:
243
+ return None
244
+ return view_data.info
245
+
246
+ def macro_get(
247
+ self,
248
+ *,
249
+ attach_opaque_data: AttachOpaqueData,
250
+ transaction_opaque_data: TransactionOpaqueData | None,
251
+ schema_name: str,
252
+ name: str,
253
+ ) -> MacroInfo | None:
254
+ """Get information about a macro."""
255
+ catalog = self._get_catalog(attach_opaque_data)
256
+ schema_data = catalog.schemas.get(schema_name)
257
+ if schema_data is None:
258
+ return None
259
+ macro_data = schema_data.macros.get(name)
260
+ if macro_data is None:
261
+ return None
262
+ return macro_data.info
263
+
264
+ # Optional methods with implementations
265
+
266
+ def catalog_version(
267
+ self,
268
+ *,
269
+ attach_opaque_data: AttachOpaqueData,
270
+ transaction_opaque_data: TransactionOpaqueData | None,
271
+ ctx: Any = None,
272
+ ) -> int:
273
+ """Get the current catalog version."""
274
+ del ctx
275
+ catalog = self._get_catalog(attach_opaque_data)
276
+ return catalog.version
277
+
278
+ def catalog_create(self, *, name: str, on_conflict: OnConflict, options: dict[str, Any]) -> None:
279
+ """Create a new catalog."""
280
+ if name in self._catalogs:
281
+ if on_conflict == OnConflict.ERROR:
282
+ msg = f"Catalog {name!r} already exists"
283
+ raise ValueError(msg)
284
+ if on_conflict == OnConflict.IGNORE:
285
+ return
286
+ # REPLACE: fall through to create
287
+
288
+ catalog = CatalogData(name=name)
289
+ # Create a placeholder attach_opaque_data for internal use
290
+ placeholder_attach_opaque_data = AttachOpaqueData(b"\x00" * 16)
291
+ catalog.schemas["main"] = SchemaData(
292
+ info=SchemaInfo(
293
+ attach_opaque_data=placeholder_attach_opaque_data,
294
+ name="main",
295
+ comment=None,
296
+ tags={},
297
+ )
298
+ )
299
+ self._catalogs[name] = catalog
300
+
301
+ def catalog_drop(self, *, name: str) -> None:
302
+ """Drop a catalog."""
303
+ if name not in self._catalogs:
304
+ msg = f"Catalog {name!r} not found"
305
+ raise ValueError(msg)
306
+ # Remove any attachments to this catalog
307
+ to_remove = [aid for aid, cname in self._attachments.items() if cname == name]
308
+ for aid in to_remove:
309
+ del self._attachments[aid]
310
+ del self._catalogs[name]
311
+
312
+ def schemas(
313
+ self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData | None
314
+ ) -> list[SchemaInfo]:
315
+ """Get a list of schemas in the catalog."""
316
+ catalog = self._get_catalog(attach_opaque_data)
317
+ result = []
318
+ for schema_data in catalog.schemas.values():
319
+ # Populate estimated_object_count from the in-memory population so
320
+ # the C++ side's eager-load gate has a real signal to act on. The
321
+ # InMemoryCatalog only models tables/views/macros — for kinds it
322
+ # cannot model at all (functions, indexes), emit explicit ``0``
323
+ # so the client triggers the zero-count RPC bypass instead of
324
+ # paying empty round-trips for kinds that genuinely cannot exist
325
+ # here. This is load-bearing for the bypass integration test.
326
+ estimated = {
327
+ "table": len(schema_data.tables),
328
+ "view": len(schema_data.views),
329
+ "macro": len(schema_data.macros),
330
+ "index": 0,
331
+ "scalar_function": 0,
332
+ "aggregate_function": 0,
333
+ "table_function": 0,
334
+ }
335
+ result.append(
336
+ SchemaInfo(
337
+ attach_opaque_data=attach_opaque_data,
338
+ name=schema_data.info.name,
339
+ comment=schema_data.info.comment,
340
+ tags=schema_data.info.tags,
341
+ estimated_object_count=estimated,
342
+ )
343
+ )
344
+ return result
345
+
346
+ def schema_create(
347
+ self,
348
+ *,
349
+ attach_opaque_data: AttachOpaqueData,
350
+ transaction_opaque_data: TransactionOpaqueData | None,
351
+ name: str,
352
+ on_conflict: OnConflict = OnConflict.ERROR,
353
+ comment: str | None,
354
+ tags: dict[str, str],
355
+ ) -> None:
356
+ """Create a new schema."""
357
+ catalog = self._get_catalog(attach_opaque_data)
358
+ if name in catalog.schemas:
359
+ msg = f"Schema {name!r} already exists"
360
+ raise ValueError(msg)
361
+ catalog.schemas[name] = SchemaData(
362
+ info=SchemaInfo(
363
+ attach_opaque_data=attach_opaque_data,
364
+ name=name,
365
+ comment=comment,
366
+ tags=tags,
367
+ )
368
+ )
369
+ self._increment_version(attach_opaque_data)
370
+
371
+ def schema_drop(
372
+ self,
373
+ *,
374
+ attach_opaque_data: AttachOpaqueData,
375
+ transaction_opaque_data: TransactionOpaqueData | None,
376
+ name: str,
377
+ ignore_not_found: bool,
378
+ cascade: bool,
379
+ ) -> None:
380
+ """Drop a schema."""
381
+ catalog = self._get_catalog(attach_opaque_data)
382
+ if name not in catalog.schemas:
383
+ if ignore_not_found:
384
+ return
385
+ msg = f"Schema {name!r} not found"
386
+ raise ValueError(msg)
387
+ schema_data = catalog.schemas[name]
388
+ if not cascade and (schema_data.tables or schema_data.views or schema_data.macros):
389
+ msg = f"Schema {name!r} is not empty, use CASCADE to drop"
390
+ raise ValueError(msg)
391
+ del catalog.schemas[name]
392
+ self._increment_version(attach_opaque_data)
393
+
394
+ @overload
395
+ def schema_contents(
396
+ self,
397
+ *,
398
+ attach_opaque_data: AttachOpaqueData,
399
+ transaction_opaque_data: TransactionOpaqueData | None,
400
+ name: str,
401
+ type: Literal[SchemaObjectType.TABLE],
402
+ ) -> Sequence[TableInfo]: ...
403
+
404
+ @overload
405
+ def schema_contents(
406
+ self,
407
+ *,
408
+ attach_opaque_data: AttachOpaqueData,
409
+ transaction_opaque_data: TransactionOpaqueData | None,
410
+ name: str,
411
+ type: Literal[SchemaObjectType.VIEW],
412
+ ) -> Sequence[ViewInfo]: ...
413
+
414
+ @overload
415
+ def schema_contents(
416
+ self,
417
+ *,
418
+ attach_opaque_data: AttachOpaqueData,
419
+ transaction_opaque_data: TransactionOpaqueData | None,
420
+ name: str,
421
+ type: Literal[
422
+ SchemaObjectType.SCALAR_FUNCTION,
423
+ SchemaObjectType.TABLE_FUNCTION,
424
+ SchemaObjectType.AGGREGATE_FUNCTION,
425
+ ],
426
+ ) -> Sequence[FunctionInfo]: ...
427
+
428
+ @overload
429
+ def schema_contents(
430
+ self,
431
+ *,
432
+ attach_opaque_data: AttachOpaqueData,
433
+ transaction_opaque_data: TransactionOpaqueData | None,
434
+ name: str,
435
+ type: Literal[SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO],
436
+ ) -> Sequence[MacroInfo]: ...
437
+
438
+ @overload
439
+ def schema_contents(
440
+ self,
441
+ *,
442
+ attach_opaque_data: AttachOpaqueData,
443
+ transaction_opaque_data: TransactionOpaqueData | None,
444
+ name: str,
445
+ type: Literal[SchemaObjectType.INDEX],
446
+ ) -> Sequence[IndexInfo]: ...
447
+
448
+ def schema_contents(
449
+ self,
450
+ *,
451
+ attach_opaque_data: AttachOpaqueData,
452
+ transaction_opaque_data: TransactionOpaqueData | None,
453
+ name: str,
454
+ type: SchemaObjectType,
455
+ ) -> Sequence[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo]:
456
+ """Get the contents of a schema.
457
+
458
+ Args:
459
+ attach_opaque_data: The attachment identifier.
460
+ transaction_opaque_data: The transaction identifier, if any.
461
+ name: The name of the schema.
462
+ type: The type of objects to return. Must be a SchemaObjectType enum.
463
+
464
+ Returns:
465
+ An iterable of TableInfo, ViewInfo, FunctionInfo, or MacroInfo objects
466
+ depending on the type parameter.
467
+
468
+ """
469
+ schema_data = self._get_schema(attach_opaque_data, name)
470
+ result: list[TableInfo | ViewInfo | FunctionInfo | MacroInfo | IndexInfo] = []
471
+
472
+ # Normalize type parameter (may be string from wire protocol)
473
+ type_enum = type if isinstance(type, SchemaObjectType) else SchemaObjectType(type)
474
+
475
+ # Return tables for TABLE type
476
+ if type_enum == SchemaObjectType.TABLE:
477
+ for table_data in schema_data.tables.values():
478
+ result.append(table_data.info)
479
+
480
+ # Return views for VIEW type
481
+ elif type_enum == SchemaObjectType.VIEW:
482
+ for view_data in schema_data.views.values():
483
+ result.append(view_data.info)
484
+
485
+ # Return macros for SCALAR_MACRO or TABLE_MACRO type
486
+ elif type_enum in (SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO):
487
+ target_macro_type = MacroType.SCALAR if type_enum == SchemaObjectType.SCALAR_MACRO else MacroType.TABLE
488
+ for macro_data in schema_data.macros.values():
489
+ if macro_data.info.macro_type == target_macro_type:
490
+ result.append(macro_data.info)
491
+
492
+ # Note: This example catalog doesn't store functions,
493
+ # so SCALAR_FUNCTION and TABLE_FUNCTION types return nothing
494
+
495
+ return result
496
+
497
+ def table_create(
498
+ self,
499
+ *,
500
+ attach_opaque_data: AttachOpaqueData,
501
+ transaction_opaque_data: TransactionOpaqueData | None,
502
+ schema_name: str,
503
+ name: str,
504
+ columns: SerializedSchema,
505
+ on_conflict: OnConflict,
506
+ not_null_constraints: list[int],
507
+ unique_constraints: list[list[int]],
508
+ check_constraints: list[str],
509
+ primary_key_constraints: list[list[int]] | None = None,
510
+ foreign_key_constraints: list[bytes] | None = None,
511
+ ) -> None:
512
+ """Create a new table."""
513
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
514
+ if name in schema_data.tables:
515
+ if on_conflict == OnConflict.ERROR:
516
+ msg = f"Table {name!r} already exists in schema {schema_name!r}"
517
+ raise ValueError(msg)
518
+ if on_conflict == OnConflict.IGNORE:
519
+ return
520
+ # REPLACE: fall through to create
521
+
522
+ schema_data.tables[name] = TableData(
523
+ info=TableInfo(
524
+ name=name,
525
+ schema_name=schema_name,
526
+ columns=columns,
527
+ not_null_constraints=not_null_constraints,
528
+ unique_constraints=unique_constraints,
529
+ check_constraints=check_constraints,
530
+ comment=None,
531
+ tags={},
532
+ )
533
+ )
534
+ self._increment_version(attach_opaque_data)
535
+
536
+ def table_drop(
537
+ self,
538
+ *,
539
+ attach_opaque_data: AttachOpaqueData,
540
+ transaction_opaque_data: TransactionOpaqueData | None,
541
+ schema_name: str,
542
+ name: str,
543
+ ignore_not_found: bool,
544
+ cascade: bool = False,
545
+ ) -> None:
546
+ """Drop a table."""
547
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
548
+ if name not in schema_data.tables:
549
+ if ignore_not_found:
550
+ return
551
+ msg = f"Table {name!r} not found in schema {schema_name!r}"
552
+ raise ValueError(msg)
553
+ del schema_data.tables[name]
554
+ self._increment_version(attach_opaque_data)
555
+
556
+ def table_comment_set(
557
+ self,
558
+ *,
559
+ attach_opaque_data: AttachOpaqueData,
560
+ transaction_opaque_data: TransactionOpaqueData | None,
561
+ schema_name: str,
562
+ name: str,
563
+ comment: str | None,
564
+ ignore_not_found: bool,
565
+ ) -> None:
566
+ """Set the comment for a table."""
567
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
568
+ table_data = schema_data.tables.get(name)
569
+ if table_data is None:
570
+ if ignore_not_found:
571
+ return
572
+ msg = f"Table {name!r} not found in schema {schema_name!r}"
573
+ raise ValueError(msg)
574
+ # Create a new TableInfo with the updated comment
575
+ old_info = table_data.info
576
+ schema_data.tables[name] = TableData(
577
+ info=TableInfo(
578
+ name=old_info.name,
579
+ schema_name=old_info.schema_name,
580
+ columns=old_info.columns,
581
+ not_null_constraints=old_info.not_null_constraints,
582
+ unique_constraints=old_info.unique_constraints,
583
+ check_constraints=old_info.check_constraints,
584
+ comment=comment,
585
+ tags=old_info.tags,
586
+ )
587
+ )
588
+ self._increment_version(attach_opaque_data)
589
+
590
+ def table_rename(
591
+ self,
592
+ *,
593
+ attach_opaque_data: AttachOpaqueData,
594
+ transaction_opaque_data: TransactionOpaqueData | None,
595
+ schema_name: str,
596
+ name: str,
597
+ new_name: str,
598
+ ignore_not_found: bool,
599
+ ) -> None:
600
+ """Rename a table."""
601
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
602
+ if name not in schema_data.tables:
603
+ if ignore_not_found:
604
+ return
605
+ msg = f"Table {name!r} not found in schema {schema_name!r}"
606
+ raise ValueError(msg)
607
+ if new_name in schema_data.tables:
608
+ msg = f"Table {new_name!r} already exists in schema {schema_name!r}"
609
+ raise ValueError(msg)
610
+ table_data = schema_data.tables.pop(name)
611
+ # Create new TableInfo with updated name
612
+ old_info = table_data.info
613
+ schema_data.tables[new_name] = TableData(
614
+ info=TableInfo(
615
+ name=new_name,
616
+ schema_name=old_info.schema_name,
617
+ columns=old_info.columns,
618
+ not_null_constraints=old_info.not_null_constraints,
619
+ unique_constraints=old_info.unique_constraints,
620
+ check_constraints=old_info.check_constraints,
621
+ comment=old_info.comment,
622
+ tags=old_info.tags,
623
+ )
624
+ )
625
+ self._increment_version(attach_opaque_data)
626
+
627
+ def view_create(
628
+ self,
629
+ *,
630
+ attach_opaque_data: AttachOpaqueData,
631
+ transaction_opaque_data: TransactionOpaqueData | None,
632
+ schema_name: str,
633
+ name: str,
634
+ definition: str,
635
+ on_conflict: OnConflict,
636
+ ) -> None:
637
+ """Create a new view."""
638
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
639
+ if name in schema_data.views:
640
+ if on_conflict == OnConflict.ERROR:
641
+ msg = f"View {name!r} already exists in schema {schema_name!r}"
642
+ raise ValueError(msg)
643
+ if on_conflict == OnConflict.IGNORE:
644
+ return
645
+ # REPLACE: fall through to create
646
+
647
+ schema_data.views[name] = ViewData(
648
+ info=ViewInfo(
649
+ name=name,
650
+ schema_name=schema_name,
651
+ definition=definition,
652
+ comment=None,
653
+ tags={},
654
+ )
655
+ )
656
+ self._increment_version(attach_opaque_data)
657
+
658
+ def view_drop(
659
+ self,
660
+ *,
661
+ attach_opaque_data: AttachOpaqueData,
662
+ transaction_opaque_data: TransactionOpaqueData | None,
663
+ schema_name: str,
664
+ name: str,
665
+ ignore_not_found: bool,
666
+ cascade: bool = False,
667
+ ) -> None:
668
+ """Drop a view."""
669
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
670
+ if name not in schema_data.views:
671
+ if ignore_not_found:
672
+ return
673
+ msg = f"View {name!r} not found in schema {schema_name!r}"
674
+ raise ValueError(msg)
675
+ del schema_data.views[name]
676
+ self._increment_version(attach_opaque_data)
677
+
678
+ def view_rename(
679
+ self,
680
+ *,
681
+ attach_opaque_data: AttachOpaqueData,
682
+ transaction_opaque_data: TransactionOpaqueData | None,
683
+ schema_name: str,
684
+ name: str,
685
+ new_name: str,
686
+ ignore_not_found: bool,
687
+ ) -> None:
688
+ """Rename a view."""
689
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
690
+ if name not in schema_data.views:
691
+ if ignore_not_found:
692
+ return
693
+ msg = f"View {name!r} not found in schema {schema_name!r}"
694
+ raise ValueError(msg)
695
+ if new_name in schema_data.views:
696
+ msg = f"View {new_name!r} already exists in schema {schema_name!r}"
697
+ raise ValueError(msg)
698
+ view_data = schema_data.views.pop(name)
699
+ # Create new ViewInfo with updated name
700
+ old_info = view_data.info
701
+ schema_data.views[new_name] = ViewData(
702
+ info=ViewInfo(
703
+ name=new_name,
704
+ schema_name=old_info.schema_name,
705
+ definition=old_info.definition,
706
+ comment=old_info.comment,
707
+ tags=old_info.tags,
708
+ )
709
+ )
710
+ self._increment_version(attach_opaque_data)
711
+
712
+ def view_comment_set(
713
+ self,
714
+ *,
715
+ attach_opaque_data: AttachOpaqueData,
716
+ transaction_opaque_data: TransactionOpaqueData | None,
717
+ schema_name: str,
718
+ name: str,
719
+ comment: str | None,
720
+ ignore_not_found: bool,
721
+ ) -> None:
722
+ """Set the comment for a view."""
723
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
724
+ view_data = schema_data.views.get(name)
725
+ if view_data is None:
726
+ if ignore_not_found:
727
+ return
728
+ msg = f"View {name!r} not found in schema {schema_name!r}"
729
+ raise ValueError(msg)
730
+ # Create a new ViewInfo with the updated comment
731
+ old_info = view_data.info
732
+ schema_data.views[name] = ViewData(
733
+ info=ViewInfo(
734
+ name=old_info.name,
735
+ schema_name=old_info.schema_name,
736
+ definition=old_info.definition,
737
+ comment=comment,
738
+ tags=old_info.tags,
739
+ )
740
+ )
741
+ self._increment_version(attach_opaque_data)
742
+
743
+ def macro_create(
744
+ self,
745
+ *,
746
+ attach_opaque_data: AttachOpaqueData,
747
+ transaction_opaque_data: TransactionOpaqueData | None,
748
+ schema_name: str,
749
+ name: str,
750
+ macro_type: MacroType,
751
+ parameters: list[str],
752
+ definition: str,
753
+ on_conflict: OnConflict,
754
+ parameter_default_values: pa.RecordBatch | None = None,
755
+ ) -> None:
756
+ """Create a new macro."""
757
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
758
+ if name in schema_data.macros:
759
+ if on_conflict == OnConflict.ERROR:
760
+ msg = f"Macro {name!r} already exists in schema {schema_name!r}"
761
+ raise ValueError(msg)
762
+ if on_conflict == OnConflict.IGNORE:
763
+ return
764
+ # REPLACE: fall through to create
765
+
766
+ schema_data.macros[name] = MacroData(
767
+ info=MacroInfo(
768
+ name=name,
769
+ schema_name=schema_name,
770
+ macro_type=macro_type,
771
+ parameters=parameters,
772
+ parameter_default_values=parameter_default_values,
773
+ definition=definition,
774
+ comment=None,
775
+ tags={},
776
+ )
777
+ )
778
+ self._increment_version(attach_opaque_data)
779
+
780
+ def macro_drop(
781
+ self,
782
+ *,
783
+ attach_opaque_data: AttachOpaqueData,
784
+ transaction_opaque_data: TransactionOpaqueData | None,
785
+ schema_name: str,
786
+ name: str,
787
+ ignore_not_found: bool,
788
+ ) -> None:
789
+ """Drop a macro."""
790
+ schema_data = self._get_schema(attach_opaque_data, schema_name)
791
+ if name not in schema_data.macros:
792
+ if ignore_not_found:
793
+ return
794
+ msg = f"Macro {name!r} not found in schema {schema_name!r}"
795
+ raise ValueError(msg)
796
+ del schema_data.macros[name]
797
+ self._increment_version(attach_opaque_data)
798
+
799
+
800
+ class InMemoryCatalogWorker(Worker):
801
+ """Example worker with InMemoryCatalog support."""
802
+
803
+ catalog_interface = InMemoryCatalog
804
+ functions = [] # No functions, just catalog support
805
+
806
+
807
+ def main() -> None:
808
+ """Run the in-memory catalog worker process."""
809
+ InMemoryCatalogWorker.main()
810
+
811
+
812
+ if __name__ == "__main__":
813
+ main()