carbon-a-database-explorer / merge_full_index.py
cgeorgiaw's picture
cgeorgiaw HF Staff
Publish full accession snapshot from bucket storage and synchronize atlas coverage
7034da5 verified
Raw History Blame
4.77 kB
"""Merge disjoint compact-index partitions and validate their inventory."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import sqlite3
from compact_index import DDL, finalize_indexes
from remote_catalog import BUCKET
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--inventory',type=Path,required=True)
p.add_argument('--parts',type=Path,nargs='+',required=True)
p.add_argument('--output',type=Path,required=True)
args=p.parse_args()
work=args.output.with_suffix('.merging.sqlite')
work.unlink(missing_ok=True)
c=sqlite3.connect(work)
c.execute('PRAGMA journal_mode=OFF'); c.execute('PRAGMA synchronous=OFF')
c.execute('PRAGMA cache_size=-262144'); c.executescript(DDL)
file_offset=row_offset=0
for path in args.parts:
c.execute('ATTACH DATABASE ? AS part',(str(path),))
manifest=json.loads(c.execute("SELECT value FROM part.metadata WHERE key='manifest'").fetchone()[0])
if manifest.get('schema_version')!=3: raise ValueError(f'Unexpected schema in {path}')
c.execute('INSERT OR IGNORE INTO contexts(assembly_accession,assembly_base,metadata_json) SELECT assembly_accession,assembly_base,metadata_json FROM part.contexts')
c.execute('CREATE TEMP TABLE context_map(old INTEGER PRIMARY KEY,new INTEGER)')
c.execute('INSERT INTO context_map SELECT p.id,c.id FROM part.contexts p JOIN contexts c ON c.metadata_json=p.metadata_json')
c.execute('INSERT INTO files SELECT id+?,path,hash,size,rows,row_groups,max_group_bytes,bytes_read,range_reads FROM part.files',(file_offset,))
c.execute('''INSERT INTO segment_data SELECT s.id+?,s.record_name,s.record_base,s.segment_start_bp,s.segment_end_bp,
s.aligned_bp_length,s.segment_index,s.segment_count,s.file_id+?,s.row_group,s.row_in_group,m.new,s.source_key
FROM part.segment_data s JOIN context_map m ON m.old=s.context_id''',(row_offset,file_offset))
file_offset=c.execute('SELECT max(id) FROM files').fetchone()[0] or 0
row_offset=c.execute('SELECT max(id) FROM segment_data').fetchone()[0] or 0
c.execute('DROP TABLE context_map'); c.commit(); c.execute('DETACH DATABASE part')
print('Merged',path,'last_row',row_offset,flush=True)
expected={r['path']:(r['hash'],r['size']) for r in map(json.loads,args.inventory.open())}
actual={r[0]:(r[1],r[2]) for r in c.execute('SELECT path,hash,size FROM files')}
if actual!=expected: raise ValueError('Merged object inventory does not match listing')
print('Building search indexes',flush=True)
finalize_indexes(c); c.commit()
rows,bases=c.execute('SELECT count(*),sum(segment_end_bp-segment_start_bp) FROM segment_data').fetchone()
if rows!=c.execute('SELECT sum(rows) FROM files').fetchone()[0]: raise ValueError('Row totals differ')
assemblies=[r[0] for r in c.execute('SELECT DISTINCT assembly_accession FROM contexts ORDER BY assembly_accession')]
examples={}
for cid,encoded in c.execute('SELECT id,metadata_json FROM contexts'):
division=json.loads(encoded)['division']
if division not in examples:
row=c.execute('SELECT record_name FROM segment_data WHERE context_id=? ORDER BY id LIMIT 1',(cid,)).fetchone()
if row: examples[division]=row[0]
manifest={'schema_version':3,'bucket_id':BUCKET,'created_at':datetime.now(timezone.utc).isoformat(),
'scope':'All published annotation Parquet objects in the dated inventory; assemblies may be partial.',
'full_snapshot':True,'source_count':len(expected),'sources':[],
'source_ledger':'SQLite files table contains every object path, hash, size and row-group summary.',
'rows':rows,'bases':bases,'assemblies':assemblies,'divisions':sorted(examples),
'example_record_names':list(examples.values()),
'inventory_sha256':hashlib.sha256(args.inventory.read_bytes()).hexdigest(),
'inventory_at':datetime.fromtimestamp(args.inventory.stat().st_mtime,timezone.utc).isoformat()}
c.execute("INSERT OR REPLACE INTO metadata VALUES ('manifest',?)",(json.dumps(manifest),)); c.commit()
if c.execute('PRAGMA quick_check').fetchone()[0]!='ok': raise ValueError('SQLite check failed')
c.execute('PRAGMA journal_mode=DELETE'); c.close(); work.replace(args.output)
report={k:v for k,v in manifest.items() if k!='assemblies'}
report.update(assembly_count=len(assemblies),index_bytes=args.output.stat().st_size)
args.output.with_suffix('.report.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report),flush=True)
if __name__=='__main__': main()