Skip to content

Commit 42e5e50

Browse files
rustyconoverclaude
andcommitted
Add catalog CLI commands to vgi-client
Expose catalog API via CLI subcommands: - catalog list/attach/detach/create/drop/version - schema list/get/create/drop/contents - table get/create/drop/rename/comment/scan-function + column ops - view get/create/drop/rename/comment - transaction begin/commit/rollback Refactored cli.py from @click.command to @click.group with invoke_without_command=True to preserve backward-compatible function invocation (--function) while adding subcommands. Added transaction methods and TransactionBeginResult to CatalogClientMixin for transaction support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 79c7e71 commit 42e5e50

9 files changed

Lines changed: 1811 additions & 9 deletions

File tree

tests/client/test_cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,13 @@ def test_invalid_attach_id_hex(self, example_worker: str) -> None:
259259
assert result.exit_code != 0
260260
assert "valid hex string" in result.output
261261

262-
def test_missing_required_function(self) -> None:
263-
"""--function is required."""
262+
def test_missing_function_shows_help(self) -> None:
263+
"""Calling CLI with no arguments shows help (group behavior)."""
264264
runner = CliRunner()
265265
result = runner.invoke(cli, [])
266-
assert result.exit_code != 0
266+
# With Click group, no subcommand and no --function shows help
267+
assert result.exit_code == 0
268+
assert "Usage:" in result.output
267269

268270

269271
class TestCLITableFunction:

vgi/client/catalog_mixin.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class CatalogEnabledClient(CatalogClientMixin, Client):
1818
import io
1919
import subprocess
2020
from collections.abc import Iterator
21+
from dataclasses import dataclass
2122
from typing import TYPE_CHECKING, Any, cast
2223

2324
import pyarrow as pa
@@ -43,6 +44,21 @@ class CatalogEnabledClient(CatalogClientMixin, Client):
4344
import structlog.stdlib
4445

4546

47+
@dataclass
48+
class TransactionBeginResult:
49+
"""Result of beginning a transaction."""
50+
51+
transaction_id: TransactionId
52+
53+
@staticmethod
54+
def deserialize(batch: pa.RecordBatch) -> "TransactionBeginResult":
55+
"""Deserialize from an Arrow record batch."""
56+
row = batch.to_pydict()
57+
return TransactionBeginResult(
58+
transaction_id=TransactionId(bytes(row["transaction_id"][0])),
59+
)
60+
61+
4662
class CatalogClientError(Exception):
4763
"""Error raised by catalog operations."""
4864

@@ -1168,3 +1184,67 @@ def view_comment_set(
11681184
comment=comment,
11691185
ignore_not_found=ignore_not_found,
11701186
)
1187+
1188+
# =========================================================================
1189+
# Transaction Methods
1190+
# =========================================================================
1191+
1192+
def transaction_begin(
1193+
self,
1194+
*,
1195+
attach_id: AttachId,
1196+
) -> TransactionBeginResult:
1197+
"""Begin a new transaction.
1198+
1199+
Args:
1200+
attach_id: The attachment ID from catalog_attach.
1201+
1202+
Returns:
1203+
TransactionBeginResult containing the transaction_id.
1204+
1205+
"""
1206+
result = self._catalog_invoke(
1207+
"catalog_transaction_begin",
1208+
attach_id=attach_id,
1209+
)
1210+
if result is None:
1211+
raise CatalogClientError("transaction_begin returned no result")
1212+
return TransactionBeginResult.deserialize(result)
1213+
1214+
def transaction_commit(
1215+
self,
1216+
*,
1217+
attach_id: AttachId,
1218+
transaction_id: TransactionId,
1219+
) -> None:
1220+
"""Commit a transaction.
1221+
1222+
Args:
1223+
attach_id: The attachment ID from catalog_attach.
1224+
transaction_id: The transaction ID from transaction_begin.
1225+
1226+
"""
1227+
self._catalog_invoke(
1228+
"catalog_transaction_commit",
1229+
attach_id=attach_id,
1230+
transaction_id=transaction_id,
1231+
)
1232+
1233+
def transaction_rollback(
1234+
self,
1235+
*,
1236+
attach_id: AttachId,
1237+
transaction_id: TransactionId,
1238+
) -> None:
1239+
"""Rollback a transaction.
1240+
1241+
Args:
1242+
attach_id: The attachment ID from catalog_attach.
1243+
transaction_id: The transaction ID from transaction_begin.
1244+
1245+
"""
1246+
self._catalog_invoke(
1247+
"catalog_transaction_rollback",
1248+
attach_id=attach_id,
1249+
transaction_id=transaction_id,
1250+
)

