Skip to content

feat(cube-cli): list dbt sync history and read one sync's logs - #11625

Open
MikeNitsenko wants to merge 6 commits into
masterfrom
mikhail/cub-4002-dbt-sync-history-logs
Open

feat(cube-cli): list dbt sync history and read one sync's logs#11625
MikeNitsenko wants to merge 6 commits into
masterfrom
mikhail/cub-4002-dbt-sync-history-logs

Conversation

@MikeNitsenko

Copy link
Copy Markdown
Contributor

关注-up to #11612, which taught the CLI to start and follow a dbt sync but not to
look back at one. This adds the two reads that were missing, so a dbt pull is fully
drivable from a terminal and from CI.

Summary

  • cube dbt history <deployment> (aliases list, ls) — recent syncs as a table:
    sync job id, status, trigger, start, duration, branch. Paged with --first/--after,
    cursor from pageInfo.endCursor.
  • cube dbt logs <deployment> <sync-job-id> — one sync's phase timeline and the text a
    failed phase produced, with failure entries in red. Same paging flags.
  • Both need only SchemaRead, pass --json through untouched, and answer a 404 with a
    sentence naming what it can mean rather than a bare status line.
  • The two --wait failure messages now name cube dbt logs for the failed run — the
    reason alone does not say which phase produced it, and that is the difference between
    a red CI step that explains itself and one that only says "dbt sync failed". Both wait
    paths build that message through one function instead of two copies of it.
  • Docs: a Sync history and logs section in the CLI reference, including the CI shape
    that prints the timeline when a gate fails, plus the updated dbt command-reference row.

Notes

  • A duration is the server's own durationMs or an empty cell — never completedAt
    minus startedAt. Those stamps are written by different processes, so their difference
    can disagree with the server's figure and, for a run that fails moments after starting,
    be negative. A test holds that line.
  • Each field is read under the name its own payload uses and the name the existing sync
    payloads use for the same thing, so a row still renders under either. Nothing is
    derived: a field no key matches stays empty, an unrecognised log entry is printed as it
    arrived rather than dropped, and a page nothing was recognised in says so and points at
    --json.
  • These two endpoints are newer than the sync endpoints the rest of the file speaks to.
    On a tenant that does not serve them yet both commands exit non-zero saying exactly
    that; nothing else in cube dbt changes behaviour.

Validation

  • cargo test (86 passed; 11 new assertions across history rows, durations, log entries
    and the failure message)
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt --all --check
  • Both commands, their --json and 404 paths, the empty and unrecognised-payload cases,
    and both --wait failure paths exercised against a stub API.

🤖 Generated with Claude Code

@MikeNitsenko
MikeNitsenko requested a review from keydunov as a code owner August 23, 2026 20:35
@mintlify

mintlify Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview Aug 23, 2026, 8:36 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions github-actions Bot added the rust 拉取请求 that update Rust code label Aug 23, 2026
@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 2m 41s —— View job


Review complete — looks good to merge; 0 high, 1 medium, 3 low (3 inline comments)

Full review
  • Read the diff (git diff origin/master...HEAD)
  • Review rust/cube-cli/src/commands/dbt.rs
  • Review docs-mintlify/reference/cli.mdx
  • Cross-check helpers (output::items/table/field, util::pick-adjacent helpers, Client::get_optional)
  • Verify the docs' CI snippet against the code paths it depends on
  • Post inline findings

General

Well-built change. Things I specifically checked and found correct:

  • The duration rule holds. history_row reads durationMs only and never derives from completedAt - startedAt; a_duration_is_the_servers_own_figure_or_nothing pins exactly the trap case (both stamps, no durationMs) rather than only the happy path. human_duration_ms passes non-durations through untouched instead of collapsing them to a confident 0s.
  • The failure-message dedup is real. Both --wait paths now return Err(failure(...)), and failure() quotes the sync id through util::shell_quote, matching the convention print_prune_hint established. The test covers blank reason, missing reason, and a metacharacter-bearing id.
  • get_optional is the right call for both reads — a 404 here is the tenant answering, and both messages name the three things it can mean instead of leaving a bare status line.
  • The docs' CI snippet actually works against the code. I traced it: wait::poll progress goes to stderr (src/wait.rs:149,155), and the FAILED branch prints wait_json — which carries syncJobId (src/commands/dbt.rs:380) — to stdout before returning the error. So sync.json is a single valid document and jq -r '.syncJobId // empty' resolves. The [ -n "$SYNC_JOB_ID" ] guard correctly handles the timeout case where no document is written.
  • Column/row coupling is tested (a_history_row_fills_every_column), and cell() looks columns up by name so a reorder can't silently point a test at the wrong index.

