Skip to content

docs: id-first consistency — README read-back + hard_delete, sql-client-spec example - #60

Merged
rohan-hotdata merged 1 commit into
mainfrom
docs/readme-id-first-corrections
Jul 24, 2026
Merged

docs: id-first consistency — README read-back + hard_delete, sql-client-spec example#60
rohan-hotdata merged 1 commit into
mainfrom
docs/readme-id-first-corrections

Conversation

@rohan-hotdata

Copy link
Copy Markdown
Contributor

Summary

Two README claims went stale and slipped through when the id-first change (0.11.0, #59) updated the quickstart. Found via a full claim-audit of the README (reading for truth, not string-searching the changed terms).

Fixes

  • Read your data back — said the same pipeline object reads with "no database IDs". Under id-first that path errors (no managed database resolved for this run) because the read has no id. Rewritten to configure database_id on the reading pipeline (the id printed on first-run create). (As-released wording — reads require a pinned id. If the read-after-write follow-up lands, this relaxes again.)
  • Feature support / hard_delete — was marked ❌; it's been supported since 0.10.0 (merge deletes flagged rows by key). Corrected to ✅ with the primary_key requirement noted.

Release note

Docs-only. Updates the GitHub README on merge; the corrected README reaches PyPI with the next version publish (the frozen v0.11.0 tag/artifact is unchanged). No release needed.

@rohan-hotdata
rohan-hotdata requested a review from a team as a code owner July 24, 2026 17:31
@rohan-hotdata
rohan-hotdata requested review from anoop-narang and removed request for a team July 24, 2026 17:31
Comment thread README.md Outdated
| `primary_key` | ✅ | Drives merge/upsert and insert-only de-duplication |
| `merge_key` | ❌ | Use `primary_key` |
| `hard_delete` | | Deletes are not propagated |
| `hard_delete` | | On `merge`, rows flagged by the hint column are deleted by key while the rest upsert; requires a `primary_key`. Boolean hint deletes on `True`, other types on non-null |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) The ⚠️ marker defined in the legend on line 126 is never used anywhere in the tables, and this row is the closest fit — support is conditional on a primary_key, and the without-key behaviour is silent: job_client.py:314-318 routes keyless merge through _combine_and_replace(..., apply_hard_delete=True), which drops the flagged rows from the load without deleting the matching rows already in the table, and logs nothing. Consider ⚠️ plus a clause saying what happens with no key, so a user who forgets primary_key doesn't read "✅" and assume deletes propagated.

Comment thread README.md
## Read your data back