vgi/client/cli.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
r"""Command-line interface for the VGI client.
22
3-
This module provides the CLI entry point for invoking VGI functions.
3+
This module provides the CLI entry point for invoking VGI functions and
4+
managing catalogs.
45
56
Usage:
67
# Table-in-out functions (with input):
@@ -20,6 +21,12 @@
2021
vgi-client --input data.parquet --function transform --args '["prefix"]' \
2122
--table-input-position 1
2223
24+
# Catalog operations:
25+
vgi-client catalog list --server vgi-example-catalog-worker
26+
vgi-client catalog attach memory --server vgi-example-catalog-worker
27+
vgi-client schema list $ATTACH_ID --server vgi-example-catalog-worker
28+
vgi-client table get $ATTACH_ID main users --server vgi-example-catalog-worker
29+
2330
"""
2431

2532
import io
@@ -121,11 +128,17 @@ def close(self) -> None:
121128

122129

123130
def _create_cli() -> Any:
124-
"""Create the CLI command. Separated for testability."""
131+
"""Create the CLI command group. Separated for testability."""
125132
import click
126133
import pyarrow.parquet as pq
127134

128-
@click.command()
135+
from vgi.client.cli_catalog import catalog
136+
from vgi.client.cli_schema import schema
137+
from vgi.client.cli_table import table
138+
from vgi.client.cli_transaction import transaction
139+
from vgi.client.cli_view import view
140+
141+
@click.group(invoke_without_command=True)
129142
@click.option(
130143
"--input",
131144
"input_file",
@@ -150,7 +163,7 @@ def _create_cli() -> Any:
150163
@click.option(
151164
"--function",
152165
"function_name",
153-
required=True,
166+
required=False,
154167
type=str,
155168
help="Name of the function to run (e.g., echo, sum_all_columns, repeat_inputs)",
156169
)
@@ -221,11 +234,13 @@ def _create_cli() -> Any:
221234
"otherwise table. Use 'scalar' for scalar functions."
222235
),
223236
)
237+
@click.pass_context
224238
def cli(
239+
ctx: click.Context,
225240
input_file: str | None,
226241
output_file: str | None,
227242
output_format: str,
228-
function_name: str,
243+
function_name: str | None,
229244
arguments: str,
230245
server_path: str,
231246
worker_stderr: bool,
@@ -235,7 +250,21 @@ def cli(
235250
attach_id: str | None,
236251
function_type: str,
237252
) -> None:
238-
"""Invoke a VGI function and display results."""
253+
"""VGI client for function invocation and catalog management.
254+
255+
When called without a subcommand and with --function, invokes a VGI function.
256+
Use subcommands (catalog, schema, table, view, transaction) for catalog ops.
257+
258+
"""
259+
# If a subcommand is being invoked, skip function invocation
260+
if ctx.invoked_subcommand is not None:
261+
return
262+
263+
# Legacy function invocation mode - requires --function
264+
if function_name is None:
265+
click.echo(ctx.get_help())
266+
return
267+
239268
try:
240269
args_list = json.loads(arguments)
241270
if not isinstance(args_list, list):
@@ -361,6 +390,13 @@ def cli(
361390
if output_writer is not None:
362391
output_writer.close()
363392

393+
# Add catalog subcommand groups
394+
cli.add_command(catalog)
395+
cli.add_command(schema)
396+
cli.add_command(table)
397+
cli.add_command(view)
398+
cli.add_command(transaction)
399+
364400
return cli
365401

366402

vgi/client/cli_catalog.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""Catalog CLI commands for VGI.
2+
3+
This module provides CLI commands for catalog operations:
4+
- list: List available catalogs
5+
- attach: Attach to a catalog
6+
- detach: Detach from a catalog
7+
- create: Create a new catalog
8+
- drop: Drop a catalog
9+
- version: Get catalog version
10+
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import click
16+
17+
from vgi.catalog import OnConflict
18+
from vgi.client.client import Client
19+
from vgi.client.cli_utils import (
20+
catalog_attach_result_to_dict,
21+
hex_to_attach_id,
22+
hex_to_transaction_id,
23+
output_json,
24+
parse_json_option,
25+
)
26+
27+
28+
@click.group()
29+
def catalog() -> None:
30+
"""Manage catalogs."""
31+
32+
33+
@catalog.command("list")
34+
@click.option("--server", required=True, help="VGI worker command")
35+
def catalog_list(server: str) -> None:
36+
"""List available catalogs."""
37+
client = Client(server)
38+
catalogs = client.catalogs()
39+
output_json(catalogs)
40+
41+
42+
@catalog.command("attach")
43+
@click.argument("name")
44+
@click.option("--server", required=True, help="VGI worker command")
45+
@click.option("--options", default="{}", help="Catalog options as JSON object")
46+
def catalog_attach(name: str, server: str, options: str) -> None:
47+
"""Attach to a catalog and return attach_id.
48+
49+
NAME is the catalog name to attach to.
50+
51+
"""
52+
opts = parse_json_option(options, "--options")
53+
client = Client(server)
54+
result = client.catalog_attach(name=name, options=opts)
55+
output_json(catalog_attach_result_to_dict(result))
56+
57+
58+
@catalog.command("detach")
59+
@click.argument("attach_id")
60+
@click.option("--server", required=True, help="VGI worker command")
61+
def catalog_detach(attach_id: str, server: str) -> None:
62+
"""Detach from a catalog.
63+
64+
ATTACH_ID is the hex-encoded attach ID from catalog attach.
65+
66+
"""
67+
client = Client(server)
68+
client.catalog_detach(attach_id=hex_to_attach_id(attach_id))
69+
output_json({"status": "detached"})
70+
71+
72+
@catalog.command("create")
73+
@click.argument("name")
74+
@click.option("--server", required=True, help="VGI worker command")
75+
@click.option(
76+
"--on-conflict",
77+
type=click.Choice(["error", "ignore", "replace"]),
78+
default="error",
79+
help="Behavior if catalog already exists",
80+
)
81+
@click.option("--options", default="{}", help="Catalog options as JSON object")
82+
def catalog_create(name: str, server: str, on_conflict: str, options: str) -> None:
83+
"""Create a new catalog.
84+
85+
NAME is the name for the new catalog.
86+
87+
"""
88+
opts = parse_json_option(options, "--options")
89+
client = Client(server)
90+
client.catalog_create(
91+
name=name,
92+
on_conflict=OnConflict(on_conflict),
93+
options=opts,
94+
)
95+
output_json({"status": "created", "name": name})
96+
97+
98+
@catalog.command("drop")
99+
@click.argument("name")
100+
@click.option("--server", required=True, help="VGI worker command")
101+
def catalog_drop(name: str, server: str) -> None:
102+
"""Drop a catalog.
103+
104+
NAME is the name of the catalog to drop.
105+
106+
"""
107+
client = Client(server)
108+
client.catalog_drop(name=name)
109+
output_json({"status": "dropped", "name": name})
110+
111+
112+
@catalog.command("version")
113+
@click.argument("attach_id")
114+
@click.option("--server", required=True, help="VGI worker command")
115+
@click.option("--transaction-id", help="Transaction ID (hex) for transactional read")
116+
def catalog_version(attach_id: str, server: str, transaction_id: str | None) -> None:
117+
"""Get the current catalog version.
118+
119+
ATTACH_ID is the hex-encoded attach ID from catalog attach.
120+
121+
"""
122+
client = Client(server)
123+
version = client.catalog_version(
124+
attach_id=hex_to_attach_id(attach_id),
125+
transaction_id=(
126+
hex_to_transaction_id(transaction_id) if transaction_id else None
127+
),
128+
)
129+
output_json({"version": version, "attach_id": attach_id})

0 commit comments

Comments
 (0)