Findings

# Severity Location Issue
1 Medium dbt.rs:321 STATUS is the only column that doesn't use pick's two-spelling fallback — and its blank suppresses the "nothing recognised" warning, so the failure is silent
2 Low dbt.rs:662-671 Blank-message fallback re-prints the timestamp/stage inside the raw JSON, and drops the red for level: error entries
3 Low dbt.rs:667 Server log text is printed with control bytes intact (deliberately un-one_lined), so an ESC sequence in dbt output reaches the terminal
4 Nit human_duration_ms's float path does a saturating as u64 cast, so an absurd value like 1e30 renders as a nonsense hour count rather than passing through. Garbage-in only; not worth code.

Nothing here blocks. Finding 1 is the only one I'd want resolved before merge, and a comment saying "status is the only spelling this endpoint uses" would resolve it just as well as the code change.

Docs

docs-mintlify/reference/cli.mdx is accurate and appropriately small — the command-reference row is updated, the new section states the SchemaRead requirement and the pageInfo.endCursor paging path, and it doesn't repeat what --help already says. Note that table output never prints endCursor, so terminal paging requires a --json round trip; the docs already say to take the cursor from --json, and every other paged command in this CLI behaves the same way, so this is consistent rather than a gap.

Not verified

I could not run cargo test, cargo clippy, or cargo fmt — the sandbox declined the commands. The PR reports 86 passing tests, clippy clean with -D warnings, and fmt clean; the review above is from reading the code, not from a local build. Please rely on CI for those.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

Comment thread rust/cube-cli/src/commands/dbt.rs Outdated
Comment thread rust/cube-cli/src/commands/dbt.rs Outdated
Comment thread rust/cube-cli/src/commands/dbt.rs Outdated
@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 2m 47s —— View job


Re-review of 1dd2bc4 — all 3 prior findings addressed; 0 new blocking issues (2 nits)

Full review
  • Read git diff origin/master...HEAD and the follow-up commit 1dd2bc4
  • Verify the three prior findings are actually addressed
  • Re-review rust/cube-cli/src/commands/dbt.rs in its current state
  • Re-check docs-mintlify/reference/cli.mdx
  • Resolve my now-stale review threads (3 resolved)

Prior findings — status

# Prior finding Status
1 STATUS skipped pick's two-spelling fallback; blank status silently defeated the "nothing recognised" warning Fixed. All cells now go through one cell closure (dbt.rs:352), status keeps a single key with the reason stated inline, and — the actual bug — the warning is re-keyed on ID_COLUMN (dbt.rs:771) instead of rows.iter().flatten().all(is_blank), so one filled column no longer suppresses it. a_history_row_fills_every_column pins HISTORY_COLUMNS[ID_COLUMN] == "SYNC JOB ID", so the warning can't drift onto another column.
2 Raw-entry fallback re-printed timestamp/stage and dropped error red Fixed. log_line (dbt.rs:423) returns paint_failure(value.to_string(), error) early, so the prefix is dropped and the red survives. an_entry_whose_text_this_build_cannot_find_is_shown_as_it_arrived asserts both halves, including assert_ne!(line, failed.to_string()) for the colour.
3 Server log text printed with control bytes intact Fixed, and correctly scoped. printable (dbt.rs:283) drops every char::is_control except \n/\t — that includes DEL (U+007F) and the C1 range, which is what an 8-bit CSI would need. Applied to message, and to time/stage via one_cell. The comment gets the reason right: one_line only ate control characters that happen to be whitespace, and ESC isn't.

Also picked up unprompted, and correct: the as u64 saturation nit (1e30 no longer renders as a five-billion-hour duration — value < u64::MAX as f64 guard plus human_duration_ms("1e30")/("inf") assertions), and cell bounding via CELL_LIMIT — a branchName carrying a newline can no longer break the row it sits in, and a 500-char trigger can't push the other columns off screen. server_text_cannot_drive_the_terminal covers both the entry and the cell path.

