Connectors
NLQueries reads two kinds of sources: databases (for structured SQL answers) and documents (for the document and hybrid agents).
Before you connect: the role to use
Give NLQueries a login that cannot write and cannot read more than you would put in an answer. The connector requires TLS, and every engine that can express a read-only execution is asked for one, but neither limits what the role can read, and neither is a substitute for a least-privilege grant.
How much the connector can enforce differs by engine, and the gap worth knowing
is DDL. A rolled-back transaction undoes an INSERT on a transactional
table; it does not undo a CREATE or DROP on an engine that commits
implicitly around DDL. (Nor does it undo an INSERT into a MyISAM or MEMORY
table on MySQL or MariaDB — those engines have no transaction to roll back, and
the server reports only warning 1196.) That
is Snowflake, and also MySQL, MariaDB and Oracle behind the generic
SQLAlchemy connector; SQLite is a third variant, running DDL outside the
transaction altogether. BigQuery has no transaction to roll back at all, so a
non-SELECT statement type is logged after the job has run rather than
prevented. On all of these the grant is doing work the connector cannot, which is
why the role matters more than the transaction does.
docs/database-hardening.md has the SQL, per engine.
Database connectors
| Connector | Install | Query history source |
|---|---|---|
| PostgreSQL | included | pg_stat_statements extension |
| MySQL | pip install "nlqueries-core[mysql]" |
Performance schema events_statements_summary_by_digest |
| Snowflake | pip install "nlqueries-core[snowflake]" |
QUERY_HISTORY view in INFORMATION_SCHEMA |
| BigQuery | pip install "nlqueries-core[bigquery]" |
INFORMATION_SCHEMA.JOBS |
| Amazon Redshift | pip install "nlqueries-core[redshift]" |
STL_QUERY (requires superuser or pg_read_all_stats) |
| SQL Server / Azure SQL | pip install "nlqueries-core[mssql]" |
sys.dm_exec_query_stats + sys.dm_exec_sql_text (requires VIEW SERVER STATE / VIEW DATABASE STATE) |
| DuckDB | pip install "nlqueries-core[duckdb]" |
None — file-based, no persisted history |
| SQLite | included (stdlib sqlite3) |
None — file-based, no persisted history |
See cli-reference.md for connect examples per type.
PostgreSQL — enabling query history capture
process-history requires the pg_stat_statements extension. Check whether it's already enabled before changing anything:
SHOW shared_preload_libraries; -- library loaded at server level?
SELECT extname FROM pg_extension WHERE extname = 'pg_stat_statements'; -- extension created in this DB?
Managed databases (AWS RDS, Google Cloud SQL, Supabase, Neon, Azure Database for PostgreSQL) pre-load the library — just run:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Self-hosted PostgreSQL requires a restart, since shared_preload_libraries is a startup-only parameter:
- Add
shared_preload_libraries = 'pg_stat_statements'topostgresql.conf(append with a comma if other libraries are already listed) - Restart PostgreSQL
- Run
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;once per database - Verify:
SELECT count(*) FROM pg_stat_statements;
Note: --days has no effect on PostgreSQL — pg_stat_statements doesn't record per-query timestamps, so all history since the last pg_stat_statements_reset() is returned. --days is honoured on Snowflake and BigQuery, which do track execution time.
Amazon Redshift
Schema descriptions are not available (no equivalent of PostgreSQL's pg_description). Row counts come from SVV_TABLE_INFO (requires table-owner or superuser; falls back to a permission-free list if inaccessible).
REDSHIFT_SOCKET_TIMEOUT_SECONDS bounds the whole connection, not just the
handshake: the driver sets it on the socket before connecting and never clears
it, so it also limits how long a query may go without sending data. It therefore
defaults to your statement timeout plus 30 seconds (150 by default) and must stay
above it — below, a long query is killed by the client and reported as a network
fault instead of being cancelled by the server.
It also caps any per-query budget: a caller passing timeout_seconds=300 against
the default 150 still dies on the socket at 150. Raise this above the largest
per-query budget you intend to allow. Setting CONNECTOR_STATEMENT_TIMEOUT_SECONDS=0
disables the socket ceiling too, so a deliberately unbounded query stays
unbounded; REDSHIFT_SOCKET_TIMEOUT_SECONDS=0 does the same on its own.
Lowering it to fail faster on an unreachable host shortens every query budget by the same amount.
The same value covers a Serverless workgroup resuming from zero, which it has to do before it answers the first connection after an idle period.
SQL Server / Azure SQL
Use alice@my-server as --user for Azure SQL with SQL authentication — the same connector covers on-premises SQL Server and Azure SQL since the T-SQL dialect is identical. If the account lacks VIEW SERVER STATE/VIEW DATABASE STATE, process-history returns empty history and the KB is built from schema introspection only.
DuckDB
No query history across connections — process-history always returns an empty list; the KB comes from schema introspection only. Primary keys are detected via duckdb_constraints(); foreign keys are skipped (rarely declared in DuckDB analytics workloads).
SQLite
Built on the standard-library sqlite3 driver, so no extra install is needed. The database credential is a file path (/data/app.db) or :memory: for a transient in-process database — there is no host, port, or credentials. No query history (process-history returns an empty list); the KB is built from schema introspection via PRAGMA table_info (columns + primary keys) and PRAGMA foreign_key_list (foreign keys). SQLite has no server-side statement timeout, so a runaway query is bounded best-effort by a watchdog that interrupts it.
Already have a SQLite URL? The generic
sqlalchemyconnector also reaches SQLite (--url "sqlite:///path.db"); the dedicatedsqlitetype just gives you the simpler file-path form.
Document connectors
| Connector | Format | Requires |
|---|---|---|
.pdf |
pip install "nlqueries-core[docs]" |
|
| Word | .docx |
pip install "nlqueries-core[docs]" |
| Excel | .xlsx |
pip install "nlqueries-core[docs]" |
| Notion | Notion pages | pip install "nlqueries-core[wiki]", NOTION_API_TOKEN |
| Confluence | Confluence spaces | pip install "nlqueries-core[wiki]", CONFLUENCE_URL, CONFLUENCE_USER, CONFLUENCE_API_TOKEN |
# SOURCE_ID is an opaque slug you choose (e.g. a UUID or short name)
nlq doc-ingest <source_id> <file_path>
# e.g.
nlq doc-ingest q1-report ./report.pdf
# Notion — requires NOTION_API_TOKEN env var; PAGE_ID is the Notion page or database ID
nlq doc-sync-notion <source_id> <page_id>
# e.g.
NOTION_API_TOKEN=secret_... nlq doc-sync-notion my-wiki-src abc123def456
# Confluence — requires CONFLUENCE_API_TOKEN env var
nlq doc-sync-confluence <source_id> <space_key> --base-url <url> --username <user>
# e.g.
CONFLUENCE_API_TOKEN=... nlq doc-sync-confluence my-src ENG \
--base-url https://acme.atlassian.net --username [email protected]
After ingestion, documents are chunked and embedded into Qdrant (required — see qdrant-setup.md). The document agent retrieves relevant chunks automatically when answering; citations (source document, page/section) are included in the answer.
Query with nlq doc-ask doc_{source_id}_chunks "..." for a document-only answer (the collection name follows the pattern doc_{source_id}_chunks), or use nlqueries query for the orchestrator to route automatically (including hybrid SQL + document answers).
Document ingestion no longer depends on langchain_text_splitters — chunking uses a small built-in chunker (nlqueries.document_connectors.chunker), so the Python 3.14 limitation that previously affected document connectors no longer applies (see troubleshooting.md for history).