Migrating from httk v1¶
The database layer of httk v1 (httk.db, with httk.db.backend.Sqlite and
httk.db.store.SqlStore) and the storage layer of httk₂ (httk.data.db, see
Database storage) solve the same problem, and the shape of a query survives the port
almost unchanged: open a searcher, bind classes to variables, add conditions,
declare outputs, iterate. What changed is how a class is declared storable, and
that several v1 constructs whose meaning depended on context — most of all
add versus add_all — were replaced by ones that mean the same thing
everywhere.
v1 snippets on this page are historical
Every block marked httk v1 is shown only so you can recognise it in your own scripts. It requires httk v1 and does not run under httk₂. The blocks marked httk₂ are the ones to copy.
What stays the same¶
store.searcher() still opens a query; search.variable(Cls) still binds a
class to a variable; search.add(...) still adds a condition; search.output( variable, name) still declares an output; iterating still runs the query.
Attribute access on a variable still builds the joins for you, two variables of
the same class still self-join, comparing a reference field against another
variable (result.structure == structure) is still how you write a join
condition, and set_limit / add_offset / count() are unchanged.
First: add versus add_all¶
This is the one v1 idiom that silently changes meaning if you port it literally. In v1 the same expression meant different things depending on which method it was passed to:
# httk v1 — historical, does not run under httk₂
search.add(search_struct.formula_symbols.is_in('Na')) # contains ANY of
search.add_all(search_struct.formula_symbols.is_in('O','Ca','Ti')) # contains ONLY these
Both lines are real v1 code (Examples/4_database/1_store_retrieve.py and
Tutorial/Step6/step6_part5.py respectively), and the first even carries the
comment “cf. the search for a material system in tutorial step6, where all
symbols must be in a set, ‘add’ vs. ‘add_all’”.
In httk₂ the reading is in the expression, not in the method. There is one
add(), and each expression carries its own placement:
# httk₂
search.add(s.symbols.has_any("Na")) # existential: some symbol is Na
search.add(s.symbols.has_only("O", "Ca", "Ti")) # for-all: every symbol is in the set
s.symbols.is_in("O", "Ca", "Ti") on a child (list-valued) field is the same
for-all reading as has_only; on a root field is_in is plain membership.
Against records NaCl, CaTiO3 and NaTiO2, has_any matches NaCl and
NaTiO2, while has_only and child-field is_in both match CaTiO3 alone.
Old → new¶
httk v1 |
httk₂ |
|---|---|
|
|
|
|
|
|
subclass |
plain frozen dataclass with |
|
|
|
|
|
gone — one |
|
|
|
|
|
|
|
iteration yields a |
|
|
|
content-id deduplication; look a record up with |
|
explicit |
One v1 trap is worth naming because it looked like a query bug: because the v1
table name came from cls.__name__, the storable class had to be re-declared
verbatim in the querying script, and search.variable(cls) would silently
create the table when it did not exist — so a renamed or slightly mistyped
class returned no matches instead of an error. httk₂ still creates a class’s
tables on first use, but you import the class rather than re-declare it, and
StorageInfo(storage_name="…") pins the stored name so renaming the class
cannot orphan the data.
Converting a store-and-search script¶
The v1 original (Examples/4_database/1_store_retrieve.py) stored two
structures with a tag and searched for anything containing sodium:
# httk v1 — historical, does not run under httk₂
import httk, httk.db
from httk.atomistic import Structure
backend = httk.db.backend.Sqlite('example.sqlite')
store = httk.db.store.SqlStore(backend)
tablesalt = httk.load('NaCl.cif')
tablesalt.add_tag('common name', 'salt')
arsenic = httk.load('As.cif')
store.save(tablesalt)
store.save(arsenic)
search = store.searcher()
search_struct = search.variable(Structure)
search.add(search_struct.formula_symbols.is_in('Na'))
search.output(search_struct, 'structure')
for match, header in list(search):
struct = match[0]
print("Found structure", struct.formula, [str(struct.get_tags()[x]) for x in struct.get_tags()])
In httk₂ the record class is declared where you use it — a plain frozen
dataclass — and the tag becomes an ordinary join class stored like any other,
looked up with store.referring:
# httk₂
from dataclasses import dataclass
from typing import Annotated, ClassVar
from httk.core import Indexed, StorageInfo
from httk.data.db import Database, SqlStore
@dataclass(frozen=True)
class Structure:
formula: Annotated[str, Indexed()]
symbols: tuple[str, ...]
@dataclass(frozen=True)
class StructureTag:
__httk_storage__: ClassVar[StorageInfo] = StorageInfo(dedup="by_value")
structure: Structure
tag: Annotated[str, Indexed()]
value: str
store = SqlStore(Database.sqlite("example.sqlite"))
tablesalt = Structure("NaCl", ("Na", "Cl"))
arsenic = Structure("As", ("As",))
with store.transaction():
store.save(tablesalt)
store.save(arsenic)
store.save(StructureTag(tablesalt, "common name", "salt"))
search = store.searcher()
s = search.variable(Structure)
search.add(s.symbols.has_any("Na"))
search.output(s, "structure")
for result in search:
structure = result.values[0]
tags = store.referring(StructureTag, field="structure", to=structure)
print("Found structure", structure.formula, [f"{tag.tag}={tag.value}" for tag in tags])
Point by point: add(... .is_in('Na')) became add(... .has_any("Na"));
delay_commit/commit became transaction(); add_tag/get_tags became a
stored join class and store.referring; and for match, header in list(search)
became a plain loop over SearchResult objects, of which result.values[0] is
the first declared output. Note that symbols is a tuple, not a list: an
instance with a list field is unhashable, and the store’s object→sid cache
(which store.referring and store.sid_of consult) is equality-keyed.
Converting a result class and its search¶
The v1 original (Tutorial/Step6/step6_part4.py and step6_part5.py) declared
a result class with httk_typed_init — once in the storing script and again,
verbatim, in the querying one — and searched it joined against Structure:
# httk v1 — historical, does not run under httk₂
class TotalEnergyResult(httk.Result):
@httk.httk_typed_init({'computation': httk.Computation, 'structure': Structure, 'total_energy': float})
def __init__(self, computation, structure, total_energy):
self.computation = computation
self.structure = structure
self.total_energy = total_energy
backend = httk.db.backend.Sqlite('example.sqlite')
store = httk.db.store.SqlStore(backend)
search = store.searcher()
search_total_energy = search.variable(TotalEnergyResult)
search_struct = search.variable(Structure)
search.add(search_total_energy.structure == search_struct)
search.add_all(search_struct.formula_symbols.is_in('O', 'Ca', 'Ti'))
search.output(search_total_energy, 'total_energy_result')
for match, header in search:
total_energy_result = match[0]
structures += [total_energy_result.structure]
energies += [total_energy_result.total_energy]
The httk₂ version declares the classes once, as dataclasses, and translates
the add_all(... .is_in(...)) post-filter into a single add(... .has_only(...)):
# httk₂
from dataclasses import dataclass
from typing import Annotated, ClassVar
from httk.core import Indexed, StorageInfo, stored_property
from httk.data.db import Database, SqlStore
@dataclass(frozen=True)
class Structure:
formula: Annotated[str, Indexed()]
symbols: tuple[str, ...]
@stored_property
def nsymbols(self) -> int: # queryable, recomputed on load
return len(self.symbols)
@dataclass(frozen=True)
class Computation:
code: str
version: str
@dataclass(frozen=True)
class TotalEnergyResult:
__httk_storage__: ClassVar[StorageInfo] = StorageInfo(indexes=(("structure", "total_energy"),))
computation: Computation
structure: Structure
total_energy: float
store = SqlStore(Database.sqlite("results.sqlite"))
vasp = Computation("VASP", "5.4.4")
runs = [
(Structure("CaTiO3", ("Ca", "Ti", "O")), -39.2),
(Structure("TiO2", ("Ti", "O")), -26.5),
(Structure("NaCl", ("Na", "Cl")), -7.1),
]
with store.transaction():
for structure, energy in runs:
store.save(TotalEnergyResult(vasp, structure, energy)) # saves referents recursively
search = store.searcher()
result = search.variable(TotalEnergyResult)
structure = search.variable(Structure)
search.add(result.structure == structure) # the join condition, as in v1
search.add(structure.symbols.has_only("O", "Ca", "Ti")) # was add_all(... .is_in(...))
search.add_sort(result.total_energy, descending=False) # was direction='ASC'
search.output(result, "total_energy_result")
structures = []
energies = []
for match in search:
total_energy_result = match.values[0]
structures.append(total_energy_result.structure)
energies.append(total_energy_result.total_energy)
This prints ['CaTiO3', 'TiO2'] and [-39.2, -26.5]: NaCl is excluded by the
for-all condition, and the sort is by total energy ascending. There is no
store.commit() at the end — the transaction() block committed on exit.
Looking records up by identity¶
v1 identified a stored object through the obj.db plugin and deduplicated on
hexhash. httk₂ has no plugin on the object: the store answers both
questions.
# httk₂
from httk.data.db import content_id
nacl = structures[0]
sid = store.sid_of(nacl) # was nacl.db.sid
found = store.fetch_by_content_id(Structure, content_id(nacl)) # was the hexhash lookup
sid_of consults the store’s in-memory cache only — it answers for instances
that passed through save() or fetch() and are still alive — while
fetch_by_content_id queries the database.
No longer available¶
These v1 features have no httk₂ replacement:
the
obj.dbplugin and theStorablebase class — storability is non-intrusive, and all storage operations go through the store;DictStoreandTrivialStore;searcher.store_table(name);searcher.sql()andsearcher.sql_query()SQL introspection;search.function(name)custom SQL functions;output(..., concat=True)(GROUP_CONCAT);arithmetic operators on columns, and the
^(xor) and//(string concatenation) operators;searcher.reset()andsearcher.duplicate(other)— build a new searcher;explicit join keys, i.e.
variable(Cls, parent=..., parentkey='Compound_id', subkey='compound_Compound_sid');variable()takes only the class and derives the joins from the schema;the
DATABASE_DEBUGandDATABASE_DEBUG_SLOWenvironment variables.