新建 pass over the current file

Nothing new that blocks. Things I re-checked and found sound:

  • human_duration_ms still reads durationMs only; a_duration_is_the_servers_own_figure_or_nothing still pins the both-stamps-no-duration trap.
  • failure() is still the single source of both --wait failure messages, still shell_quotes the id.
  • util::status_of is unaffected — still used by the wait paths (dbt.rs:138,483,522,577,673) and by deployments.rs; swapping the history cell to one_cell didn't orphan it.
  • pick's blank-is-not-an-answer rule survives the one_cell wrapping, since one_cell is applied after the key choice, not before it.
  • rows[ID_COLUMN] indexing is safe: history_row always returns HISTORY_COLUMNS.len() cells, and the test holds that.

Nits (no action expected)

  1. dbt.rs:437 — the raw-entry fallback prints value.to_string() unbounded, while every other server string on the path is capped at CELL_LIMIT. An entry carrying a large payload field floods the terminal. Deliberate ("shown as it arrived") and arguably right for a fallback; noting only for the asymmetry.
  2. printable keeps \n in message by design, so a hostile dbt error can still emit lines that look like additional timeline entries. Strictly weaker than the escape channel that was closed, and un-fixable without giving up multi-line compile errors.

Docs

docs-mintlify/reference/cli.mdx is unchanged since my last pass and still accurate. I re-traced the CI snippet: wait::poll progress goes to stderr, the FAILED branch prints wait_json (carrying syncJobId) to stdout before returning the error, so sync.json is a single valid document and jq -r '.syncJobId // empty' resolves; the [ -n "$SYNC_JOB_ID" ] guard covers the timeout case where nothing was written.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again — this review is from reading the code. The PR reports them clean; please rely on CI.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

@MikeNitsenko

Copy link
Copy Markdown
Contributor Author

All four findings addressed in 1dd2bc4.

# Finding Resolution
1 STATUS skips the two-spelling rule, and its blank defeats the warning Comment resolution, not the second key: status is the field the sync endpoints already publish and whose terminal values this file acts on, so state would be an invention. The asymmetry is gone — every cell reads through one cell reader, with status's single key explained next to it. The warning now keys on the sync job id, which one filled column no longer defeats.
2 Fallback repeats the prefix and drops the red Raw entry prints alone (the JSON carries what the prefix repeated), and paint_failure colours it too.
3 Server log text keeps its control bytes printable drops every control character but \n/\t. Note the premise needed correcting toward the finding: one_line only drops control characters that are whitespace, and ESC is not — so nothing here was guarding that channel.
4 Float path saturates on 1e30 Bounded before the cast, so it passes through as the nonsense it is.

Cells are now bounded and single-line too, so a value carrying a newline can no longer break the row it sits in.

