Datasource API
Purpose
engine.datasources: the engine's datasources, listed with their status,
added and removed at runtime, tested, and their indexed schema; and the
methods behind Connect a database: the adapter types with their connection
forms, and a test of a datasource before it is added.
Responsibilities
- List every configured datasource with its status, capabilities and masked connection.
- List every adapter type the registry can name, installed or not, with its connection model's JSON Schema.
- Test a configured datasource, as
check()does, or aDatasourcethat is not added, registering nothing. - Add a
Datasource(validated, built, not connected) and remove one. - Return the indexed schema as the planner is given it, as one caller may see it.
Key Modules
packages/nl2sql/src/nl2sql/api/datasource_api.py(DatasourcesAPI, internal)packages/nl2sql/src/nl2sql/api/types.py(DatasourceInfo,DatasourceType)packages/nl2sql/src/nl2sql/datasources/registry.pypackages/nl2sql/src/nl2sql/config/models.py(Datasource)
Public Surface
| method | returns | meaning |
|---|---|---|
list() |
list[DatasourceInfo] |
Every configured datasource, in config order, failed ones included. |
get(datasource_id) |
DatasourceInfo |
One configured datasource. Raises ValueError for an unknown id. |
types() |
list[DatasourceType] |
Every adapter the engine's registry can name, sorted by name, whether its driver is installed or not. An adapter module imports without its driver, so the connection schema is there before the driver is. A plugin that fails to import, or implements another interface version, is left out. |
test(datasource) |
DatasourceStatus |
See Testing a datasource. |
add(datasource) |
None |
Validates connection against the adapter's connection model and index as an IndexScope, builds the adapter without connecting, registers it, and adds the Datasource to engine.config; its description and sample questions are what indexing reads. Raises ValueError if the id is taken, the type is unknown, a reference does not resolve, index does not validate (naming the index. field) or the adapter rejects its connection settings (naming the field, with "did you mean" for an unknown key, and never a value). Index it next: engine.index.build([datasource.id]). |
remove(datasource_id) |
None |
Takes the datasource out of routing and out of engine.config, and closes its adapter's pool. Its index entries stay until the next full index.build or index.clear. Raises ValueError for an unknown id. |
schema(datasource_id, *, user_context) |
dict |
The indexed schema as the planner is given it, as user_context may see it: {"datasource_id", "tables": [...]}, tables sorted by name, each with name, schema, row_count, description, columns (name, type, nullable, primary_key, description) and foreign_keys (columns, references_table, references_columns). A table the caller's roles deny is left out, and so is a foreign key that refers to one; a caller whose roles do not reach the datasource gets no tables. user_context is required: there is no default view, so code that forgot it fails instead of showing everything. Pass an admin's UserContext for the whole schema. Read from the snapshot the datasource's live index build was made from, not the database; before the first index tables is empty. |
DatasourceInfo is a frozen dataclass, exported from nl2sql:
| field | meaning |
|---|---|
id |
The datasource id. |
type |
Its type (the adapter's engine type for an adapter object). |
description |
Its description, or None. |
status |
ready once check() or test() connected to it, failed when one did not, unchecked before either ran. |
capabilities |
The capabilities its adapter declares, sorted. Empty for a failed datasource: nothing is routed to it. |
connection |
The configured connection, with every secret field masked as ***. References (${env:...}) are shown masked too. |
reason |
Why it failed, or None. |
DatasourceType is a frozen dataclass, exported from nl2sql:
| field | meaning |
|---|---|
name |
The name a datasource's type uses: postgres. |
title |
The adapter's title, for a form: PostgreSQL. |
installed |
Every one of its driver modules is found. |
install_hint |
What to install when it is not: pip install "nl2sql-engine[postgres]". |
connection_schema |
The JSON Schema of its connection model (connection_model.model_json_schema()): the form. A secret field is "format": "password", "writeOnly": true, which a form renders as a password input; a field with "group": "advanced" is folded away. |
Testing a datasource
test() takes a configured datasource's id or a Datasource.
- An id connects to that configured datasource, as
check()does:readyputs it back in routing,failedkeeps it out. RaisesValueErrorfor an unknown id. - A
Datasourcethat is not added: builds its adapter, runstest_connection, closes the adapter, and registers nothing. It is what a Connect a database form calls beforeadd. A failure is reported, never raised. Anadapterobject is tested as given and not closed: the caller owns it.
A failed status carries the stage, the reason and the fix:
stage |
When | fix |
|---|---|---|
config |
The type is unknown (with the closest name), or connection does not validate against the adapter's model (the field, and "did you mean" for an unknown key; never a value) |
What to correct |
driver |
A module in the adapter's driver_modules is not installed |
The adapter's install_hint |
| the adapter's stage | test_connection failed: the ConnectionCheck's stage (network, auth, database, permission), or connect where the adapter does not say |
The ConnectionCheck's fix, or a generic one |
from nl2sql import Datasource
{t.name: t.installed for t in engine.datasources.types()} # {'duckdb': True, 'mssql': False, ...}
form = next(t for t in engine.datasources.types() if t.name == "postgres").connection_schema
candidate = Datasource(id="sales", type="postgres",
connection={"host": "db.internal", "database": "sales", "user": "nl2sql",
"password": "${env:SALES_DB_PASSWORD}"})
status = engine.datasources.test(candidate) # registers nothing
if status.status == "ready":
engine.datasources.add(candidate)
else:
print(status.stage, status.reason, status.fix)
from nl2sql import NL2SQL, Datasource, UserContext
engine = NL2SQL.from_config("nl2sql.yaml")
engine.check()
[(d.id, d.status) for d in engine.datasources.list()] # [('shop', 'ready'), ('hr', 'failed')]
engine.datasources.get("hr").reason
engine.datasources.schema("shop", user_context=UserContext(roles=["analyst"]))["tables"]
engine.datasources.add(Datasource(id="ledger", type="sqlite", connection={"database": "ledger.db"}))
engine.datasources.test("ledger").status # 'ready'
engine.index.build(["ledger"])
To add every datasource of another nl2sql.yaml:
from nl2sql import load_config
for datasource in load_config("other/nl2sql.yaml").datasources:
engine.datasources.add(datasource)
See Datasources for the Datasource
fields, and When a datasource fails to start
for running degraded.