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,97 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Transaction CLI commands for VGI.
4
+
5
+ This module provides CLI commands for transaction operations:
6
+ - begin: Begin a new transaction
7
+ - commit: Commit a transaction
8
+ - rollback: Rollback a transaction
9
+
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import click
15
+
16
+ from vgi.client.cli_utils import (
17
+ bytes_to_hex,
18
+ hex_to_attach_opaque_data,
19
+ hex_to_transaction_opaque_data,
20
+ output_json,
21
+ )
22
+ from vgi.client.client import Client
23
+
24
+
25
+ @click.group()
26
+ def transaction() -> None:
27
+ """Manage transactions in a catalog."""
28
+
29
+
30
+ @transaction.command("begin")
31
+ @click.option("--attach-opaque-data", required=True, help="Hex-encoded attach ID")
32
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
33
+ def transaction_begin(attach_opaque_data: str, worker: str) -> None:
34
+ """Begin a new transaction.
35
+
36
+ Returns a transaction_opaque_data that can be used with other catalog operations.
37
+
38
+ """
39
+ client = Client(worker)
40
+ tx_id = client.catalog_transaction_begin(attach_opaque_data=hex_to_attach_opaque_data(attach_opaque_data))
41
+ if tx_id is None:
42
+ output_json({"error": "Catalog does not support transactions"})
43
+ return
44
+ output_json(
45
+ {
46
+ "transaction_opaque_data": bytes_to_hex(tx_id),
47
+ "attach_opaque_data": attach_opaque_data,
48
+ }
49
+ )
50
+
51
+
52
+ @transaction.command("commit")
53
+ @click.argument("transaction_opaque_data")
54
+ @click.option("--attach-opaque-data", required=True, help="Hex-encoded attach ID")
55
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
56
+ def transaction_commit(transaction_opaque_data: str, attach_opaque_data: str, worker: str) -> None:
57
+ """Commit a transaction.
58
+
59
+ TRANSACTION_ID is the hex-encoded transaction ID from transaction begin.
60
+
61
+ """
62
+ client = Client(worker)
63
+ client.catalog_transaction_commit(
64
+ attach_opaque_data=hex_to_attach_opaque_data(attach_opaque_data),
65
+ transaction_opaque_data=hex_to_transaction_opaque_data(transaction_opaque_data),
66
+ )
67
+ output_json(
68
+ {
69
+ "status": "committed",
70
+ "transaction_opaque_data": transaction_opaque_data,
71
+ "attach_opaque_data": attach_opaque_data,
72
+ }
73
+ )
74
+
75
+
76
+ @transaction.command("rollback")
77
+ @click.argument("transaction_opaque_data")
78
+ @click.option("--attach-opaque-data", required=True, help="Hex-encoded attach ID")
79
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
80
+ def transaction_rollback(transaction_opaque_data: str, attach_opaque_data: str, worker: str) -> None:
81
+ """Rollback a transaction.
82
+
83
+ TRANSACTION_ID is the hex-encoded transaction ID from transaction begin.
84
+
85
+ """
86
+ client = Client(worker)
87
+ client.catalog_transaction_rollback(
88
+ attach_opaque_data=hex_to_attach_opaque_data(attach_opaque_data),
89
+ transaction_opaque_data=hex_to_transaction_opaque_data(transaction_opaque_data),
90
+ )
91
+ output_json(
92
+ {
93
+ "status": "rolled_back",
94
+ "transaction_opaque_data": transaction_opaque_data,
95
+ "attach_opaque_data": attach_opaque_data,
96
+ }
97
+ )
@@ -0,0 +1,441 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Shared utilities for VGI CLI commands.
4
+
5
+ This module provides common utilities used across CLI command groups:
6
+ - Hex string conversion for AttachOpaqueData and TransactionOpaqueData
7
+ - JSON to Arrow schema conversion for table columns
8
+ - Output formatting helpers
9
+
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ import click
18
+ import pyarrow as pa
19
+
20
+ from vgi.catalog import AttachOpaqueData, TransactionOpaqueData
21
+
22
+ if TYPE_CHECKING:
23
+ from vgi.catalog import CatalogAttachResult, FunctionInfo, SchemaInfo, TableInfo, ViewInfo
24
+ from vgi.catalog.catalog_interface import ScanFunctionResult
25
+ from vgi.client import Client
26
+
27
+ # Map of type names to PyArrow types for JSON schema definitions
28
+ ARROW_TYPE_MAP: dict[str, pa.DataType] = {
29
+ # Signed integers
30
+ "int8": pa.int8(),
31
+ "int16": pa.int16(),
32
+ "int32": pa.int32(),
33
+ "int64": pa.int64(),
34
+ # Unsigned integers
35
+ "uint8": pa.uint8(),
36
+ "uint16": pa.uint16(),
37
+ "uint32": pa.uint32(),
38
+ "uint64": pa.uint64(),
39
+ # Floating point
40
+ "float16": pa.float16(),
41
+ "float32": pa.float32(),
42
+ "float64": pa.float64(),
43
+ # Strings and binary
44
+ "string": pa.string(),
45
+ "utf8": pa.utf8(),
46
+ "large_string": pa.large_string(),
47
+ "binary": pa.binary(),
48
+ "large_binary": pa.large_binary(),
49
+ # Boolean
50
+ "bool": pa.bool_(),
51
+ "boolean": pa.bool_(),
52
+ # Date types
53
+ "date32": pa.date32(),
54
+ "date64": pa.date64(),
55
+ # Timestamp types (microsecond precision by default)
56
+ "timestamp": pa.timestamp("us"),
57
+ "timestamp_s": pa.timestamp("s"),
58
+ "timestamp_ms": pa.timestamp("ms"),
59
+ "timestamp_us": pa.timestamp("us"),
60
+ "timestamp_ns": pa.timestamp("ns"),
61
+ # Duration types
62
+ "duration": pa.duration("us"),
63
+ "duration_s": pa.duration("s"),
64
+ "duration_ms": pa.duration("ms"),
65
+ "duration_us": pa.duration("us"),
66
+ "duration_ns": pa.duration("ns"),
67
+ # Time types
68
+ "time32": pa.time32("ms"),
69
+ "time64": pa.time64("us"),
70
+ }
71
+
72
+
73
+ def hex_to_bytes(hex_string: str) -> bytes:
74
+ """Convert a hex string to bytes.
75
+
76
+ Args:
77
+ hex_string: Hexadecimal string (e.g., "deadbeef")
78
+
79
+ Returns:
80
+ Bytes representation
81
+
82
+ Raises:
83
+ click.ClickException: If hex string is invalid
84
+
85
+ """
86
+ try:
87
+ return bytes.fromhex(hex_string)
88
+ except ValueError as e:
89
+ raise click.ClickException(f"Invalid hex string '{hex_string}': {e}") from e
90
+
91
+
92
+ def hex_to_attach_opaque_data(hex_string: str) -> AttachOpaqueData:
93
+ """Convert a hex string to AttachOpaqueData.
94
+
95
+ Args:
96
+ hex_string: Hexadecimal string (e.g., "deadbeef")
97
+
98
+ Returns:
99
+ AttachOpaqueData
100
+
101
+ Raises:
102
+ click.ClickException: If hex string is invalid
103
+
104
+ """
105
+ return AttachOpaqueData(hex_to_bytes(hex_string))
106
+
107
+
108
+ def hex_to_transaction_opaque_data(hex_string: str) -> TransactionOpaqueData:
109
+ """Convert a hex string to TransactionOpaqueData.
110
+
111
+ Args:
112
+ hex_string: Hexadecimal string (e.g., "deadbeef")
113
+
114
+ Returns:
115
+ TransactionOpaqueData
116
+
117
+ Raises:
118
+ click.ClickException: If hex string is invalid
119
+
120
+ """
121
+ return TransactionOpaqueData(hex_to_bytes(hex_string))
122
+
123
+
124
+ def bytes_to_hex(data: bytes) -> str:
125
+ """Convert bytes to a hex string.
126
+
127
+ Args:
128
+ data: Bytes to convert
129
+
130
+ Returns:
131
+ Hexadecimal string representation
132
+
133
+ """
134
+ return data.hex()
135
+
136
+
137
+ def json_to_arrow_schema(columns: list[dict[str, str]]) -> pa.Schema:
138
+ """Convert JSON column definitions to PyArrow schema.
139
+
140
+ Args:
141
+ columns: List of dicts with 'name' and 'type' keys.
142
+ Example: [{"name": "id", "type": "int64"}]
143
+
144
+ Returns:
145
+ PyArrow Schema
146
+
147
+ Raises:
148
+ click.ClickException: If type is unknown or column definition is invalid.
149
+
150
+ """
151
+ fields = []
152
+ for i, col in enumerate(columns):
153
+ if "name" not in col:
154
+ raise click.ClickException(f"Column {i} missing 'name' field: {json.dumps(col)}")
155
+ if "type" not in col:
156
+ raise click.ClickException(f"Column {i} missing 'type' field: {json.dumps(col)}")
157
+
158
+ type_name = col["type"]
159
+ if type_name not in ARROW_TYPE_MAP:
160
+ valid_types = ", ".join(sorted(ARROW_TYPE_MAP.keys()))
161
+ raise click.ClickException(
162
+ f"Unknown type '{type_name}' for column '{col['name']}'. Valid types: {valid_types}"
163
+ )
164
+
165
+ fields.append(pa.field(col["name"], ARROW_TYPE_MAP[type_name]))
166
+
167
+ return pa.schema(fields)
168
+
169
+
170
+ def arrow_schema_to_json(serialized: bytes) -> list[dict[str, str | bool]]:
171
+ """Convert serialized Arrow schema to JSON for display.
172
+
173
+ Args:
174
+ serialized: Serialized Arrow schema bytes
175
+
176
+ Returns:
177
+ List of column definitions with name, type, and optional flags (varargs, const)
178
+
179
+ """
180
+ reader = pa.BufferReader(serialized)
181
+ schema = pa.ipc.read_schema(reader) # type: ignore[arg-type]
182
+ result: list[dict[str, str | bool]] = []
183
+ for f in schema:
184
+ type_str = str(f.type)
185
+ is_varargs = False
186
+ is_const = False
187
+ if f.metadata:
188
+ # Check for vgi:any metadata (output schema)
189
+ if f.metadata.get(b"vgi:any") == b"true":
190
+ type_str = "any"
191
+ # Check for vgi_type metadata (argument schema)
192
+ elif f.metadata.get(b"vgi_type") == b"table":
193
+ type_str = "table"
194
+ elif f.metadata.get(b"vgi_type") == b"any":
195
+ type_str = "any"
196
+ # Check for varargs metadata
197
+ if f.metadata.get(b"vgi_varargs") == b"true":
198
+ is_varargs = True
199
+ # Check for const metadata (ConstParam)
200
+ if f.metadata.get(b"vgi_const") == b"true":
201
+ is_const = True
202
+
203
+ entry: dict[str, str | bool] = {"name": f.name, "type": type_str}
204
+ if is_varargs:
205
+ entry["varargs"] = True
206
+ if is_const:
207
+ entry["const"] = True
208
+ result.append(entry)
209
+ return result
210
+
211
+
212
+ def output_json(data: Any) -> None:
213
+ """Output data as JSON to stdout.
214
+
215
+ Args:
216
+ data: Data to serialize as JSON
217
+
218
+ """
219
+ click.echo(json.dumps(data))
220
+
221
+
222
+ def parse_json_option(value: str, option_name: str) -> Any:
223
+ """Parse a JSON string from a CLI option.
224
+
225
+ Args:
226
+ value: JSON string to parse
227
+ option_name: Name of the option (for error messages)
228
+
229
+ Returns:
230
+ Parsed JSON value
231
+
232
+ Raises:
233
+ click.ClickException: If JSON is invalid
234
+
235
+ """
236
+ try:
237
+ return json.loads(value)
238
+ except json.JSONDecodeError as e:
239
+ raise click.ClickException(f"Invalid JSON for {option_name}: {e}") from e
240
+
241
+
242
+ def schema_info_to_dict(schema_info: SchemaInfo) -> dict[str, Any]:
243
+ """Convert SchemaInfo to a dictionary for JSON output.
244
+
245
+ Args:
246
+ schema_info: SchemaInfo object from catalog
247
+
248
+ Returns:
249
+ Dictionary representation
250
+
251
+ """
252
+ return {
253
+ "name": schema_info.name,
254
+ "comment": schema_info.comment,
255
+ "tags": dict(schema_info.tags),
256
+ }
257
+
258
+
259
+ def table_info_to_dict(table_info: TableInfo) -> dict[str, Any]:
260
+ """Convert TableInfo to a dictionary for JSON output.
261
+
262
+ Args:
263
+ table_info: TableInfo object from catalog
264
+
265
+ Returns:
266
+ Dictionary representation
267
+
268
+ """
269
+ return {
270
+ "name": table_info.name,
271
+ "schema_name": table_info.schema_name,
272
+ "columns": arrow_schema_to_json(table_info.columns),
273
+ "not_null_constraints": table_info.not_null_constraints,
274
+ "unique_constraints": table_info.unique_constraints,
275
+ "check_constraints": table_info.check_constraints,
276
+ "comment": table_info.comment,
277
+ "tags": dict(table_info.tags),
278
+ }
279
+
280
+
281
+ def view_info_to_dict(view_info: ViewInfo) -> dict[str, Any]:
282
+ """Convert ViewInfo to a dictionary for JSON output.
283
+
284
+ Args:
285
+ view_info: ViewInfo object from catalog
286
+
287
+ Returns:
288
+ Dictionary representation
289
+
290
+ """
291
+ return {
292
+ "name": view_info.name,
293
+ "schema_name": view_info.schema_name,
294
+ "definition": view_info.definition,
295
+ "comment": view_info.comment,
296
+ "tags": dict(view_info.tags),
297
+ }
298
+
299
+
300
+ def function_info_to_dict(function_info: FunctionInfo) -> dict[str, Any]:
301
+ """Convert FunctionInfo to a dictionary for JSON output.
302
+
303
+ Args:
304
+ function_info: FunctionInfo object from catalog
305
+
306
+ Returns:
307
+ Dictionary representation
308
+
309
+ """
310
+ result: dict[str, Any] = {
311
+ "name": function_info.name,
312
+ "schema_name": function_info.schema_name,
313
+ "function_type": function_info.function_type.value,
314
+ "arguments": arrow_schema_to_json(function_info.arguments),
315
+ "description": function_info.description,
316
+ "tags": dict(function_info.tags),
317
+ # Scalar function behavior fields (None for non-scalar)
318
+ "stability": (function_info.stability.name if function_info.stability else None),
319
+ "null_handling": (function_info.null_handling.name if function_info.null_handling else None),
320
+ # Documentation fields (convert CatalogExample to dict for JSON)
321
+ "examples": [
322
+ {"sql": ex.sql, "description": ex.description} if hasattr(ex, "sql") else ex
323
+ for ex in function_info.examples
324
+ ],
325
+ "categories": function_info.categories,
326
+ # Table function capabilities (None for scalar)
327
+ "projection_pushdown": function_info.projection_pushdown,
328
+ "filter_pushdown": function_info.filter_pushdown,
329
+ "order_preservation": (function_info.order_preservation.name if function_info.order_preservation else None),
330
+ "max_workers": function_info.max_workers,
331
+ # Aggregate function fields
332
+ "order_dependent": function_info.order_dependent.name,
333
+ "distinct_dependent": function_info.distinct_dependent.name,
334
+ # Settings
335
+ "required_settings": function_info.required_settings,
336
+ }
337
+ # Only include output_schema for scalar functions
338
+ if function_info.function_type.value == "scalar":
339
+ result["output_schema"] = arrow_schema_to_json(function_info.output_schema)
340
+ return result
341
+
342
+
343
+ def catalog_attach_result_to_dict(result: CatalogAttachResult) -> dict[str, Any]:
344
+ """Convert CatalogAttachResult to a dictionary for JSON output.
345
+
346
+ Args:
347
+ result: CatalogAttachResult object
348
+
349
+ Returns:
350
+ Dictionary representation with attach_opaque_data as hex
351
+
352
+ """
353
+ return {
354
+ "attach_opaque_data": bytes_to_hex(result.attach_opaque_data),
355
+ "supports_transactions": result.supports_transactions,
356
+ "supports_time_travel": result.supports_time_travel,
357
+ "catalog_version_frozen": result.catalog_version_frozen,
358
+ "catalog_version": result.catalog_version,
359
+ "attach_opaque_data_required": result.attach_opaque_data_required,
360
+ "default_schema": result.default_schema,
361
+ "settings": [bytes_to_hex(s) for s in result.settings],
362
+ "resolved_data_version": result.resolved_data_version,
363
+ "resolved_implementation_version": result.resolved_implementation_version,
364
+ }
365
+
366
+
367
+ def scan_function_result_to_dict(result: ScanFunctionResult) -> dict[str, Any]:
368
+ """Convert ScanFunctionResult to a dictionary for JSON output.
369
+
370
+ ScanFunctionResult allows the VGI DuckDB extension to call any DuckDB
371
+ function with specified positional and named arguments, and load any
372
+ required extensions.
373
+
374
+ Args:
375
+ result: ScanFunctionResult object
376
+
377
+ Returns:
378
+ Dictionary representation with function_name, positional_arguments,
379
+ named_arguments, and required_extensions.
380
+
381
+ """
382
+ return {
383
+ "function_name": result.function_name,
384
+ "positional_arguments": [arg.as_py() for arg in result.positional_arguments],
385
+ "named_arguments": {name: arg.as_py() for name, arg in result.named_arguments.items()},
386
+ "required_extensions": result.required_extensions,
387
+ }
388
+
389
+
390
+ def get_attach_opaque_data_from_options(
391
+ client: Client,
392
+ attach_opaque_data: str | None,
393
+ catalog: str | None,
394
+ attach_options: dict[str, Any] | None,
395
+ ) -> tuple[AttachOpaqueData, bool]:
396
+ """Get attach_opaque_data from either explicit --attach-opaque-data or auto-attach via --catalog.
397
+
398
+ This helper supports two workflows:
399
+ 1. Explicit attach_opaque_data: Use a pre-obtained attach_opaque_data (for stateful catalogs)
400
+ 2. Auto-attach: Attach to catalog on-the-fly (for stateless catalogs)
401
+
402
+ Args:
403
+ client: VGI Client instance
404
+ attach_opaque_data: Hex-encoded attach ID (from --attach-opaque-data option)
405
+ catalog: Catalog name (from --catalog option)
406
+ attach_options: Options for catalog attach (from --attach-options option)
407
+
408
+ Returns:
409
+ Tuple of (attach_opaque_data, is_stateful) where is_stateful indicates if
410
+ a warning should be shown for stateful catalogs using auto-attach.
411
+
412
+ Raises:
413
+ click.ClickException: If neither attach_opaque_data nor catalog is provided,
414
+ or if both are provided.
415
+
416
+ """
417
+ if attach_opaque_data and catalog:
418
+ raise click.ClickException(
419
+ "Cannot specify both --attach-opaque-data and --catalog. "
420
+ "Use --attach-opaque-data for stateful catalogs or --catalog for auto-attach."
421
+ )
422
+
423
+ if not attach_opaque_data and not catalog:
424
+ raise click.ClickException(
425
+ "Must specify either --attach-opaque-data or --catalog. "
426
+ "Use --attach-opaque-data with a previously attached catalog, "
427
+ "or --catalog to auto-attach."
428
+ )
429
+
430
+ if attach_opaque_data:
431
+ return hex_to_attach_opaque_data(attach_opaque_data), False
432
+
433
+ # Auto-attach via --catalog
434
+ assert catalog is not None
435
+ options = attach_options or {}
436
+ result = client.catalog_attach(name=catalog, options=options, data_version_spec=None, implementation_version=None)
437
+
438
+ # Return the attach_opaque_data and whether this is a stateful catalog
439
+ # (is_stateful=True means caller should warn about using --catalog
440
+ # with a stateful catalog)
441
+ return result.attach_opaque_data, result.attach_opaque_data_required