Skip to content

feat(db,measurement): db admin + measurement listing (PR3)#2

Merged
xe-nvdk merged 3 commits into
mainfrom
feat/pr3-db-admin
Jun 1, 2026
Merged

feat(db,measurement): db admin + measurement listing (PR3)#2
xe-nvdk merged 3 commits into
mainfrom
feat/pr3-db-admin

Conversation

@xe-nvdk

@xe-nvdk xe-nvdk commented May 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds arcctl db {list,show,create,drop} and arcctl measurement list.
  • Wires internal/client/database.go against Arc's /api/v1/databases surface.
  • New setCrossDBHeaders on the client so cross-database endpoints never accidentally inherit the per-client x-arc-database default — "absence is explicit."
  • db drop follows the project-wide destructive-ops rule: client-side y/N prompt by default, --yes to skip; on server refusal the verbatim error surfaces (e.g. the "Set delete.enabled=true in arc.toml" hint).
  • All list/show output formats (table, json, csv) sort by name so the three formats agree on row order. Server-reported count is passed through verbatim rather than re-derived from len(slice).
  • New helpers (addCommonConnectionFlags, addTimeoutFlag, confirmDestructive) so future PRs don't drift on flag wording.

Test plan

  • go test -race -count=1 ./... — 25 new tests across internal/client/ and internal/commands/
  • gofmt -l . && go vet ./... clean
  • End-to-end smoke against arc serve (with both delete.enabled=true and =false):
    • db list table / json / csv all render and sort correctly
    • db create + db show + measurement list happy paths
    • db drop interactive: empty answer aborts; n aborts; y deletes; --yes skips prompt
    • db drop with delete.enabled=false → server's "Set delete.enabled=true in arc.toml to enable" surfaces verbatim
    • Errors: drop non-existent → 404 surfaced; create reserved → 400 surfaced; create invalid chars → 400 with full validation rule; drop reserved → 403 surfaced
    • measurement list falls back to default_database; clean client-side error when neither is set
  • Empty-result rendering covered: (no databases), (no measurements yet), (no measurements in database "X")

Internal review

Matrix + single deep reviewer per .claude/CLAUDE.md. Findings addressed pre-commit:

  • B1 Blockerdb drop was missing the y/N prompt + --yes flag. Fixed: mirrors config delete's pattern, reads from cmd.InOrStdin() (testable), table-driven tests cover y/YES/n/NO/empty/EOF/whitespace cases.
  • H1db show JSON Count now mirrors the server's list.Count rather than len(measurements). Safer against future server-side pagination.
  • M2 + M3 — JSON output now sorts before encoding, so the three formats agree on row order across all list commands.
  • S2 — New test pins the setCommonHeaders default-fallback semantic so accidental cross-DB drift gets caught.
  • S3 + S4 — README roadmap flipped to shipped; addTimeoutFlag factors out 5× duplicated --timeout flag definitions.
  • M1 deferreddb show partial-failure UX (fail-loud is defensible per matrix; revisit if real-world friction surfaces).

Related Arc issue

Discovered while building this PR: Basekick-Labs/arc#471POST /api/v1/databases accepts any-valid-token; should require admin. Filed separately; arcctl works against current server behavior either way.

Companion docs PR

Per the per-PR docs rule, docs land in docs.basekick.net at docs/cli/db.md + docs/cli/measurement.md + the cli/index.md status table.

🤖 Generated with Claude Code

PR3 of the phased arcctl build-out. Adds `arcctl db {list,show,
create,drop}` and `arcctl measurement list` subcommands plus the
matching internal/client/database.go.

Wire surface (verified against arc/internal/api/databases.go):
- GET    /api/v1/databases                      (any-valid-token)
- POST   /api/v1/databases                      (any-valid-token)  *see #471
- GET    /api/v1/databases/:name                (any-valid-token)
- GET    /api/v1/databases/:name/measurements   (any-valid-token)
- DELETE /api/v1/databases/:name?confirm=true   (admin + delete.enabled=true)

Highlights
- client/database.go: 5 typed wrappers with bounded response reads,
  consistent error decoding (re-uses PR2's decodeWriteError), URL
  escaping on :name path segments
- client.go: new setCrossDBHeaders() so cross-database endpoints
  never accidentally inherit the client's default x-arc-database
  header — the "absence is explicit" pattern
- db drop: client-side y/N prompt (per CLAUDE.md destructive-ops
  rule), --yes / -y to skip. Surfaces the server's verbatim error
  on 403 (e.g. "Set delete.enabled=true in arc.toml to enable")
- All list/show output formats (table / json / csv) sort by name
  so the three formats agree on row order; server-reported Count
  is passed through verbatim (not re-derived from len(slice))
- measurement list falls back to active connection's
  default_database when --database omitted; client-side error
  before any network call if neither set
- Three flag helpers (addCommonConnectionFlags, addTimeoutFlag,
  confirmDestructive) so future PRs don't drift on flag wording

Verification
- 13 client tests (httptest): list/get/create/delete + cross-db
  header pinning + URL escaping + delete-disabled-error +
  not-admin-error + reserved-name-rejection
- 12 command tests: render dispatch (table/json/csv), empty-result
  paths, sort stability, JSON round-trip, CSV column order,
  confirmDestructive table-driven cases (y/yes/empty/n/NO/EOF/
  whitespace)
- End-to-end smoke against `arc serve` (with delete.enabled=true
  AND =false): every command + every error path verified. The
  new drop prompt smoked through 4 cases (empty=abort, n=abort,
  y=delete, --yes=no prompt)

Internal review (matrix + single deep reviewer) addressed:
- B1 Blocker: db drop missing y/N prompt + --yes flag → fixed
  (matches CLAUDE.md destructive-ops rule, mirrors config delete)
- H1: db show JSON Count now mirrors server's list.Count (not
  re-derived from len(measurements)) — safer against future
  server-side pagination
