Query mmCIF (PDBx) structural-biology files with SQL, right inside DuckDB.
ATTACH a .cif file and every mmCIF category shows up as a normal DuckDB table — with column types inferred from the bundled PDBx/mmCIF dictionary and its supported extensions. No ETL, no schema design, no Python parsing loop: just SQL over macromolecular structure data.
mmCIF (also called PDBx) is the standard file format of the Protein Data Bank — it describes every experimentally determined macromolecular structure: proteins, nucleic acids, ligands, crystals, and all the experimental metadata behind them. A single entry is a set of categories (tables) such as atom_site (one row per atom with 3D coordinates), entity, struct_ref_seq, or reflns (diffraction reflections), linked by keys.
The format is powerful but awkward to analyze: files are large, syntax is quirky (multi-line fields, quoted values, ./? nulls), and joining related categories by hand is tedious. This extension turns that into plain SQL.
- Zero-pipeline analysis — install as a community extension and start querying immediately.
- Typed out of the box — column types come from the mmCIF dictionary type index, so
Cartn_xis aDOUBLEandlabel_seq_idis aBIGINT..and?becomeNULL. - Dictionary extensions included — IHMCIF, flrCIF, 3DEM and ModelCIF support integrative models, fluorescence/FRET, electron microscopy and AlphaFold DB models automatically, with types, documentation links and relationships.
- Gzip support — RCSB-style
*.cif.gzfiles (for examplehttps://files.rcsb.org/download/1AMB.cif.gz) are auto-detected and decompressed. - Relationships as data — discover how categories reference each other programmatically with
mmcif_relationships(), instead of browsing the mmcif dictionary website. - Fast — custom cif parser/writer inspired by the RCSB mmcif ccp libraries, with DuckDB's vectorized execution on top.
- Safe by default — attached databases are read-only unless you explicitly opt in to write mode.
- Ligands too — Small molecules or ligand files from the RCSB work the same way as macromolecular entries (see Ligands).
Load the extension into a DuckDB shell started with unsigned extensions allowed:
duckdb(Extension is available since v1.5.5, upgrade DuckDB if necessary.)
INSTALL mmcif FROM community;
LOAD mmcif;
ATTACH '1amb_updated.cif' AS mmcifdb (TYPE mmcif);
USE mmcifdb;
SHOW TABLES; -- every category in the file
SELECT count(*) FROM atom_site;
-- 438
SELECT type_symbol, count(*) AS n
FROM atom_site
GROUP BY 1
ORDER BY n DESC;
SELECT Cartn_x, Cartn_y, Cartn_z, type_symbol
FROM atom_site
WHERE type_symbol = 'ZN';Column types are inferred from the combined dictionary (dict/mmcif_type_index.tsv.gz):
DESCRIBE atom_site;
-- Cartn_x DOUBLE, label_seq_id BIGINT, type_symbol VARCHAR, ...The extension bundles the current PDBx/mmCIF v5 dictionary together with IHMCIF, flrCIF, 3DEM and ModelCIF. All are available by default: no dictionary selection or downloads are needed when opening a file.
See dictionary extensions for usage, dictionary coverage and ER diagrams from real archive examples.
Query a file without attaching it:
-- List categories and their dictionary documentation
SELECT * FROM mmcif_tables('1amb_updated.cif');
-- List columns and their types
SELECT * FROM mmcif_columns('1amb_updated.cif');
-- Find relationships between categories
SELECT * FROM mmcif_relationships('1amb_updated.cif');
-- List data blocks
SELECT * FROM mmcif_blocks('1amb_updated.cif');
-- Read a category
SELECT * FROM mmcif_scan('1amb_updated.cif', 'atom_site');All five functions also accept /dev/stdin:
cat 1amb_updated.cif | duckdb -c "SELECT * FROM mmcif_scan('/dev/stdin', 'atom_site')"Relationships in test/data/1amb_updated.cif:
Read the same category from a glob or a list of paths/globs. Plain CIF and gzip files can be mixed:
SELECT filename, id, Cartn_x
FROM mmcif_scan('structures/**/*.cif.gz', 'atom_site');
SELECT * FROM mmcif_scan(['first.cif', 'more/*.cif.gz'], 'atom_site');When the input matches multiple files, filename identifies each row's source.
It is only added when more than one file matches. Missing values become NULL;
files without the requested category contribute no rows.
By default, scans include all dictionary columns. For custom categories/items,
or to include only columns present in your files, use column_source := 'files':
SELECT * FROM mmcif_scan('structures/*.cif', 'my_category',
column_source := 'files');Each scan combines rows from the same category across all matched files; no join or aggregation is needed.
For long scans, enable the progress bar and choose how many threads to use:
SET enable_progress_bar = true;
SET threads = 6; -- Reduce this to use less memory
SELECT count(*) FROM mmcif_scan('structures/**/*.cif.gz', 'atom_site');ATTACH and the metadata functions take one file; use mmcif_scan for multiple
files. To select a specific data block in every file, add data_block := 'name'
(see Multiple data blocks).
Besides macromolecular entries, ligand (small molecules) files such as ATP can be queried directly.
ATTACH 'https://files.rcsb.org/ligands/download/ATP.cif' AS liganddb (TYPE mmcif);
USE liganddb;
SELECT id, name, type, formula FROM chem_comp;
-- ATP, "ADENOSINE-5'-TRIPHOSPHATE", NON-POLYMER, "C10 H16 N5 O13 P3"A file can contain multiple blocks, each beginning with data_name, such as
the wwPDB Chemical Component Dictionary (components.cif.gz),
which has a separate block for each chemical component. List their names:
SELECT * FROM mmcif_blocks('components.cif.gz'); -- block_name (without data_)Existing functions and ATTACH use the first block by default. Select another
block by name (case-insensitive), without the data_ prefix. For example,
select data_ATP from the Chemical Component Dictionary:
SELECT * FROM mmcif_scan('components.cif.gz', 'chem_comp', data_block := 'ATP');
SELECT * FROM mmcif_tables('components.cif.gz', data_block := 'ATP');
SELECT * FROM mmcif_columns('components.cif.gz', data_block := 'ATP');
SELECT * FROM mmcif_relationships('components.cif.gz', data_block := 'ATP');
ATTACH 'components.cif.gz' AS atpdb (TYPE mmcif, DATA_BLOCK 'ATP');Attached mmcif databases are read-only; data definition and mutation statements are rejected:
CREATE TABLE foo(i int); -- Binder Error: mmcif databases are read-only - cannot CREATE TABLE
DELETE FROM atom_site; -- Binder Error: mmcif databases are read-only - cannot DELETEPass READ_WRITE TRUE to ATTACH to edit the file with SQL. INSERT, UPDATE, and DELETE mutate the parsed .cif data in memory (row_id is the physical row index of the category table); the file is written back on COMMIT, CHECKPOINT, and DETACH. ROLLBACK re-parses the file from disk. Data definition statements (CREATE/DROP/ALTER) stay disabled.
ATTACH '1amb_updated.cif' AS wdb (TYPE mmcif, READ_WRITE TRUE);
USE wdb;
BEGIN;
INSERT INTO atom_site (label_atom_id, Cartn_x, type_symbol) VALUES ('O1', 3.5, 'O');
UPDATE atom_site SET type_symbol='ZZ' WHERE label_atom_id='O1';
DELETE FROM atom_site WHERE label_atom_id='O1';
COMMIT; -- writes the mutated tables back to the attached fileNote
The COMMIT will overwrite the file you ATTACH-ed. Make a copy if you do not want to overwrite the original.
Ready-to-run example scripts live in docs/examples/:
keep_chain_A.sql— filters an mmCIF file down to a single auth chain (chain A of3PLZ, downloaded from https://files.rcsb.org/download/3PLZ.cif.gz), deleting rows in every dependent category that do not belong to that chain. Usesmmcif_relationships()to work out which tables reference chains.keep_chain_D2A.sql— keeps only chain D of3PLZand renames it to chain A (in both the auth and label chain-id namespaces), producing a plain chain-A file; a copy3PLZ_D2A.cif.gzis edited so the original is never touched.spatial_atoms.sql— 3D geometry analysis ofatom_sitecoordinates with the spatial extension: binding-pocket residues around the3PLZinhibitor, chain–chain interface contacts, hydration shell, bounding box, rigid-body transforms, and Cα trace export.metadata_multiple.sql— joins category scans by filename to extract resolution and deposition date across a collection.metadata.sql— structure metadata as SQL (entry id, resolution, experimental method, software, label/auth chain mapping, residue counts, UniProt accession).secondary_structure.sql— helix/sheet residue counts and ratios fromstruct_conf/struct_sheet_range.confidence_filter.sql— AlphaFold pLDDT (B-iso) confidence counting and write-mode residue filtering.uniprot_mapping.sql— UniProt chain-mapping extraction and injection viastruct_ref/struct_ref_seqfor user authored entries or the_pdbx_sifts_unp_segmentsfor sifts computed entries.
This extension was inspired by
- https://github.com/DeepRank/pdb2sql - Fast and versatile biomolecular structure PDB file parser using SQL queries - made by co-worker
- https://github.com/project-gemmi/gemmi - macromolecular crystallography library and utilities - lossy round trip via its Structure class read/write
See CONTRIBUTING.md for build and test instructions.