Skip to content

feat: combine collapse and delete-marker correctness, plus per-package release automation (RPL-6000, RPL-6780) - #249

Open
johnnyC9000 wants to merge 10 commits into
developmentfrom
fix/rpl-5795-combine-soft-delete-ordering
Open

johnnyC9000 wants to merge 10 commits into
developmentfrom
fix/rpl-5795-combine-soft-delete-ordering

Conversation

@johnnyC9000

@johnnyC9000 johnnyC9000 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

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.

Commits
1. Same-batch create+delete discards the insert's data 09e7a72
2. Per-package npm release automation 4aef416, 0d3dc73, 0019b04, a05e0a6, 35a3901
3. Cross-group ordering hooks (merged from #252, #253) 60a5f53, 193ec6e

1. 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 combine batch, the collapse in common/datawarehouse/combine.js discarded the insert's data (lastObj = data), leaving a bare tombstone that the loader wrote as a sparse row — data columns null, date/dimension FKs 1, 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:

  • insert/update → delete: the delete wins, its markers stamped onto the accumulated record → created-then-soft-closed, not a bare tombstone.
  • delete → insert/update: the later write wins and reactivates the entity — loading data clears the deleted flag.
  • two writes: deep-merge (unchanged). Lone/leading delete: unchanged bare tombstone.

Extracting it lets the collapse be unit-tested without importing combine.js, which pulls in leo-sdk/aws-sdk.

Same-batch sequence (arrival order) Before After
insert, delete bare sparse tombstone (data lost) populated + soft-closed
delete, insert reactivated (write wins) unchanged
insert, update deep-merged unchanged
lone delete bare tombstone unchanged

2. Per-package npm release automation

Adds .github/workflows/release.yaml and .github/scripts/plan-release.mjs, plus .nvmrc pinning Node 22.

  • Push to master publishes clean conventional-commit semver to the latest dist-tag. workflow_dispatch on any branch publishes x.y.z-rc.<run_id> to the rc tag, which is how release candidates come off feature branches without touching latest.

  • plan-release.mjs derives each package's bump from conventional commits, baselines against the newest stable version actually published to npm (taking the max with package.json rather 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 -rc and drifting versions onto clean minors:

    common 4.0.13-rc → 4.1.0 · postgres 4.0.24-rc → 4.1.0 · entity-table 3.0.22-rc → 3.1.0 · elasticsearch 2.0.5 → 2.1.0 · mongo 3.0.7 → 3.1.0 · mysql 3.0.3 → 3.1.0 · oracle 2.0.1 → 2.1.0 · redshift 3.0.3 → 3.1.0 · sqlserver 4.0.1 → 4.1.0

    0d3dc73 briefly pushed common to 5.1.0; 0019b04 and a05e0a6 pin it back to the v4 line, and 35a3901 adds 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 at line.substr(42). This carries it on the record as __leo_seq__ when a client opts in via client.emitCombineSequence, and restores the opts argument combine() accepted but dropped. Default off — with emitSequence false 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). checkforDelete wrote every marker under a column literally named id. 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 into common/datawarehouse/delete-marker.js and resolves the table's real natural key. Composite and unresolvable keys keep the historical id shape rather than guess.


Tests

common/ had no combine coverage before this branch. Added:

  • common/test/datawarehouse/combine.test.js — 7 cases across every collapse path
  • common/test/datawarehouse/combine-sequence.test.js — 4 cases on sequence carry-over through the fold
  • common/test/datawarehouse/delete-marker.test.js — 6 cases on marker keying, including distinct-group and non-id natural keys

All three are pure-function tests with no leo-sdk import, so they run without AWS credentials or a configured RStreams environment. npm test in common/ is green (17 tests). eslint reports 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.yaml and plan-release.mjs have no tests. The release path has been exercised by running it, not by a suite.


Scope and rollout

  • Shared 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.
  • First consumer of the collapse fix is a downstream Delta-table connector, which carries the companion importDimension change; the two close the defect together.
  • Redshift/postgres unaffected until a consumer on that path bumps its pin. The same collapse defect is latent there behind a short read window; the mirror fix is to run the soft-close flush after the insert.
  • feat(common): opt-in order-field fold in datawarehouse combine — 4.x line (PMT-4302) #254 (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:

  1. Highrelease.yaml:14. The push trigger still lists fix/rpl-5795-combine-soft-delete-ordering alongside master, so every push to this branch runs the release pipeline. Non-master refs publish as -rc.<run_id>, so this is rc-channel only, but the branch name would remain in the trigger on master after merge.
  2. Highplan-release.mjs:207. git cat-file -e succeeds with empty stdout, so !gitOrNull(...) is always true and readEventBefore always returns null. resolveRange then falls back to HEAD~1..HEAD, which with --no-merges can miss a whole push.
  3. Mediumplan-release.mjs:344-361. The ceiling check only rejects when the npm-baseline candidate exceeds the cap. If package.json is already above the ceiling, maxVersion keeps 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.js with 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 its opts again and can opt in to stamping __leo_seq__ via client.emitCombineSequence (default off, so existing emitted records stay unchanged).

Delete markers: delete-marker.js keys markers on the table’s real natural-key column (via load.js), fixing batches where non-id keys left the NK undefined and collapsed many deletes into one combine group.

Release: New .github/workflows/release.yaml and plan-release.mjs plan bumps from conventional commits per package folder, baseline against npm’s max stable version (with temporary major ceilings), commit version bumps/tags on master, and publish via OIDC trusted publishing (latest on master, x.y.z-rc.<run_id> on other branches). Adds .nvmrc (Node 22), repository.directory on manifests, and bumps all nine connector packages to clean minor versions.

Tests: New unit tests for combineRecords, delete-marker keying, and sequence carry-over in common/test/datawarehouse/.

Reviewed by Cursor Bugbot for commit 34819d4. Bugbot is set up for automated code reviews on this repo. Configure here.

…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>
@ch-snyk-sa

ch-snyk-sa commented Aug 12, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@johnnyC9000 johnnyC9000 changed the title fix(combine): preserve insert data on same-batch create+delete (RPL-5795) fix(combine): preserve insert data on same-batch create+delete (RPL-6000) Aug 12, 2026
dawilk and others added 2 commits August 17, 2026 11:35
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
Comment thread .github/scripts/plan-release.mjs Outdated

on:
push:
branches: [master, fix/rpl-5795-combine-soft-delete-ordering]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0d3dc73. Configure here.

dawilk added 2 commits August 17, 2026 11:46
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.

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

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).

Fix All in Cursor

❌ 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.

Comment thread .github/scripts/plan-release.mjs
…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>
@pmogren pmogren changed the title fix(combine): preserve insert data on same-batch create+delete (RPL-6000) feat: combine collapse and delete-marker correctness, plus per-package release automation (RPL-6000, RPL-6780) Aug 31, 2026
pmogren and others added 2 commits August 31, 2026 18:37
…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>
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.

4 participants