Start with a narrower promise
This example answers analytical questions over approved datasets. The model can interpret a request and propose a query. A separate backend authorizes and executes a limited class of reads. The system does not accept arbitrary database commands or grant access through conversation.
Assume a signed-in user, enforceable tenant policies, and a maintained catalog. Dataset owners define measures, joins, units, and freshness. Model quality cannot compensate for an undefined measure or an incorrect relationship between tables.
Resolve the question before writing SQL
The catalog exposes only metadata and examples the caller may see. It includes dataset grain, allowed joins, metric definitions, and relevant time fields. If “active users” could mean recent sign-ins or enabled accounts, ask which definition applies before proposing a query.
Record the chosen definition, time interval, timezone, and catalog version. The model proposes structured query intent and SQL against that scope. Treat instructions found in descriptions or retrieved text as untrusted content; they cannot change the user's identity or the executor's permissions.
Check access at execution
The backend binds the request to server-verified identity and checks the referenced datasets, columns, functions, and tenant rows. These checks happen before protected values reach the model. The database role must be read-only within the intended scope, with row policies that apply to that role; a table owner or privileged role can bypass some row-security configurations.
A parser inspects the SQL syntax tree against an allowlist and resolves identifiers against the catalog. Bind literal values as parameters where supported. Neither parsing nor parameters authorize data access. Restrict functions and connector operations too: a query that begins with SELECT can still be expensive or invoke an unintended capability.
Bound the read and its retries
Execute through a service with scoped credentials and controlled session settings. Apply time, scan, memory, output-size, and concurrency limits independently of the model. A small result limit does not make a large join cheap. Reject requests whose required scope cannot be bounded.
Keep a query ID and cancellation path. A timeout, denied request, empty result, and successful result are different outcomes. A retry gets a bounded attempt budget and may see different source data. Do not silently broaden filters or substitute another tenant's cached answer to make the conversation continue.
Explain the result that actually ran
Build the result record from execution: query ID, SQL and bound inputs, metric definition, source versions when available, execution time, row count, and any truncation. For an aggregate, preserve grouping, filters, units, and the time window. The displayed chart and narrative should refer to that same record.
The model may explain the returned values, but its explanation does not verify the calculation. Report missing inputs, stale sources, and incomplete results explicitly. An empty set means no matching rows were returned under this query and access scope; it does not automatically mean the measured quantity is zero.
Test the boundary as well as the answer
Evaluate expected queries and answers separately from permission enforcement, denied resources, ambiguous definitions, cancellation, and injected instructions. Track refusals, execution failures, clarification requests, and result mismatches without logging unnecessary protected data.
This design adds catalog maintenance and limits the questions the assistant can answer. For recurring reports, parameterized query templates or a conventional dashboard may offer a clearer, cheaper path. Use model-generated queries where the extra flexibility justifies the additional checks.