Storage
The storage section of nl2sql.yaml says where the engine keeps
its data, and which store keeps each part of it. Only path is required:
A relative path is relative to nl2sql.yaml in a file, and to the working
directory when the Config is built in code. nl2sql init writes .nl2sql
and adds .nl2sql/ to .gitignore.
Everything the engine writes lives under it:
Under path |
What | Was (0.4) |
|---|---|---|
index/ |
The vector index (Chroma), one collection per embedding model: nl2sql__<model slug>__<dimension>. |
VECTOR_STORE, VECTOR_STORE_COLLECTION |
state.db |
SQLite: schema snapshots, the plan cache and playground feedback. | SCHEMA_STORE_PATH |
traces/ |
Run traces (settings.traces.mode), kept 14 days by default. |
TRACE_DIR |
results/ |
Result files, <tenant_id>/<request_id>/<sub_query_id>.parquet. Kept 7 days by default. See Artifact Store. |
RESULT_ARTIFACT_BASE_URI |
The four stores
Each part of that data has a slot, and each slot a store, named by type.
Every sub-block is optional; left out, a slot uses its built-in store, so the
section above is the same as:
path: .nl2sql
index: { type: chroma } # <path>/index/
state: { type: sqlite } # <path>/state.db
traces: { type: files, max_age_days: 14 } # <path>/traces/
results: { type: local, max_age_days: 7 } # <path>/results/
| Slot | Built-in | Holds |
|---|---|---|
index |
chroma |
The vector index, one namespace per embedding model, and each datasource's live build |
state |
sqlite |
Schema snapshots, the plan cache, feedback |
traces |
files |
Run traces |
results |
local |
Result files |
A store's other keys are its own, checked by the store when the engine is
built, so a misspelt key is an error. They may be ${provider:key}
references, resolved through the secret providers when the store
is built:
Store names share one entry point group, nl2sql.stores, so a name is unique
across the four slots. A store in the wrong slot is refused with the stores
installed for that slot: "'chroma' is an index store; results needs a results
store. Installed: local."
In code, a slot takes a StoreConfig or a store object, used as given:
from nl2sql import Storage, StoreConfig
Storage(path=".nl2sql")
Storage(path=".nl2sql", traces=StoreConfig(type="files", max_age_days=30))
Storage(path=".nl2sql", index=my_index_store) # any object whose slot is "index"
The store interfaces are in nl2sql_sdk.stores.
A plugin registers a store in the nl2sql.stores entry point group, or in
code with registry.add_store(name, cls).
Every slot is read through its interface, so a plugin store works in any slot it fills.
Retention
The files trace store and the local results store take max_age_days,
14 and 7 by default (at least 1). It is a key of those two stores, not of
every store.
engine.storage.purge() deletes the traces and result files last written
before that age, and the audit observer's day files older
than its max_age_days (90 by default), and returns the counts,
{"traces": 3, "results": 12, "audit": 1} (no audit when the engine runs no
audit observer). It publishes the counts as the
retention.purged event, which the
audit observer writes. A
plugin trace or results store is purged when it has a max_age_days
attribute, and left alone when it does not (a bucket with its own lifecycle
rule, say). Three things call it:
nl2sql storage purge, by hand or from cron;nl2sql-api, at startup and then once a day while it serves (a failed purge is logged and tried again the next day);- your own code.
Feedback and schema snapshots have no age limit: feedback is cleared by hand
(nl2sql feedback clear), and after each index build the snapshots nothing can
read any more are deleted (see The state).
What is where: engine.storage.info()
engine.storage.info()
# {"index": {"type": "chroma", "location": ".../.nl2sql/index"},
# "state": {"type": "sqlite", "location": ".../.nl2sql/state.db"},
# "traces": {"type": "files", "location": ".../.nl2sql/traces", "max_age_days": 14},
# "results": {"type": "local", "location": ".../.nl2sql/results", "max_age_days": 7},
# "namespace": "nl2sql__local-all-minilm-l6-v2__384"}
type is the store's name, or the class name of a store object; location
is its folder or file, or None for a store that does not have one.
The state
The state store keeps the schema snapshots (immutable, one row per version),
the plan cache, the playground's feedback, and the lock that lets one index
build run at a time. The engine applies the store's pending migrations when it
starts; the built-in sqlite store adopts a 0.5 state.db as it is, and
applies them in one BEGIN IMMEDIATE transaction, so engines or CLI runs
opening a new state.db at the same moment apply each one exactly once. A
store with nothing pending takes no lock, so a read-only state.db opens. See
The state store.
The index
The engine embeds; the index store keeps entries and vectors in a
namespace per embedding model, nl2sql__<model slug>__<dimension> (for
example nl2sql__local-all-minilm-l6-v2__384), filters them, and records
each datasource's live build as a BuildPointer (its build_id and the
schema_version it was made from). Switching a pointer is the commit, and a
run reads the pointers once when it starts. See
Indexing.
- Changing the embedding model opens a new, empty namespace:
check()reports the indexmissing, andnl2sql indexbuilds it. Switching back finds the old namespace still built. nl2sql index prune(engine.index.prune()) drops every namespace but the current model's, including 0.5'snl2sql_storecollection, which 0.6 never reads. Run it once after upgrading, and after settling on a new model.- The
chromastore opensindex/on the first build; reading a missing folder or namespace creates nothing.
Sharing
Two engines with different paths share nothing. The commands that only read
these stores (nl2sql cache clear, nl2sql feedback, nl2sql trace show,
nl2sql storage purge) find them from the same config, so run them with the
same --config.
There is no built-in S3 or ADLS results store. The ResultStore interface is
four methods; Artifact Store
has an S3 example.
In a container, mount a volume and point the path at it: