Reprocessing and maintenance¶
Choose a replay policy¶
FTP imports retain append as their default. Choose another policy explicitly
when a scope has already been published through the managed importer:
| Policy | Existing managed scope | Absent scope |
|---|---|---|
append |
Append another publication; duplicates remain possible | Publish |
skip_same |
Skip only when all active publications have the same source SHA-256 and parser version; otherwise fail | Publish |
error_if_exists |
Fail without changing data | Publish |
replace |
Validate staging, delete the exact source scope, insert and replace its active manifest in one transaction | Publish |
import omnisus as sus
report = sus.import_dataset(
"sim_obitos",
scopes=[sus.ScopeKey(uf="RR", ano=2023)],
policy="skip_same",
run_id="sim-rr-2023-review-01",
)
print(report.run_id, report.rows, report.failed)
omnisus import sim_obitos --year 2023 --ufs RR --policy skip_same --run-id sim-rr-2023-review-01
Source identity combines the compressed DBC SHA-256 with an explicit staging
parser version and the SHA-256 of what the import reads from the dictionary (its
encoding and x-identity). Editing labels, code maps or claims does not change it;
a parser algorithm change must bump that version. skip_same first compares the server listing (path, size and
server time of each file) and the parser version with the active publication; if
nothing changed, it skips the scope without downloading. Any difference downloads
the file and compares its SHA-256. It does not remove duplicates already created
by append.
replace requires nonempty staging with every row belonging to the exact scope.
For monthly data this includes UF, year and month, even when the physical table
partition omits UF. Event rows are never deduplicated with DISTINCT.
Rows created by older importers or direct SQL have no trustworthy source
manifest. Policies other than append reject such scopes. Inventory or rebuild
them explicitly in a separate target before switching; a later append cannot
retroactively certify the old rows. Manifest row counts detect some external
changes but do not audit arbitrary edits that preserve counts. External SQL
writes remain outside the managed contract. IBGE has a separate publication
model described in its source documentation.
Quando um ano passa de preliminar a final¶
For a dataset with a prelim_dir (SIM, SINASC, SINAN), DATASUS eventually
moves a year from the preliminary directory to the final one under the same
name. Nothing in the lake changes by itself: outdated(dataset, lake=lake)
compares the files recorded on each active publication with what the server
lists today and returns only the scopes that differ.
import omnisus as sus
with sus.Lake.local() as lake:
moved = sus.outdated("sim_obitos", lake=lake)
sus.import_dataset(
"sim_obitos", scopes=moved,
policy="replace", run_id="sim-final-2026",
)
outdated is read-only; pass its result to import_dataset with
policy="replace" and an explicit run_id, the same contract as any other
replacement. A scope the server no longer lists at all is a withdrawal, a
different fact, and outdated does not return it.
When DATASUS moves or republishes files¶
outdated also catches a row whose directory moved outright — not just a
year that went from preliminary to final — and a same-name republish (another
size or server mtime at the same path). Compare with refresh=True so the
listing isn't served from cache:
import omnisus as sus
with sus.Lake.local() as lake:
stale = sus.outdated("sinasc_nascidos_vivos", lake=lake, refresh=True)
sus.import_dataset("sinasc_nascidos_vivos", scopes=stale, policy="replace", run_id="sinasc-1996-move")
A scope whose recorded source URI differs from where the server lists the file today is reported even when its bytes did not change, because provenance must name where the bytes are. Publications without a recorded source are always reported, since nothing shows they match the server.
Inspect an interrupted run¶
import omnisus as sus
try:
report = sus.import_dataset(
"sim_obitos", scopes=[sus.ScopeKey(uf="RR", ano=2023)], run_id="sim-rr-2023-review-01"
)
except sus.ImportAbortedError as exc:
print(exc.report.rows, exc.unresolved) # determined outcomes; positions to inspect
raise
Choose and retain run_id before starting an import. A failed COMMIT can have
succeeded in the catalog despite the missing acknowledgement. After closing the
unusable handle, reopen the lake and inspect the durable manifest:
with sus.LakeReader() as reader:
published = {p["scope"] for p in reader.publications(run_id="sim-rr-2023-review-01")}
failed_attempts = reader.attempts(run_id="sim-rr-2023-review-01")
Publications include publication, run and batch IDs, the decoded scope,
source hash, parser version, row count and active status. A scope absent from
published did not commit under that run ID, because data and manifest commit
in one transaction; choose run IDs you never reuse, or that inference is void.
Superseded manifests remain for inspection.
The failure table records known failures separately after data rollback at the
end of a completed run; a crash or unknown transaction outcome can prevent that
log from being written. Its absence is not evidence of success.
ImportAbortedError.report contains determined outcomes and unresolved lists
input positions needing inspection. Repeated input positions are preserved.
A skip that depends on an uncommitted publication shares that batch's outcome;
it cannot remain successful after rollback. Reconcile publications before
retrying an unknown commit, rather than appending the entire run again.
Remove a scope¶
Lake.delete_scope(table, scope) deletes the rows of one source scope and
retires every publication within it (active = false) in one managed
transaction, so data and manifest never disagree. A yearly scope on a monthly
table removes all twelve months and retires each month's publication. Rows that
never had a publication are removed as well; when rows_deleted exceeds the
retired publications' row sum, unmanaged rows were present.
with sus.Lake.local() as lake:
result = lake.delete_scope("sih_aih_reduzida", sus.ScopeKey(uf="RR", ano=2023))
print(result.rows_deleted, result.publications_retired)
Retired publications stay in the manifest for inspection; they are not
distinguished from ones superseded by replace.
Migrate a legacy lake¶
Rows written before publications existed have no manifest and are never certified in place. Migrate by rebuilding, so provenance exists from the first import:
- Choose a new target. Do not point it at the old catalog or storage.
- Import each dataset with
available()as the plan and an explicitrun_id;policy="skip_same"makes reruns idempotent. - Verify
publications()covers every scope you expect, and compare row counts with the old lake where that matters to you. - Switch consumers to the new target string. Keep the old lake read-only until nothing reads it, then delete it.
The old lake is not modified at any step.
Coordinate writers and bound downloads¶
A local Lake handle acquires a cooperative lock on the canonical catalog path
before opening DuckDB and holds it until close. Concurrent handles fail with
WriterBusyError from omnisus.lake.locking. The sidecar .writer.lock file
remains after close; its presence alone does not mean a writer is active. Do not
delete it while handles may be running. This protocol also covers maintenance.
External SQL clients, network filesystems and cloud catalogs require external
coordination; distributed recovery has not been validated.
import_dataset and import_research accept max_payload_bytes (default 512 MiB)
and max_inflight_bytes (default 1 GiB). CLI equivalents are
--max-payload-bytes and --max-inflight-bytes, in bytes. The budget must be at
least the payload cap. Before downloading, a scope reserves the sum of its files'
listed sizes (every part of a split month at once) and holds it while the
payloads are queued and consumed; other scopes wait until the budget has room.
A scope with a file listed above the payload cap, or with a total above the
budget, fails with fetch_failed without a download, and the reason names the
bytes needed and the limit to raise. A file exceeding the cap still fails during
receipt. Cancellation closes sockets and waits for the worker before releasing
the reservation; a DNS/connect phase without a socket can still take until its
timeout.
These limits cover compressed payloads, not total process memory. Decompression still materializes the whole DBF. Parsing spools bounded batches to temporary disk and writes reconciled Parquet in a second pass. Provide enough temporary disk for DBF, Arrow spool and Parquet.
The reproducible benchmark is scripts/benchmark_resources.py. On the measured
132,440-row amplified fixture with 100,000-row batches, peak RSS changed from
1328.5 to 758.0 MiB, elapsed time from 3.705 to 6.571 seconds, and sampled peak
temporary disk from 61.9 to 112.5 MiB. Row order/hash and schema matched. This is
one run per mode on repeated fixture records; it does not establish national
throughput or a universal memory bound. Disk sampling every 5 ms is a lower bound.
The amplified case feeds repeated DBF records directly and bypasses DBC
decompression; its elapsed times do not measure the complete DBC pipeline. A
separate 3,311-row real DBC case exercises decompression in the same benchmark.
The repository's evidence/benchmark-d6-default.json records the environment,
corpus hashes and full results.
Compact, expire history and clean files¶
These are separate operations:
lake.optimize(table)merges adjacent files while retaining snapshots.lake.expire_snapshots(older_than=cutoff)simulates removing older history.lake.cleanup_files(older_than=cutoff)simulates deletion of obsolete files eligible under DuckLake's retention rules. It does not expire snapshots.
from datetime import UTC, datetime
cutoff = datetime(2026, 8, 1, tzinfo=UTC)
with sus.Lake.local() as lake:
compacted = lake.optimize("sim_obitos")
history_preview = lake.expire_snapshots(older_than=cutoff)
files_preview = lake.cleanup_files(older_than=cutoff)
The new expiration and cleanup APIs default to dry_run=True. Pass
dry_run=False to execute after selecting a retention cutoff suitable for the
history you need. Cutoffs must include a timezone. Returned dictionaries expose
DuckLake's operation results.
omnisus lake optimize sim_obitos
omnisus lake expire-snapshots --before 2026-08-01T00:00:00+00:00 --dry-run
omnisus lake cleanup-files --before 2026-08-01T00:00:00+00:00 --dry-run
Use --execute instead of --dry-run to apply expiration or cleanup. Failures
exit nonzero.