Validation

  • cargo test (89 passed, 3 new tests: line rendering, the unreadable-entry fallback, and control characters in server text)
  • cargo clippy --all-targets -- -D warnings, cargo fmt --all --check
  • Re-exercised against a stub API serving ESC ]0;…BEL and ESC [2J inside a log message, an entry with a failure level and no message field, and a history page whose rows fill other columns but name no id — escapes arrive as inert text, the fallback keeps its red, and the warning fires.

@MikeNitsenko

Copy link
Copy Markdown
Contributor Author

Aligned both commands with the shapes the endpoints publish (f26eb2b):

  • dbt history --status/--trigger — the list endpoint filters on both (RUNNING|COMPLETED|FAILED|CANCELLED|UNKNOWN, manual|api|webhook|agent|unknown). Sent through unchecked on purpose: the vocabularies are the server's, and a filter this build has not heard of is one the server can still honour, where a hard-coded list would refuse it. A bad value is a loud 400 naming the field.
  • dbt logs no longer takes --first/--after. One sync's timeline is one page, bounded by the number of phases it ran — the flags would have been accepted here and ignored there.
  • Each log line now carries how long its phase took, sharing one bracket with the phase name ([dbt-compile 31s]) so a multi-line failure is interrupted by neither. On a phase timeline the timings are half the content.
  • Field aliases are gone. They were insurance taken out before the shapes were settled, and every one was dead — syncJobId, status, trigger, startedAt, durationMs, branchName on a run; timestamp, level, phase, message, durationMs on a log entry. status is no longer the odd column out, since no column carries an alias now (re: the earlier thread).

Two behaviours worth flagging for reviewers:

  • A listed run can be CANCELLED or UNKNOWN, unlike the two values the status endpoint calls terminal. Nothing in history acts on a status, so they pass through as they arrived.
  • A cancelled sync therefore shows as CANCELLED in history but fails a --wait gate — the status endpoint owes a polling caller a terminal answer, and the reason it prints says the sync was cancelled. Documented in the new docs section.

--json still carries everything the six columns leave out (the synced dbt ref, the failed phase, per-phase timings, manifest counts), which the docs now say.

Validation

  • cargo test (89 pass), cargo clippy --all-targets -- -D warnings, cargo fmt --all --check
  • Re-exercised against a stub serving the published record shapes: CANCELLED/RUNNING rows with a null durationMs (empty cell, never derived from the stamps), both filters, --first with hasNextPage, a log entry with a null phase (no empty brackets), and an error-level multi-line failure.

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 2m 18s —— View job


Re-review of f26eb2b — no blocking issues; 0 high, 0 medium, 2 low (1 inline comment)

Full review
  • Read the new commit f26eb2b
  • Re-review rust/cube-cli/src/commands/dbt.rs in its current state
  • Re-check docs-mintlify/reference/cli.mdx
  • Check existing review threads (all 3 prior threads already resolved — no dupes to skip)
  • Post findings

What this commit changes, and how it holds up

pick is gone, and every field now reads one name. This is the change with the most surface, and the reasoning in the commit message is the right one to make explicit: the second spellings were speculative, and speculative fallbacks are worse than absent ones because they make a wrong shape look partly right. The residual risk is narrow and correctly scoped — the all-blank-id warning still catches a page this build can't read at all, and a single renamed field now blanks one column silently. The comment above the warning already says it doesn't claim to catch that, so nothing here reads as an oversight. status is no longer the odd column out, which retires my earlier finding at the root rather than by comment.

logs dropping --first/--after is the right call and costs nothing. Both flags were introduced in this same PR (eb50aa5), so no released surface is being removed — and accepting a paging flag the server ignores is worse than not offering one. &Vec::new() matches the result path in the same file.

The shared bracket is well-judged. [dbt-compile 1s] keeps metadata on one side and the entry's own text on the other, so a multi-line compile error isn't interrupted mid-way — and labels.join(" ") on a filtered vec means neither half missing leaves a hole or a bare parenthesis. a_log_line_carries_the_phase_and_its_timing pins all four combinations including the both-absent one (!bare.contains('[')).

Things I re-checked and found still sound:

  • human_duration_ms still reads durationMs only; the u64::MAX bound on the float path survives; the running-run case (durationMs: null) is now pinned to an empty cell alongside the both-stamps case.
  • printable is unchanged and still applied to message, and to phase/time via one_cell. The new duration goes through one_cell before parsing, so an unparseable value lands in the bracket bounded rather than raw.
  • failure() is untouched — still the single source of both --wait messages, still shell_quotes the id.
  • HISTORY_COLUMNS/history_row are still the same width, and ID_COLUMN is still index 0 with a test naming the column.
  • log_entry's error levels widened past the documented error; the comment gets the asymmetry right (colour is not acted on, so over-reddening is the cheap direction).

Findings

# Severity Location Issue
1 Low dbt.rs:83-88 --status/--trigger take no nonempty parser, unlike --ref/--branch in the same file — --status "" sends status= rather than being refused (inline)
2 Low dbt.rs:83-88 The two filter vocabularies differ in case in adjacent help lines, and a wrong case is a silently empty table rather than the loud 400 the pass-through reasoning assumes (inline, same thread)
3 Nit dbt.rs:456 A line with a timing but no phase renders [340ms], which reads as a phase named 340ms until you see a two-part bracket next to it. Tested and deliberate; noting only because the bracket now means two things.

Neither low finding blocks. Both are about the same seam — nothing stands between a typed filter value and the query string — and both would be resolved by a sentence of help text as well as by code.

Docs

docs-mintlify/reference/cli.mdx tracks the code change accurately: the command-reference row names the two new flags, the logs-takes-no-paging-flags statement matches the struct, and the CANCELLED note is a genuinely useful reconciliation — history showing CANCELLED while --wait calls the same run a failure is exactly the kind of thing a CI author hits once and puzzles over. One claim I can't verify from this repo: "A page holds at most 100 runs, so a larger --first returns 100 with pageInfo.hasNextPage set." That's server behaviour, not CLI behaviour — nothing here clamps or warns on --first 500, which is fine, but the sentence is only as good as the endpoint's actual cap.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again (third time this PR) — this review is from reading the code. The PR reports them clean; please rely on CI.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

Comment thread rust/cube-cli/src/commands/dbt.rs Outdated
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 1m 47s —— View job


Re-review of 03349a2 — prior finding fully addressed; 0 high, 0 medium, 2 low (2 inline comments)

Full review
  • Read the new commit 03349a2
  • Verify the prior --status/--trigger finding is addressed
  • Review the util.rs addition (nonempty_filter) and its interaction with the command-tree walk test
  • Re-check docs-mintlify/reference/cli.mdx
  • Resolve my now-stale thread (1 resolved; the other 3 were already resolved)
  • Post findings

Prior finding — status

Both halves of my last comment are resolved, and by the right mechanism in each case.

1. Empty values are refused. nonempty_filter (util.rs:189) is a third sibling alongside nonempty/nonempty_ref, with a message specific to what an empty filter does rather than what an empty branch does — which is the reason I said this was an addition rather than a reuse. The message names the actual mechanism (push sends status=, not nothing), matching the two above it.

2. Case is spelled out rather than guessed at. The help now says case-sensitive on both lines and the docs say both case-sensitive as spelled here. This is the honest resolution: the alternative — normalising case in the CLI — would have contradicted the pass-through-unchecked reasoning that justifies not hard-coding the vocabulary at all, since a normalisation is a claim about values this build has not heard of. The comment at dbt.rs:754 now states both halves of the policy (unchecked against a list, but empty refused at parse time) in one place.

Things I checked around the change

  • The command-tree walk is unaffected. only_the_listed_branch_arguments_refuse_an_empty_value filters on id.contains("branch") || id == "ref" || ..., so status/trigger are outside the walk and neither list needed an entry. Good — the two new flags aren't branch arguments, and forcing them into that test's exact-match lists would have overloaded what it asserts.
  • The shared-prefix assertion is the right addition. The new loop over [nonempty(""), nonempty_ref(""), nonempty_filter("")] pins that all three open with EMPTY_VALUE_REFUSED — which is exactly the string the walk partitions on at util.rs:697. Without it, a fourth helper written for a branch argument with a fresh message would make the walk record a refusal as a parse failure and assert with a misleading message. This closes that at the source.
  • push is unchanged and still Some("") → status=, so the parser is genuinely the only thing standing between the two.
  • The dbt.rs diff is help text, the parser attribute, and comment — no behavioural change to the request path beyond the refusal.

Findings

# Severity Location Issue
1 Low util.rs:195 nonempty_filter tests s.trim() but returns s, so --status " FAILED" travels padded — and padding is never meaningful in a five-word server vocabulary, unlike in a branch name (inline)
2 Nit cli.mdx:385 The reflow left one ~118-char line where the section wraps at ~85 (inline)

Neither blocks.

Docs

Accurate and still small. The case-sensitivity clause lands in the right sentence — right where both vocabularies are enumerated, so the reader sees the claim and the spellings together rather than in separate paragraphs. Same standing caveat as last time: "A page holds at most 100 runs" is server behaviour I can't verify from this repo.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again (fourth time this PR) — this review is from reading the code. The PR reports them clean; please rely on CI.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

Comment thread rust/cube-cli/src/util.rs
Comment thread docs-mintlify/reference/cli.mdx Outdated
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 1m 45s —— View job


Re-review of 2f36fe1 — both prior findings addressed; no new findings, nothing blocking

Full review
  • Read the new commit 2f36fe1
  • Verify both prior findings are addressed
  • Re-check util.rs — the trim divergence, its doc comment, and its tests
  • Re-check the command-tree walk for interaction with the change
  • Re-check docs-mintlify/reference/cli.mdx
  • Resolve my now-stale threads (2 resolved; the other 4 were already resolved)

Prior findings — status

# Prior finding Status
1 nonempty_filter guards on s.trim() but returns s, so --status " FAILED" travels padded Fixed (util.rs:207). Returns s.trim().to_string(), and the doc comment now states the divergence rather than leaving it to be read as drift — naming branch_or_placeholder as the counter-case, which is exactly where the opposite rule is argued. nonempty_filter(" FAILED\n") == "FAILED" pins it, and — better — the same test pins nonempty(" main ") == " main " and nonempty_ref(" main ") == " main " beside it, so the divergence is asserted from both sides. A later "consistency" edit that trims all three now fails on the two that must not.
2 cli.mdx:385 left one ~118-char line where the section wraps at ~85 Fixed. Lines 382–388 now wrap at 79–86, matching the surrounding prose. Content unchanged.

Checks around the change

  • The trim is correctly scoped and correctly ordered. It applies only to nonempty_filter, whose only two callers are --status/--trigger (dbt.rs:85,89); nonempty/nonempty_ref still return their input byte-for-byte. The trim also lands after the emptiness check, so an all-whitespace value is still the refusal (with the filter-specific message) rather than an empty Ok.
  • The command-tree walk is unaffected. only_the_listed_branch_arguments_refuse_an_empty_value filters on id.contains("branch") || id == "ref" || id.ends_with("_ref") || id.starts_with("ref_") (util.rs:677-680), so status/trigger are outside it; neither the refuses nor the accepts list needed an entry, and the EMPTY_VALUE_REFUSED shared-prefix assertion from the previous commit still holds for all three helpers.
  • --after is untouched, and that's right. It's the one remaining free-text flag on history without a parser — but it's untouched across all ~18 paged commands in this CLI (reports, deployments, users, folders, …), and a cursor is an opaque server token where trimming would be a change to the value rather than a cleanup of it. Not this PR's line to move.
  • Nothing in dbt.rs changed in this commit beyond what the previous review already covered; the diff is util.rs (behaviour + comment + tests) and the docs reflow.

Docs

cli.mdx is unchanged in substance and still accurate. Same standing caveat as the last two passes: "A page holds at most 100 runs" is server behaviour I can't verify from this repo.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again (fifth time this PR) — this review is from reading the code. The PR reports them clean; please rely on CI.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

MikeNitsenko and others added 5 commits August 27, 2026 11:49
`cube dbt` could start and follow a sync, but not look back at one. Two
commands complete it:

- `cube dbt history <deployment>` lists recent syncs — id, status, trigger,
  start, duration, branch — paged with `--first`/`--after`.
- `cube dbt logs <deployment> <sync-job-id>` prints a sync's phase timeline
  and the text a failed phase produced, colouring failure entries.

A duration is the server's own `durationMs` or an empty cell — never the
difference of two stamps written by different processes, which can disagree
with it and, for a run that fails moments after starting, be negative.

The two `--wait` failure messages now name `cube dbt logs` for the run that
failed, which is the difference between a CI step that explains itself and one
that only says "dbt sync failed"; both paths build that message through one
function instead of two copies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Every history cell is read the same way, through one `cell` reader: `status`
  keeps its single key, now with the reason it is the one field that cannot have
  a second spelling.
- Cells are bounded and single-line, so a value carrying a newline can no longer
  break the row it sits in, nor an unbounded one the layout.
- Server log text is stripped of control characters other than the line breaks
  and tabs the timeline keeps on purpose. `one_line` was never the guard it looks
  like: ESC is not whitespace, so a hostile dbt error could have retitled a
  window or overwritten the lines above it in a CI log.
- The raw-entry fallback no longer repeats the timestamp and stage the JSON
  already carries, and keeps its red when the entry says it is a failure — the
  entry this build understood least is the last place to drop that signal.
- `history`'s "could not read this" warning keys on the sync job id rather than
  on every cell being blank, which one filled column was enough to defeat.
- `human_duration_ms` rejects a float too large to cast, which saturated into a
  confident five-billion-hour duration instead of passing through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Aligns both commands with what the endpoints they call actually publish.

- `dbt history` takes `--status` and `--trigger`, sent through unchecked: the two
  vocabularies are the server's, and a filter this build has not heard of is one
  the server can still honour, where a list hard-coded here would refuse it.
- `dbt logs` drops `--first`/`--after`. One sync's timeline is one page, bounded
  by the number of phases it ran, so the flags were accepted here and ignored
  there — a promise of paging that does not exist.
- A log line now carries how long its phase took, sharing one bracket with the
  phase name so a multi-line failure is interrupted by neither. The timings are
  half of what makes this a timeline rather than a list of remarks.
- Fields are read under the names the endpoints publish, and only those: the
  second spellings were insurance taken out before the shapes were settled, and
  every one of them was dead. `status` is no longer the odd column out, since no
  column carries an alias now.

A listed run can be CANCELLED or UNKNOWN as well as the two the status endpoint
calls terminal. Nothing here acts on a status, so they pass through as they
arrived; the docs note that a cancelled run is still a failure to a `--wait`
gate, which needs a terminal answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… out

- `--status` and `--trigger` carry a `nonempty_filter` parser, like every other
  free-text argument in the tree. An empty value is not dropped — `push` sends
  `status=` — so a CI script whose `$STATUS` did not expand would have listed
  whatever the server made of an empty filter.
- Both vocabularies are the server's and they do not share a case (statuses
  upper, triggers lower), so the help and the docs now spell that out: a
  mis-cased value is the one mistake that may come back as an empty table rather
  than as a complaint, and an empty table reads as an answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A filter is one word out of a vocabulary the server publishes, and no member of