The **same `pipeline` object** that writes can read, through dlt's standard [dataset interface](https://dlthub.com/docs/general-usage/dataset-access/dataset) — no Hotdata-specific code, no database IDs, no hand-written SQL:
Reads address the managed database by **id** (names aren't identifiers), so point the reading pipeline at the `database_id` you pinned after the first load (see [Quickstart](#quickstart)). Then dlt's standard [dataset interface](https://dlthub.com/docs/general-usage/dataset-access/dataset) reads it back — no Hotdata-specific code, no hand-written SQL:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) Same claim-audit target as the ones this PR fixes, just above the diff: the tagline on line 9 ("then read it back through the same pipeline") and the highlight on line 17 still describe the read as happening through the same pipeline. That holds only when database_id was pinned on the writing pipeline — which the Quickstart deliberately doesn't do on first run, which is exactly why this section now constructs a second pipeline. A short qualifier on line 9 would keep the intro and this section consistent.

claude[bot]
claude Bot previously approved these changes Jul 24, 2026
rohan-hotdata added a commit that referenced this pull request Jul 24, 2026
Keyless merge falls back to _dlt_id, which a source's delete row won't match,
so without a primary_key flagged rows are dropped from the batch but existing
rows aren't deleted (silently). ✅ overstated it — use ⚠️ with the no-key
caveat. Addresses claude[bot]'s review on PR #60.
Comment thread README.md
## Read your data back

The **same `pipeline` object** that writes can read, through dlt's standard [dataset interface](https://dlthub.com/docs/general-usage/dataset-access/dataset) — no Hotdata-specific code, no database IDs, no hand-written SQL:
Reads address the managed database by **id** (names aren't identifiers), so point the reading pipeline at the `database_id` you pinned after the first load (see [Quickstart](#quickstart)). Then dlt's standard [dataset interface](https://dlthub.com/docs/general-usage/dataset-access/dataset) reads it back — no Hotdata-specific code, no hand-written SQL:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) Anchored here because it's the nearest diff line — the actual spot is Verify a load, lines 266–277. Those CLI examples still address the database by name (hotdata databases tables list --database sales, hotdata query ... -d sales), which sits awkwardly next to line 96 ("names are not unique, so a name can't identify one") and next to this section, which now tells readers to address reads by id. The Quickstart actively produces the ambiguous case: an unpinned second run creates another database labelled sales, so -d sales then has two candidates and the verify step may inspect the wrong one.

If the CLI takes an id, reusing the db_abc123 printed on first-run create would put the whole README on one addressing story; if it's name-only, a one-line "the CLI resolves by label — pin the id if you have duplicates" covers it. Out of this PR's diff, so entirely your call.

claude[bot]
claude Bot previously approved these changes Jul 24, 2026

@claude claude 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.

Both corrections check out against the code: reads resolve id-first (hotdata_client.py:119 raises no managed database resolved for this run with no pinned id), and the hard_delete ⚠️ row matches both paths — keyed merge splits and deletes by key (job_client.py:296-310), keyless merge falls back to _dlt_id in combine_tables (merge.py:149-153) so flagged rows are dropped without propagating deletes. One out-of-diff super nit inline, non-blocking.

@rohan-hotdata rohan-hotdata changed the title docs: correct README read-back (id-first) + hard_delete support docs: id-first consistency sweep (README read-back, hard_delete, spec/handover/dep-compat) Jul 24, 2026
@rohan-hotdata rohan-hotdata changed the title docs: id-first consistency sweep (README read-back, hard_delete, spec/handover/dep-compat) docs: id-first consistency — README read-back + hard_delete, sql-client-spec example Jul 24, 2026
Comment thread docs/sql-client-spec.md
Comment on lines +459 to +462
# NOTE (id-first, 0.11.0): reads resolve the managed DB by id. In a single
# process the read needs a database_id — pin the id logged on the first-run
# create above via hotdata(database_id="db_..."). Without it the read raises
# "no managed database resolved". See README "Read your data back".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) The Acceptance line 9 lines below (line 471) is the claim this note contradicts, and the sweep left it: "step 2 prints DataFrames matching what step 1 wrote — with no database IDs, no -d flag, no CLI — proving the same pipeline object writes and reads."

The note is correct — execute_sql goes through _require_db(), and the resolved record is cached on the HotdataClientConfiguration instance (_hotdata_db, hotdata_client.py:72-79), which a later pipeline.dataset() doesn't share because dlt builds a fresh config per client. So the read does need the pinned id even in one process, and "no database IDs" is exactly the stale claim this PR is auditing for. Worth striking the "no database IDs" clause (keep "no -d flag, no CLI, no hand-written SQL") so the acceptance criterion and the note directly above it agree.

claude[bot]
claude Bot previously approved these changes Jul 24, 2026

@claude claude 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.

nit: (not blocking) Scope mismatch in the metadata, not the diff: the title says spec/handover/dep-compat and commit 63aa527's message describes edits to docs/sql-client-handover.md and docs/dependency-compatibility.md, but the PR changes only README.md and docs/sql-client-spec.md — neither of those files exists in the repo (docs/ holds architecture.md, runbook.md, sql-client-spec.md). Either those edits belong to a different repo and got described here by mistake, or they were dropped. Worth trimming the title/commit body before merge so the history doesn't record a sweep that didn't happen.

The two content corrections check out: keyless merge routes through _combine_and_replace(..., apply_hard_delete=True) with fallback_key="_dlt_id", so flagged rows are dropped from the batch and existing rows survive — the ⚠️ wording matches. The keyed path (job_client.py:289-303) and the boolean-vs-non-null rule (merge.py:56-63) match too.

rohan-hotdata added a commit that referenced this pull request Jul 24, 2026
Addresses two README review nits on PR #60:
- intro tagline said 'read it back through the same pipeline' — reads need the
  pinned database_id, so reword to dlt's dataset interface.
- 'Verify a load' addressed the DB by name (-d sales); unpinned reruns can mint
  duplicate 'sales' DBs, making the name ambiguous. Use the id (db_abc123, as
  printed on first-run create) so the whole README addresses by id.
Comment thread docs/sql-client-spec.md
Comment on lines +459 to +462
# NOTE (id-first, 0.11.0): reads resolve the managed DB by id. In a single
# process the read needs a database_id — pin the id logged on the first-run
# create above via hotdata(database_id="db_..."). Without it the read raises
# "no managed database resolved". See README "Read your data back".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) This note is correct, but the Acceptance line 9 lines below (line 471) still asserts the opposite: "step 2 prints DataFrames matching what step 1 wrote — with no database IDs, no -d flag, no CLI — proving the same pipeline object writes and reads." As of 0.11.0 the read goes through _require_db() (hotdata_client.py:104-118), which raises KeyError("no managed database resolved for this run (set database_id)") unless an id is configured — and tests/test_e2e_inmemory.py:_dest pins database_id for every pipe.dataset() test, so there's no in-process path that reads without one.

Leaving the acceptance criterion as-is makes this doc contradict itself within ten lines, which is the exact class of stale claim the PR is auditing for. Suggest striking just the "no database IDs" clause and keeping the rest.

Comment thread docs/sql-client-spec.md

# 2) READ (unlocked by this spec)
# NOTE (id-first, 0.11.0): reads resolve the managed DB by id. In a single
# process the read needs a database_id — pin the id logged on the first-run

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) Two more name-based leftovers in this same file that the sweep didn't reach — flagging together since the commit describes a "full claim-audit of docs/":

  • line 260: "right after resolving name→id" — there is no name→id resolution left; _bind_by_id fetches GET /databases/{id}.
  • lines 485–486 (the §15 flow diagram): client.execute_sql(sql, database="roundtrip_demo") / (resolve name→id, submit, poll, fetch Arrow).

Either is a one-word fix, or an archival banner on the doc as a whole would cover them along with the example on line 451 that still constructs hotdata(database_name=..., ...) with no id.

Separately: the commit message for 63aa527 ("docs: id-first consistency sweep across docs/") describes edits to docs/sql-client-handover.md (archival banner) and docs/dependency-compatibility.md (struck roadmap note), but neither file exists in docs/ (only architecture.md, runbook.md, sql-client-spec.md) and neither appears in the diff — the commit only touched this file. Worth amending the message before merge so the history of a claim-audit PR doesn't itself record two changes that never happened.

Comment thread README.md
@rohan-hotdata
rohan-hotdata force-pushed the docs/readme-id-first-corrections branch from 21e5eab to dca3cb6 Compare July 24, 2026 17:54
Comment thread README.md Outdated
| `primary_key` | ✅ | Drives merge/upsert and insert-only de-duplication |
| `merge_key` | ❌ | Use `primary_key` |
| `hard_delete` | | Deletes are not propagated |
| `hard_delete` | ⚠️ | **With** a `primary_key`: on `merge`, flagged rows are deleted by key while the rest upsert (boolean hint deletes on `True`, other types on non-null). **Without** a `primary_key`, flagged rows are dropped from the batch but existing matching rows are *not* deleted — deletes silently don't propagate |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) The "With a primary_key" half is true only for the default upsert merge strategy. With merge_strategy="insert-only" and a primary_key, job_client.py:314-318 routes to _combine_and_replace("insert-only", apply_hard_delete=True), which drops the flagged rows from the batch and leaves existing matching rows in place — same silent non-propagation as the keyless case. tests/test_job_client.py:348 (_hd_table(strategy="insert-only") sets keyed=True) and :389 both assert exactly that.

Since the strategies table two rows up lists insert-only as ✅, a user on that strategy reads this row's "with a primary_key" clause and gets the wrong answer. Suggest scoping the condition to the strategy, e.g. "On merge with a primary_key (default upsert strategy): …", and folding insert-only in with the keyless case ("flagged rows are dropped from the batch but existing rows are not deleted").

claude[bot]
claude Bot previously approved these changes Jul 24, 2026
Comment thread docs/sql-client-spec.md
# NOTE (id-first, 0.11.0): reads resolve the managed DB by id. In a single
# process the read needs a database_id — pin the id logged on the first-run
# create above via hotdata(database_id="db_..."). Without it the read raises
# "no managed database resolved". See README "Read your data back".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: (not blocking) The note is right, but the Acceptance line nine lines down (line 471–472) still reads "with no database IDs, no -d flag, no CLI — proving the same pipeline object writes and reads." So the doc now asserts both sides within ten lines, in the PR whose stated job is retiring exactly this stale claim. Striking just the "no database IDs" clause (keeping "no -d flag, no CLI") makes the acceptance criterion agree with the note directly above it.

Comment thread README.md
Comment on lines +104 to +107
pipeline = dlt.pipeline(
pipeline_name="my_pipeline",
destination=hotdata(workspace_id="your_workspace_id", database_id="db_abc123"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) This rebinds the same pipeline name from the Quickstart but with a slimmer destination — declared_tables (and database_name) are gone. A reader who pastes both into one script and later calls pipeline.run(...) on this object still writes to the right database (id is pinned) but has silently dropped declared_tables, which the "Multiple tables" section (line ~264) says you need when adding a table on a later run.

Showing it as the Quickstart config plus database_id would keep one pipeline definition across write and read:

Suggested change
pipeline = dlt.pipeline(
pipeline_name="my_pipeline",
destination=hotdata(workspace_id="your_workspace_id", database_id="db_abc123"),
)
pipeline = dlt.pipeline(
pipeline_name="my_pipeline",
destination=hotdata(
workspace_id="your_workspace_id",
database_id="db_abc123", # the id printed on first-run create
declared_tables=["orders"],
),
)

claude[bot]
claude Bot previously approved these changes Jul 24, 2026
Bring the docs in line with 0.11.0's id-first addressing (#59):

README.md
- Read-back: reads resolve the managed database by id, so configure
  database_id on the reading pipeline (drops the stale "no database IDs"
  claim that now errors).
- hard_delete: marked conditional (⚠️) — deletes propagate only on the
  default `upsert` merge strategy with a primary_key; keyless merge and
  insert-only drop flagged rows without removing existing ones.
- Intro tagline and "Verify a load" now address the database by id, so the
  whole README tells one addressing story (avoids the ambiguous
  duplicate-name case).

docs/sql-client-spec.md
- Note that the write→read example needs a pinned database_id under id-first.

Docs-only; found via a full claim-audit of the docs rather than a string search.
Comment thread docs/sql-client-spec.md
print(info)

# 2) READ (unlocked by this spec)
# NOTE (id-first, 0.11.0): reads resolve the managed DB by id. In a single

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

super nit: (not blocking) Anchored on the nearest diff line — the actual spot is docs/runbook.md:43, which the sweep didn't reach:

  1. Build pipeline with destination=hotdata(database_name=..., declared_tables=[...]).

Under id-first that recipe makes every run of the new pipeline create a fresh managed database — same class of stale name-addressing guidance this PR is retiring, and it contradicts the demo section eleven lines above (runbook.md:34-37), which already tells you to pass --database-id on later runs. A database_id=... in the tuple, or a trailing "(pin database_id to reuse the same database on later runs)", covers it.

Everything else checks out: the hard_delete row matches job_client.py:287-322 and merge.py:148-168 (keyed non-insert-only merge splits upsert/delete; insert-only and keyless merge drop the flagged rows without removing existing ones — tests/test_job_client.py:389,402), the read-back section matches _require_db() (hotdata_client.py:104-119), and the ⚠️ legend on line 131 now has a user.

@claude claude 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.

Cycle-2 re-review: prior blocking items — none were blocking; the substantive nits are addressed. hard_delete is now ⚠️ and correctly scoped to keyed non-insert-only merge, the intro tagline no longer claims same-pipeline read-back, the read example carries declared_tables, and "Verify a load" is id-addressed (flag confirmed by the author). Verified the new claims against job_client.py, merge.py, hotdata_client.py, and tests/test_job_client.py. One out-of-diff super nit left inline.

@rohan-hotdata
rohan-hotdata merged commit b14daf4 into main Jul 24, 2026
2 checks passed
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