feat: combine collapse and delete-marker correctness, plus per-package release automation (RPL-6000, RPL-6780) - #249
johnnyC9000 wants to merge 10 commits into
Conversation
…795)
A dimension/fact delete in this connector is a soft close, so it needs the
target row to exist. When an entity was created and then deleted within one
combine batch, the collapse discarded the insert's data (lastObj = data),
leaving a bare tombstone that the loader then wrote as a sparse row (data
columns null, date/dimension FKs defaulted to 1, never closed).
Fix the collapse to preserve data while keeping last-event-wins ordering:
- insert-then-delete: the delete wins, but the insert's data is carried onto it,
so the row is created-then-soft-closed instead of tombstoned.
- delete-then-insert: the later write wins and reactivates the entity (the delete
is dropped), matching ES-2516 ("clear the deleted flag when loading data" — a
data load after a delete undeletes the row).
The collapse decision is extracted into a pure, leo-sdk-free module
(combine-records.js) so it can be unit-tested without the leo-sdk/aws-sdk require
chain. Adds combine unit tests (there were none before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Adds a conventional-commit-driven release workflow that discovers the nine publishable packages, resolves each one's next version from the highest stable version on npm (reconciled against package.json), packs and publishes only changed packages, and authenticates via npm Trusted Publishing (OIDC) instead of a static NPM_TOKEN. Master publishes clean semver to latest; other branches publish x.y.z-rc.<run_id> to the rc dist-tag. Also fixes the repository field on all nine package.json files (missing or using the deprecated git:// protocol), which npm provenance requires.
- leo-connector-common@5.1.0 - leo-connector-elasticsearch@3.1.0 - leo-connector-entity-table@4.1.0 - leo-connector-mongo@4.1.0 - leo-connector-mysql@3.1.0 - leo-connector-oracle@2.1.0 - leo-connector-postgres@5.1.0 - leo-connector-redshift@4.1.0 - leo-connector-sqlserver@4.1.0
|
|
||
| on: | ||
| push: | ||
| branches: [master, fix/rpl-5795-combine-soft-delete-ordering] |
There was a problem hiding this comment.
Feature branch left in push trigger
High Severity
The push trigger still lists fix/rpl-5795-combine-soft-delete-ordering, so every push to that branch runs the full release pipeline and can publish rc packages to npm. The header comments say RCs are meant to come only from workflow_dispatch; this looks like temporary test scaffolding that would also remain on master after merge.
Reviewed by Cursor Bugbot for commit 0d3dc73. Configure here.
npm already has leo-connector-common v5 prereleases published, so the existing highest-stable-version baseline would resolve new patch/minor releases as v5.x. v5 is being developed on a separate branch and isn't ready to publish yet, so cap the baseline lookup at v4 for this package and exclude it from a run entirely (rather than silently publishing into v5) if a breaking-change commit would otherwise cross the ceiling.
An earlier push accidentally triggered a real (non-dry-run) release.yaml run before the v4 version ceiling was in place, which committed leo-connector-common@5.1.0 to this branch. Reset it to 4.1.0, the version the ceiling-respecting resolver actually computes.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a05e0a6. Configure here.
…eous bumps leo-connector-common's v5.1.0 was already corrected to v4.1.0 in a05e0a6, but the other 5 packages whose majors diverge from feature/aws-sdk-v3-again (elasticsearch, entity-table, mongo, postgres, redshift) still carried the same erroneous major-jump from the same release.yaml run before ceilings were in place. Add a VERSION_CEILINGS entry for every leo-connector-* package (prior major from development for the 6 that diverge; current major for mysql/oracle/sqlserver as a no-op guard against a future collision), then revert each affected package.json/package-lock.json to <dev major>.1.0 - the version the ceiling-respecting resolver actually computes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
#252) combine() groups records by natural key, so an FK-keyed delete's synthetic `_del_<value>` marker lands in a group of its own and is never compared against the affected row's writes. The ordering information needed to compare them already exists — combine assigns every record a batch-global arrival counter before grouping — but it is written only into the sort-line prefix and dropped when the line is parsed back. Carry that counter on the emitted record as `__leo_seq__` so a consumer can order two records that came out of different groups. Underscore-prefixed to match the internal-column convention, so a connector's "does this record carry data columns?" checks keep classifying a bare tombstone as data-less. - combine.js: `opts.emitSequence`, default off. Assigned after parseValues so the field name is never subject to its key normalization. - combine-records.js: when a delete wins the fold, the collapsed record takes the delete's counter rather than keeping the earlier write's — it now represents the delete and must be ordered as of it. - load.js: derives the option from a `client.emitCombineSequence` capability, the same opt-in shape #250 uses for resolveDeleteKeys. With emitSequence off the emitted records are byte-identical to before, so every existing connector is unaffected. Also restores combine()'s discarded `opts` argument (Object.assign was called with a single argument, so dateFormat could never be overridden); no in-repo caller passed opts, so nothing changes today. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-6780) (#253) checkforDelete writes every delete marker's grouping value under a column literally named `id`. combine() groups records by the table's natural-key columns, so for any table whose natural key is named something else that column is left undefined on every marker — they all hash to the same combine group and all but one are silently dropped. Reproduced through the real load.js pipeline with f_shipping_label_package (nk `package_id`), the one table in the current dw_fields audit with a non-`id` natural key: three deletes keyed by `package_id` reach importFact as ONE marker, and that survivor is a hybrid — `id` from the first record, __leo_delete_id__ from the last. Two of the three deletes never happen. Write the value under the natural key's actual name instead. A delete keyed by the natural key carries the real key, so it groups with that row's writes; a delete keyed by anything else carries a `_del_`-prefixed value under the same column, which keeps it isolated from real rows while staying distinct per id. When the natural key is unknown here or composite, the historical `id` shape is preserved rather than guessed at. For every table whose natural key IS `id` — which is every table on RPL-6780's exposure list — the emitted marker is byte-identical to before. Verified by running an nk=`id` table through the same pipeline: `id: 'R1'` for a natural-key delete and `id: '_del_555'` for an FK-keyed one, exactly as today. The marker builder is split into delete-marker.js so it can be unit-tested without leo-sdk, which does not load in this package — the same reason combine-records.js was split out of combine.js. This is a data-loss fix and is independent of how RPL-6780's ordering question is settled; it changes no ordering behavior. The bug is latent today: the affected table is live in production (100k rows, loading normally) but no producer emits delete events for it, and its Delta history shows no delete-flush at all. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…air the push-range existence check Two defects in plan-release.mjs, both found by Cursor Bugbot on PR #249. readEventBefore never returned a sha. 'git cat-file -e' is a predicate: it exits 0 and prints nothing on success, so gitOrNull returns "" and '!gitOrNull(...)' is always true. On a master push with no release tags yet, resolveRange therefore fell through to HEAD~1..HEAD, which under --no-merges loses every commit but the last for a multi-commit or multi-merge push. Adds a gitOk predicate that tests the exit status; gitOrNull stays for the seven call sites whose commands print on success. The ceiling guard checked the bump candidate rather than the version that would actually be published. 'next = maxVersion(candidate, pkg.version)' ran after the check, so a package.json already parked above its ceiling — the state this branch was in at 0019b04, corrected by hand in a05e0a6 — passed the guard and carried the higher version into the publish matrix. Computes next first and guards that. Verified both against the four ceiling cases and against real and nonexistent shas: only the package.json-above-ceiling case changes outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eferences This is a public repository. Source comments, test titles and fixture values carried issue-tracker keys, a downstream table and column name, and a private repo name from the environment this library is developed against. None of it is needed to understand or reproduce the behavior being described, and published leo-connector-* tarballs carry these files onto the public npm registry. Replaces each with the behavior it names: the same-batch collapse defect, the reactivation rule, a table whose natural key is not named `id`. Test fixtures move to generic identifiers. Comments, describe titles and fixture strings only - no logic changes, and the suite passes unchanged (17 tests). Lint error count is identical before and after; the 41 pre-existing style errors are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>


What's on this branch
Three separable concerns, in commit order. The middle one is unrelated to the other two and is here because per-package publishing had to exist before either library change could reach a consumer.
09e7a724aef416,0d3dc73,0019b04,a05e0a6,35a390160a5f53,193ec6e1. Same-batch create+delete discards the insert's data
Problem. A dimension/fact delete in this connector is a soft close, so it needs the target row to exist. When an entity is created and then deleted within one
combinebatch, the collapse incommon/datawarehouse/combine.jsdiscarded the insert's data (lastObj = data), leaving a bare tombstone that the loader wrote as a sparse row — data columns null, date/dimension FKs1, never closed. Confirmed against a production dimension table, where a measurable population of such rows had accumulated.Fix. Collapse same-key records with last-event-wins ordering, but never discard data. The decision moves into a pure module,
common/datawarehouse/combine-records.js:Extracting it lets the collapse be unit-tested without importing
combine.js, which pulls inleo-sdk/aws-sdk.2. Per-package npm release automation
Adds
.github/workflows/release.yamland.github/scripts/plan-release.mjs, plus.nvmrcpinning Node 22.Push to
masterpublishes clean conventional-commit semver to thelatestdist-tag.workflow_dispatchon any branch publishesx.y.z-rc.<run_id>to therctag, which is how release candidates come off feature branches without touchinglatest.plan-release.mjsderives each package's bump from conventional commits, baselines against the newest stable version actually published to npm (taking the max withpackage.jsonrather than trusting it alone), and enforces a per-package major ceiling so a bump cannot stray into a reserved major line.Publishing uses npm Trusted Publishing with OIDC provenance. Per the workflow's own header, each package is configured on npmjs.com to accept publishes only from this repo running this exact workflow file — so the file must not be renamed.
Manifests gain
repository.directory(required for provenance in a monorepo), and the nine packages are normalized off their-rcand drifting versions onto clean minors:common4.0.13-rc → 4.1.0 ·postgres4.0.24-rc → 4.1.0 ·entity-table3.0.22-rc → 3.1.0 ·elasticsearch2.0.5 → 2.1.0 ·mongo3.0.7 → 3.1.0 ·mysql3.0.3 → 3.1.0 ·oracle2.0.1 → 2.1.0 ·redshift3.0.3 → 3.1.0 ·sqlserver4.0.1 → 4.1.00d3dc73briefly pushedcommonto 5.1.0;0019b04anda05e0a6pin it back to the v4 line, and35a3901adds the ceilings that prevent a repeat.3. Cross-group ordering hooks (#252, #253)
Both were merged into this branch rather than into
development, so they land together with the collapse fix they build on.#252 — opt-in per-record arrival sequence (
60a5f53).combine()already assigns every record a batch-global arrival counter before grouping by natural key, then discards it atline.substr(42). This carries it on the record as__leo_seq__when a client opts in viaclient.emitCombineSequence, and restores theoptsargumentcombine()accepted but dropped. Default off — withemitSequencefalse the emitted records are byte-identical to before, so the postgres/Redshift path is unaffected.#253 — key a delete marker by the table's real natural key (
193ec6e).checkforDeletewrote every marker under a column literally namedid. For a table whose natural key is named something else, that left the natural-key column undefined on every marker, so all of them hashed into one combine group and every delete but the last was silently dropped — data loss, not just misordering. Marker construction moves intocommon/datawarehouse/delete-marker.jsand resolves the table's real natural key. Composite and unresolvable keys keep the historicalidshape rather than guess.Tests
common/had nocombinecoverage before this branch. Added:common/test/datawarehouse/combine.test.js— 7 cases across every collapse pathcommon/test/datawarehouse/combine-sequence.test.js— 4 cases on sequence carry-over through the foldcommon/test/datawarehouse/delete-marker.test.js— 6 cases on marker keying, including distinct-group and non-idnatural keysAll three are pure-function tests with no
leo-sdkimport, so they run without AWS credentials or a configured RStreams environment.npm testincommon/is green (17 tests).eslintreports 41 pre-existing style errors on these files, unchanged by this branch — the linter and the committed style have been out of sync here for a while.Not covered:
release.yamlandplan-release.mjshave no tests. The release path has been exercised by running it, not by a suite.Scope and rollout
leo-connector-common; consumers pin an exact version, so nothing floats in automatically — a change reaches a connector only when that connector bumps its pin.importDimensionchange; the two close the defect together.combineOrder) is stacked on this branch and targets it directly, so this branch must not be renamed or deleted while that PR is open.Open review findings
Three Cursor Bugbot findings are unresolved as of
193ec6e, all in the release automation:release.yaml:14. The push trigger still listsfix/rpl-5795-combine-soft-delete-orderingalongsidemaster, so every push to this branch runs the release pipeline. Non-masterrefs publish as-rc.<run_id>, so this isrc-channel only, but the branch name would remain in the trigger onmasterafter merge.plan-release.mjs:207.git cat-file -esucceeds with empty stdout, so!gitOrNull(...)is always true andreadEventBeforealways returns null.resolveRangethen falls back toHEAD~1..HEAD, which with--no-mergescan miss a whole push.plan-release.mjs:344-361. The ceiling check only rejects when the npm-baseline candidate exceeds the cap. Ifpackage.jsonis already above the ceiling,maxVersionkeeps the higher value and the package stays in the publish matrix.🤖 Generated with Claude Code
Note
Medium Risk
Warehouse load/combine behavior changes affect dimension/fact correctness when consumers upgrade
leo-connector-common; new release automation can publish wrong versions or miss commits if misconfigured (open review notes on push triggers and commit-range logic).Overview
Fixes datawarehouse batch collapse and delete-marker grouping in
leo-connector-common, and adds per-package npm release automation for the monorepo.Combine / soft-delete: Same-natural-key collapse moves to
combine-records.jswith last-event-wins rules that keep row data when insert/update is followed by delete (populated soft-close instead of a sparse tombstone), and drop delete intent when a write follows a delete.combine()also honors itsoptsagain and can opt in to stamping__leo_seq__viaclient.emitCombineSequence(default off, so existing emitted records stay unchanged).Delete markers:
delete-marker.jskeys markers on the table’s real natural-key column (viaload.js), fixing batches where non-idkeys left the NK undefined and collapsed many deletes into one combine group.Release: New
.github/workflows/release.yamlandplan-release.mjsplan bumps from conventional commits per package folder, baseline against npm’s max stable version (with temporary major ceilings), commit version bumps/tags onmaster, and publish via OIDC trusted publishing (lateston master,x.y.z-rc.<run_id>on other branches). Adds.nvmrc(Node 22),repository.directoryon manifests, and bumps all nine connector packages to clean minor versions.Tests: New unit tests for
combineRecords, delete-marker keying, and sequence carry-over incommon/test/datawarehouse/.Reviewed by Cursor Bugbot for commit 34819d4. Bugbot is set up for automated code reviews on this repo. Configure here.