it has a space in it — so ` FAILED` could only ever match nothing, landing in the
exact failure this argument's help was written to prevent: an empty table that
reads as an answer. `$(jq -r …)` and a value read out of a file are the ordinary
ways to acquire the padding.

The two helpers beside it still return what they were given, because a branch
name is the caller's own and `--branch '  x  '` can name a branch that exists.
A test pins the divergence rather than leaving it to be read as an oversight.

Also rewraps the docs paragraph the previous commit left one line too long.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MikeNitsenko
MikeNitsenko force-pushed the mikhail/cub-4002-dbt-sync-history-logs branch from 2f36fe1 to 4632e05 比较 August 27, 2026 06:49
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 2m 10s —— View job


Re-review of 4632e05 — rebase-only push, tree unchanged; no new findings, nothing blocking

Full review
  • Work out what changed since my last pass (2f36fe1)
  • Re-review rust/cube-cli/src/commands/dbt.rs
  • Re-review rust/cube-cli/src/util.rs
  • Re-check docs-mintlify/reference/cli.mdx
  • Try to run cargo test / clippy / fmt
  • Check existing review threads (all 6 already resolved — nothing to resolve, no dupes to skip)

What this push is

A rebase, not a change. 2f36fe1 is no longer reachable (git cat-file -t 2f36fe1 → not a valid object) and all five commits now carry the same authored date, so the branch was replayed onto current master. The dbt.rs blob is 953db6a — the same SHA the PR's file list reports and the same content my last pass reviewed.

