7. Search the local database¶
Continuing with the UnitcellStructureRecord and store from the previous
step, build a query with searcher(), then freeze it into a reusable result
set with results(). The query vocabulary is shared by SQL and MongoDB stores.
7.1. Bind a variable and freeze a query¶
Bind a record class to a variable, add conditions, and declare the output when the query is ready:
from httk.atomistic import UnitcellStructureRecord
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("O"))
results = search.results(structure=structure)
for row in results:
print("Found", len(row.structure.species_at_sites), "sites")
Result rows are lazy UnitcellStructureRecord instances. Calling results()
freezes the query plan; later changes to search do not change this result set.
By default store.searcher() returns every row of every lineage but only main
entries — only_main_alt=True hides named alternatives. Pass only_latest=True
to restrict root variables to each lineage’s latest revision, and
only_main_alt=False to include alternatives.
7.2. Reuse, slice, and inspect result sets¶
Result sets can be iterated again, sliced, and indexed. Add a few distinct structures here so the slice has its own positions:
from httk.atomistic import UnitcellStructure
for element in ("Xe", "Kr", "Ar"):
store.save(
UnitcellStructure(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[0, 0, 0]],
species_at_sites=[element],
)
)
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("Xe", "Kr", "Ar"))
results = search.results(structure=structure)
tail = results[1:]
print("sizes", len(results), len(tail))
print("reuse", len(results), len(list(results)))
print("positions", results[1].structure.species_at_sites, tail[0].structure.species_at_sites)
print("first", results.first().structure.species_at_sites)
one_search = store.searcher()
one_structure = one_search.variable(UnitcellStructureRecord)
one_search.add(one_structure.species_at_sites.has_any("Xe"))
one = one_search.results(structure=one_structure).one()
print("one", one.structure.species_at_sites)
The slice has its own positions for iteration, len(), indexing, first(),
one(), and column(). It does not re-query the database. one() requires
exactly one result; it raises NoResultError or MultipleResultsError
otherwise.
7.3. Read result rows¶
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("Xe"))
results = search.results(structure=structure)
for row in results:
print(row.structure.species_at_sites)
A row exposes each declared output by name (row.structure above).
scalars(name) also exists, for collecting a single output as a plain column
of values rather than rows.
7.4. Read exact columns or explicit floats¶
column(name) exposes scalar projections without hydrating whole records. A
column of integers, Fraction values, or FracScalar values supports exact
to_fracvector(); .floats() is the explicit approximate view. Floats,
surds, strings, datetimes, and other non-rational projections are rejected by
to_fracvector().
from fractions import Fraction
for charge in (Fraction(1, 2), Fraction(-1, 3)):
store.save(
UnitcellStructure(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[0, 0, 0]],
species_at_sites=["Xe"],
charge=charge,
)
)
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.charge.is_in(Fraction(1, 2), Fraction(-1, 3)))
results = search.results(charge=structure.charge)
charges = results.column("charge")
print("exact", list(charges))
print("floats", list(charges.floats()))
print("fracvector", charges.to_fracvector())
Variable-length child projections are rejected when results() is declared;
reference-path projections are supported.
7.5. Process rows with cursor()¶
cursor() bounds hydrated record/proxy objects, not the raw result values
retained by the result set. It reuses an unhashable record proxy, so a cursor
row expires when the cursor advances. Build views while the row is live, and
fill any component you will need before advancing:
from httk.atomistic import UnitcellStructureView
from httk.store.backend.sql import ExpiredCursorRowError
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("Xe", "Kr"))
results = search.results(structure=structure)
reuse = results.cursor()
first = next(reuse).structure
second = next(reuse).structure
print("proxy reused", first is second)
cursor = results.cursor()
live = next(cursor).structure
view = UnitcellStructureView(live)
filled_before_advance = view.species_at_sites
next(cursor)
print("filled view", filled_before_advance)
try:
live.cell
except ExpiredCursorRowError:
print("later fill: expired")
Components filled into a view before advancing remain readable. A later fill
through the expired cursor row raises ExpiredCursorRowError.
7.6. Query list fields as sets¶
List fields use explicit set operations. This record has two site species, so the three predicates make their meanings visible:
from httk.atomistic import UnitcellStructure
set_record = UnitcellStructure(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[0, 0, 0], [1 / 2, 1 / 2, 1 / 2]],
species_at_sites=["Xe", "O"],
)
store.save(set_record)
for operation in ("has_any", "has_only", "is_in"):
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
expression = getattr(structure.species_at_sites, operation)("Xe", "O")
search.add(expression)
print(operation, len(search.results(structure=structure)))
has_any means at least one child value is in the set. has_only means every
child value is in it, and is_in has that same for-all meaning on a child
field. On a root field, is_in is ordinary membership. Negating a set
expression negates the set statement, not the whole row.
Strong links and weak links between records are queried through a links
namespace: v.links.<name>.<field> filters through a link, and
v.links.<name> is a set-valued results() output that yields each row’s
linked records as a tuple. Strong links pin provenance to a revision; weak
links are mutable associations to a lineage. For example,
record.links.product_of == structure joins a collected result to its
structure; see Collect the results. Reverse strong-link queries also use
the links namespace. The versioned httk-store database guide has the full
relationship contract.
7.7. Chain references and make self-joins¶
A reference path creates its join automatically. Two variables of one class can be compared to make a self-join:
from httk.atomistic import UnitcellStructure
from httk.atomistic.models.structure.semantics import StructureSymmetry
for element, spacegroup in (("Rn", 225), ("Og", 225), ("He", 62)):
store.save(
UnitcellStructure(
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[0, 0, 0]],
species_at_sites=[element],
symmetry=StructureSymmetry(spacegroup),
)
)
search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.symmetry.space_group_it_number == 225)
print("automatic join", len(search.results(structure=structure)))
search = store.searcher()
left = search.variable(UnitcellStructureRecord)
right = search.variable(UnitcellStructureRecord)
search.add(left.species_at_sites.has_any("Rn"))
search.add(left.symmetry.space_group_it_number == right.symmetry.space_group_it_number)
search.add(right.species_at_sites.has_any("Og"))
print("self join", [row.other.species_at_sites for row in search.results(other=right)])
The first condition follows symmetry into its record automatically. The
second query joins two UnitcellStructureRecord variables through their shared
space-group value.
7.8. Fetch an entry family through StructureEntry¶
StructureEntry is a logical, non-instantiable family key rather than the
concrete record class. Fetch with the family key and the stable content ID:
from httk.atomistic import StructureEntry
record = store.fetch_entry(StructureEntry, restored.id)
print(type(record).__name__, record.id == restored.id)
An entry store can register unit-cell, fundamental-domain, and asymmetric-unit
records under the same family. fetch_entry(StructureEntry, id) then returns
whichever concrete record owns that structural ID, so callers need not choose a
representation-specific fetch method.
7.9. Declare custom-record stores explicitly¶
A first-time store containing only private custom dataclasses declares the empty mapping explicitly. Reopening the database loads that persisted declaration:
from httk.store import SqliteStore
custom_store = SqliteStore("custom.sqlite", entry_records={})
reopened_store = SqliteStore("custom.sqlite")
print("custom store reopened", reopened_store is not custom_store)
The explicit entry_records={} declaration says that this is a private store
with no queryable entry families; it prevents an old or unversioned database
from being silently treated as the current storage protocol.
See the complete query DSL example in the versioned httk-store documentation listed by the module directory.
See also the Storing, querying, and serving data topic page for the current storage and querying vocabulary.