Skip to content

v0.16 — idempotent writes (upsert + CSV --upsert)#16

Merged
wavyx merged 8 commits into
mainfrom
feat/v0.16-idempotent-bulk
Jun 11, 2026
Merged

v0.16 — idempotent writes (upsert + CSV --upsert)#16
wavyx merged 8 commits into
mainfrom
feat/v0.16-idempotent-bulk

Conversation

@wavyx

@wavyx wavyx commented Jun 11, 2026

Copy link
Copy Markdown
Owner

v0.16 — idempotent writes

Match-or-create that never guesses, plus the CSV equivalent.

Added

  • person upsert / org upsert / deal upsert — match by --by (a built-in key or a searchable custom field), then create if absent or PATCH only the changed fields if exactly one matches. More than one match refuses with exit 65 (search exact_match is not a unique key, so every candidate is re-verified against its full record before the count decides). --dry-run previews; table prints a one-line summary, --output json the full action result.
  • person import / org import --upsert --match-on <field> — the CSV equivalent: each row matched on its --match-on value, then created or PATCHed. Per-row failures (ambiguous / empty match) are collected without aborting; reported as created/updated/unchanged counts. A batch whose failures are all data-validation errors exits 65, else 1.

Changed

  • diffBody compares emails/phones by value set (case-insensitive, ignoring primary/label) and label_ids/multi-option custom fields order-insensitively, so an unchanged re-run issues no PATCH.
  • CSV import rejects duplicate column headers (exit 65) instead of silently dropping data.

Quality

  • Contrarian review (5 dimensions, adversarial verify): fixed NaN-on-numeric-custom-field (exit 65), spurious-PATCH idempotency, lost exit code in batch failures, duplicate-header data loss. Dismissed a false positive (per-entity /search does support fields).
  • Live-tested on the sandbox, which caught two bugs the static review could not: per-entity /search caps limit at 100 (not 500), and the search result item is a lossy projection (emails/phones as bare strings, custom_fields unattributed) — so lookup now uses search as a candidate finder and verifies against each candidate's full record. Verified end-to-end: create → unchanged on re-run → updated on change → exit 65 on a duplicate.

100% coverage on touched files; npm run lint clean.

wavyx added 8 commits June 11, 2026 11:47
Thin commands over a shared upsertWithDefs/summarizeUpsert lib pair:
fetch field defs, build the write body (--field/--body), match by --by
(built-in key or searchable custom field), then create or PATCH only the
changed fields. Ambiguous matches refuse with exit 65 — never guess.
--dry-run previews without writing; table prints a one-line summary,
json emits the full action result.
Adds an idempotent CSV mode: with --upsert --match-on <field>, each row is
matched on that field's per-row value, then created if absent or PATCHed
(changed fields only) if exactly one matches. Ambiguous rows (and empty
match values) are collected as per-row failures and exit 1 without aborting
the batch — never guessing which record to write. --dry-run looks up and
reports created/updated/unchanged counts without writing. Shared
bulkUpsertRows lib runs the batch through bulkRun's pacing.
- lookup: reject a non-numeric value for a numeric custom field (exit 65)
  instead of coercing to NaN — NaN loses every comparison, so a match would
  silently miss and create / inject a NaN value.
- diffBody: set-aware field equality so re-running an unchanged upsert emits
  no PATCH. emails/phones compare by their value set (case-insensitive,
  ignoring the primary/label flags the API echoes); label_ids and multi-option
  custom fields compare order-insensitively.
- bulk: preserve each failed item's exit code; CSV --upsert now exits 65 when
  every row failure is a data-validation error (ambiguous / empty match) and 1
  for mixed or transport errors.
- import: reject duplicate CSV column headers (exit 65) rather than silently
  keeping only the last cell.

Dismissed: a finding that per-entity v2 /search rejects the 'fields' param —
the OpenAPI spec confirms persons/deals/organizations search all accept
'fields' (enum incl. custom_fields) and 'cursor'.
CHANGELOG 0.16.0; README idempotent-writes section; bulk guide gains an
upsert (match-or-create) section + CSV --upsert/--match-on; agents page gains
the upsert capability paragraph. Regenerated command reference (145 commands)
and cli-stats. Version bump to 0.16.0.
Two bugs only live testing on the sandbox surfaced:

- The per-entity /search endpoints cap `limit` at 100 (the API 400s on more,
  despite the 500 list cap) — was sending 500, so every lookup errored.
- The search result `item` is a lossy projection: emails/phones come back as
  bare strings (not {value} objects) and custom_fields as an unattributed
  value array. Verifying against it (and using it as the record to diff)
  silently discarded every match, so each upsert created a duplicate.

lookupByField now treats the scoped exact_match search purely as a candidate-id
finder, then fetches each candidate's full record and re-verifies the matched
field against the authoritative shape — which also gives diffBody a correct
record. Verified live: create → unchanged on re-run → updated on change →
exit 65 on a duplicate.
Matching depends on Pipedrive's search index, which is eventually consistent,
so upserting the same key twice in quick succession can still create a
duplicate before the first write is indexed. Document the gotcha and the
mitigation (key on a stable external id; let the index settle).
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@wavyx wavyx merged commit acf9616 into main Jun 11, 2026
11 checks passed
@wavyx wavyx deleted the feat/v0.16-idempotent-bulk branch June 16, 2026 12:53
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.

2 participants