I verified the rebase preserved the reviewed state rather than trusting the SHA:

Previously fixed Still present
nonempty_filter returns s.trim().to_string() util.rs:207 ✓, with the divergence argued in the doc comment and pinned from both sides by the nonempty(" main ") / nonempty_ref(" main ") assertions beside it
Field aliases (pick) removed; status no longer the odd column out ✓ — no pick in the file; every cell reads one name through the single cell closure
Warning re-keyed on ID_COLUMN rather than all-cells-blank dbt.rs:794 ✓, with HISTORY_COLUMNS[ID_COLUMN] == "SYNC JOB ID" still pinned
printable drops every control char but \n/\t ✓, applied to message and via one_cell to phase/time/every table cell
Raw-entry fallback prints alone and keeps its red log_line early-returns paint_failure(value.to_string(), error)
u64::MAX bound before the float cast ✓, with human_duration_ms("1e30") / ("inf") asserted
Docs paragraph rewrapped ✓ — lines 382–388 run 81–86 chars

Rebase-conflict check

The only commit on master touching rust/cube-cli is the v1.7.28 version bump, so there was nothing here for the replay to silently reconcile. I re-checked the four helpers this PR leans on against their current master definitions and all still match the assumptions in the diff:

  • util::push<T: ToString>Some("") still sends key=, so nonempty_filter is genuinely the only thing between an unexpanded $STATUS and the query string.
  • output::items — still unwraps items/data, still falls back to vec![other] for a non-envelope object, which is exactly the case the id-column warning is there to catch.
  • output::table — still measures cells by chars().count(), so CELL_LIMIT bounding a cell actually bounds the column.
  • Client::get_optional — still maps 404 to None, so both new 404 messages are reached the way they're written.

