Skip to content

Commit 277212d

Browse files
author
lin.zhang
committed
v0.5.15: default async SQL, job result cmd, --no-limit, --sync, profile detail, SHOW limit protection, fix ai_message help injection
- SQL execution defaults to async, returns job_id immediately; use --sync for synchronous - New 'job result <job_id>' command: polls is_job_finished then fetches result set - Add --no-limit flag to sql and job result to disable row limit protection - SHOW statements now covered by row limit protection (client-side truncation) - Rename 'profile show' to 'profile detail' (show kept as hidden alias) - Fix output.error: only inject --help into ai_message for Click UsageError, not business errors - Use fetchmany(size) instead of fetchall() when limit is known
1 parent 731c71f commit 277212d

14 files changed

Lines changed: 207 additions & 128 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ This command provides an interactive installer to add bundled skills (from `cz_c
236236
cz-cli profile list
237237

238238
# Show a single profile
239-
cz-cli profile show <name>
239+
cz-cli profile detail <name>
240240

241241
# Create profile (username/password)
242242
cz-cli profile create <name> --username <user> --password <pass> \

cz_cli/cli_group.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class CLIGroup(click.Group):
5959
"""Click Group that auto-injects common options into every command."""
6060

6161
def add_command(self, cmd: click.Command, name: str | None = None) -> None:
62+
_inject_connection_options(cmd)
6263
_inject_profile_option(cmd)
6364
_inject_output_option(cmd)
6465
_inject_debug_option(cmd)
@@ -131,3 +132,50 @@ def _inject_profile_option(cmd: click.Command) -> None:
131132
expose_value=False,
132133
)
133134
cmd.params = [_opt] + cmd.params
135+
136+
137+
def _connection_callback(ctx: click.Context, param: click.Option, value: str | None) -> str | None:
138+
"""Store a connection override in ctx.obj when provided at subcommand level."""
139+
if value is not None:
140+
ctx.ensure_object(dict)
141+
ctx.obj[param.name] = value
142+
return value
143+
144+
145+
# (name, [cli flags], help text, extra kwargs)
146+
_CONNECTION_OPTIONS: list[tuple[str, list[str], str, dict]] = [
147+
("jdbc_url", ["--jdbc"], "JDBC connection URL", {}),
148+
("pat", ["--pat"], "Personal Access Token", {}),
149+
("username", ["--username"], "Username", {}),
150+
("password", ["--password"], "Password", {}),
151+
("service", ["--service"], "Service endpoint", {}),
152+
("protocol", ["--protocol"], "Protocol (https/http)", {"type": click.Choice(["https", "http"], case_sensitive=False)}),
153+
("instance", ["--instance"], "Instance name", {}),
154+
("workspace", ["--workspace"], "Workspace name", {}),
155+
("schema", ["--schema", "-s"], "Default schema", {}),
156+
("vcluster", ["--vcluster", "-v"], "Virtual cluster", {}),
157+
]
158+
159+
160+
def _inject_connection_options(cmd: click.Command) -> None:
161+
"""Add connection override options to *cmd* if not already present."""
162+
existing_names = {getattr(p, "name", None) for p in cmd.params}
163+
existing_flags: set[str] = set()
164+
for p in cmd.params:
165+
existing_flags.update(getattr(p, "opts", []))
166+
existing_flags.update(getattr(p, "secondary_opts", []))
167+
for param_name, flags, help_text, extra in _CONNECTION_OPTIONS:
168+
if param_name in existing_names:
169+
continue
170+
if any(f in existing_flags for f in flags):
171+
continue
172+
opt = click.Option(
173+
flags,
174+
default=None,
175+
help=help_text,
176+
callback=_connection_callback,
177+
expose_value=False,
178+
**extra,
179+
)
180+
opt._injected_connection = True # type: ignore[attr-defined]
181+
cmd.params.append(opt)

cz_cli/commands/job.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,74 @@ def job_status(ctx: click.Context, job_id: str) -> None:
178178
return
179179

180180
try:
181-
summary = conn.get_job_summary(job_id)
181+
progress = conn.get_job_progress(job_id)
182182
log_operation("job status", ok=True)
183-
output.success(summary, fmt=fmt)
183+
output.success(progress, fmt=fmt)
184184
except Exception as exc:
185185
log_operation("job status", ok=False, error_code="JOB_STATUS_ERROR")
186186
output.error("JOB_STATUS_ERROR", str(exc), fmt=fmt)
187187
finally:
188188
conn.close()
189+
190+
191+
@job_cmd.command("result")
192+
@click.argument("job_id")
193+
@click.option("--no-limit", is_flag=True, help="Disable automatic row limit protection.")
194+
@click.pass_context
195+
def job_result(ctx: click.Context, job_id: str, no_limit: bool) -> None:
196+
"""Fetch result set of a SQL job by job ID (waits if still running)."""
197+
import logging
198+
import time
199+
from cz_cli.connection import get_connection
200+
from cz_cli.connection_ctx import connection_kwargs_from_ctx
201+
from cz_cli.masking import mask_rows
202+
from cz_cli.commands.sql import _truncate_large_fields, DEFAULT_TRUNCATE_LEN, ROW_PROBE_LIMIT
203+
204+
logger = logging.getLogger(__name__)
205+
fmt: str = ctx.obj.get("format", "json")
206+
profile: str | None = ctx.obj.get("profile")
207+
jdbc_url: str | None = ctx.obj.get("jdbc_url")
208+
209+
try:
210+
conn = get_connection(jdbc_url=jdbc_url, profile=profile, **connection_kwargs_from_ctx(ctx))
211+
except Exception as exc:
212+
log_operation("job result", ok=False, error_code="CONNECTION_ERROR")
213+
output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
214+
return
215+
216+
timer = output.Timer()
217+
try:
218+
cursor = conn.cursor()
219+
try:
220+
with timer:
221+
while True:
222+
if cursor.is_job_finished(job_id):
223+
break
224+
logger.debug(f"job_id: {job_id} is running...")
225+
time.sleep(0.2)
226+
227+
cursor.get_result_set(job_id)
228+
if cursor.description is not None:
229+
columns = [d[0] for d in cursor.description]
230+
ai_msg = None
231+
if no_limit:
232+
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
233+
else:
234+
rows = [dict(zip(columns, row)) for row in cursor.fetchmany(ROW_PROBE_LIMIT)]
235+
if len(rows) > ROW_PROBE_LIMIT - 1:
236+
rows = rows[:ROW_PROBE_LIMIT - 1]
237+
ai_msg = f"Results truncated to 100 rows. Use --no-limit to fetch all: cz-cli job result --no-limit {job_id}"
238+
rows = _truncate_large_fields(rows, DEFAULT_TRUNCATE_LEN)
239+
rows = mask_rows(columns, rows)
240+
log_operation("job result", ok=True, rows=len(rows), time_ms=timer.elapsed_ms)
241+
output.success_rows(columns, rows, time_ms=timer.elapsed_ms, fmt=fmt, extra={"job_id": job_id}, ai_message=ai_msg)
242+
else:
243+
log_operation("job result", ok=True, time_ms=timer.elapsed_ms)
244+
output.success({"job_id": job_id, "message": "Job completed with no result set."}, time_ms=timer.elapsed_ms, fmt=fmt)
245+
finally:
246+
cursor.close()
247+
except Exception as exc:
248+
log_operation("job result", ok=False, error_code="JOB_RESULT_ERROR")
249+
output.error("JOB_RESULT_ERROR", str(exc), fmt=fmt)
250+
finally:
251+
conn.close()

cz_cli/commands/profile.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def _save_profiles(data: dict[str, Any]) -> None:
9494
@click.group("profile", cls=CLIGroup)
9595
@click.pass_context
9696
def profile_cmd(ctx: click.Context) -> None:
97-
"""Manage connection profiles. Use --show-secret on list/show to reveal secrets."""
97+
"""Manage connection profiles. Use --show-secret on list/detail to reveal secrets."""
9898

9999

100100
@profile_cmd.command("list")
@@ -155,7 +155,7 @@ def list_profiles(ctx: click.Context, show_secret: bool) -> None:
155155
output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
156156

157157

158-
@profile_cmd.command("show")
158+
@profile_cmd.command("detail")
159159
@click.argument("name")
160160
@click.option(
161161
"--show-secret",
@@ -179,7 +179,7 @@ def show_profile(ctx: click.Context, name: str, show_secret: bool) -> None:
179179
default_profile = data.get("default_profile")
180180
profile = profiles.get(name)
181181
if profile is None:
182-
log_operation("profile show", ok=False, error_code="PROFILE_NOT_FOUND")
182+
log_operation("profile detail", ok=False, error_code="PROFILE_NOT_FOUND")
183183
output.error("PROFILE_NOT_FOUND", f"Profile '{name}' not found", fmt=fmt)
184184
return
185185

@@ -191,10 +191,10 @@ def show_profile(ctx: click.Context, name: str, show_secret: bool) -> None:
191191
if result.get("password"):
192192
result["password"] = "******"
193193

194-
log_operation("profile show", ok=True)
194+
log_operation("profile detail", ok=True)
195195
output.success(result, fmt=fmt)
196196
except Exception as exc:
197-
log_operation("profile show", ok=False, error_code="INTERNAL_ERROR")
197+
log_operation("profile detail", ok=False, error_code="INTERNAL_ERROR")
198198
output.error("INTERNAL_ERROR", str(exc), fmt=fmt)
199199

200200

@@ -527,11 +527,16 @@ def use_profile(ctx: click.Context, name: str) -> None:
527527
list_profiles.epilog = examples_epilog(list_profiles.examples) # type: ignore[attr-defined]
528528

529529
show_profile.examples = [ # type: ignore[attr-defined]
530-
{"cmd": "cz-cli profile show dev", "desc": "Show config for 'dev' profile (password/PAT redacted by default)"},
531-
{"cmd": "cz-cli profile show dev --show-secret", "desc": "Show config including plaintext password/PAT"},
530+
{"cmd": "cz-cli profile detail dev", "desc": "Show config for 'dev' profile (password/PAT redacted by default)"},
531+
{"cmd": "cz-cli profile detail dev --show-secret", "desc": "Show config including plaintext password/PAT"},
532532
]
533533
show_profile.epilog = examples_epilog(show_profile.examples) # type: ignore[attr-defined]
534534

535+
# Keep "show" as a hidden alias for backward compatibility.
536+
profile_cmd.add_command(show_profile, "show")
537+
# Hide the alias from help output.
538+
profile_cmd.commands["show"].hidden = True
539+
535540
create_profile.examples = [ # type: ignore[attr-defined]
536541
{
537542
"cmd": "cz-cli profile create dev --username alice --password s3cr3t --instance my-inst --workspace ws1",

0 commit comments

Comments
 (0)