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. Stream a single output with scalars()

When a result has one output, scalars() yields that output directly:

search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("Xe"))
results = search.results(structure=structure)

for record in results.scalars():
    print("scalar", record.species_at_sites)

With multiple outputs, pass the output name to scalars(name) to select one column explicitly.

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. Use the backend-neutral low-level form

The portable protocol declares an output with search.output(...) and yields plain values plus output names. SQL code will usually prefer results(), but this form works across Searcher implementations:

search = store.searcher()
structure = search.variable(UnitcellStructureRecord)
search.add(structure.species_at_sites.has_any("Xe"))
search.output(structure, "structure")

for (values,), names in search:
    print(names, values.species_at_sites)

See the complete low-level protocol and cursor() contract in the versioned httk-store database guide.

7.7. 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.

7.8. 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"))
search.output(right, "other")
print("self join", [row.other.species_at_sites for row in search.results()])

The first condition follows symmetry into its record automatically. The second query joins two UnitcellStructureRecord variables through their shared space-group value.

7.9. 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.10. 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 Backend, SqlStore

custom_store = SqlStore(Backend.sqlite("custom.sqlite"), entry_records={})
reopened_store = SqlStore(Backend.sqlite("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.