Fresh pass

Nothing new that blocks. The couple of things I'd flag if this were a first read are all previously-argued and previously-answered: printable keeping \n in message (deliberate — multi-line compile errors are the point), the unbounded raw-entry fallback (deliberate — "shown as it arrived"), and [340ms] for a timing with no phase (a shape the endpoint doesn't produce; tested only so the renderer degrades sanely). One observation, not a finding: log_line writes ANSI to stdout unconditionally — no NO_COLOR or tty check — but that's the file-wide and repo-wide convention (output::table bolds its header on stdout the same way) and --json is the machine path, so it's consistent rather than a gap.

Docs

cli.mdx is unchanged in substance. Same standing caveat as the last three passes: "A page holds at most 100 runs" is server behaviour I can't verify from this repo.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again (sixth time this PR) — this review is from reading the code. The PR reports 89 tests passing and clippy/fmt clean; please rely on CI. Since this push is a rebase onto a master that only bumped the version, a green CI run here should be the same green as before.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

The fixture claimed an `updatedAt` on a listed run. The endpoint deliberately
does not publish one — the column behind it is frozen at the launch insert, so a
field with that name would never update — and a fixture that carries what the
transport does not is the kind of self-consistent wrong stub that green-lights a
reader nobody has actually exercised.

Nothing read it, so this is fidelity only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @MikeNitsenko's task in 57s —— View job


