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,907 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Table CLI commands for VGI.
4
+
5
+ This module provides CLI commands for table operations:
6
+ - get: Get table info
7
+ - create: Create a new table
8
+ - drop: Drop a table
9
+ - rename: Rename a table
10
+ - comment: Set or clear table comment
11
+ - scan-function: Get scan function for a table
12
+ - column: Column subcommands (add, drop, rename, etc.)
13
+
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import click
19
+
20
+ from vgi.catalog import OnConflict, SerializedSchema, SqlExpression
21
+ from vgi.client.cli_utils import (
22
+ get_attach_opaque_data_from_options,
23
+ hex_to_transaction_opaque_data,
24
+ json_to_arrow_schema,
25
+ output_json,
26
+ parse_json_option,
27
+ scan_function_result_to_dict,
28
+ table_info_to_dict,
29
+ )
30
+ from vgi.client.client import Client
31
+
32
+
33
+ @click.group()
34
+ def table() -> None:
35
+ """Manage tables in a schema."""
36
+
37
+
38
+ @table.command("get")
39
+ @click.argument("schema_name")
40
+ @click.argument("name")
41
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
42
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
43
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
44
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
45
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex) for transactional read")
46
+ def table_get(
47
+ schema_name: str,
48
+ name: str,
49
+ attach_opaque_data: str | None,
50
+ catalog_name: str | None,
51
+ attach_options: str,
52
+ worker: str,
53
+ transaction_opaque_data: str | None,
54
+ ) -> None:
55
+ """Get information about a table.
56
+
57
+ SCHEMA_NAME is the schema containing the table.
58
+ NAME is the table name.
59
+
60
+ """
61
+ client = Client(worker)
62
+ opts = parse_json_option(attach_options, "--attach-options")
63
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
64
+ client, attach_opaque_data, catalog_name, opts
65
+ )
66
+ if is_stateful and catalog_name:
67
+ click.echo(
68
+ "Warning: Using --catalog with a stateful catalog. "
69
+ "Consider using --attach-opaque-data for session persistence.",
70
+ err=True,
71
+ )
72
+ table_info = client.table_get(
73
+ attach_opaque_data=resolved_attach_opaque_data,
74
+ transaction_opaque_data=(
75
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
76
+ ),
77
+ schema_name=schema_name,
78
+ name=name,
79
+ )
80
+ if table_info:
81
+ output_json(table_info_to_dict(table_info))
82
+ else:
83
+ output_json({"error": "not_found", "schema": schema_name, "name": name})
84
+
85
+
86
+ @table.command("create")
87
+ @click.argument("schema_name")
88
+ @click.argument("name")
89
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
90
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
91
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
92
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
93
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
94
+ @click.option(
95
+ "--columns",
96
+ required=True,
97
+ help='Column definitions as JSON array: [{"name":"id","type":"int64"}]',
98
+ )
99
+ @click.option(
100
+ "--on-conflict",
101
+ type=click.Choice(["error", "ignore", "replace"]),
102
+ default="error",
103
+ help="Behavior if table already exists",
104
+ )
105
+ @click.option(
106
+ "--not-null",
107
+ multiple=True,
108
+ type=int,
109
+ help="Column index with NOT NULL constraint (can repeat)",
110
+ )
111
+ @click.option(
112
+ "--unique",
113
+ multiple=True,
114
+ help="Column indices for unique constraint as comma-separated list (can repeat)",
115
+ )
116
+ @click.option("--check", multiple=True, help="SQL check constraint (can repeat)")
117
+ def table_create(
118
+ schema_name: str,
119
+ name: str,
120
+ attach_opaque_data: str | None,
121
+ catalog_name: str | None,
122
+ attach_options: str,
123
+ worker: str,
124
+ transaction_opaque_data: str | None,
125
+ columns: str,
126
+ on_conflict: str,
127
+ not_null: tuple[int, ...],
128
+ unique: tuple[str, ...],
129
+ check: tuple[str, ...],
130
+ ) -> None:
131
+ """Create a new table.
132
+
133
+ SCHEMA_NAME is the schema to create the table in.
134
+ NAME is the name for the new table.
135
+
136
+ """
137
+ client = Client(worker)
138
+ opts = parse_json_option(attach_options, "--attach-options")
139
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
140
+ client, attach_opaque_data, catalog_name, opts
141
+ )
142
+ if is_stateful and catalog_name:
143
+ click.echo(
144
+ "Warning: Using --catalog with a stateful catalog. "
145
+ "Consider using --attach-opaque-data for session persistence.",
146
+ err=True,
147
+ )
148
+
149
+ columns_json = parse_json_option(columns, "--columns")
150
+ arrow_schema = json_to_arrow_schema(columns_json)
151
+
152
+ # Parse unique constraints: each is a comma-separated list of column indices
153
+ unique_constraints = []
154
+ for u in unique:
155
+ indices = [int(i.strip()) for i in u.split(",")]
156
+ unique_constraints.append(indices)
157
+
158
+ client.table_create(
159
+ attach_opaque_data=resolved_attach_opaque_data,
160
+ transaction_opaque_data=(
161
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
162
+ ),
163
+ schema_name=schema_name,
164
+ name=name,
165
+ columns=SerializedSchema(arrow_schema.serialize().to_pybytes()),
166
+ on_conflict=OnConflict(on_conflict),
167
+ not_null_constraints=list(not_null),
168
+ unique_constraints=unique_constraints,
169
+ check_constraints=list(check),
170
+ )
171
+ output_json({"status": "created", "schema": schema_name, "name": name})
172
+
173
+
174
+ @table.command("drop")
175
+ @click.argument("schema_name")
176
+ @click.argument("name")
177
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
178
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
179
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
180
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
181
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
182
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if not found")
183
+ def table_drop(
184
+ schema_name: str,
185
+ name: str,
186
+ attach_opaque_data: str | None,
187
+ catalog_name: str | None,
188
+ attach_options: str,
189
+ worker: str,
190
+ transaction_opaque_data: str | None,
191
+ ignore_not_found: bool,
192
+ ) -> None:
193
+ """Drop a table.
194
+
195
+ SCHEMA_NAME is the schema containing the table.
196
+ NAME is the table name to drop.
197
+
198
+ """
199
+ client = Client(worker)
200
+ opts = parse_json_option(attach_options, "--attach-options")
201
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
202
+ client, attach_opaque_data, catalog_name, opts
203
+ )
204
+ if is_stateful and catalog_name:
205
+ click.echo(
206
+ "Warning: Using --catalog with a stateful catalog. "
207
+ "Consider using --attach-opaque-data for session persistence.",
208
+ err=True,
209
+ )
210
+ client.table_drop(
211
+ attach_opaque_data=resolved_attach_opaque_data,
212
+ transaction_opaque_data=(
213
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
214
+ ),
215
+ schema_name=schema_name,
216
+ name=name,
217
+ ignore_not_found=ignore_not_found,
218
+ )
219
+ output_json({"status": "dropped", "schema": schema_name, "name": name})
220
+
221
+
222
+ @table.command("rename")
223
+ @click.argument("schema_name")
224
+ @click.argument("name")
225
+ @click.argument("new_name")
226
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
227
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
228
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
229
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
230
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
231
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if not found")
232
+ def table_rename(
233
+ schema_name: str,
234
+ name: str,
235
+ new_name: str,
236
+ attach_opaque_data: str | None,
237
+ catalog_name: str | None,
238
+ attach_options: str,
239
+ worker: str,
240
+ transaction_opaque_data: str | None,
241
+ ignore_not_found: bool,
242
+ ) -> None:
243
+ """Rename a table.
244
+
245
+ SCHEMA_NAME is the schema containing the table.
246
+ NAME is the current table name.
247
+ NEW_NAME is the new name for the table.
248
+
249
+ """
250
+ client = Client(worker)
251
+ opts = parse_json_option(attach_options, "--attach-options")
252
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
253
+ client, attach_opaque_data, catalog_name, opts
254
+ )
255
+ if is_stateful and catalog_name:
256
+ click.echo(
257
+ "Warning: Using --catalog with a stateful catalog. "
258
+ "Consider using --attach-opaque-data for session persistence.",
259
+ err=True,
260
+ )
261
+ client.table_rename(
262
+ attach_opaque_data=resolved_attach_opaque_data,
263
+ transaction_opaque_data=(
264
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
265
+ ),
266
+ schema_name=schema_name,
267
+ name=name,
268
+ new_name=new_name,
269
+ ignore_not_found=ignore_not_found,
270
+ )
271
+ output_json(
272
+ {
273
+ "status": "renamed",
274
+ "schema": schema_name,
275
+ "old": name,
276
+ "new": new_name,
277
+ }
278
+ )
279
+
280
+
281
+ @table.command("comment")
282
+ @click.argument("schema_name")
283
+ @click.argument("name")
284
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
285
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
286
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
287
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
288
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
289
+ @click.option("--set", "comment_text", help="Set comment to this text")
290
+ @click.option("--clear", is_flag=True, help="Clear the comment")
291
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if not found")
292
+ def table_comment(
293
+ schema_name: str,
294
+ name: str,
295
+ attach_opaque_data: str | None,
296
+ catalog_name: str | None,
297
+ attach_options: str,
298
+ worker: str,
299
+ transaction_opaque_data: str | None,
300
+ comment_text: str | None,
301
+ clear: bool,
302
+ ignore_not_found: bool,
303
+ ) -> None:
304
+ """Set or clear a table's comment.
305
+
306
+ SCHEMA_NAME is the schema containing the table.
307
+ NAME is the table name.
308
+
309
+ Use --set to set the comment, or --clear to remove it.
310
+
311
+ """
312
+ if not comment_text and not clear:
313
+ raise click.ClickException("Must specify --set or --clear")
314
+ if comment_text and clear:
315
+ raise click.ClickException("Cannot specify both --set and --clear")
316
+
317
+ client = Client(worker)
318
+ opts = parse_json_option(attach_options, "--attach-options")
319
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
320
+ client, attach_opaque_data, catalog_name, opts
321
+ )
322
+ if is_stateful and catalog_name:
323
+ click.echo(
324
+ "Warning: Using --catalog with a stateful catalog. "
325
+ "Consider using --attach-opaque-data for session persistence.",
326
+ err=True,
327
+ )
328
+ client.table_comment_set(
329
+ attach_opaque_data=resolved_attach_opaque_data,
330
+ transaction_opaque_data=(
331
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
332
+ ),
333
+ schema_name=schema_name,
334
+ name=name,
335
+ comment=None if clear else comment_text,
336
+ ignore_not_found=ignore_not_found,
337
+ )
338
+ action = "cleared" if clear else "set"
339
+ output_json({"status": f"comment_{action}", "schema": schema_name, "name": name})
340
+
341
+
342
+ @table.command("scan-function")
343
+ @click.argument("schema_name")
344
+ @click.argument("name")
345
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
346
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
347
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
348
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
349
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex) for transactional read")
350
+ @click.option("--at-unit", help="Time travel unit (e.g., 'timestamp', 'version')")
351
+ @click.option("--at-value", help="Time travel value")
352
+ def table_scan_function(
353
+ schema_name: str,
354
+ name: str,
355
+ attach_opaque_data: str | None,
356
+ catalog_name: str | None,
357
+ attach_options: str,
358
+ worker: str,
359
+ transaction_opaque_data: str | None,
360
+ at_unit: str | None,
361
+ at_value: str | None,
362
+ ) -> None:
363
+ """Get the scan function for a table.
364
+
365
+ SCHEMA_NAME is the schema containing the table.
366
+ NAME is the table name.
367
+
368
+ """
369
+ client = Client(worker)
370
+ opts = parse_json_option(attach_options, "--attach-options")
371
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
372
+ client, attach_opaque_data, catalog_name, opts
373
+ )
374
+ if is_stateful and catalog_name:
375
+ click.echo(
376
+ "Warning: Using --catalog with a stateful catalog. "
377
+ "Consider using --attach-opaque-data for session persistence.",
378
+ err=True,
379
+ )
380
+ result = client.table_scan_function_get(
381
+ attach_opaque_data=resolved_attach_opaque_data,
382
+ transaction_opaque_data=(
383
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
384
+ ),
385
+ schema_name=schema_name,
386
+ name=name,
387
+ at_unit=at_unit,
388
+ at_value=at_value,
389
+ )
390
+ output_json(scan_function_result_to_dict(result))
391
+
392
+
393
+ # Column subcommands
394
+ @table.group("column")
395
+ def column() -> None:
396
+ """Manage table columns."""
397
+
398
+
399
+ @column.command("add")
400
+ @click.argument("schema_name")
401
+ @click.argument("table_name")
402
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
403
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
404
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
405
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
406
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
407
+ @click.option(
408
+ "--column",
409
+ "column_def",
410
+ required=True,
411
+ help='Column definition as JSON: {"name":"col","type":"int64"}',
412
+ )
413
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
414
+ @click.option("--if-not-exists", is_flag=True, help="Don't error if column already exists")
415
+ def column_add(
416
+ schema_name: str,
417
+ table_name: str,
418
+ attach_opaque_data: str | None,
419
+ catalog_name: str | None,
420
+ attach_options: str,
421
+ worker: str,
422
+ transaction_opaque_data: str | None,
423
+ column_def: str,
424
+ ignore_not_found: bool,
425
+ if_not_exists: bool,
426
+ ) -> None:
427
+ """Add a column to a table.
428
+
429
+ SCHEMA_NAME is the schema containing the table.
430
+ TABLE_NAME is the table to add the column to.
431
+
432
+ """
433
+ client = Client(worker)
434
+ opts = parse_json_option(attach_options, "--attach-options")
435
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
436
+ client, attach_opaque_data, catalog_name, opts
437
+ )
438
+ if is_stateful and catalog_name:
439
+ click.echo(
440
+ "Warning: Using --catalog with a stateful catalog. "
441
+ "Consider using --attach-opaque-data for session persistence.",
442
+ err=True,
443
+ )
444
+
445
+ col_json = parse_json_option(column_def, "--column")
446
+ arrow_schema = json_to_arrow_schema([col_json])
447
+
448
+ client.table_column_add(
449
+ attach_opaque_data=resolved_attach_opaque_data,
450
+ transaction_opaque_data=(
451
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
452
+ ),
453
+ schema_name=schema_name,
454
+ name=table_name,
455
+ column_definition=SerializedSchema(arrow_schema.serialize().to_pybytes()),
456
+ ignore_not_found=ignore_not_found,
457
+ if_column_not_exists=if_not_exists,
458
+ )
459
+ output_json(
460
+ {
461
+ "status": "column_added",
462
+ "schema": schema_name,
463
+ "table": table_name,
464
+ "column": col_json["name"],
465
+ }
466
+ )
467
+
468
+
469
+ @column.command("drop")
470
+ @click.argument("schema_name")
471
+ @click.argument("table_name")
472
+ @click.argument("column_name")
473
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
474
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
475
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
476
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
477
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
478
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
479
+ @click.option("--if-exists", is_flag=True, help="Don't error if column doesn't exist")
480
+ @click.option("--cascade", is_flag=True, help="Drop dependent constraints")
481
+ def column_drop(
482
+ schema_name: str,
483
+ table_name: str,
484
+ column_name: str,
485
+ attach_opaque_data: str | None,
486
+ catalog_name: str | None,
487
+ attach_options: str,
488
+ worker: str,
489
+ transaction_opaque_data: str | None,
490
+ ignore_not_found: bool,
491
+ if_exists: bool,
492
+ cascade: bool,
493
+ ) -> None:
494
+ """Drop a column from a table.
495
+
496
+ SCHEMA_NAME is the schema containing the table.
497
+ TABLE_NAME is the table to drop the column from.
498
+ COLUMN_NAME is the column to drop.
499
+
500
+ """
501
+ client = Client(worker)
502
+ opts = parse_json_option(attach_options, "--attach-options")
503
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
504
+ client, attach_opaque_data, catalog_name, opts
505
+ )
506
+ if is_stateful and catalog_name:
507
+ click.echo(
508
+ "Warning: Using --catalog with a stateful catalog. "
509
+ "Consider using --attach-opaque-data for session persistence.",
510
+ err=True,
511
+ )
512
+ client.table_column_drop(
513
+ attach_opaque_data=resolved_attach_opaque_data,
514
+ transaction_opaque_data=(
515
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
516
+ ),
517
+ schema_name=schema_name,
518
+ name=table_name,
519
+ column_name=column_name,
520
+ ignore_not_found=ignore_not_found,
521
+ if_column_exists=if_exists,
522
+ cascade=cascade,
523
+ )
524
+ output_json(
525
+ {
526
+ "status": "column_dropped",
527
+ "schema": schema_name,
528
+ "table": table_name,
529
+ "column": column_name,
530
+ }
531
+ )
532
+
533
+
534
+ @column.command("rename")
535
+ @click.argument("schema_name")
536
+ @click.argument("table_name")
537
+ @click.argument("column_name")
538
+ @click.argument("new_column_name")
539
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
540
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
541
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
542
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
543
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
544
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
545
+ def column_rename(
546
+ schema_name: str,
547
+ table_name: str,
548
+ column_name: str,
549
+ new_column_name: str,
550
+ attach_opaque_data: str | None,
551
+ catalog_name: str | None,
552
+ attach_options: str,
553
+ worker: str,
554
+ transaction_opaque_data: str | None,
555
+ ignore_not_found: bool,
556
+ ) -> None:
557
+ """Rename a column.
558
+
559
+ SCHEMA_NAME is the schema containing the table.
560
+ TABLE_NAME is the table containing the column.
561
+ COLUMN_NAME is the current column name.
562
+ NEW_COLUMN_NAME is the new name for the column.
563
+
564
+ """
565
+ client = Client(worker)
566
+ opts = parse_json_option(attach_options, "--attach-options")
567
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
568
+ client, attach_opaque_data, catalog_name, opts
569
+ )
570
+ if is_stateful and catalog_name:
571
+ click.echo(
572
+ "Warning: Using --catalog with a stateful catalog. "
573
+ "Consider using --attach-opaque-data for session persistence.",
574
+ err=True,
575
+ )
576
+ client.table_column_rename(
577
+ attach_opaque_data=resolved_attach_opaque_data,
578
+ transaction_opaque_data=(
579
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
580
+ ),
581
+ schema_name=schema_name,
582
+ name=table_name,
583
+ column_name=column_name,
584
+ new_column_name=new_column_name,
585
+ ignore_not_found=ignore_not_found,
586
+ )
587
+ output_json(
588
+ {
589
+ "status": "column_renamed",
590
+ "schema": schema_name,
591
+ "table": table_name,
592
+ "old_column": column_name,
593
+ "new_column": new_column_name,
594
+ }
595
+ )
596
+
597
+
598
+ @column.command("set-default")
599
+ @click.argument("schema_name")
600
+ @click.argument("table_name")
601
+ @click.argument("column_name")
602
+ @click.argument("expression")
603
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
604
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
605
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
606
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
607
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
608
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
609
+ def column_set_default(
610
+ schema_name: str,
611
+ table_name: str,
612
+ column_name: str,
613
+ expression: str,
614
+ attach_opaque_data: str | None,
615
+ catalog_name: str | None,
616
+ attach_options: str,
617
+ worker: str,
618
+ transaction_opaque_data: str | None,
619
+ ignore_not_found: bool,
620
+ ) -> None:
621
+ """Set the default value for a column.
622
+
623
+ SCHEMA_NAME is the schema containing the table.
624
+ TABLE_NAME is the table containing the column.
625
+ COLUMN_NAME is the column to set the default for.
626
+ EXPRESSION is the SQL expression for the default value.
627
+
628
+ """
629
+ client = Client(worker)
630
+ opts = parse_json_option(attach_options, "--attach-options")
631
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
632
+ client, attach_opaque_data, catalog_name, opts
633
+ )
634
+ if is_stateful and catalog_name:
635
+ click.echo(
636
+ "Warning: Using --catalog with a stateful catalog. "
637
+ "Consider using --attach-opaque-data for session persistence.",
638
+ err=True,
639
+ )
640
+ client.table_column_default_set(
641
+ attach_opaque_data=resolved_attach_opaque_data,
642
+ transaction_opaque_data=(
643
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
644
+ ),
645
+ schema_name=schema_name,
646
+ name=table_name,
647
+ column_name=column_name,
648
+ expression=SqlExpression(expression),
649
+ ignore_not_found=ignore_not_found,
650
+ )
651
+ output_json(
652
+ {
653
+ "status": "default_set",
654
+ "schema": schema_name,
655
+ "table": table_name,
656
+ "column": column_name,
657
+ }
658
+ )
659
+
660
+
661
+ @column.command("drop-default")
662
+ @click.argument("schema_name")
663
+ @click.argument("table_name")
664
+ @click.argument("column_name")
665
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
666
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
667
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
668
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
669
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
670
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
671
+ def column_drop_default(
672
+ schema_name: str,
673
+ table_name: str,
674
+ column_name: str,
675
+ attach_opaque_data: str | None,
676
+ catalog_name: str | None,
677
+ attach_options: str,
678
+ worker: str,
679
+ transaction_opaque_data: str | None,
680
+ ignore_not_found: bool,
681
+ ) -> None:
682
+ """Remove the default value from a column.
683
+
684
+ SCHEMA_NAME is the schema containing the table.
685
+ TABLE_NAME is the table containing the column.
686
+ COLUMN_NAME is the column to remove the default from.
687
+
688
+ """
689
+ client = Client(worker)
690
+ opts = parse_json_option(attach_options, "--attach-options")
691
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
692
+ client, attach_opaque_data, catalog_name, opts
693
+ )
694
+ if is_stateful and catalog_name:
695
+ click.echo(
696
+ "Warning: Using --catalog with a stateful catalog. "
697
+ "Consider using --attach-opaque-data for session persistence.",
698
+ err=True,
699
+ )
700
+ client.table_column_default_drop(
701
+ attach_opaque_data=resolved_attach_opaque_data,
702
+ transaction_opaque_data=(
703
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
704
+ ),
705
+ schema_name=schema_name,
706
+ name=table_name,
707
+ column_name=column_name,
708
+ ignore_not_found=ignore_not_found,
709
+ )
710
+ output_json(
711
+ {
712
+ "status": "default_dropped",
713
+ "schema": schema_name,
714
+ "table": table_name,
715
+ "column": column_name,
716
+ }
717
+ )
718
+
719
+
720
+ @column.command("set-type")
721
+ @click.argument("schema_name")
722
+ @click.argument("table_name")
723
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
724
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
725
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
726
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
727
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
728
+ @click.option(
729
+ "--column",
730
+ "column_def",
731
+ required=True,
732
+ help='Column definition as JSON: {"name":"col","type":"string"}',
733
+ )
734
+ @click.option("--using", "expression", help="SQL expression to convert values")
735
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
736
+ def column_set_type(
737
+ schema_name: str,
738
+ table_name: str,
739
+ attach_opaque_data: str | None,
740
+ catalog_name: str | None,
741
+ attach_options: str,
742
+ worker: str,
743
+ transaction_opaque_data: str | None,
744
+ column_def: str,
745
+ expression: str | None,
746
+ ignore_not_found: bool,
747
+ ) -> None:
748
+ """Change the type of a column.
749
+
750
+ SCHEMA_NAME is the schema containing the table.
751
+ TABLE_NAME is the table containing the column.
752
+
753
+ The --column option specifies the column name and new type.
754
+
755
+ """
756
+ client = Client(worker)
757
+ opts = parse_json_option(attach_options, "--attach-options")
758
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
759
+ client, attach_opaque_data, catalog_name, opts
760
+ )
761
+ if is_stateful and catalog_name:
762
+ click.echo(
763
+ "Warning: Using --catalog with a stateful catalog. "
764
+ "Consider using --attach-opaque-data for session persistence.",
765
+ err=True,
766
+ )
767
+
768
+ col_json = parse_json_option(column_def, "--column")
769
+ arrow_schema = json_to_arrow_schema([col_json])
770
+
771
+ client.table_column_type_change(
772
+ attach_opaque_data=resolved_attach_opaque_data,
773
+ transaction_opaque_data=(
774
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
775
+ ),
776
+ schema_name=schema_name,
777
+ name=table_name,
778
+ column_definition=SerializedSchema(arrow_schema.serialize().to_pybytes()),
779
+ expression=SqlExpression(expression) if expression else None,
780
+ ignore_not_found=ignore_not_found,
781
+ )
782
+ output_json(
783
+ {
784
+ "status": "type_changed",
785
+ "schema": schema_name,
786
+ "table": table_name,
787
+ "column": col_json["name"],
788
+ }
789
+ )
790
+
791
+
792
+ @column.command("set-not-null")
793
+ @click.argument("schema_name")
794
+ @click.argument("table_name")
795
+ @click.argument("column_name")
796
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
797
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
798
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
799
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
800
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
801
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
802
+ def column_set_not_null(
803
+ schema_name: str,
804
+ table_name: str,
805
+ column_name: str,
806
+ attach_opaque_data: str | None,
807
+ catalog_name: str | None,
808
+ attach_options: str,
809
+ worker: str,
810
+ transaction_opaque_data: str | None,
811
+ ignore_not_found: bool,
812
+ ) -> None:
813
+ """Add NOT NULL constraint to a column.
814
+
815
+ SCHEMA_NAME is the schema containing the table.
816
+ TABLE_NAME is the table containing the column.
817
+ COLUMN_NAME is the column to add NOT NULL to.
818
+
819
+ """
820
+ client = Client(worker)
821
+ opts = parse_json_option(attach_options, "--attach-options")
822
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
823
+ client, attach_opaque_data, catalog_name, opts
824
+ )
825
+ if is_stateful and catalog_name:
826
+ click.echo(
827
+ "Warning: Using --catalog with a stateful catalog. "
828
+ "Consider using --attach-opaque-data for session persistence.",
829
+ err=True,
830
+ )
831
+ client.table_not_null_set(
832
+ attach_opaque_data=resolved_attach_opaque_data,
833
+ transaction_opaque_data=(
834
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
835
+ ),
836
+ schema_name=schema_name,
837
+ name=table_name,
838
+ column_name=column_name,
839
+ ignore_not_found=ignore_not_found,
840
+ )
841
+ output_json(
842
+ {
843
+ "status": "not_null_set",
844
+ "schema": schema_name,
845
+ "table": table_name,
846
+ "column": column_name,
847
+ }
848
+ )
849
+
850
+
851
+ @column.command("drop-not-null")
852
+ @click.argument("schema_name")
853
+ @click.argument("table_name")
854
+ @click.argument("column_name")
855
+ @click.option("--attach-opaque-data", help="Hex-encoded attach ID")
856
+ @click.option("--catalog", "catalog_name", help="Catalog name for auto-attach")
857
+ @click.option("--attach-options", default="{}", help="Attach options as JSON")
858
+ @click.option("--worker", "-w", required=True, help="VGI worker command")
859
+ @click.option("--transaction-opaque-data", help="Transaction ID (hex)")
860
+ @click.option("--ignore-not-found", is_flag=True, help="Don't error if table not found")
861
+ def column_drop_not_null(
862
+ schema_name: str,
863
+ table_name: str,
864
+ column_name: str,
865
+ attach_opaque_data: str | None,
866
+ catalog_name: str | None,
867
+ attach_options: str,
868
+ worker: str,
869
+ transaction_opaque_data: str | None,
870
+ ignore_not_found: bool,
871
+ ) -> None:
872
+ """Remove NOT NULL constraint from a column.
873
+
874
+ SCHEMA_NAME is the schema containing the table.
875
+ TABLE_NAME is the table containing the column.
876
+ COLUMN_NAME is the column to remove NOT NULL from.
877
+
878
+ """
879
+ client = Client(worker)
880
+ opts = parse_json_option(attach_options, "--attach-options")
881
+ resolved_attach_opaque_data, is_stateful = get_attach_opaque_data_from_options(
882
+ client, attach_opaque_data, catalog_name, opts
883
+ )
884
+ if is_stateful and catalog_name:
885
+ click.echo(
886
+ "Warning: Using --catalog with a stateful catalog. "
887
+ "Consider using --attach-opaque-data for session persistence.",
888
+ err=True,
889
+ )
890
+ client.table_not_null_drop(
891
+ attach_opaque_data=resolved_attach_opaque_data,
892
+ transaction_opaque_data=(
893
+ hex_to_transaction_opaque_data(transaction_opaque_data) if transaction_opaque_data else None
894
+ ),
895
+ schema_name=schema_name,
896
+ name=table_name,
897
+ column_name=column_name,
898
+ ignore_not_found=ignore_not_found,
899
+ )
900
+ output_json(
901
+ {
902
+ "status": "not_null_dropped",
903
+ "schema": schema_name,
904
+ "table": table_name,
905
+ "column": column_name,
906
+ }
907
+ )