A CSV import can report “120 rows updated” and still overwrite the wrong customer records.

The difficult part is deciding what a row means when a customer already exists, a field is blank, or someone changes the CRM after the preview. Parsing the file is only the first boundary.

Consider this fictional import into a small service company's CRM:

External customer ID Name Phone Assigned owner
C-104 Northfield Repairs empty cell Sam
C-104 Northfield Repairs Ltd 555-0100 blank
C-205 Harbor Studio 555-0101 Ines

The identifiers and businesses are illustrative. The two C-104 rows conflict before the importer reaches the database. A generic “upsert every row” instruction hides that decision.

Decide what a blank cell means

A blank value might mean “leave the existing value unchanged,” “clear this field,” or “the exporter didn't include it.” Those are different operations.

Represent the choice explicitly in the import mapping. If clearing is supported, make it distinguishable from an omitted field. Show the consequence in the preview: retained, replaced or cleared. Do the same for defaults such as the assigned owner.

Avoid using visual emptiness as a destructive instruction. A business user shouldn't have to know whether an empty string becomes SQL NULL to understand the outcome.

Choose an identity rule before choosing an upsert

Use a stable identifier whose scope is understood, such as the source-system customer ID within a particular account. A name is usually an unsafe merge key. An email address can also change or be shared.

PostgreSQL's INSERT documentation describes how ON CONFLICT uses a conflict target and can either skip or update a conflicting row. That machinery doesn't decide whether two records represent the same customer. The application must establish the identity rule and permitted updates.

Detect duplicate keys inside the incoming file as a separate validation step. An operator should see both source row numbers and a proposed resolution. Silently choosing whichever row happened to run last makes the result depend on processing order.

Bind approval to a specific import plan

Store an import ID, the source file's fingerprint, mapping version, normalization rules and validation results. The preview should describe the exact batch awaiting approval.

If the file or mapping changes, require a new preview. If existing CRM records can change while the operator reviews the file, define a freshness rule. For example, compare the reviewed record version when applying each update and hold a row whose current version differs.

A file fingerprint alone cannot detect changes in the destination database. The approval and commit checks need to cover both sides of the proposed change.

Make the transaction boundary visible

A single transaction can provide an all-or-nothing database change for the work inside it. PostgreSQL's transaction tutorial explains this atomicity. A rollback issued before commit cancels that transaction's changes.

After a successful commit, “undo import” requires a separate design. Restoring a previous field value blindly could overwrite a legitimate edit made after the import. Keep enough change provenance to identify what the import actually wrote, and require conflict handling before reversing it.

For larger jobs, a deliberately chunked import may be appropriate. Its interface should show committed, rejected and unprocessed rows separately. An interrupted batch is not equivalent to zero writes.

Keep external effects out of the guesswork

Updating the CRM may trigger notifications, accounting synchronization or onboarding. A database rollback won't automatically retract an email that has already been sent.

Record intended downstream work durably, dispatch it under a defined retry policy and keep its state separate from the row's import state. Retrying the import should not silently send a second welcome message.

Use an acceptance matrix before importing live data

These are proposed test cases, not a claim that a particular importer has passed them:

Test input or event Expected evidence
Two incoming rows share an identity key Explicit conflict with both row numbers
A mapped phone field is blank The chosen retain-or-clear policy is shown
A record changes after preview Revalidation or a visible conflict hold
The operator repeats the same import request Existing batch status, without duplicate effects
Processing stops after one committed chunk Accurate partial-progress receipt
Undo meets a newer staff edit Manual resolution rather than a blind overwrite

A useful handover includes the approved mapping, identity policy, import receipt, rejected-row report and recovery procedure. That gives a nontechnical owner something concrete to inspect before trusting the next bulk change.

Muhammad Abdullah is co-founder of StackEndBox, a software engineering company building web applications, custom software and integrations.