All notable changes to MRRC will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Bump quick-xml to 0.41.0, fixing the RUSTSEC-2026-0194/0195 DoS advisories on the MARCXML
read/write path. The BIBFRAME/RDF path is not covered:
oxrdfxmlpinsquick-xml ^0.37and still resolves to the affected 0.37.5. Sincebibframeis a default feature (and is always enabled in the Python wheel), a default build still contains the affected code on the RDF serialization path. Both advisories are DoS-class. - Bump crossbeam-epoch to 0.9.20, fixing the RUSTSEC-2026-0204 invalid-pointer-dereference advisory (reached transitively through rayon).
- Gate PyPI publishing behind a required-reviewer GitHub environment, so a release cannot upload without maintainer approval.
- Harden the CI and release workflows: pin all third-party actions to commit SHAs, disable git-credential persistence in checkouts, scope job token permissions to least privilege, and add a zizmor workflow-security scan to CI.
- Bump pytest from 9.1.0 to 9.1.1
- Bump ruff from 0.15.17 to 0.15.21
- Bump pymdown-extensions from 10.21.3 to 11.0.1
- Bump foldhash from 0.1.5 to 0.2.0
- Bump CodSpeedHQ/action from 4.17.5 to 4.18.5
- Bump softprops/action-gh-release from 3.0.0 to 3.0.2
- Bump actions/checkout from 6.0.3 to 7.0.0
- Bump actions/cache from 5 to 6.1.0
- Bump pyright from 1.1.410 to 1.1.411
- Bump syrupy from 5.3.2 to 5.5.3
- Bump regex from 1.12.4 to 1.13.0
- Bump memchr from 2.8.2 to 2.8.3
- Bump crossbeam-channel from 0.5.15 to 0.5.16
- Bump mypy from 2.1.0 to 2.3.0
- Bump pymarc from 5.3.1 to 5.4.0
- Bump astral-sh/setup-uv from 8.2.0 to 8.3.2
- Bump actions/setup-python from 6.2.0 to 6.3.0
- Bump docker/setup-qemu-action from 4.1.0 to 4.2.0
- Bump anyhow from 1.0.102 to 1.0.103 (dev-dependency, used by examples only; picks up the
RUSTSEC-2026-0190
Error::downcast_mut()unsoundness fix, which does not affect the published library or wheel)
- New cargo-fuzz targets covering the in-memory byte-source parse entry points
(
parse_record_from_bytes/parse_record_from_shared_bytes) and the three ISO 2709 writers (write_arbitrary_records, over arbitrary records), run nightly alongside the existing binary-MARC targets.
ProducerConsumerPipelinesends parsed records to its consumer one batch per file chunk instead of one record at a time. Itschannel_capacityargument now counts batches (each one parsed file chunk) rather than records, and defaults to 4; callers that set it explicitly should adjust.
- The binary writers now reject a single field whose serialized length exceeds
9999 bytes instead of emitting a corrupt directory. The ISO 2709 directory
entry stores field length in a fixed 4-digit field, so an over-long field
previously produced a malformed 13-byte entry; it now returns a
WriterErrornaming the offending tag. Affects the bibliographic, authority, and holdings writers. parse_batch_parallel(Rust and the Python extension) now returns aMarcErrorinstead of panicking when given record boundaries whoseoffset + lengthexceeds the buffer or overflowsusize. The function takes caller-supplied boundaries; values fromRecordBoundaryScannerare always in range, so this only affects callers passing boundaries directly.
- Binary ISO 2709 writing is roughly 2.8–3.2× faster: the bibliographic
MarcWriternow serializes fields into reused buffers instead of allocating a temporaryVecper field and per record, dropping per-record allocations from ~68 to ~1. Reading is modestly faster (~10%) from removing a redundant per-field tag-string clone inRecord::add_field. - Reading binary MARC is a few percent faster (~5–6% measured): the tag-keyed
field maps now hash with
foldhashinstead of the standard library'sSipHash, dropping cryptographic-hash overhead that was unnecessary for short, trusted MARC tags. Record output and field ordering are unchanged. - Constructing a reader from a Python
bytesobject no longer copies the whole buffer up front: the reader borrows the immutablebytesand slices records out of it, avoiding a full-dataset copy at construction for large in-memory inputs.bytearrayinput is unchanged (still snapshotted to an owned buffer).
- Python —
record.leaderis now a property, not a method (userecord.leader, notrecord.leader()).Leaderconstructs from a 24-character string or asLeader()with properties assigned; the old field keyword arguments (Leader(record_type=..., ...)) are removed. - Python —
Record.get_fields()returns control fields in record order (was fixed ascending-tag order). - Rust —
MarcErrorand thePipelineError,RecoveryMode,ValidationLevel,EncodingAnalysis,RdfFormat, andIndicatorValidationenums are#[non_exhaustive]: exhaustivematches need a wildcard arm, and external construction goes through the newMarcErrorconstructors. - Rust —
from_pathconstructors now returnMarcReader<BufReader<File>>(wasMarcReader<File>). - Rust —
recovery::try_recover_recordis removed; truncated-record salvage now runs through the ISO 2709 skeleton walk. - Build — MSRV is now Rust 1.88; both crates use edition 2024.
- Community files: issue and pull request templates, and a security policy (
SECURITY.md) pointing at GitHub private advisories. parse_record_from_bytes: parse one complete MARC record from in-memory bytes with no reader I/O and no per-record copies. The PythonMARCReaderread path now uses it, collapsing the former chain of per-record buffer copies between the source and the parser to a single pymarc-compatibility stash (current_chunk).parse_record_from_shared_bytes: parse from a buffer the caller already holds behind anArc, without taking ownership. The PythonMARCReaderuses it so thecurrent_chunkstash shares one allocation with the parser instead of cloning, andcurrent_chunkis now read lazily — iterating without inspecting it copies no record bytes into Python.- Criterion benches for the serialization formats that had no CI perf signal — CSV, MODS, Dublin Core, BIBFRAME (Turtle), and MARC-in-JSON (both directions for MODS and MARC-in-JSON) — plus a single-thread parser-pool bench that tracks the producer-consumer pipeline's per-record cost deterministically under simulation.
- The pymarc parity oracle now executes in CI and check.sh instead of silently skipping:
a new
oracleextra pins pymarc in uv.lock (Dependabot adjudicates behavior changes on bump PRs), and the oracle extends beyond iteration shape to value-level comparisons — title,format_field(),value(), andas_marc()byte-equality over the 1k corpus. MarcError::metadata()returns anErrorMetadatasnapshot of every structured field an error carries. The per-field accessors,Display/detailed()rendering, JSON output, and the Python exception mapping all read from this single per-variant table now; rendered and serialized output is unchanged.MarcError(enum and variants) and the grower enums (PipelineError,RecoveryMode,ValidationLevel,EncodingAnalysis,RdfFormat,IndicatorValidation) are now#[non_exhaustive], so adding a variant or field is no longer a breaking change. Downstreammatches need a wildcard arm;MarcErrorconstruction outside the crate goes through new public constructors (invalid_field,truncated_record,record_length_invalid,fatal_reader_error) andwith_*positional setters.copy.deepcopy()support forRecord,Field, andLeader: deep copies are fully independent of the original (and of any record), whilecopy.copy()stays shallow, matching pymarc. Deep-copying a live field handle yields a detached snapshot of its current data.- A reproducible three-way benchmark (
scripts/benchmark_comparison.py, withexamples/benchmark_native.rsfor the native column) and published figures (docs/benchmarks/): the Python wrapper reads ~7× pymarc per record, ~30× on the parallel bulk path, ~1.6× extract, and ~6.5× roundtrip, against a native-Rust ceiling of ~9×/~35× on the reference host. Replaces the prior "needs re-measurement" hedge.
- The parallel parse helpers (
parse_batch_parallel/parse_batch_parallel_limited) and the blockingProducerConsumerPipelinereads now release the GIL during the parallel parse and the channel wait, so other Python threads keep running instead of being blocked for the duration. (The parallel helpers now take ownership of the buffer, required for sound GIL release.) Record.get_fields()with no arguments is faster: it made one PyO3 call per control tag (nine) plus one per data field, and now fetches all control fields in a single call. Control fields come back in record order rather than fixed ascending-tag order (a difference only for records whose control fields are stored out of order; record order matches pymarc).- Iterating a
MARCReaderis faster: wrapping each parsed record no longer builds a throwaway inner_Recordand two_Leaderobjects only to discard them, lifting per-record read throughput (path and bytes inputs alike). Output is unchanged. - Writing ISO 2709 records (
MARCWriterand the authority/holdings writers) allocates less: each directory entry's length and start-position digits are written straight into the output buffer instead of through a per-fieldformat!. - MARCXML deserialization and 880-linkage parsing no longer recompile their regexes on
every call (hoisted to
LazyLockstatics). Parsing MARCXML records one at a time — e.g.parse_xml_to_arrayin a loop — is substantially faster; batch/whole-document parsing already amortized the cost and is unchanged. - Reading from a Python file-like object (
MARCReader(open(path, "rb")),BytesIO, etc.) now reads the source in 256 KiB chunks and slices records out in Rust, instead of twofile.read()calls plus agetattr("read")per record. Thereadmethod is bound once and each chunk is borrowed viaPyBytesrather than copied. Output is unchanged; file-path and bytes inputs were already chunked and are unaffected. Cargo.lockis now committed, so CI, wheel builds, and local checkouts resolve the same dependency versions; Dependabot manages version bumps as reviewable diffs.- Removed unused dependencies (
bytes,nom,encoding_rs,csv, bindings-cratetempfile) and demoted example-onlyanyhow/flate2to dev-dependencies, shrinking the published crate's dependency tree. check.sh now runscargo macheteto keep it so. - The version is now declared once, in
[workspace.package]: both crates inherit it andpyproject.tomlreads it viadynamic = ["version"]. Shared dependencies andrust-versionare also workspace-inherited, so they cannot drift between crates. - BIBFRAME/RDF support is now behind the default-on
bibframecargo feature. Default builds and the Python wheel are unchanged;--no-default-featuresdrops the oxrdf/oxrdfio dependency tree for MARC-only users. - Record parsing no longer copies every record's bytes into the error-diagnostics
buffer: the parse buffer is shared by refcount instead, deleting a per-record
alloc+memcpy that every read path paid so that the under-1% of records that error
could render hex dumps. Error
bytes_nearoutput is unchanged. - Tuned the release profile: fat LTO,
codegen-units = 1, and debuginfo stripping. Wheels get ~2-3% higher read throughput and a 21% smaller extension binary. - The Python leader surface now matches pymarc:
record.leaderis a property (was a method) and assignable from aLeaderor 24-character string;Leaderrenders and compares as its 24-character MARC 21 string (str(),len(),==with str,repr) and constructs from one (Leader('00136nam a2200061 4500')). The oracle now compares leaders against pymarc value-for-value. RustLeadergains aDisplayimpl. - Migrated both crates to Rust edition 2024 and raised the MSRV from 1.87 to 1.88
(let-chains require 1.88). The workspace now uses resolver v3, so dependency
resolution respects
rust-versioninstead of breaking the MSRV build on upgrades. - Releases now publish a source distribution and macOS universal2 wheels, so Intel Macs
and platforms outside the wheel matrix can install mrrc (previously "no matching
distribution"). PyPI metadata completed: real author, PEP 639 license expression,
Typing :: Typedclassifier, and project URLs (homepage, docs, changelog, issues). - File-path readers now buffer their reads (64 KiB): Python
MARCReader(path)(and the authority/holdings readers) and Rustfrom_pathpreviously issued two-plusread(2)syscalls per record. Rustfrom_pathconstructors now returnMarcReader<BufReader<File>>instead ofMarcReader<File>. - Updated pyo3 to 0.29, resolving RUSTSEC-2026-0176 (out-of-bounds read in
nth/nth_backonPyList/PyTupleiterators in pyo3 ≤0.28). - Updated quick-xml to 0.40. MARCXML text and attribute decoding now applies XML 1.0 end-of-line and attribute-value normalization explicitly (previously the XML 1.1 EOL set, which additionally folded NEL/LSEP — those are no longer normalized, matching MARCXML's XML 1.0 reality).
- Corrected the declared Rust MSRV from 1.71 to 1.87 — the floor the dependency tree
already required, so 1.71 never actually built; a new CI job now verifies the
workspace compiles on the declared MSRV so the claim cannot drift again. The accurate
MSRV unlocked clippy-driven cleanups: the MARC-8 tables now use
std::sync::LazyLock(dropping thelazy_staticdependency) and I/O error construction usesstd::io::Error::other. - Truncated records no longer allocate or zero-pad a buffer of the leader's claimed length: the reader grows the record buffer as bytes arrive (8 KiB steps) and lenient salvage parses the short body directly, so allocation is bounded by actual input size instead of a claimed maximum-length record on a small stub.
- Deleted four dead manual-profiling bench harnesses (
profiling_harness,detailed_profiling,rayon_profiling,rayon_file_io_profiling). They produced no CI or CodSpeed signal, referenced documents that no longer exist, and one wrote scratch files to/tmp. The bench target now contains only criterion benches. - The placeholder
mrrcbinary (src/main.rs, a banner print) and the orphanedtests/create_sample_data.py(fixtures come fromscripts/generate_benchmark_fixtures.py). - The docs.rs front page no longer embeds the README (whose CI badges and repo-relative links 404 off GitHub); it now renders the curated crate-level docs instead.
recovery::try_recover_record: truncated-record salvage now runs through the shared ISO 2709 skeleton's clamped directory walk, which slices fields at their data-area offsets — intact fields ahead of the truncation point are now recovered instead of dropped. Non-digit directory bytes on the salvage walk keep theirInvalidField(E106) shape but now also count against the reader's recovered-error cap.
Recordis now iterable:for field in recordyields each field as a wrappedmrrc.Field(control and data, in record order), matching pymarc. It previously raisedTypeError.Record.remove_field_at()now returns themrrc.Fieldwrapper (with__getitem__and the other pymarc conveniences) instead of the bare_mrrc.Fieldextension type.copy.copy()of aRecordno longer raisesRecursionError: the wrapper's attribute delegation now stops cleanly when the inner record is absent during copy reconstruction.- Corrected Python documentation errors that broke copy-pasted code: the README and
mrrc.read()examples calledrecord.title()(it is a property); docs showed imports and reader constructors that do not exist in Python (MarcError,MARCReader.from_path(),.with_source()); the API reference stated the wrongrecovery_modedefault ("strict"; it is"permissive") and omitted thevalidation_levelandmax_errorskeyword arguments. - Corrected documentation drift: the Rust API reference showed
parse_batch_parallelwith the wrong arity and a stale 5-variantMarcErrortable;parse_batch_parallel/parse_batch_parallel_limitedwere missing from the Python API reference; the release procedure understated the wheel count and used retired issue-tracker commands.
- Bump ruff from 0.15.16 to 0.15.17
- Bump syrupy from 5.3.1 to 5.3.2
- Bump pytest from 9.0.3 to 9.1.0
- Bump CodSpeedHQ/action from 4.17.0 to 4.17.5
mrrc.StaleFieldError(subclass ofMrrcException), raised when a field handle is used after fields were removed from its record.- Coverage-guided fuzz targets for the JSON (
parse_json,parse_marcjson), MARCXML (parse_marcxml), MODS (parse_mods), and MARC-8 (decode_marc8) read paths, run nightly alongside the existing binary-MARC targets. - CI and
.cargo/check.shnow runstubtestto verifymrrc/_mrrc.pyimatches the compiled extension, preventing type-stub drift. - A documentation-build check runs on pull requests that touch
docs/,mkdocs.yml, or the Python sources the API reference is generated from, so a broken docs build is caught before merge instead of only on the main deploy. The docs build now usesmkdocs build --stricteverywhere (PR check, deploy, and.cargo/check.sh), turning broken links and unresolved mkdocstrings references into errors. - A process-label lint (in
.cargo/check.shand CI) fails the build when source, tests, or non-CHANGELOG docs embed implementation-process labels (bead IDs, PR/issue numbers, or release-tagged claims like "since 0.8.1"), which belong in git history and the CHANGELOG instead.
- The Python API reference is now generated from source with mkdocstrings
instead of hand-maintained signature tables, so it cannot drift from the
code. The page also gained reference sections for the query DSL
(
FieldQuery,TagRangeQuery,SubfieldPatternQuery,SubfieldValueQuery) and the parallel-processing classes. - Documentation fixes: the BIBFRAME-to-MARC guide example now uses the real
RdfGraph.parse(...)API (it previously referenced a nonexistentBibframeGraph.from_turtle), and docstring examples are fenced so they render as code blocks rather than leaking headings into the reference. - Reader option arguments (
recovery_mode,validation_level,max_errors) are now keyword-only onMARCReader,AuthorityMARCReader, andHoldingsMARCReader, matching the documented signatures; pass them by name. The record source remains the first positional argument. - The
mrrc/_mrrc.pyitype stubs now match the compiled extension exactly: missing members were added and entries absent at runtime were removed, so stub-driven type checking reflects the real API. - CI PR workflows add cancel-in-progress concurrency groups and per-job
timeouts, skip the heavy wheel and benchmark jobs on Dependabot pull requests
(lint, tests, and semver still run), and share one
rust-cacheconfiguration across the Rust compile jobs. - Documentation now states explicitly that CSV and Dublin Core are write-only (export) formats: both are lossy projections of a MARC record, so MRRC emits them but does not parse them back into MARC.
- Documentation no longer presents specific throughput figures or pymarc multipliers as current measurements; early benchmark results are summarized with the caveat that they need re-measurement, and the benchmark docs now describe the measurement infrastructure and the procedure for producing citable numbers.
- The Python CodSpeed CI job now runs in simulation mode (previously walltime), making PR performance-regression detection deterministic on hosted runners. Parallel-throughput benchmarks (Python and Rust) are excluded from CodSpeed, since Valgrind serializes threads; they remain runnable locally via pytest and criterion.
- CI Windows jobs are pinned to
windows-2025(previouslywindows-latest) ahead of GitHub's 2026-06-15 redirect of that label to a new image. - Pull requests now build and test a slim wheel matrix (8 jobs) instead of the
full 30; the full
os × pythonmatrix still runs on push to main and on release tags. The standalone Rustbuild.ymlworkflow is removed; its examples-compile step moved into the test workflow, and cross-OS core compilation remains covered by the maturin wheel matrix..cargo/check.shgains a--releaseflag to build the Python extension in release mode for perf-sensitive debugging. - Continuous integration trims redundant work: the Python pull-request
benchmark matrix (
pytest-benchmarkacross five Python versions) is dropped in favor of the CodSpeed regression gate, CHANGELOG linting now runs in CI, and commits that touch only.beads/**no longer trigger the test, benchmark, wheel, and lint workflows. .cargo/check.shnow compilesexamples/and benchmarks (cargo bench --no-run) in its full pre-push run, catching breakage that previously surfaced only in CI.
- The Python
Record.fields_by_tag(tag)method. Use the pymarc-standardget_fields(tag), which returns the same live-handle fields for a data tag and also accepts multiple tags and control (00X) tags. The RustRecord::fields_by_tagis unchanged.
Record.remove_field(field)now removes exactly the given field instead of every field sharing its tag, matching pymarc; detached fields match by value and raiseValueErrorwhen absent.remove_field/remove_fieldswith a control tag now remove the control field (previously a silent no-op).fields()now enumerates repeated control tags (006/007) identically toget_fields(). (#248)- Python in-place field edits now persist to the record, matching pymarc.
Fields obtained from a record (
record[tag],get_field,get_fields,fields) are live handles: indicator and subfield edits, and control-field.dataassignment, write through to the record. (#241) - Corrected documentation to match the current API. The Rust API-reference
"Key Methods" tables now use real method names, distinguish public fields
from methods, and show accurate return types; the
AuthorityRecord/HoldingsRecordexamples use the real::builder(leader)API. Several Rust and Python tutorial/reference examples were fixed (query DSL types,subfields_by_code, error matching, batch parsing,get_fieldsfor pymarc-style access,record.leader()). Removed documentation for MARC-8 output, which is not supported — MRRC writes UTF-8. Reported by @acdha.
- Bump ruff from 0.15.12 to 0.15.15
- Bump syrupy from 5.1.0 to 5.3.1
- Bump pyright from 1.1.409 to 1.1.410
- Bump astral-sh/setup-uv from 7 to 8.1.0
- Bump CodSpeedHQ/action from 4 to 4.17.0
- Bump actions/checkout from 6 to 6.0.3
- Bump codecov/codecov-action from 6 to 6.0.1
get_field(tag)andget_field_or_err(tag)accessors on all three record types (Record,AuthorityRecord,HoldingsRecord), in both Rust and Python.get_fieldreturns the first matching field asOption<&Field>/None;get_field_or_errraisesmrrc.FieldNotFound(E105) withfield_tagandrecord_control_numberpopulated. Existingget_fieldsis unchanged.max_errorskwarg on PythonMARCReader. Caps the total recovered errors across alenient/permissivestream; the read after the (N+1)-th raisesmrrc.FatalReaderError(E099).None(default) and0both disable the cap. Inert in strict mode.- Documented pymarc 5.3.1 exception-class-name parity in the error-handling reference and the migration guide: the mapping table, the names mrrc deliberately omits, the known hierarchy divergences, and porting recipes.
MARCReader.current_exceptionandMARCReader.current_chunk— pymarc-compatible accessors. After each__next__,current_chunkholds the bytes just read andcurrent_exceptionholds the exception swallowed underpermissive=True(orNoneon a clean read). See the migration guide for the encoding-strictness divergence.- New
validation_levelreader kwarg ("structural"default,"strict_marc"), orthogonal torecovery_modeand applied uniformly across all three readers. Atstrict_marc, indicator bytes (E201), subfield-code bytes (E202), and UTF-8 decoding (E301) enforce MARC 21 byte-level rules; atstructuralthose bytes are accepted as-is and invalid UTF-8 falls back toU+FFFD. - Per-record diagnostics.
record.errors(Rust + Python) carries the typed exceptions for non-fatal defects recovered inlenient/permissivemodes (always empty instrict).MARCReader.iter_with_errors()yields(record, errors)tuples, and underpermissive=Trueyields(None, [exception])so unsalvageable records stay observable.record.errorsis on all three readers;iter_with_errorsis bibliographic-only. - Strict-mode parsing now verifies that the byte at the leader's claimed
end-of-record position is
RECORD_TERMINATOR(0x1D); a different byte firesEndOfRecordNotFound(E006). Previously the byte was unchecked and a malformed record with the wrong terminator parsed silently. Lenient and permissive modes are unchanged — the recovery cap continues to absorb the disagreement via existing directory/field paths. validation_level="strict_marc"also runs MARC 21 semantic checks: per-tag indicator rules (e.g. 245 first indicator must be0or1) and leader-byte semantics, firing E201 and E002 respectively (both recoverable inlenient/permissive).IsbnValidator/EncodingValidatorremain opt-in helpers; see the validators reference.MarcErrornow implementsClone(Rust), enabling inspection of recovered errors onrecord.errorsafter lenient parsing. Clone is lossy for the three variants wrapping foreign causes (IoError,XmlError,JsonError): it preserves the rendered message but drops the non-string inner cause.SubfieldPatternQuerynow exposes its regex via apatterngetter (RustSubfieldPatternQuery::pattern()), and itsrepr()includes the pattern —<SubfieldPatternQuery tag=084 subfield=a pattern="^abc">— so the most useful field for debugging a query is recoverable from the REPL or logs. Thetagandsubfield_codegetters are now also declared in the type stubs. Thanks to @acdha (#226).
- Leader errors from the MARCXML, JSON, and marcjson readers now carry
record_index(previously stripped) — identifying the failing record in a multi-record collection, or1for single-record APIs. Affects E001–E004 from these paths; the ISO 2709 path was already enriched. MarcError::IoError(E007) raised mid-record — when the underlying source fails while reading a record's data area — now carriesrecord_index,byte_offset, andsource_name, instead of the context-freeFrom<io::Error>fallback that left themNone. I/O failures at a record boundary (before a record is in progress) stay context-free by design; Python'sOSErrorsurface is unchanged.- Retired the internal cumulative-budget perf-gate CI workflow added during the error-handling epic — it served its purpose and the cumulative v0.8.0→v0.8.1 hot-path cost stayed negligible. Codspeed continues general perf tracking, and the benchmarks still run locally.
- Python
MARCReader/AuthorityMARCReader/HoldingsMARCReadernow default torecovery_mode="permissive"(was"strict"), matching the pymarc / marc4j convention — a fresh reader iterates past per-record defects instead of aborting on the first. The Rust core'smrrc::MarcReaderkeepsStrict. Passrecovery_mode="strict"explicitly for the old behavior; thepermissive=Truepymarc-compat path is unchanged. See the error-handling guide for the trade-offs. - Leader-validation errors now fire the field-specific variants their
documentation describes:
RecordLengthInvalid(E001) for non-digit bytes 0-4 orrecord_length < 24,BaseAddressInvalid(E003) for non-digit bytes 12-16 ordata_base_address < 24, andBaseAddressNotFound(E004) fordata_base_address > record_length. Previously all of these collapsed toInvalidLeader(E002) or, for E004, fell through to a misleadingInvalidFieldfrom directory parsing. - A non-digit byte in a directory entry's length or start-position field now
fires
DirectoryInvalid(E101) withfield_tagand a precisebyte_offset, instead of the misleadingInvalidField(E106) it previously forwarded. - The performance-tuning, migration-from-pymarc, and working-with-large-files guides now point to the Query DSL guide where field filtering is discussed, so readers discover the indicator/range/pattern/subfield matching path. Thanks to @acdha (#234).
- The Rust examples throughout the docs (quickstart, tutorials, reference) now
match the real API and compile:
field.get_subfield('a')(char), the publictag/indicator1/indicator2fields, theFieldQuery::new()builder withrecord.fields_matching(&query),Leader::from_bytes(...), therecord_to_json/record_to_marcxml/record_to_marcjsonconversion functions, anduse mrrc::RecordHelpers;forrecord.title(). Many previously referenced methods that don't exist. Reported by @acdha (#233).
- Two out-of-bounds slice panics in the lenient/permissive recovery path:
salvage attempts no longer crash when a directory entry's
start_positionlies past the buffer (now bailing out of the salvage branch), and the control-field decode path now guards against zero-length directory entries (whereend_position == start_position) before invokingsaturating_sub(1)on the slice end. Surfaced by the recovery-mode-consistency fuzz target. MarcReaderrejects non-ASCII bytes in directory entry tags (firingDirectoryInvalid/ E101) instead of lossily substitutingU+FFFDand producing records whose tag re-encodes to more than 3 bytes.MarcWriterand the authority/holdings writers also refuse records whose tags aren't 3 ASCII bytes, returningWriterError(E404). Surfaced by the error-classification fuzz target's round-trip assertion.TruncatedRecord(E005) now surfaces onrecord.errorsinlenientandpermissivemodes instead of being silently swallowed (it had been cascading into a misleading E201). Strict mode is unchanged.- Python
mrrc.MARCReadernow honorsrecovery_modeon short body reads: a truncated body no longer raisesTruncatedRecord(E005) before the recovery-aware parser runs, so inlenient/permissiveit lands onrecord.errorsinstead. Strict mode still raises. - Release workflow now attaches wheel assets to the GitHub Release page
automatically. Previously,
actions/checkoutran afterdownload-artifactand wipeddist/before the gh-release step, leaving the release page with notes but zero assets. Steps reordered so checkout runs first. MarcWriterand the authority/holdings writers now refuse records whose serialized length or base address exceeds the ISO 2709 5-digit limit (99999 bytes), returningWriterError(E404) with positional context — instead of silently emitting an unparseable leader (or, for holdings, the wrongInvalidFieldvariant).mrrc.MARCWriter.write_recordnow raises the typedmrrc.WriterError(E404) instead of a plainOSError; the binding had been collapsing every writerMarcErrorintoio::Erroracross the FFI boundary.InvalidLeader(E002) errors now carry the full positional context (record_index,byte_offset,record_byte_offset,source_name) the v0.8.0 error work promised; the leader-validation path had been building the variant message-only and discarding every positional field.TruncatedRecord(E005) raised across the Python FFI now preserves the same positional context (record_index,byte_offset,record_byte_offset) the Rust core attaches; Python callers previously saw a wrongactual_lengthand missing stream-position metadata.AuthorityMarcReaderandHoldingsMarcReaderatvalidation_level="strict_marc"no longer tripInvalidLeader(E002) on leader bytes valid for their own record type. Each reader now applies its own MARC 21 format's leader rules (Authority / Holdings) rather than the Bibliographic allowed-value sets; bibliographic dispatch is unchanged.
- Bump urllib3 from 2.6.3 to 2.7.0
- Bump mypy from 1.20.2 to 2.1.0
MarcErrorvariants restructured to struct form carrying positional metadata (record_index,byte_offset,record_byte_offset,record_control_number,field_tag,indicator_position,subfield_code,found,expected,source_name). Wrapping variants (IoError,XmlError,JsonError) chain via#[source]soError::source()walks correctly;Displayoutput changed for every variant. Direct constructors and pattern matches need updating. Migration: match on.code()("E001"–"E099","E101"+) instead of variant names — codes are stable across future enum changes.MarcError::InvalidRecordremoved. Use the new specific variants (DirectoryInvalid,RecordLengthInvalid, …) orInvalidFieldas the fall-through.MarcError::ParseErrorremoved. Same migration asInvalidRecord: the new specific variants andInvalidFieldcover the cases thatParseErrorused to wrap. Match on.code()for stable identity.recovery::RecoveryContextremoved from the public re-exports. No external consumer; the type was an internal helper that accumulated messages and dropped them. Pattern-match onMarcErrorinstead.recovery::try_recover_recordtakes a&ParseContextas a fifth parameter. The previous v0.7 signature constructedRecoveryContextinternally; v0.8 callers pass theParseContextthat carries record-index / byte-offset state.
- Structured positional context on every parse error — record
index, absolute and record-relative byte offsets, 001 control number,
field tag (and indicator position or subfield code where applicable),
offending bytes (capped at 32), and source filename. New
MarcError::detailed()multi-line diagnostic includes a hex-dump of the byte window around the error offset; the defaultDisplayis a one-liner. Both produce byte-for-byte identical output across Rust and Python via matching_format()/detailed()methods on every Python exception class. - Typed Python exception subclasses for every
MarcErrorvariant (e.g.InvalidIndicator,BadSubfieldCode,TruncatedRecord,XmlError,JsonError,WriterError,FatalReaderError) extending the closest pymarc-named parent so existing pymarc-styleexceptclauses keep catching the same conditions. Each carries positional kwargs, supports pickle round-trip, and survives the PyO3 boundary with all attributes intact. - Stable error codes (
E001–E007,E099,E101,E105,E106,E201,E202,E301,E401,E402,E404) on every variant viaMarcError::code()/slug(), with matchingcode/slug/help_url()on every Python class. Codes never get renumbered (policy inCONTRIBUTING.md);MRRC_DOCS_BASE_URLoverrides the help-URL host. All codes documented indocs/reference/error-codes.md. - Structured error serialization via
to_dict()/to_json()on every Python exception (suitable for ELK / Datadog / Splunk) andMarcError::to_json_value()/to_json()on the Rust side; bytes fields hex-encoded,_causeflattens the exception chain,schema_version: 1for forward-compat. NewBytesNearpublic struct exposes the captured byte window. with_source()/from_path()builder methods onMarcReader,AuthorityMarcReader, andHoldingsMarcReader.from_pathpopulates the source name from the file path so emitted errors carry the filename.- Per-stream recovered-error cap
(#110) via
with_max_errors(n)on all three readers. InLenient/Permissivemodes the reader counts each recovered failure and halts withMarcError::FatalReaderError(E099) once the cap (default 10000) trips. Pass0to disable. - Per-field lenient/permissive recovery parity across all three
ISO 2709 readers (#121,
#122).
AuthorityMarcReaderandHoldingsMarcReadernow honor the same per-field error sites the bibliographic reader does (bad field-length / start-position digits, field-extends-past-data, data-field-too-short-for-indicators,parse_data_fieldfailure). Strict-mode behavior unchanged at every site; lenient/permissive mode skips the offending entry, counts the recovery against the per-stream cap, and continues. Truncated-record dispatch in lenient/permissive mode no longer returns a hard error on a short read; instead it notes the recovery and falls through to best-effort directory parsing. - 8 property tests (
tests/properties.rs) covering binary, MARCXML, and MARCJSON round-trips plus four ISO 2709 structural invariants (leader length, directory tiling, indicator byte set, subfield code shape).ProptestConfig { cases: 64 }; full suite ~3s locally. Primer atdocs/contributing/formal-methods.md(#111). - Coverage-guided fuzzing
(#90,
#115). Standalone
fuzz/Cargo workspace with cargo-fuzz; nightly CI matrix runsparse_record(full reader) androundtrip_binary(reader → writer → reader coupling) for 5 minutes daily at 03:00 UTC. Findings are not a PR gate; reproducers get copied intotests/data/fuzz-regressions/to run on every PR. Triage playbook indocs/contributing/fuzzing.md. docsextra inpyproject.tomlfor mkdocs site dependencies; CI builds viauv sync --extra docs --no-install-projectso the docs-only job doesn't need a Rust toolchain.
- Shared ISO 2709 parsing primitives + generic skeleton
(#125).
src/iso2709.rsowns the leader read, truncation-aware record-data read, single-entry directory parsing, ASCII numeric helpers, and the control-field-tag predicate. Newiso2709_skeleton::Iso2709Buildertrait +parse_iso2709_record<R, B>skeleton drives one record's parse end-to-end; each reader implementsIso2709Buildervia a small adapter (~60 lines each) soread_recordcollapses to a one-line dispatch. ~200 lines of near-duplicateread_recordbody across three readers replaced by one shared implementation. Newrecovery::RecoveryCapstruct consolidates the per-stream cap state machine that had been duplicated. Behavior changes that fall out of the unification:AuthorityMarcReadernow treats too-short data fields as a strict-Err / lenient-skip event with cap accounting (was silently skipped in all modes);HoldingsMarcReader's field-extends-beyond-data lenient branch now salvages a clamped slice (matching bib); the holdings "field exceeds data" error message changed wording to match the other two readers. The skeleton is<R: Read, B: Iso2709Builder>with nodyndispatch so trait calls monomorphize and inline at every call site, preserving hot-loop characteristics. Per-type quirks (authority's tag UTF-8 strictness + trailing0x1Ftrim, holdings' strict UTF-8 on control fields) preserved via trait-method overrides; the wider strict-vs-lossy unification is tracked separately as a follow-up. HoldingsMarcReader::with_max_errorsis now active. Originally landed inert when the cap was introduced (no recovery sites in the holdings path); the recovery sites added in this cycle hook into it.MarcErrorsource-error chain walks correctly forIoError,XmlError,JsonError(was previously empty). Code that was checkingError::source() == Nonemay need updating.- Pinned Rust toolchain to 1.95.0
(#96,
#97) via
rust-toolchain.toml. Library MSRV (Cargo.toml→rust-version = "1.71") is unchanged. - CI: skip workflows on docs-only changes. Added
paths-ignorefor**.md,docs/**,mkdocs.yml,LICENSE, and.gitignoretolint,test,build,python-build,benchmark-python, andbenchmark-rustworkflows. A docs-only push or PR previously fired ~47 jobs across the six workflows; it now fires zero. Mixed PRs (code + docs) still run normally —paths-ignoreskips only when every changed path matches. .cargo/check.shbuilds the docs site (mkdocs build) in full mode, surfacing broken cross-links pre-push. Skipped under--quick. Requires thedocsextra (uv sync --all-extras).- mkdocs warnings cleanup. Excluded
docs/history/(archival per CLAUDE.md) from the published site, removed its nav entry, and fixed broken cross-links and a stale anchor in active docs. Build warnings down from 18 to 4.
- Python typing fidelity improvements. Stub gaps in
mrrc/_mrrc.pyiclosed (parse_batch_parallel/parse_batch_parallel_limitedsignatures,Field.delete_subfield,Record.to_marc21, module-level__version__) and several wrapper-side narrowing issues fixed.mypy mrrc/andpyright mrrc/now both report zero errors and run in.cargo/check.shfull mode. Type-only — no runtime behavior change. - MARCXML reader: missing XML 1.1 §2.11 end-of-line normalization
in text and CDATA content
(#112). Switched both
arms of
read_leaf_textfromdecode()toxml_content()so CR / CRLF / NEL / LSEP normalize to LF per spec. Domain impact is small (MARC field content rarely carries line separators) but the divergence from spec was real. - Read-path performance regression from structured-error refactor
(#117). The
ParseContextrefactor stopped the compiler from inliningparse_data_field, costing ~15-17% on read hot paths vs v0.7.6 (investigated in #116). Restored to within +9-11% of baseline via#[inline(always)]onparse_data_fieldpaired with shrinkingParseContext::current_field_tagtoOption<[u8; 3]>. The compact context is what lets forced inlining avoid ballooning L1-i cache usage on parallel workloads. - CI: Clippy
collapsible_matcherrors insrc/bibframe/converter.rsafter Rust stable advanced to 1.95.0 (#96, #97). - CI: ASAN job failed with
-Zbuild-stdon stable; setsRUSTUP_TOOLCHAIN=nightlyon the ASAN step (#105). - CI: Miri job could not run
instasnapshot tests; settingINSTA_WORKSPACE_ROOTskips thecargo metadataspawn (#106). - CI: docs deploy broke when #130
switched the install line to build mrrc itself; now uses
uv sync --extra docs --no-install-project(#131).
- Bump ruff from 0.15.11 to 0.15.12
- Bump mypy from 1.20.1 to 1.20.2
- Bump pyright from 1.1.408 to 1.1.409
MARCReadererror-handling flags (#78, #80): pymarc-compatible kwargs forMARCReader:to_unicode: accepted for compatibility, warns ifFalse(mrrc always converts to Unicode).permissive: yieldsNonefor bad records instead of raising, matching pymarc behavior.recovery_mode: exposes mrrc'sRecoveryModefor salvaging partial data from malformed records.- Thanks to @acdha for filing #78.
- Repeated control fields silently dropped (#77, #79): Control fields (e.g., multiple 007s) were stored in
IndexMap<String, String>, causing later values to overwrite earlier ones. Changed toIndexMap<String, Vec<String>>across all three record types (Record,AuthorityRecord,HoldingsRecord). Updated all 7 serialization formats (ISO 2709, JSON, MARCJSON, MARCXML, CSV, Dublin Core/MODS, BIBFRAME) to emit all values. The pymarc-compatibleget_fields()API now correctly returns all repeated control fields, matching pymarc behavior. Thanks to @acdha for reporting. delete_subfield()did not actually delete (#81, #82): Was returning the value without removing the subfield. Added RustField::delete_subfield()method, exposed via PyO3, and updated the Python wrapper to delegate. Now matches pymarc behavior.recovery_modevalidation for authority/holdings readers (#82): Authority and holdings readers now reject invalidrecovery_modevalues withValueErrorinstead of silently defaulting to strict.
- Simplified control field API: Removed
get_control_field_values()accessor fromRecord— redundant with direct access to the publiccontrol_fieldsmap.get_control_field()remains as a convenience for non-repeatable tags (001, 003, 005, 008). - Reduced code duplication across Python bindings and Rust core (#82):
- Extracted
_wrap_field()and_wrap_record()helpers in the Python wrapper, replacing 16+ inconsistent wrapping patterns. - Extracted shared
reader_helpers.rsfor PyO3 reader backends, reducing ~225 lines of copy-paste between authority and holdings readers. - Extracted
control_field_char_at()utility for 008 field parsing, replacing 5 duplicated patterns across authority and holdings record types.
- Extracted
- Bump ruff from 0.15.8 to 0.15.10
- Bump mypy from 1.19.1 to 1.20.1
- Bump pytest from 9.0.2 to 9.0.3
- Bump softprops/action-gh-release from 2 to 3
- Bump actions/upload-pages-artifact from 4 to 5
Record.get(tag)for pymarc compatibility: Dict-likerecord.get('245')returns the first matching field or a default value, mirroring pymarc'sRecord.get(). Delegates to existingget_field().Field.is_control_field()andControlField.is_control_field()for pymarc compatibility: ReturnsFalseon data fields andTrueon control fields, matching pymarc's unifiedField.is_control_field()API.Record.__str__andRecord.__repr__: The PythonRecordwrapper now delegates to the Rust implementation instead of showing the default<mrrc.Record object at 0x...>.str(rec)returnsRecord(type=a)andrepr(rec)returns<Record type=a fields=N>.SubfieldPatternQuerynegation flag (#63, #73):SubfieldPatternQuery("020", "a", r"^978-", negate=True)pushes pattern inversion into Rust so non-matching fields never cross the FFI boundary. Thanks to @acdha@code4lib.social for suggesting this.SubfieldValueQuerynegation flag (#64, #74):SubfieldValueQuery("650", "a", "History", negate=True)pushes value inversion into Rust. Works with both exact and partial matching.- Comprehensive pymarc API compatibility (#71, #72): Full drop-in replacement for pymarc. Thanks to @mistersql@mastodon.social for highlighting several of these compatibility gaps. Changes include:
- Record accessors as
@property: All 17 record accessors (title,author,isbn,issn,subjects,location,notes,publisher,uniform_title,sudoc,issn_title,issnl,pubyear,series,physical_description, plus aliasesphysicaldescription,uniformtitle,addedentries) are now properties, matching pymarc'srecord.titlesyntax. - Unified
ControlFieldintoField:Field('001', data='12345')creates a control field.ControlFieldremains as a backward-compatible subclass. Control field content accessed via.dataattribute. Record['xxx']raisesKeyErrorfor missing tags (userecord.get(tag)for safe access), matching pymarc behavior.Record.as_marc()/as_marc21(): Returns ISO 2709 bytes.Record.as_json()/as_dict(): pymarc-compatible MARC-in-JSON serialization.Field.value(): Space-joined subfield values.Field.format_field(): Human-readable text representation.Field.as_marc()/as_marc21(): Field-level binary serialization.Field.add_subfield(code, value, pos=N): Positional insert support.Field.linkage_occurrence_num(): Extract $6 linkage info.Field.convert_legacy_subfields(): Classmethod for old flat-list format.add_field(*fields): Accepts multiple fields at once.remove_field(*fields): Accepts multiple fields, returns None.remove_fields(*tags): Bulk removal by tag.add_ordered_field(*fields): Tag-sorted insert.add_grouped_field(*fields): Insert after same-tag group.parse_xml_to_array(),parse_json_to_array(),map_records(): Module-level convenience functions.- MARC constants:
LEADER_LEN,DIRECTORY_ENTRY_LEN,END_OF_FIELD,END_OF_RECORD,SUBFIELD_INDICATOR,MARC_XML_NS,MARC_XML_SCHEMA. - Exception hierarchy:
MrrcExceptionbase class withRecordLengthInvalid,RecordLeaderInvalid,BaseAddressInvalid,BaseAddressNotFound,RecordDirectoryInvalid,EndOfRecordNotFound,FieldNotFound,FatalReaderError, andBadSubfieldCodeWarning. pubyearreturnsstr(notint), matching pymarc.
- Record accessors as
- Record accessors are now
@property:record.title()→record.title. Affects all 17 accessors:title,author,isbn,issn,issn_title,issnl,subjects,notes,location,series,sudoc,publisher,pubyear,physical_description,uniform_title, plus newphysicaldescription,uniformtitle,addedentries. pubyearreturnsstr: Previously returnedOptional[int], now returnsOptional[str]to match pymarc.Record['xxx']raisesKeyErrorfor missing tags: Previously returnedNone. Userecord.get(tag)for safe access that returnsNone.ControlFieldunified intoField:ControlFieldis now a subclass ofField. Control field content is in.data(not.value).Field('001', data='12345')is the preferred constructor.remove_field()returnsNone: Previously returned a list of removed fields.add_field()/remove_field()accept*args: Signature changed from single field to*fields. Existing single-arg calls still work.- Default Field indicators changed from
'0'to' ': Matches pymarc. Only affects fields created without explicit indicators.
SubfieldPatternQueryandSubfieldValueQuerymarked#[non_exhaustive](#74): Prevents external struct literal construction, requiring use of constructors. Future field additions will not be semver-breaking.
- Bump pygments from 2.19.2 to 2.20.0 (#67)
- Bump ruff from 0.15.4 to 0.15.8
- Bump actions/deploy-pages from 4 to 5
- Bump codecov/codecov-action from 5 to 6
mrrc.__version__now reports the correct version: Previously hardcoded as"0.1.0"in bothmrrc/__init__.pyandsrc-python/src/lib.rs. Now derived fromCargo.tomlat compile time viaenv!("CARGO_PKG_VERSION"), eliminating the need to manually update version strings during releases.
- Migration guide updated: Added
record.get()andfield.is_control_field()examples todocs/guides/migration-from-pymarc.md, reflecting closer pymarc API parity. - All docs and examples updated for pymarc API compatibility: Record accessors now shown as properties (
record.titlenotrecord.title()), control field access uses.data, missing-field access patterns userecord.get()or try/except, and new methods/constants/exceptions are documented throughout the API reference, migration guide, quickstart, tutorials, and runnable examples.
- Proptest binary round-trip property test (#39, #42): New
tests/properties.rsuses proptest to generate arbitrary structurally valid MARC records and verify that serialization to ISO 2709 and parsing back produces identical results. Covers leader fields, control fields, and data fields with indicators and subfields. - Cargo-semver-checks CI workflow (#37, #40): New
.github/workflows/semver.ymlrunscargo-semver-checkson PRs that touchsrc/orCargo.tomlto catch accidental semver-breaking changes. - Nightly Miri CI workflow (#38, #41): New
.github/workflows/miri.ymlrunscargo +nightly miri test --libon a daily schedule to detect undefined behavior. serialization_never_panicsproperty test (#44, #46): New proptest verifyingMarcWriternever panics or errors on any generated record, catching serialization bugs the round-trip test might miss.- Deduplicate control field tags in proptest (#43, #45):
arb_record()now uses aHashSetto skip duplicate control field tags, ensuring generated records are structurally valid per MARC.
check.sh --quicknow skips Python tests (#53, #56):--quickpreviously skipped the maturin build but still ran Python tests against a stale extension. Now--quickruns rustfmt, clippy, ruff, Rust tests, and doc tests (~10s). Full mode adds doc check, audit, maturin build, and Python tests (~16s).
- Serialization functions now accept wrapped Python Records (#57, #58):
record_to_json(),record_to_xml(),record_to_marcjson(),record_to_mods(),record_to_dublin_core(), andrecord_to_dublin_core_xml()previously rejectedRecordobjects returned byxml_to_record()and other deserialization functions withTypeError: 'Record' object is not an instance of 'Record'. The PyO3 functions expected the raw_mrrc.Recordtype but received the Python wrappermrrc.Record. All six functions now use a sharedextract_record()helper that accepts both types, matching the pattern already used byrecord_to_csv(). record_to_dublin_core_xmlnow exported from Python wrapper (#59): The function existed in the Rust extension (_mrrc) but was missing frommrrc/__init__.pyimports and__all__, making it inaccessible asmrrc.record_to_dublin_core_xml().- Skip rayon tests under Miri (#47, #49): Annotate 4
rayon_parser_pooltests with#[cfg_attr(miri, ignore)]to work around a known stacked borrows violation incrossbeam-epoch0.9.18 (crossbeam-rs/crossbeam#1181). Tracking re-enablement in #48. - Arithmetic overflow panic on malformed leaders (#32):
MarcReader,AuthorityReader, andHoldingsReadernow returnErr(MarcError::InvalidLeader)instead of panicking whenrecord_lengthordata_base_addressin the leader is less than 24. NewLeader::validate_for_reading()method performs the check in all three binary readers. - Python examples used Python file I/O instead of Rust I/O (#53, #54): All Python examples now pass file path strings to
MARCReader/MARCWriterinstead ofopen()file objects, using the Rust I/O backend which releases the GIL. Updated type stub docstring examples to match.
- Agent docs overhaul (#50): New
CLAUDE.mdwith project overview, key files, build/test commands, and architecture reference. RewrittenAGENTS.mdreplaces inception-era framing, migrates allbdreferences tobr. - Docs navigability improvements (#51): Added
_mrrc.pyitype stub admonition to Python API reference page. Moved Design and History under Contributing in mkdocs nav. - Context7 configuration (#52): New
context7.jsonconfiguring documentation indexing for source, bindings, Python package, docs, and examples. - Migrate bd references to br (#55): Migrated
.github/copilot-instructions.mdanddocs/contributing/release-procedure.mdfrombdtobrcommands, with explicitbr sync --flush-only+ git commit workflow. - Updated migration guide (
docs/guides/migration-from-pymarc.md) to recommend path-basedMARCReaderinput instead of Python file objects, with comments explaining GIL release and multi-thread parallelism benefits.
- Standard MARCXML support (#15):
xml_to_record()andrecord_to_xml()/to_xml()now produce and parse standard LOC MARCXML. Output includes XML declaration,xmlns="http://www.loc.gov/MARC21/slim"namespace, andtag/ind1/ind2/codeas XML attributes (not child elements). Parsing accepts all three namespace forms: default namespace (<record xmlns="...">), prefix namespace (<marc:record xmlns:marc="...">), and no namespace (<record>). Newxml_to_records()function parses<collection>wrappers containing multiple records. Rust module renamed fromxmltomarcxml(record_to_marcxml(),marcxml_to_record(),marcxml_to_records()); Python API names unchanged for pymarc compatibility. get_linked_fields()for 880 alternate script field linkage (#19): New pymarc-compatiblerecord.get_linked_fields(field)returns all 880 fields linked via subfield $6 occurrence matching. Also addsget_linked_field()(singular),get_original_field()(reverse lookup from 880 to original), andget_field_pairs(tag)(original + 880 tuples). FixedLinkageInfoparser to handle real MARC script identification codes:(2(Hebrew),(3(Arabic),$1(CJK),(N(Cyrillic),(S(Greek). Includes tests for Hebrew, Arabic, Chinese, Russian, and Greek scripts plus linkage across multiple field types (title, author, publisher, subject, series, notes, added entries).MARCReader.backend_typeproperty: Exposes the reader's I/O backend as a string ("rust_file","cursor", or"python_file") for diagnostics and testing.- Dependabot configuration: Enabled automated dependency updates for
cargo,uv, andgithub-actionsecosystems (weekly schedule). - GIL release verification tests: Deterministic tests proving
py.detach()actually releases the GIL during record parsing. Usessys.setswitchinterval(100.0)to suppress automatic GIL switching, then verifies a background counter thread makes progress — which can only happen if the GIL is explicitly released. Covers all three backends:rust_file,cursor, andpython_file.
- Removed 22 duplicate inherent methods from
Record(#26): Helper methods liketitle(),publisher(),isbn(), etc. were defined both as inherent methods onRecordand on theRecordHelperstrait. The inherent methods shadowed the trait, causing maintenance burden and confusion. Removed all duplicates; callers now use theRecordHelperstrait (already re-exported in the public API). Rust users may need to adduse mrrc::RecordHelpers;if not already imported. - Replaced flaky GIL release test with deterministic verification (#3): The
test_pathlib_path_threading_equivalent_to_strtest used timing-based speedup ratios to verify GIL release, which was unreliable on shared CI runners. Replaced withTestBackendTypeRouting(6 tests asserting each input type routes to the correct backend) andTestPathlibThreadingCorrectness(2 tests verifying parallel reads produce correct results without speedup assertions). - quick-xml 0.31 → 0.39 (#7): Migrated MODS XML parser to quick-xml 0.39 API.
BytesText::unescape()replaced bydecode()with explicit entity resolution via newEvent::GeneralRefhandler (predefined XML entities and numeric character references).Reader::trim_text()moved toReader::config_mut().trim_text(). - pyo3 0.27 → 0.28 (#8): Migrated PyO3 bindings to 0.28 API for free-threaded Python support.
Python::with_gil()→Python::attach(),Python::assume_gil_acquired()→Python::assume_attached(). Addedfrom_py_objectannotation to all 10#[pyclass]types implementingClone. - thiserror 1.0 → 2.0 (#6): Updated error derive macro.
- nom 7.1 → 8.0 (#5): Updated parser combinator library.
- ruff 0.14.14 → 0.15.2 (#9): Updated Python linter.
- GitHub Actions updates: actions/checkout 4 → 6 (#14), codecov/codecov-action 4 → 5 (#13), astral-sh/setup-uv 4 → 7 (#12), actions/download-artifact 4 → 7 (#11), moonrepo/setup-rust 0 → 1 (#10), actions/setup-python 5 → 6 (#24), actions/upload-artifact 4 → 6 (#23), actions/upload-pages-artifact 3 → 4 (#22), actions/cache 4 → 5 (#21), softprops/action-gh-release 1 → 2 (#20).
- Format conversion functions now return wrapped Python objects (#16):
marcjson_to_record(),json_to_record(),xml_to_record(),mods_to_record(), andmods_collection_to_records()previously returned raw RustPyRecordobjects. Field subscripting (field['a']),get_subfields(), and leader indexing (leader[9]) all failed on these records. Now all format conversion functions wrap results in the PythonRecordclass, matching the behavior of records fromMARCReader. publisher()now checks 264 (RDA) as fallback (#17): Previously only returned $b from field 260. Now falls back to field 264 with indicator2='1' (publication) for RDA-cataloged records. Also updatedplace_of_publication(),publication_date(),publication_info(), andpublication_year()/pubyear()to check 264 as fallback, matching pymarc behavior.subjects()now covers all 6XX fields (#18): Previously only returned $a values from field 650 (topical term). Now matches pymarc'ssubjects()coverage: 600, 610, 611, 630, 648, 650, 651, 653, 654, 655, 656, 657, 658, 662, 690-691, 696-699. Also updatedsubjects_with_subdivision()andsubjects_with_note()to search all subject fields.- Removed flaky timing assertion from
test_backend_comparison_1k: The benchmark'sassert median_rustfile < median_pythonfile * 2failed intermittently on shared CI runners due to disk I/O variance. Timing data is still printed for informational purposes.
- Updated Python quick example in
docs/index.mdto use path string instead of file I/O wrapper, demonstrating the GIL-releasing code path.
- MODS XML read support:
mods_to_record(xml_str)andmods_collection_to_records(xml_str)parse MODS XML (single record or<modsCollection>) into MARCRecordobjects. CoverstitleInfo,name,subject,originInfo,physicalDescription,abstract,note,genre,classification,location,relatedItem,recordInfo,identifier,accessCondition,tableOfContents,targetAudience, andlanguageelements with conformance tests using LOC-derived fixtures. - Cross-compiled Linux wheels (mrrc-experiments#16): Release workflow now builds
aarch64andi686Linux wheels via maturin-action Docker cross-compilation, increasing wheel count from 15 to 25 per release. - Field constructor
subfields=andindicators=kwargs (mrrc-experiments#15):Fieldnow acceptssubfields=(list ofSubfield) andindicators=(list/tuple of two strings) as keyword arguments, enabling inline construction matching pymarc style:Field('245', indicators=['0', '1'], subfields=[Subfield('a', 'Title')]). The Rust binding already supported this signature; the Python wrapper now passes through both kwargs. - Record constructor
fields=kwarg (mrrc-experiments#15):Recordnow accepts an optionalfields=keyword argument (list ofField), enabling inline record construction:Record(fields=[Field(...), ...]). This goes beyond pymarc parity (pymarc'sRecord.__init__does not acceptfields=) as a UX improvement.Record()with no arguments also now works, defaulting toLeader(). - Constructor kwargs tests: 8 new tests in
TestConstructorKwargscoveringindicators=,subfields=,fields=, combined usage, full inline construction, and backward compatibility. - PEP 561
py.typedmarker: Added to rootmrrc/package so type checkers recognize shipped type stubs. - Project layout documentation: New
docs/contributing/project-layout.mddescribes the three-layer architecture (core Rust → PyO3 bindings → Python package), how maturin and the Cargo workspace connect them, directory structure, build commands, and common development workflows. - Pre-push git hook:
.githooks/pre-pushruns.cargo/check.shbefore every push, preventing code from reaching CI that would fail local checks. Opt-in viagit config core.hooksPath .githooks. check.sh --quickflag: Skips docs, audit, and maturin build for faster inner-loop iteration (keeps fmt, clippy, tests, ruff).
- GitHub releases now include changelog notes:
python-release.ymlauto-extracts the relevant CHANGELOG.md section and includes it in the GitHub Release body. Backfilled v0.7.1 release notes. - Pymarc compatibility tests fully enabled: Removed 10
pytest.skipguards fromtest_pymarc_compatibility.py— all features (to_json, to_xml, to_marcjson, to_dublin_core, to_marc21, writer, roundtrip, test data) were already implemented. 88/88 tests now pass with 0 skipped. - Documentation updated for inline construction: Quickstart, API reference, migration guide, writing tutorial, and examples now show
Field(subfields=..., indicators=...)andRecord(fields=...)as the primary construction pattern, withadd_subfield()/add_field()as the incremental alternative. Migration guide updated to reflectRecord()no longer requires an explicitLeaderargument and field creation is now closer to pymarc. - Type stubs updated:
Field.__new__andRecord.__new__in.pyinow include the new keyword-only parameters. - Type stubs enriched: Merged rich docstrings into
mrrc/_mrrc.pyifrom the oldsrc-pythonstubs — Leader properties with MARC position descriptions, MARCReader GIL/concurrency/thread-safety docs, MARCWriter context manager protocol, BibframeConfig setter methods and property getters, RdfGraphparse()/triples()/__len__(). Addedmods_to_recordandmods_collection_to_recordsstubs. - Removed stale
src-python/python/directory: Deleted redundant Python package directory (1,235 lines) left over from the Phase 5 layout migration. Rootmrrc/is now the sole Python package location. - Test suite audit: Deleted 5 redundant/inert files from
src-python/tests/, migrated 4 test files totests/python/(backend parity, type detection, rayon parser pool, record boundary scanner), extracted 1 pipeline regression test. Deletedsrc-python/tests/entirely. Python test count: 364 → 433. - Ruff lint compliance: Fixed 76 ruff errors across
mrrc/andtests/python/(bare except, unused imports/variables, walrus operator patterns, f-string placeholders, true/false comparisons). Added ruff check step tocheck.sh. - Rust integration tests in check.sh: Changed
cargo test --libtocargo test --lib --tests, adding 17 integration test files (bibframe, mods, field query, concurrent GIL, etc.) to the local CI script. - CI workflow alignment with check.sh: Closed gaps where CI was missing checks that check.sh runs locally. Added clippy on mrrc-python to
lint.yml(was only linting mrrc core). Added ruff Python lint job tolint.yml(viauvx). Fixed rustdoc scope from--package mrrcto--allto include PyO3 bindings. Changedtest.ymlfrom--lib --binsto--lib --tests --package mrrc, adding 15+ integration test files that were skipped in CI. Removed unused mypy/pyright install frompython-build.ymltest-wheels job. Upgradedactions/cachefrom v3 to v4 across all workflows. - CI efficiency improvements: Switched all
pip installcalls touv pip install --systemacrosspython-build.yml,benchmark-python.yml, andpython-release.ymlfor faster dependency resolution. Consolidated three separate cargo cache actions (registry, index, build) into single multi-path caches across five workflows. Switched cargo-audit and cargo-tarpaulin fromcargo install(compiles from source) to pre-built binaries viataiki-e/install-action. Combined two tarpaulin runs into one (--out Xml Html). Upgradedcodecov/codecov-actionfrom v3 to v4,actions/setup-pythonfrom v4 to v5. Removed redundantpip install maturinand single-element Rust version matrix frombuild.yml.
- Rust formatting: Applied
cargo fmttosrc/mods.rsandsrc-python/src/formats.rs(pre-existing match arm brace style). - Clippy doc lint: Escaped
OCoLCintests/mods_conformance_tests.rsdoc comment to satisfyclippy::doc_markdown. - Flaky CI benchmark:
test_bytesio_vs_file_isolationswitched from mean to median for I/O overhead measurement, preventing single-iteration CI runner spikes from failing the test. Removed hard assertion (test value is diagnostic, not a correctness gate).
- BREAKING: Removed 7 experimental serialization formats (mrrc-experiments#19): Deleted protobuf, arrow, parquet, flatbuffers, messagepack, cbor, and avro support (~15,100 lines across 78 files). These added significant build complexity and dependency weight without proven adoption. Only ISO 2709 and BIBFRAME remain.
- BIBFRAME promoted to core: BIBFRAME conversion is now always-compiled rather than feature-gated (
format-bibframe), reflecting its importance as the LOC linked data format. Dependenciesoxrdfioandoxrdfare now non-optional. - Python bindings simplified: Removed format-specific classes (ProtobufReader, ArrowWriter, etc.),
mrrc/analytics.py, and format submodules. Simplifiedread()/write()helpers and__init__.pyexports. - Clean repo history: Recreated repository as
dchud/mrrcwith a clean two-commit history, replacingdchud/mrrc-experiments. Build artifacts, large test fixtures, and accumulated cruft from the experimental phase are no longer in git history. - Test fixtures: Removed 25MB
100k_records.mrcfrom repo; gitignored to prevent re-commit. Regenerable viascripts/generate_benchmark_fixtures.py. Benchmark tests depending on 100k fixture removed. - Build simplification: Removed
build.rs(was only for protobuf/flatbuffers code generation),src/generated/,proto/, and ~20 dependencies fromCargo.toml. - Local CI script: Updated
.cargo/check.shto useuv runfor maturin and pytest steps instead of manual venv activation. - 100k fixture scrub: Removed remaining references to
100k_records.mrcfrom scripts, benches, CI workflow names, and public docs (benchmarks, contributing, tests README). Fixture generation script no longer produces the 100k file. - Developer docs standardized on uv: Replaced manual venv activation and bare
maturin develop/pytestcommands withuv sync/uv runequivalents across installation, development setup, testing, and migration guides. - Release procedure updated: Refreshed for OIDC trusted publishing (replaces
PYPI_API_TOKEN), addedinstallation.mdandquickstart-rust.mdto version-bump checklist. - Rust dependency version in docs: Updated hardcoded
mrrc = "0.6"to"0.7"in installation and quickstart guides.
- Flaky benchmark test:
test_backend_comparison_1know uses 5 iterations with median instead of 3 iterations with mean, making it resilient to single-iteration CI runner I/O spikes. - CI: Python release workflow: Added
actions/setup-pythonbeforematurin-actionto properly set Python version;python-versionis not a valid input for maturin-action - CI: Python release OIDC publishing: Removed
password:secret frompypa/gh-action-pypi-publishstep; action auto-detects OIDC token fromid-token: writepermission - CI: Re-enabled ASAN memory-safety workflow: Upstream Rust issue (rust-lang/rust#144168) that caused zerocopy nightly incompatibility has been resolved
- CI: ASAN workflow zerocopy_derive fix: Removed
-Zavoid-dev-depsflag which caused zerocopy_derive proc-macro resolution failures on nightly - CI: CodSpeed benchmark integration: Re-linked repository with CodSpeed after clean repo creation
- Beads: Gitignore rotated daemon logs: Added
daemon-*.log.gzpattern to.beads/.gitignoreand removed accidentally committed 2.7MB rotated log file
- pathlib.Path GIL Release Tests: Added tests verifying that
pathlib.Pathobjects achieve the same GIL release behavior as string pathstest_pathlib_path_sequential_baseline: Baseline sequential reading with Path objectstest_pathlib_path_threading_equivalent_to_str: Verifies Path and str have equivalent threading speedup (within 30%)test_pathlib_vs_str_equivalent_performance: Confirms both use the same RustFile backend- Location:
tests/python/test_pathlib_gil_release.py
- Dropped Python 3.9 support: Python 3.9 reached end-of-life in October 2025
- Added Python 3.13 support: Stable release from October 2024
- Added Python 3.14 support: Stable release from October 2025
- Updated minimum Python version from 3.9 to 3.10 in pyproject.toml, CI workflows, and documentation
- Note: RHEL 9 users should use
dnf module enable python3.11or containers for Python 3.10+
- bytes crate upgrade: Upgraded
bytesfrom 1.9.0 to 1.11.1 to address RUSTSEC-2026-0007
- CodSpeed Rust Benchmark Integration (PR #8): Added continuous performance tracking for Rust benchmarks
- Integrated
codspeed-criterion-compatv4.3.0 as drop-in replacement for criterion - New
benchmark-rust.ymlworkflow runs Rust benchmarks viacargo codspeed - Added CodSpeed badge to README.md
- Integrated
- Benchmark Workflow Naming Consistency: Renamed workflows for clarity
python-benchmark.yml→benchmark-python.yml("Benchmarks: Python")codspeed.yml→benchmark-rust.yml("Benchmarks: Rust")- Groups benchmark workflows alphabetically, makes target language clear at a glance
- CodSpeed Action v4 Migration: Fixed benchmark workflow after upgrading from deprecated v1
- Added required
mode: walltimeparameter for standard CI runners - CodSpeed v4 now requires explicit mode selection ('simulation' or 'walltime')
- Added required
- Fixed benchmark documentation links: Updated internal links in
index.md,query-dsl.md, andarchitecture.mdto use lowercase filenames - Testbed Design Document (
docs/design/ideas-for-test-projects.md):- Added comprehensive state management design (YAML + SQLite hybrid)
- Added interaction models section (centralized vs local/private testing)
- Added repository growth timeline and pruning strategies
- Added MkDocs documentation structure with Divio-style organization
- Updated project phases (7 phases including documentation and initial data)
- Consolidated duplicate sections for cleaner document structure
- File Renames and Reorganization:
- Renamed
streaming-large-files.md→working-with-large-files.mdto better reflect content scope - Renamed benchmark docs to lowercase convention:
RESULTS.md→results.md,FAQ.md→faq.md,README.md→index.md - Updated all cross-references and mkdocs.yml navigation
- Renamed
- Fixed Broken Links:
- Fixed README.md tutorial links that were returning 404s
- Updated to point to actual tutorial paths (
tutorials/python/reading-records/,tutorials/rust/reading-records/)
- Formatting Fixes:
- Fixed bullet list formatting in
threading-python.md,performance-tuning.md,results.md,migration-from-pymarc.md - Added blank lines after colons before bullet lists (required by some markdown parsers)
- Fixed bullet list formatting in
- Content Improvements:
- Added uv example to
installation.mdfor building from source - Added hardware info (2025 MacBook Air M4) to
performance-tuning.md - Added Rust/Python format name columns to bibframe RDF serialization table
- Added "Same?" column to
migration-from-pymarc.mdAPI comparison tables for clarity - Removed emojis from
migration-from-pymarc.mdsection headers - Fixed
encoding.md: removed non-existentdetect_encodingimport, notedEncodingValidatoris Rust-only - Simplified
python-api.mdformat note (all formats bundled in wheel, no extra steps) - Added feature-gated format installation link to
formats.md
- Added uv example to
- Complete restructure using Material for MkDocs:
- New tabbed navigation with Getting Started, Tutorials, Guides, Reference, Examples sections
- Light/dark theme toggle with persistent preference
- Built-in search with instant results
- Mobile-responsive layout
- Getting Started:
quickstart-python.md: 5-minute Python quickstart with installation, reading, writing examplesquickstart-rust.md: 5-minute Rust quickstart with Cargo setup and basic operations
- Tutorials (split from monolithic guides):
- Python tutorials: reading-records, writing-records, querying-fields, format-conversion, concurrency (5 files)
- Rust tutorials: reading-records, writing-records, querying-fields, format-conversion, concurrency (5 files)
- Reference Documentation:
marc-primer.md: MARC record structure explained for non-librarians (leader, directory, fields, indicators, subfields)encoding.md: Character encoding reference (MARC-8, UTF-8, escape sequences, detection)python-api.md: Complete Python API reference (Record, Field, MARCReader, formats, BIBFRAME)rust-api.md: Complete Rust API reference (types, builders, traits, feature flags)
- Examples Index: Organized links to 29 example files by category (Python/Rust)
- README Reduction: Streamlined from 897 to 115 lines per documentation best practices
- Archived Legacy Docs: Moved superseded files to
docs/history/legacy-docs-superseded/
- Bidirectional MARC ↔ BIBFRAME conversion (
format-bibframefeature):marc_to_bibframe(): Convert MARC bibliographic records to BIBFRAME 2.0 RDF graphsbibframe_to_marc(): Reverse conversion from BIBFRAME RDF back to MARC records- Full implementation of LOC MARC21 to BIBFRAME 2.0 Conversion Specifications
- Support for all MARC content types: books, serials, music, maps, visual materials
- Python BIBFRAME Bindings (2026-01-29):
mrrc.marc_to_bibframe(record, config): Python wrapper for MARC→BIBFRAME conversionmrrc.bibframe_to_marc(graph): Python wrapper for BIBFRAME→MARC conversionmrrc.BibframeConfig: Configuration class with all options exposedmrrc.RdfGraph: RDF graph class withserialize()method for all formatsmrrc.RdfFormat: Enum for output format selection (RdfXml, NTriples, Turtle, JsonLd)
- Hub/Expression Support (2026-01-29):
- MARC 240 (Uniform Title) creates bf:Hub for expression-level grouping
- Work → hasExpression → Hub → hasInstance → Instance linking pattern
- Hub → expressionOf → Work reverse relationship
- Supports translations, versions, and other expression-level variants
- Item Creation from Holdings (2026-01-29):
- MARC 852 (Location) creates bf:Item entities linked to Instance
- Call number from $h/$i, sublocation from $b, barcode from $p
- MARC 876-878 (Item Information) support for detailed holdings
- Multiple Items per Instance with sequential URI generation
- Classification Support (2026-01-29):
- MARC 050 → bf:ClassificationLcc (Library of Congress Classification)
- MARC 060 → bf:ClassificationNlm (National Library of Medicine)
- MARC 080 → bf:ClassificationUdc (Universal Decimal Classification)
- MARC 082 → bf:ClassificationDdc (Dewey Decimal Classification)
- MARC 084 → bf:Classification (Other schemes with source in $2)
- classificationPortion ($a) and itemPortion ($b) properties
- RDF Serialization Formats:
- RDF/XML (W3C standard, maximum interoperability)
- N-Triples (simple line-based triple format)
- Turtle (human-readable, compact RDF syntax)
- JSON-LD (modern web standard, JSON representation)
- Configuration Options (
BibframeConfig):- Custom base URI for generated resources
- Output format selection
- Authority linking control
- BFLC extension support
- Comprehensive Test Suite (111 tests):
- 48 unit tests for individual MARC→BIBFRAME mappings (including Hub, Item, Classification)
- 26 validation tests for BIBFRAME 2.0 ontology compliance
- 16 integration tests with real-world record types
- 16 round-trip tests documenting acceptable data loss
- 5 baseline comparison tests against official LOC tool
- Examples:
- Rust:
marc_to_bibframe.rs,bibframe_to_marc.rs,bibframe_batch.rs - Python:
marc_to_bibframe.py,bibframe_roundtrip.py,bibframe_config.py
- Rust:
- Documentation:
- README section with conversion examples
- API documentation with docstrings
- Known limitations and data loss documentation
- Record fields now preserve insertion order instead of sorting by tag
- Uses
IndexMapfor field storage inRecord,AuthorityRecord, andHoldingsRecord - Enables round-trip fidelity: serialization and deserialization now preserve original field order
- Field iteration via
fields()andcontrol_fields_iter()returns fields in insertion order - Breaking change for code assuming tag-sorted order: Use
fields_by_tag()for explicit tag-based access
- Uses
- Unplanned changes during implementation:
fields_in_range()now iterates and filters (was usingBTreeMap::range())remove_fields_by_tag()now usesshift_remove()to preserve order of remaining fields
- Performance impact: ~17-22% regression in record parsing benchmarks (acceptable trade-off for round-trip fidelity)
- Baseline: 9.5ms for 10k records → After: 11.3ms for 10k records
- Still significantly faster than pymarc (~4x improvement maintained)
- SmallVec for Subfield Storage:
- Replaced
Vec<Subfield>withSmallVec<[Subfield; 4]>for subfield arrays - Targets common case: 85-90% of real-world MARC records have ≤4 subfields per field
- Performance gain: +4.6% roundtrip throughput (read+write cycle)
- Zero-copy inline storage for typical records, automatic heap spillover for large records
- Maintains API compatibility - transparent to users
- Replaced
- Parse Digits Optimization:
- Eliminated string allocations in
parse_digits()parser combinator - Reduced numeric field parsing overhead with direct byte-based validation
- Contributes to overall +6.0% combined optimization gain (measured across parse + serialize pipeline)
- Eliminated string allocations in
- Apache Arrow Columnar Format (
format-arrowfeature):ArrowWriter: Converts MARC records to Arrow RecordBatch for analytics workflowsArrowReader: Reads Arrow IPC files back to MARC records with full fidelity- Preserves field sequence and control field data through round-trip conversion
- Performance: 96% compression ratio, ~865k records/second throughput
- Enables SQL queries over MARC data via DuckDB and Polars integration
- FlatBuffers Zero-Copy Format (
format-flatbuffersfeature):FlatbuffersWriter: Serializes MARC records to FlatBuffers binary formatFlatbuffersReader: Zero-copy deserialization without parsing overhead- Ideal for memory-constrained environments and streaming applications
- Performance: 64% memory savings vs JSON, 1M+ records/second zero-copy access
- MessagePack Compact Binary (
format-messagepackfeature):MessagePackReader/MessagePackWriterfor compact binary serialization- 25% smaller than JSON with equivalent structure preservation
- Cross-platform interoperability with 50+ language implementations
- Performance: ~750k records/second throughput
- FormatReader/FormatWriter Traits: Unified interface enabling consistent API across all formats
- All Formats Available in Python: Arrow, FlatBuffers, MessagePack, and Protobuf readers/writers
- Format-Agnostic Helpers:
mrrc.read(path_or_data, format=None): Auto-detect format from file extension or contentmrrc.write(records, path, format=None): Write records to any supported format
- Type Stubs (
.pyifiles): Full IDE autocompletion and type checking support mrrc/formats/Package: Organized modules for format-specific operationsmrrc.formats.marc: ISO 2709 reading/writingmrrc.formats.protobuf: Protobuf serialization with schema evolutionmrrc.formats.arrow: Arrow/Parquet operations with analytics integrationmrrc.formats.flatbuffers: FlatBuffers zero-copy accessmrrc.formats.messagepack: MessagePack compact binary for APIs
mrrc.analyticsModule: SQL and DataFrame operations over MARC datato_duckdb(records): Create DuckDB relation for SQL queriesto_polars(records): Create Polars DataFrame for data analysis- Enables filtering, aggregation, and joins across millions of records
export_to_parquet()Helper: Convert Arrow tables to Parquet files for data lakes- Integration Examples: Working examples demonstrating analytics workflows
- New User Guides:
INSTALLATION_GUIDE.md: Complete installation instructions for Python and Rust, feature flags, platform-specific notes, troubleshootingFORMAT_SELECTION_GUIDE.md: Decision tree for choosing the right format, comparison matrix, use case recommendationsPYTHON_TUTORIAL.md: Comprehensive Python tutorial covering reading, writing, field access, format conversion, Query DSLRUST_TUTORIAL.md: Complete Rust tutorial with builder pattern, traits, parallel processing with RayonSTREAMING_GUIDE.md: Large file handling patterns, O(1) memory streaming, parallel processing strategies
- Format Support Matrix: Added comprehensive format comparison table to README.md
- Enhanced Python Docstrings: All format modules include use cases, performance notes, and working examples
- Binary Format Comparison: Evaluated MessagePack, CBOR, Avro, Arrow, FlatBuffers against library requirements
- Strategy Documentation: Decision rationale and benchmarks archived in
docs/history/format-research/
- Example Code Quality: Resolved clippy warnings in example code for clean builds
- Code Formatting: Applied rustfmt to all recent additions for consistency
- Benchmark Documentation Accuracy (2026-01-19):
- Refreshed all benchmark measurements with latest performance data
- Clarified that reported numbers are post-warm-up (JIT stabilized)
- Updated comparison baselines to reflect recent optimizations
- Architecture Documentation: Enhanced documentation structure in docs/design/
- Python Query DSL: Exposed Rust Query DSL to Python with full feature parity
FieldQuery: Builder pattern for complex field matching (tag, indicators, subfields)TagRangeQuery: Match fields within a tag range (e.g., 600-699 for all subjects)SubfieldPatternQuery: Regex matching on subfield valuesSubfieldValueQuery: Exact or partial string matching on subfield values
- Record Query Methods: New methods on Record for advanced field searching
fields_by_indicator(tag, indicator1=None, indicator2=None): Filter by indicatorsfields_in_range(start_tag, end_tag): Find fields within a tag rangefields_matching(query): Use FieldQuery objects for complex matchingfields_matching_range(query): Use TagRangeQuery for range-based matchingfields_matching_pattern(query): Regex matching via SubfieldPatternQueryfields_matching_value(query): String matching via SubfieldValueQuery
- Query DSL Documentation: Comprehensive guide at docs/QUERY_DSL.md covering:
- Philosophy: Why multiple query types (performance, clarity)
- All query types with examples
- Practical cataloging scenarios (LCSH filtering, ISBN-13, subject analysis)
- Comparison table vs pymarc's get_fields()
- Query DSL Tests: 42 new tests in tests/python/test_query_dsl.py
- Unified Testing Workflow: Single command (
.cargo/check.sh) for full pre-push verification (~30s)- Runs rustfmt, clippy, documentation, security audit, maturin build, and Python tests
- Uses pytest marker-based test selection (
-m "not benchmark") to run 314 core tests in ~6s - Excludes 61 benchmark tests from default run (available via
pytest -m benchmark) - Documented in AGENTS.md with command reference table and CI alignment
- CI Workflow Fixes:
- Fixed ASAN memory-safety workflow: Exclude dev-dependencies (zerocopy) that have nightly Rust incompatibility
- Fixed coverage workflow: Exclude PyO3 bindings package to avoid Python linker errors in tarpaulin
- Format Conversion API Naming: Standardized across all format modules for consistency
- Added
record_to_csv()for single-record CSV export (delegates torecords_to_csv()) - Documented
records_to_csv()plural pattern: semantically correct for batch-oriented tabular format - All format modules now follow:
record_to_format()(single) +records_to_format()(batch where applicable)
- Added
- CSV Export to Python: Exposed Rust CSV functions to Python bindings
record_to_csv(record): Export single record to CSV formatrecords_to_csv(records): Export multiple records to CSV (batch mode)records_to_csv_filtered(records, filter_fn): Export with custom field filtering (filter_fn takes tag string, returns bool)- Handles both direct PyRecord and wrapped Record instances for flexibility
- Completes API consistency with other format converters (JSON, XML, MARCJSON, Dublin Core, MODS)
- Memory Safety CI: Added ASAN (AddressSanitizer) workflow for nightly memory safety checks
- Runs on schedule (daily) and manual dispatch
- Validates no memory safety issues in core library
- Local
.cargo/check.sh --memory-checksoption for developers with nightly toolchain
- Phase Reference Removal: Cleaned up implementation-plan-internal references from all user-facing code
- Removed "Phase A-H" and gate nomenclature from source code comments
- Removed from Python wrapper module documentation
- Removed from test file names and test documentation
- Removed from example code
- Codebase now approachable to new users unfamiliar with development history
- Test File Reorganization:
test_h_gate_benchmarking.py→test_parallel_benchmarking.pytest_h5_integration.py→test_integration.pytest_queue_state_machine_c2.py→test_queue_state_machine.pytest_memory_profiling_c4.py→test_memory_profiling.py
- Leader Value Validation Helpers: Complete MARC 21 leader position reference implementation
Leader.get_valid_values(position)- Returns dict of valid values for each leader positionLeader.is_valid_value(position, value)- Validates values per MARC 21 specificationLeader.get_value_description(position, value)- Gets human-readable descriptions- Support for positions 5 (Record Status), 6 (Type of Record), 7 (Bibliographic Level), 17 (Encoding Level), 18 (Cataloging Form)
- Indicators as Tuple-Like Object: Full support for pymarc-compatible indicator access
field.indicators[0]andfield.indicators[1]indexingfield.indicatorsunpacking support- Backward compatibility with
field.indicator1andfield.indicator2properties
- Control Field Access Pattern: Support for pymarc's
record['001'].valuepattern- ControlField wrapper with
.valueproperty for control fields (001-009) - Both
record['001'].valueandrecord.control_field('001')patterns work identically - Backward compatibility maintained
- ControlField wrapper with
- pymarc API Parity Plan: Moved completed work to docs/history/ for archival
- API Compatibility Summary: Historical record of all 7 API gaps identified and resolved
- README Enhancement: Updated to highlight full pymarc API compatibility as primary feature
- Migration Guide: Comprehensive guide showing all compatible patterns with minimal migration path
- README: Removed "nearly" qualifier - now describes "full" pymarc API compatibility
- Python Test Suite: Expanded from 88+ to comprehensive coverage of all parity features
- Documentation Structure: Organized completed parity work into history directory
- No performance regressions - all existing benchmarks maintained
- Leader validation methods are zero-copy (dictionary lookups only)
- MARC 21 Reference Data: All leader position value mappings per MARC 21 specification
- API Stability: Python wrapper API now guaranteed stable for pymarc compatibility
- Backward Compatibility: All new features fully backward compatible with existing code
- Python Wheel Build Workflow: Fixed multi-platform wheel building for Ubuntu, macOS, and Windows
- Configured maturin to use
-i pythonX.Yflag for manylinux Python version selection (per maturin docs) - Fixed Windows glob expansion in test step using PowerShell conditionals
- Removed deprecated mypy/pyright strict type checking (to be addressed in future typing effort)
- Wheels now build and test successfully for Python 3.9, 3.10, 3.11, and 3.12 across all platforms
- Configured maturin to use
- Performance Reference Table (benchmarks/RESULTS.md): Updated to use pymarc as baseline (1.0x) for clearer speedup comparison
- Real-World Performance Scenarios: Standardized table formats across all four scenarios for consistent speedup and time-saved metrics
- PERFORMANCE.md Executive Summary: Updated to clarify recommended strategies (ProducerConsumerPipeline for single-file, ThreadPoolExecutor for multi-file)
- Documentation Audit: Moved completed audit to docs/history/ for archival
- GIL Release during I/O: Python wrapper now releases GIL during record parsing for true multi-thread parallelism
- Three-Phase Pattern: Robust pattern separates Python object access (GIL held) from CPU-intensive parsing (GIL released)
- Measured Performance: 2.04x speedup on 2 threads, 3.20x on 4 threads (Phase H benchmarking)
- BatchedMarcReader: Queue-based buffering reduces GIL contention from N to N/100 operations
- SmallVec Optimization: 4 KB inline buffer avoids allocations for ~85-90% of MARC records
- ReaderBackend Enum: Unified reader supporting multiple input types with automatic selection
- RustFile: Pure Rust file I/O (zero GIL overhead)
- CursorBackend: In-memory bytes (zero GIL overhead)
- PythonFile: Python file objects (GIL-managed)
- File Path Support: Direct file path input bypasses Python I/O layer entirely
- Bytes/Bytearray Support: In-memory MARC data via CursorBackend
- Automatic Detection: Input type automatically detected, optimal backend selected
- PERFORMANCE.md: Comprehensive guide with threading patterns, benchmarking methods, and tuning recommendations
- Threading Examples: Practical concurrent_reading.py and concurrent_writing.py examples
- API Documentation: Updated PyMarcReader/PyMarcWriter docstrings with threading guidance
- README Threading Section: Concrete speedup numbers (2.04x for 2 threads, 3.20x for 4)
- Benchmark Documentation: Updated docs/benchmarks/ with Phase H results
- Python Wrapper API: No breaking changes (fully backward compatible)
- Performance Profile: Single-thread throughput stable, multi-thread speedup now available
- Reader Construction: Accepts str/Path/bytes/bytearray in addition to file objects
- GIL Release Bug (Phase B): SmallVec copy pattern properly avoids borrow checker violations
- Error Handling (Phase B): ParseError conversion happens after GIL re-acquisition
- GIL Crossing (Phase C): py.detach() correctly releases GIL during Phase 2 parsing
- Threading Speedup: 2.04x (2 threads), 3.20x (4 threads) vs sequential reading
- Memory Overhead: SmallVec buffering <3% overhead vs single-threaded
- GIL Contention: Reduced from linear to O(n/100) with BatchedMarcReader
- File I/O: Pure Rust backend (file paths) eliminates all GIL overhead
- Phase H Integration: Producer-consumer pipeline with Rayon parallel record scanning
- Backpressure: Queue-based buffering prevents runaway producer threads
- Thread Safety: Each thread requires own reader instance (not Send/Sync by design)
- Efficiency: pymrrc 92% efficient vs pure Rust Rayon baseline
- PyO3 Python Wrapper: Full Python bindings via PyO3/Maturin
- Python Parallel Benchmarks: Threading and multiprocessing performance analysis
- pymarc Compliance Test Suite: 75+ tests validating compatibility with pymarc API
- Memory Usage Profiling: Benchmarks comparing Python wrapper overhead vs Rust native
- Context Manager Support: Python
withstatement support for file I/O
- Rust Parallel Benchmarks: Performance testing with rayon for concurrent MARC processing
- Rayon Parser Pool (H.4b): Parallel batch processing functions via Rayon thread pool
parse_batch_parallel()- Unlimited parallel parsing with dynamic work distributionparse_batch_parallel_limited()- Bounded parallel parsing respecting configured thread pool- Record boundary scanning and parallel record assembly
- Thread-safe batch processing for large MARC files
- Comprehensive Benchmark Suite:
- 1K/10K/100K record read performance
- Field access overhead measurements
- JSON/XML serialization performance
- Roundtrip (read+write) benchmarks
- Sequential vs parallel processing comparison
- Benchmark Results Documentation: Real measured performance data with pymarc comparisons
- CI Optimization: Caching and performance gate integration
- Design Documentation: Architecture and design decision documentation
- Benchmarking Documentation: Feasibility studies and performance analysis
- Executive Summaries: High-level overview of parallel processing capabilities
- Reorganized Docs: Consolidated design/ and history/ into docs/ hierarchy
- Examples: Real-world usage patterns demonstrating library features
- Migration Guide: Comprehensive guide highlighting near-100% API compatibility with pymarc
- Record.to_marc21(): Convert records back to ISO 2709 binary format
- Enhanced Python API: Full feature parity with Rust API in Python wrapper
- MARCWriter Fixes: Fixed write() method and improved record serialization
- Fixed deprecated PyO3 type alias warning
- Fixed 20+ clippy linting violations in benchmark files (missing semicolons in closure statements)
- Suppressed benchmark-specific documentation warnings at file level
- Fixed MARCWriter record serialization issues
- Cleaned up CI pipeline to pass all quality gates
- H.4a Bug Fix: RecordBoundaryScanner now correctly scans for 0x1D (record terminator) instead of 0x1E (field terminator) per ISO 2709 specification
- Improved documentation structure and organization
- Enhanced Python test coverage with comprehensive pymarc compatibility suite
- ISO 2709 Binary Format: Full read/write support for MARC records in the standard binary interchange format
- Record Types: Support for three MARC record types:
- Bibliographic records (standard MARC records)
- Authority records (Type Z) for standardized headings and cross-references
- Holdings records (Types x/y/v/u) for item location and enumeration data
- Builder Pattern API: Fluent, idiomatic Rust interface for record construction
- Field Access API: Comprehensive methods for reading, filtering, and iterating over fields
fields_by_tag()- Get fields by tagfields_by_indicator()- Filter by indicatorsfields_in_range()- Get fields within a tag rangefields_with_subfield()- Get fields containing specific subfields
- FieldQuery Builder: Complex criteria-based field matching (tags, indicators, subfields)
- TagRangeQuery: Range-based field lookups (e.g., 600-699)
- Subfield Pattern Matching: Regex-based field filtering
- Linked Field Navigation: Support for MARC 880 (Alternate Graphical Representation) fields
- Parse linkage information from subfield 6
- Bidirectional lookups between original and 880 fields
- Authority Control Helpers: Query traits for authority-specific operations
- Format-Specific Traits:
BibliographicQueriesfor bibliographic recordsAuthorityQueriesfor authority recordsHoldingsQueriesfor holdings records
- JSON: Generic JSON representation with fields as keys
- MARCJSON: Standard JSON-LD format for MARC records
- XML: XML representation with proper field/subfield structure
- CSV: Tabular export format for spreadsheet applications
- Dublin Core: Simplified 15-element metadata schema
- MODS: Metadata Object Description Schema for detailed descriptions
- MARC-8 (Legacy): Full support for legacy MARC-8 encoding with:
- Basic Latin (ASCII)
- ANSEL Extended Latin with diacritical marks
- Hebrew (ESC ) 2)
- Arabic (ESC ) 3, 4)
- Cyrillic (ESC ( N)
- Greek (ESC ( S)
- Subscripts/Superscripts/Special character sets
- East Asian (Chinese, Japanese, Korean via EACC)
- UTF-8 (Modern): Full Unicode support for modern MARC records
- Automatic Detection: Encoding detection from MARC leader position 9
- RecordHelpers Trait: Available on all record types via blanket implementation
title()- Extract main titleauthor()/authors()- Extract author namessubjects()- Extract subject headingsisbns()- Extract ISBN valuesissns()- Extract ISSN valuespublication_info()- Extract publication details- Record type helpers:
is_book(),is_music(),is_map(),is_serial(),is_audiovisual(),is_electronic_resource()
- MarcRecord Trait: Common interface for all record types (control field operations)
- GenericRecordBuilder: Unified builder for all record types
- FieldCollection Trait: Standardized field collection management
- Unified field storage pattern across Record, AuthorityRecord, and HoldingsRecord
- MarcError Type: Comprehensive error handling for MARC operations
- Recovery Mode: Graceful handling of truncated/malformed MARC records
- Result Type: Convenient
Result<T>alias for library operations
- 282+ comprehensive unit and integration tests
- Test data with:
- Simple bibliographic records
- Music scores
- Records with control fields (008)
- Multiple records in one file
- Authority records
- Holdings records
- Multilingual records
- MARC-8 encoded records
- Comprehensive API documentation with doc comments
- Module-level documentation with examples
- Examples directory with real-world usage patterns:
- Creating records with builders
- Reading and querying fields
- Converting between formats
- Working with authority and holdings records
- MARC-8 encoding demonstration
- Multilingual record handling
- CSV export
- Rust-Idiomatic: Leverages iterators, Result types, and ownership patterns naturally
- Zero-Copy Where Possible: Efficient memory usage for large record sets
- Format Flexibility: Support for multiple serialization formats out of the box
- Compatibility: Maintains data fidelity with pymarc and standard MARC tools
- Extensible: Trait-based architecture allows easy addition of new query types and formats
None known at this time. The following have been resolved:
- ✓ Field indicator validation with MARC21 semantics (implemented in 0.1.0)
- ✓ MARC-8 combining character handling with Unicode NFC normalization (implemented in 0.1.0)
- Minimum Rust Version: 1.70
- Dependencies: serde, serde_json, regex, unicode-normalization
- License: MIT
- Repository: https://github.com/dchud/mrrc
- Documentation: https://docs.rs/mrrc
- Crates.io: https://crates.io/crates/mrrc
- Issue Tracking: GitHub Issues and Beads issue tracking system
- Status: Active development, experimental (APIs may change)