Most “chat with your database” demos stop at prompt engineering: dump the schema, ask the model for SQL, hope nothing breaks.
That fails in production for boring reasons:
- the schema is too large for one context window
- the model invents joins that do not exist
- aliases and dialect syntax drift
- a “helpful” write statement sneaks in
- one wide result set blows up the agent context
Solon AI (v4.0.3) packages this as Text2SqlTalent: a Talent that turns natural language into guarded SQL execution, not a raw prompt template.
Why a Talent, not a bare SQL tool
In Solon AI, a Tool is an atomic function. A Talent packages tools with instruction, activation, and SOP-style constraints.
Text-to-SQL needs that packaging:
- schema strategy depends on table count
- dialect rules must live next to the tools
- safety is not optional decoration
So you hang it with defaultTalentAdd(...), not by exposing a naked execute_sql method and hoping the system prompt is enough.
Maven module:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-ai-talent-text2sql</artifactId>
</dependency>
What Text2SqlTalent actually owns
From the official design, the closed loop is:
- detect dialect from the live connection
- load table remarks and foreign-key edges for the allow-listed tables
- inject either full schema or a catalog map into the agent instruction
- expose
execute_sql(andget_table_schemain DYNAMIC mode) - repair, guard, page, and truncate before results re-enter the model
That is the product boundary: intent → SQL → safe observation.
Schema modes: FULL vs DYNAMIC
| Mode | Default trigger | What the model sees | Extra tool |
|---|---|---|---|
SchemaMode.FULL |
table count <= 20
|
full column metadata in instruction | none |
SchemaMode.DYNAMIC |
table count > 20, or forced |
table list + FK map only | get_table_schema |
FULL is for small, high-precision analytics surfaces. DYNAMIC is for larger catalogs where dumping every column on every turn is wasteful.
You can also force the mode:
Text2SqlTalent sqlTalent = new Text2SqlTalent(dataSource, "users", "orders", "order_refunds")
.schemaMode(SchemaMode.DYNAMIC)
.maxRows(50)
.readOnly(true)
.maxContextLength(8000);
Dialects that travel with the Talent
Built-in adapters cover the common production set:
| Dialect | Matched products | Identifier quoting | Pagination style |
|---|---|---|---|
| MySQL | MySQL, MariaDB, TiDB | backticks | LIMIT n |
| PostgreSQL | PostgreSQL, Kingbase, Highgo | double quotes | LIMIT n |
| Oracle | Oracle, Dameng | double quotes |
ROWNUM wrap |
| SQLite | SQLite | double quotes | LIMIT n |
| H2 | H2 | double quotes |
LIMIT n (can switch to MySQL dialect in MySQL mode) |
| Generic | fallback | double quotes | FETCH FIRST n ROWS ONLY |
Auto-detection uses DatabaseMetaData.getDatabaseProductName(). You can override:
Text2SqlTalent sqlTalent = new Text2SqlTalent(dataSource, "users", "orders")
.dialect(new PostgreDialect())
.readOnly(true);
Custom dialects register through SqlDialectManager.register(...).
Four safety rails on every execute
These are not “best practices in a blog comment”. They are runtime behavior:
-
Read-only gate — with
readOnly(true)(default), non-SELECTstatements are blocked -
Alias repair — missing dialect quotes on
AS aliasare fixed before execution -
Pagination inject — if the SQL has no
LIMIT/ROWNUM/FETCH FIRST/TOP, the Talent appends dialect paging usingmaxRows(default 50) -
Result truncate — JSON larger than
maxContextLength(default 8000) is cut and marked truncated
That is why Text2SqlTalent is useful even when the model is already “pretty good at SQL”.
Minimal agent wiring
Constructor forms:
// DataSource + explicit table allow-list
new Text2SqlTalent(dataSource, "users", "orders", "order_refunds")
// or an existing SqlUtils instance
new Text2SqlTalent(sqlUtils, "users", "orders")
ReAct agent example from the official pattern:
Text2SqlTalent sqlTalent = new Text2SqlTalent(dataSource, "users", "orders", "order_refunds")
.maxRows(50)
.readOnly(true)
.maxContextLength(8000);
ReActAgent agent = ReActAgent.of(chatModel)
.role("Finance analyst")
.instruction("Analyze order and refund data. Amounts are in CNY. Prefer table comments when field names are ambiguous.")
.defaultTalentAdd(sqlTalent)
.build();
String answer = agent.prompt("How many refund orders did Zhang San have in 2024, and what is the total amount?")
.call()
.getContent();
ChatModel can also take the same Talent through defaultTalentAdd(...) when you do not need the full ReAct loop.
Built-in tools
| Tool | When exposed | Role |
|---|---|---|
execute_sql |
always | run SELECT with repair + guards + paging + truncate |
get_table_schema |
DYNAMIC only | fetch one table’s column metadata on demand |
In FULL mode the second tool stays hidden on purpose. The schema is already in the instruction; another discovery tool would only add noise.
How a turn actually runs
- Attach / init — dialect select, remarks, imported keys
- Instruction — environment context + schema or catalog map + dialect-specific rules
-
Optional probe — in DYNAMIC mode, model calls
get_table_schemafor the tables it needs -
Execute — model calls
execute_sql; Talent repairs and guards - Observe — truncated JSON comes back; dialect error hints help the next retry
That loop is why this belongs next to ReActAgent, not next to a one-shot chat completion.
Production checklist
- Allow-list tables in the constructor — do not expose the whole database if the task only needs three tables
-
Prefer DB-level read-only accounts even though Talent already defaults to
readOnly(true) - Write real COMMENT / remarks on tables and key columns — the Talent surfaces them as semantic hints
- Force DYNAMIC when column volume is high, even if table count is under 20
-
Keep
maxRowsandmaxContextLengthintentional — they are context-budget controls, not cosmetics - Override dialect when you run compatibility modes that auto-detect poorly
When not to use it
- you only need one fixed report query with known parameters → a normal Tool is enough
- the user is not allowed to explore data at all → ship parameterized APIs, not NL2SQL
- the schema is hostile (no keys, no comments, overloaded column names) → fix metadata first
Bottom line
Text2SqlTalent is not “LLM writes SQL”. It is a domain Talent with:
- adaptive schema injection
- dialect-aware execution
- multi-layer safety rails
- progressive table discovery when the catalog grows
If your agent needs to answer business questions against real tables, start here instead of pasting DDL into the system prompt.
References
- Text2SqlTalent deep dive: https://solon.noear.org/article/1354
- Maven module
solon-ai-talent-text2sql: https://solon.noear.org/article/1367 - Tool vs Talent: https://solon.noear.org/article/1335
- ReActAgent config / talents: https://solon.noear.org/article/1293
- Preset Talent catalog: https://solon.noear.org/article/1352
Solon version used for this write-up: v4.0.3.
Top comments (0)