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.