Re-review of d4b80af — a one-line test-fixture change; inert, no findings

Full review
  • Work out what changed since my last pass (4632e05)
  • Verify the fixture change is inert
  • Try to run cargo test / clippy / fmt
  • Check existing review threads (all 6 already resolved — nothing to resolve, no dupes to skip)

What this push is

One commit, one line, inside #[cfg(test)]. a_run_renders_the_record_the_list_endpoint_publishes no longer claims an updatedAt on its run fixture:

-            "durationMs": 912_345, "updatedAt": "2026-08-24T10:15:12Z",
-            "stats": { "cubeCount": 12 }
+            "durationMs": 912_345, "stats": { "cubeCount": 12 }

No production code changed. git diff 4632e05..d4b80af touches rust/cube-cli/src/commands/dbt.rs only, and only inside the test module.

Verified inert

history_row (dbt.rs:345-368) reads exactly six names through its single cell closure — syncJobId, status, trigger, startedAt, durationMs, branchName — so the removed key was never on a read path. The pick two-spelling fallback that could once have reached a second name for one of those columns was removed back in f26eb2b, so there's no indirect reader either. updatedAt appears nowhere else in cube-cli's dbt path (only in workbooks.rs, folders.rs, workspace.rs, reports.rs, all unrelated column tables). The assertion below it is unchanged and still expects the same six cells.

The reasoning in the commit message is the right one, and worth keeping: a fixture that carries a field the transport doesn't publish is a stub that agrees with itself and with nothing else. The field it drops is the one where that's most load-bearing — a updatedAt sitting next to completedAt invites a future reader to reach for it as a fallback stamp, which is precisely the derivation human_duration_ms refuses. Removing it closes that door at the fixture rather than by comment.

The three fields the fixture still carries but doesn't assert on (deploymentId, gitRef, stats.cubeCount) are the right ones to keep: unlike updatedAt they are published, and they're what the docs point --json at, so the fixture still shows what the six columns leave out.

Standing state

Everything from my previous six passes is unchanged and still holds — nonempty_filter's trim and its both-sides assertions, the ID_COLUMN-keyed warning, printable, the u64::MAX bound, the single failure() source for both --wait messages, and the docs section. Nothing in this commit could have moved any of them.

Docs

docs-mintlify/reference/cli.mdx untouched since 2f36fe1. Same standing caveat as the last four passes: "A page holds at most 100 runs" is server behaviour I can't verify from this repo.

Not verified

cargo test, cargo clippy and cargo fmt were declined by the sandbox again (seventh time this PR) — this review is from reading the code. Given the change is a deleted JSON key that no code path reads, the risk of it breaking a build is about as low as it gets; CI will confirm.

· branch `mikhail/cub-4002-dbt-sync-history-logs`

注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

pr:community Contribution from Cube.js community members. rust 拉取请求 that update Rust code

项目

None yet

Development

Successfully merging this pull request may close these issues.

1 participant