SQL connectors through the API¶
The concept behind tools is in Tools.
A SQL connector gives the model one fixed query against a database of your organisation. You
write the query when you register the connector; the model only fills in values. Each connector
becomes one tool, sql__<name>. Core connects to your database server itself, over TLS, with a
user name and password you store once. Postgres, MySQL, SQL Server and Oracle are supported.
You set up a SQL connector through the API, as below, or in the console under Tools > SQL
connectors. There you add one on a page of its own, read a connector's connection, statement and
versions, edit it into a new version, delete it, and press Test to have Core log in and run
SELECT 1. The password is never shown back; leave it empty when you edit to keep the stored one. A
connector installed from a publication cannot be edited or deleted there.
Your key needs the tooling capability, and the database server's host must be on your
organisation's allowed hosts.
Register a connector¶
curl -s -X POST "$CORE_URL/sql_connectors" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "orders",
"description": "Looks up the recent orders of one customer.",
"dialect": "postgres",
"host": "db.your-app.example",
"database": "shop",
"statement": "SELECT id, status, total FROM {$schema}.orders WHERE customer_id = {customer} ORDER BY created_at DESC",
"parameters": {"type": "object",
"properties": {"customer": {"type": "string", "description": "The customer number"}},
"required": ["customer"]},
"install_values": {"schema": {"type": "identifier", "value": "public"}},
"max_rows": 20,
"auth": {"type": "basic", "username": "core_reader", "password": "..."}}'
The answer is 201 with the connector, version 1. Offer it with
{"type": "tool", "name": "sql__orders"} in tools[], or include it in a
toolset.
| Field | What you fill in |
|---|---|
name |
what the model calls the tool (sql__<name>): lowercase letters, digits, - and _, at most 48 characters. It cannot change after creation |
description |
what the tool does, as the model reads it, at most 1024 characters |
dialect |
postgres, mysql, sqlserver or oracle |
host |
the server's host name or address, with no scheme, port or path |
port |
optional; 5432, 3306, 1433 or 1521 by default, depending on the dialect |
database |
the database; for Oracle, the service name |
statement |
the one query the tool runs, see The query |
parameters |
a JSON Schema object with the arguments the model fills in |
mode |
read (the default) or write, see Reading and writing |
max_rows |
the most rows one call returns: 1 to 1,000, 100 by default |
timeout |
seconds one call may take; Core uses the shorter of this and the deployment's tool timeout, 30 seconds unless your operator set another |
tls, ca_pem |
how Core checks the server's certificate, see TLS |
install_values |
values you set once, see The query |
call_variables |
values your application sends per request, see The query |
auth |
{"type": "basic", "username", "password"}; there is no other kind |
region, labels, background, metadata |
as for an API connector, see below |
The password is stored encrypted and never returned: GET answers the kind, the user name and
has_secret. Give Core a database user that may do exactly what the query needs, and for a read
tool a user that cannot write.
GET /sql_connectors lists your organisation's connectors, GET /sql_connectors/{id} reads one,
PATCH changes one and DELETE removes one. A delete answers 409 naming the agents while an
agent's latest version names the tool; remove it there first.
The query¶
The query is fixed; the model only chooses values. statement holds three kinds of placeholder:
| Placeholder | Who gives the value | Example |
|---|---|---|
{name} |
the model, as an argument from parameters |
WHERE customer_id = {customer} |
{$name} |
you, once, in install_values |
FROM {$schema}.orders |
{@name} |
your application, per request, in tool_variables |
AND tenant = {@account} |
Core hands every value to the driver as a bound parameter, never as SQL text. A value in an
argument therefore cannot change the query, whatever it contains. That is why a placeholder may
only stand where a value goes: after an operator, a comma, a bracket or a word such as WHERE,
VALUES or LIMIT.
The one exception is an install value of type identifier: the name of a schema, a table or a
column. Core writes it into the query, quoted by the dialect's own rule. Only you set such a name,
never the model. In Oracle a quoted name keeps its case, so write it as the server stores it,
usually in upper case.
An install value is an object {type, value, allowed, description} whose type is string,
number, enum (with allowed) or identifier. Every {$name} needs a value and every value
must appear in the query. An install value is not a secret; the password belongs in auth.
You declare call variables in call_variables as for an API connector, and your application sends
their values in tool_variables on the request (Make a model call).
The model never sees them. An optional variable with no value and no default binds as NULL; a
required one the request leaves out answers 400 on tool_variables.<name>.
An optional argument the model leaves out also binds as NULL. When a required argument is
missing, nothing runs and the model reads Tool arguments were missing a required value.
Core refuses at registration, with 422 on statement:
- more than one statement;
- a placeholder inside a string, a quoted name or a comment, or where a name goes;
- a placeholder that
parameters,install_valuesorcall_variablesdoes not declare; - an install value the query does not use. A call variable the query does not use is refused too,
with
422oncall_variables; an argument inparametersthe query does not use is allowed; CREATE,ALTER,DROP,TRUNCATE,RENAME,GRANT,REVOKE,COMMITandROLLBACK;- anything whose reading depends on the server's settings: in Postgres and MySQL a backslash
inside a string, and in MySQL a
--not followed by a space and the comment forms/*!,/*+and/*M!.
Each dialect adds its own. SQL Server refuses a GO. Oracle refuses a semicolon at the end, a
PL/SQL block (BEGIN, DECLARE), RETURNING and a bind variable of your own such as :name.
An argument that is a list is sent as JSON text in SQL Server and Oracle, since those drivers have no list parameter.
Reading and writing¶
With mode: "read" Core accepts only a query that starts with SELECT or WITH (in Postgres also
VALUES or TABLE), with no INSERT, UPDATE, DELETE, MERGE or INTO. The query runs in a
read-only transaction that is always rolled back. SQL Server has no read-only transaction: there
the check on the query is what keeps a read tool from writing. It therefore also refuses EXEC,
EXECUTE, OPENROWSET, OPENQUERY and OPENDATASOURCE in a read. Give such a connector a user
that cannot write.
With mode: "write" the query may also start with one of these:
| Dialect | Write statements |
|---|---|
| Postgres | INSERT, UPDATE, DELETE, MERGE, CALL |
| MySQL | INSERT, UPDATE, DELETE, REPLACE, CALL |
| SQL Server | INSERT, UPDATE, DELETE, MERGE, EXEC of a procedure |
| Oracle | INSERT, UPDATE, DELETE, MERGE, CALL |
INTO is refused anywhere except in an INSERT, a REPLACE or a MERGE, since SELECT ... INTO
creates a table and MySQL's INTO OUTFILE writes a file on the server. The query runs in one
transaction that is committed. Core never retries a call. In SQL Server a write tool refuses dynamic SQL
(EXEC (...), sp_executesql) and EXECUTE AS.
TLS¶
Core always talks to your database encrypted; there is no setting without TLS.
tls |
What Core checks |
|---|---|
verify-full (the default) |
the server's certificate, and that it carries the host's name |
require |
encrypts, and checks the certificate only against a ca_pem you give with it. For SQL Server the driver always checks the certificate, so there require skips only the name check |
ca_pem is your own server's certificate authority, as PEM. Leave it out to check the certificate
against the authorities the deployment trusts. A MySQL server that offers no TLS is refused before
Core logs in.
What the model gets back¶
The answer is structured: {columns, rows, row_count, truncated}, with each row a list of values
in the order of columns. The model reads it as compact JSON; a direct
call answers it in structured.
{"columns": ["id", "status", "total"],
"rows": [["A-1042", "shipped", "129.95"], ["A-1038", "open", "18.50"]],
"row_count": 2, "truncated": false}
- A decimal is text, so no digit is lost; a time is ISO 8601; bytes are
[binary]. - At most
max_rowsrows come back, and at most what fits in a tool result (32 KiB).truncated: truesays rows were left out. - A query that answers no rows, such as an
UPDATE, gives the number of rows it changed inrow_count.
When a call fails, the model reads one fixed sentence, never the server's error message, which can hold the values the query ran with. The turn goes on.
| The model reads | What happened | What you can do |
|---|---|---|
Denied by policy. |
the host is not on your allowed hosts, the key's rules refuse it, or the deployment blocks the host or the address it resolves to (a private or loopback address, a name such as localhost or one ending in .internal) |
put the host on the list, or make the server reachable at an address the deployment does not block, see Allowed hosts |
Tool authentication failed. |
the server refused the user name or password | store a new auth with PATCH |
Database unreachable. |
the server did not answer, or the TLS check failed | check host, port, tls and ca_pem with the test |
Database query timed out. |
the query took longer than the timeout |
make the query faster or the timeout wider |
Database refused the query. |
the server refused the query, or a write query answered more than Core may read | try the query on the server yourself, as the same user |
The audit trail and the turn's trace name the kind of failure and the server's error code (the SQLSTATE class or the error number), never the message itself.
Test a connector¶
POST /sql_connectors/{id}/test logs in and runs SELECT 1, under the same host rules as a call.
Nothing is stored.
curl -s -X POST "$CORE_URL/sql_connectors/sqlc_01J.../test" \
-H "Authorization: Bearer $CORE_KEY"
The answer is always 200:
{"object": "sql_connector_test", "ok": true, "code": null}
On a failure ok is false and code says why:
egress_denied (the host or its address is not allowed), unreachable, auth_failed, timeout
or refused.
Allowed hosts¶
Core reaches your database only when its host name is on your organisation's allowed hosts, the
same list an API connector uses (Allowed destinations). The
egress field on every read says per host whether it is allowed (allowed) and whether the
deployment blocks it (blocked).
The deployment blocks an address in a private network, a loopback address and a name that exists
only inside a network, such as localhost or a name ending in .internal. Your database server
must therefore be reachable at an address Core may dial. When the server sends Core on to another
host, as an Azure SQL gateway or an Oracle listener does, that host must be allowed too.
region says where the server is served, as for an API connector
(Where the host is served).
Versions¶
A connector is versioned. A PATCH that changes what the tool does (the query, the arguments, the
server, the mode, the limits, the install values, the call variables or background) makes a new
version. A PATCH of only auth, region, labels or metadata makes none.
GET /sql_connectors/{id}/versions lists the versions, newest first (?order=asc for oldest first), and
GET /sql_connectors/{id}/versions/{version} reads one. Pin a version on a request:
{ "type": "tool", "name": "sql__orders", "version": 2 }
Without version a turn gets the latest (Pin a connector version).
Background, labels and a picture¶
- Background.
backgrounddecides whether the model may start the tool as a task, with the same fields as for an API connector (Run a tool in the background). It belongs to the version. - Labels.
labelsis your organisation's own classification, which rules about people are written over (Labels on everything). For a request that names a person, the same rules oversql__<name>decide whetherGET /sql_connectorslists the connector and whether it may be changed or deleted (Registrations). - Picture.
PUT /tools/sql__orders/coverputs a picture on the tool, which the console shows beside its name: PNG, JPEG or WebP as the raw body, at most 512 KiB unless your operator set another limit.DELETEremoves it. The model never sees the picture.
Share in a publication¶
A SQL connector can go into a publication as a member of
kind sql_connector. As for an API connector, say what happens to the login:
{"kind": "sql_connector", "id": "sqlc_01J...", "auth": "parameter", "auth_parameter": "database_login"}
auth |
What happens |
|---|---|
parameter |
every organisation that installs it fills in its own user name and password |
carried |
your own login travels with it, encrypted; only in a publication to organisations below you |
none |
no login travels with it, and the installed copy has none |
Every install value becomes a parameter of the bundle, with your value as its default. Whoever
installs it fills in their own schema name or value, and Core checks the query with it again at
installation. A copy installed with auth: "none" cannot log in: a request that offers its tool
answers 503 with The SQL connector's stored credential could not be read. Choose parameter
when every installer should use a login of their own. A copy installed under the Hide contents term answers the query, the host, the
database and the arguments as null; the tool works as usual. You cannot change a copy from a
publication, its picture included: such a PUT or DELETE answers 409.
Errors at registration¶
| Status | When |
|---|---|
409 |
a SQL connector with this name already exists; or, on DELETE, an agent still names the tool |
422 on statement |
the query is not allowed, see The query |
422 on call_variables |
a declaration the query does not use, or a name that is also an argument or an install value |
422 on ca_pem |
not PEM, or not a certificate the server can load |
503 |
the deployment has no key to encrypt the password with; ask your operator |
Every route has a twin under /tenants/{tenant_id}/sql_connectors for an administrator working with
an administrator token, with the same fields and answers.