- M2 + M3: JSON output now sorts before encoding, so table/json/
  csv agree on row order across all list commands
- S2: new test pinning the setCommonHeaders default-fallback
  semantic so accidental cross-db drift gets caught
- S3 + S4: README roadmap flipped, addTimeoutFlag factors out 5x
  duplicated --timeout flag definition
- M1 deferred: db show partial-failure UX (fail-loud is defensible
  per matrix; will revisit if real-world friction surfaces)

Related Arc issue
- #471 — POST /api/v1/databases accepts any-valid-token; should
  require admin. Discovered while building this PR; filed separately.

Companion docs PR lands in docs.basekick.net (docs/cli/{db,
measurement}.md + index.md status table update).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@xe-nvdk

xe-nvdk commented May 31, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist review please

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements database and measurement administration features for arcctl (PR3), adding the db and measurement subcommands. It introduces client methods for listing, showing, creating, and deleting databases, as well as listing measurements, along with comprehensive unit tests. The feedback suggests a UX improvement in the db drop command to validate the client configuration before prompting the user for destructive confirmation, preventing unnecessary prompts if the configuration is invalid.

Comment thread internal/commands/db.go Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the db and measurement subcommands for the arcctl CLI, mapping to Arc's database administration and measurement listing endpoints. It introduces the necessary client methods, command-line interfaces, and comprehensive unit tests. The review feedback highlights a potential issue where empty or nil slices for databases and measurements could serialize to null instead of [] in JSON outputs, and suggests using make and copy to ensure consistent JSON serialization.

Comment thread internal/commands/db.go Outdated
Comment thread internal/commands/db.go Outdated
Comment thread internal/commands/measurement.go Outdated
Ignacio Van Droogenbroeck added 2 commits May 31, 2026 20:55
Move the buildClient call ahead of the y/N confirmation in `db drop`
so a misconfigured connection fails fast instead of making the user
confirm a delete that can't be sent. No functional change beyond
ordering — same code, two blocks swapped + a four-line "why" comment.

Found by Gemini in #2
inline comment on internal/commands/db.go:240. The original matrix +
deep-reviewer pass assumed a valid client config and never exercised
the prompt-then-fail UX path.

Tests + lint clean; existing confirmDestructive tests still pass
since they exercise the prompt mechanics in isolation, not the
wrapping order.
#2)

Switch from `append([]T(nil), src...)` to `make([]T, len(src)); copy()`
in the three render-paths that emit JSON. The append-to-nil form
preserves nil when src is empty/nil; the make form always yields a
non-nil slice. The difference is invisible for table/csv output but
matters for `-o json` consumers (jq, frontends, JSON-schema validators
with `minItems: 0`) that distinguish `"databases": null` from
`"databases": []`.

Fixed sites
- internal/commands/db.go:323-329       renderDatabaseList
- internal/commands/db.go:385-389       renderDatabaseShow
- internal/commands/measurement.go:95-101 renderMeasurementList

Regression tests
- TestRenderDatabaseList_JSONEmpty_IsArrayNotNull
- TestRenderDatabaseShow_JSONEmpty_IsArrayNotNull
- TestRenderMeasurementList_JSONEmpty_IsArrayNotNull

Tests verified via mutation: reverting one site to append-to-nil
makes the corresponding test fail with the exact `"databases": null`
output Gemini flagged; reapplying the fix passes.

Found by Gemini in #2
on commit a77d514 (three inline comments at db.go:326, db.go:382,
measurement.go:98). The earlier matrix exercised empty-result paths
for table/csv output but didn't check the JSON shape — adding a
"nil-vs-empty slice JSON encoding" axis to the destructive-ops
matrix template for future PRs.
@xe-nvdk

xe-nvdk commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

@gemini-code-assist review please

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the database and measurement administration subcommands (db and measurement) for the arcctl CLI tool, corresponding to PR3 of the development roadmap. It introduces new client methods to interact with the Arc database endpoints (list, show, create, drop, and list measurements) and wires them up to the CLI command tree with support for table, JSON, and CSV output formats. Comprehensive unit tests have been added to verify the client behavior, command execution, and output rendering. No review comments were provided, so there is no additional feedback to address.

@xe-nvdk xe-nvdk merged commit 24def42 into main Jun 1, 2026
2 checks passed
xe-nvdk pushed a commit that referenced this pull request Jun 1, 2026
)

Two findings from Gemini's review on #3,
both Medium.

#1 — `--skip-rows` accepts negative values silently
A negative value passes through to `ImportCSV`, then gets dropped by
the `opts.SkipRows > 0` gate in the client. User typed `--skip-rows -5`,
nothing happens, no error. Now validated in the command's RunE before
any client call. Regression test driven through cobra so the wired flag
path is covered end-to-end; mutation-verified by disabling the guard
(downstream "no database" check fires instead).

#2 — `mw.Close()` on error paths writes trailing boundary into broken pipe
In the multipart goroutine, the error paths used to call `mw.Close()`
*before* `pw.CloseWithError(err)`. `multipart.Writer.Close()` writes
the trailing boundary `--<boundary>--\r\n` into the pipe — but the
preceding content is already known-bad (CreateFormFile failed, or
io.Copy errored mid-file). Result: a multipart payload that's
syntactically valid but semantically truncated, confusing for a reader
attempting to make sense of partial data. Now: error paths skip
`mw.Close()` entirely and signal the error on the pipe; happy path
still closes the writer normally to emit the trailing boundary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant