Merge two deliveries¶
Available in: Python · Node · CLI · Browser
When: two deliveries of one project need to become one file — a phased site investigation, or a resubmission that revises some boreholes and adds others — and you want the engine to reconcile them, not a manual copy-paste.
Merging is KEY-aware, not a file concatenation. Rows are matched on each group's dictionary KEY headings, so a re-sorted borehole list still merges onto its prior self. Files merge in argument order — a later file wins a KEY conflict — and the result is a union: a row present in only one file is kept (silence is not deletion). Every surface reports the same audit: the per-row revisions a later file made, plus any warnings.
lat merge <files…> --out <merged.ags> reconciles two or more deliveries on
disk. The optional --tran-issue / --tran-date stamp the merged file's own
transmission record (it genuinely is a new transmission):
lat merge examples/sample_site.ags examples/phase2.ags --out examples/merged.ags \
--tran-issue 3 --tran-date 2024-01-15
merged 2 files → examples/merged.ags (6998 bytes)
warning [parent_revised_check_children]: PROJ was revised; verify child group(s) ["LOCA"] are still consistent (merge performs no cross-group consistency check)
1 row revision(s):
PROJ ["LAT-DEMO"]: changed ["PROJ_NAME"] (from file[1])
The summary names each revision — here the second delivery changed
PROJ_NAME, matched on the PROJ_ID KEY LAT-DEMO — and flags that a revised
parent (PROJ) has child groups worth re-checking. When two files declare a
column with different AGS types, merge errors (exit 6) rather than guess;
--on-type-clash promote or --on-type-clash widen settles it (see
When the two files disagree on a type).
laterite.merge(*sources, …) takes two or more of anything read accepts
(paths, text, bytes, Ags4File handles) and returns a MergeResult — the
merged bytes plus the warnings and per-row revisions audit:
import laterite
res = laterite.merge(
"phase1.ags", "phase2.ags",
tran=TranStamp(
issue="3", date="2024-01-15", producer="Us",
recipient="Client", status="Merged",
),
)
for rev in res.revisions:
print(rev["group"], rev["key"], "changed", rev["changed"])
res.save("merged.ags")
An AGS-type clash between two files raises MergeConflictError by default;
on_type_clash="promote" or "widen" settles it (see
below). res.text decodes the merged
bytes, and res.save(path) writes them.
merge(sources, opts) runs the same leaf and returns the same
{ bytes, warnings, revisions, text }:
import { merge } from "laterite";
const res = merge(["phase1.ags", "phase2.ags"], {
tran: {
issue: "3", date: "2024-01-15", producer: "Us",
recipient: "Client", status: "Merged",
},
});
for (const rev of res.revisions) {
console.log(rev.group, rev.key, "changed", rev.changed);
}
A bare string is a path in Node, so pass a Buffer / Uint8Array when a
delivery only exists in memory. A type clash throws MergeConflictError;
{ onTypeClash: "promote" } or "widen" settles it (see
below).
Open the web app's Tools → Merge tool, keep the
file you already loaded as the base, and drop in the incoming delivery. The same
reconciliation runs compiled to WebAssembly, entirely client-side: you get the
per-row revision audit, choose how to settle a type clash, and download the
merged .ags — neither file leaves your machine.
When the two files disagree on a type¶
One delivery types LOCA_GL as 2DP, the next types it 5DP. Merge will not
guess: by default it errors (exit 6 / MergeConflictError). You choose how
to settle it.
| mode | what the merged column becomes | your values |
|---|---|---|
error (default) |
— | merge refuses |
widen |
X (free text) |
kept byte-for-byte |
promote |
the greatest precision — 2DP + 5DP → 5DP |
coarser values zero-padded: 10.00 → 10.00000 |
widen is lossless on the bytes but throws the type away, and X is the
least informative answer available. promote keeps the column numeric.
promote never rounds and never demotes. It only ever appends zeros, so no
digit you wrote is ever changed, and taking the maximum precision is the only
direction that cannot destroy data — which also makes the result independent of
argument order (unlike a KEY conflict, where the later file deliberately wins). A
value it cannot pad losslessly is kept verbatim and warned about, never rounded.
It is deliberately limited to nDP. Significant figures (3SF) and scientific
notation (2SCI) fall back to widen, because decimal places are a formatting
convention but significant figures are a claim about measured precision — padding
3SF to 5SF would assert two digits the instrument never resolved.
Why promote matters downstream
_content_hash fingerprints a row's values, canonicalised through the declared
type — so 10.00 hashes as a number under 2DP but as a string under
X. A widened column therefore stops matching its own typed source, while a
promoted one still dedups against it.
A conflicting UNIT is fatal in every mode
TYPE has a universal absorber (X); UNIT has none. There is no supertype of
metres and millimetres, and merge will never convert — so if two files declare
different UNITs for one heading, merge refuses in every mode, promote
included. Reconcile the UNIT row at source.
One caveat everywhere: identity is KEY-based, so correcting a KEY value
(a LOCA_ID typo BH1 → BH01) reads as a different row, not an edit — both
persist. Fix KEY typos in the source before merging.
See also: Diff two revisions · Born typed