Aliases, the catalog, and the central rule
The data service writes its own SQL: it uses an alias instead of a database name, names that it looks up in the database's catalog, and values that it binds as parameters. This page explains the model behind every request, and the two routes for statements that callers write themselves: checked statements, which the service parses and checks object by object, and unchecked pass-through, which you enable separately.
Key features
- Databases are visible to callers only under the alias names you release.
- Every identifier is checked against the catalog, and every value is bound as a parameter.
- Statements written by callers take one of two routes: checked—parsed, with every object checked against the catalog and the rules—or unchecked, on a route you enable separately, limited by the permissions of the database login.
Core concepts
Aliases instead of database names
Callers never see a database name. They address databases through the alias names you release, and the mapping to connection strings lives in this service's configuration.
Callers aren't separated by port either, but by their credentials and the scope assigned to them:
everyone reaches the service on the same HTTPS port (default 8433, modeled on SQL Server's 1433).
The HTTP port 8434 responds only on the server itself, for tools, health checks, and an upstream web
server.
The central rule
The service runs no SQL that it hasn't generated or checked itself. It generates SQL from a typed contract: a released resource identifier, selected fields, structured filter nodes, sort specifications, and paging specifications. A statement that a caller writes runs only after the service has parsed it and checked every object (checked statements). The only exception is unchecked pass-through, which you have to enable separately.
The service refuses:
| Refused | Why |
|---|---|
| Caller SQL that the service can't parse and check—except on the enabled pass-through route | It would bypass the checkpoint |
| Unchecked table or column names | Identifiers can't be parameterized, only checked against an allow list |
| Free-form sort expressions | Same reason |
| Freely chosen procedure names | Otherwise every procedure in the database would become callable |
| Retrieval without an upper limit | A single call could otherwise tie up the service |
All values are parameterized. All identifiers are checked against released names. These are two different safeguards, and both are necessary.
The one exception: the pass-through route
Through POST /api/v1/cf/{alias}/sql.json, the service accepts a statement that the caller wrote and
runs it. This isn't a loophole in the rule but a separate route that you have to enable—and,
deliberately, the only one on which the service runs text without evaluating it.
Why it exists. A client that can only address one object at a time can't express a join, a grouping, or a set-based change at all. It would have to fetch every row involved and do the work locally instead of receiving only the result, and the effort grows with the size of the tables, not with the size of the result. That's exactly why database tools have pass-through queries.
What limits it—and what doesn't. The service doesn't read the statement: it doesn't parse it,
doesn't match it against a pattern, and doesn't classify it as reading or writing. Such a check would
be a promise this service can't keep; SELECT alone says nothing about what can be reached through
views, functions, and ownership chains. The limit is therefore where it's actually enforced: in the
permissions of the login under which the statement runs. Whatever that login may do, a pass-through
statement may do—including reading, writing, and schema changes. Which login that is depends on how
the caller signed in to the service (see
Identity pass-through):
| Caller | Statement runs as | Access to pass-through |
|---|---|---|
Service key; Windows account on an alias without Persons: Impersonate | the alias's login | the rule names sql literally |
| SQL Server login (HTTP Basic) | a separate connection for that login | any rule for the alias that isn't read-only |
Windows account or personal key with an assigned SQL Server login, on an alias with Persons: Impersonate | EXECUTE AS LOGIN for the person | any rule for the alias that isn't read-only |
A chosen database identity of kind Login or Own | EXECUTE AS LOGIN for the shared or the person's own login | any rule for the alias that isn't read-only |
In the cases without sql, the rule's object lists don't limit the statement; it runs with all the
permissions of that login.
Under the person's own login, SQL Server checks every statement against that login's permissions—
just as with a direct connection through Microsoft's ODBC driver. The sql right would add nothing
there, but it would deny every user without it the joins, groupings, and function calls that Access
sends as statements of its own; every saved query that spans several linked tables falls into that
category. Under the alias's login, nothing but the rule limits an unchecked statement, so there
sql remains the only door to it. Callers without sql send
checked statements instead. On an alias with
Persons: Impersonate, a Windows account without its own SQL Server login is refused when the session
opens (403) and never falls back to the alias's login. A personal key whose person has no SQL Server
login on record, on the other hand, runs under the alias's login, so the first row of the table
applies to it.
In addition, these conditions always apply; otherwise the service responds with 403:
| Condition | Where it's set |
|---|---|
| The alias accepts custom statements | AllowPassThrough: true on the database entry |
| The caller isn't read-only | ReadOnly: false |
An existing * under Resources does not enable the route. The * is a statement about the
object kinds in the catalog; if it could also mean „and any statement you write,“ every rule written
earlier would have expanded the moment this route was added. A read-only caller is refused outright
rather than restricted to reading statements: the service can't tell the two apart, so read-only here
means no custom statements at all. The startup check catches both contradictions: if a rule names
sql and is also read-only, the service doesn't start.
Checked statements: without the sql right
Access sends joins, subqueries, totals across several tables, functions, and set-based changes across
linked tables as one statement, and doesn't compute them itself if the driver refuses them. With
Microsoft's ODBC driver they run, because SQL Server checks the permissions. So that the same works
through the service without giving every user the sql right, the service accepts checked
statements at the same address—from every caller whose rules name the alias, if the alias allows
it (AllowCheckedStatements, on by default).
Checked means that the service parses the statement with Microsoft's T-SQL parser and accepts it only if:
- each of its statements is one of these:
SELECT(with joins, subqueries,UNION,WITH, grouping, window functions),INSERT,UPDATE,DELETE, or a call to a named procedure. Several statements in one batch (SELECT …; SELECT …) run as they do with Microsoft's ODBC driver; each is checked on its own, and aGOseparator line is refused; - every construct it uses is on a closed list—no schema changes, no dynamic SQL, no
SELECT … INTO, no variables other than the bound parameters, no names qualified with a database or server, and no function that describes the server, the login, or other objects; - every object it names is in the alias's catalog and is released for what the statement does with it, under the same rules as a request on the object itself: reading like
GET, inserting likePOST, updating likePATCH, deleting likeDELETE, and running a procedure likePOST. The target of a change also needs read permission, because a condition on its columns reveals values. Rights per object (Objects,ExceptObjects) apply as well.
What runs isn't the caller's text but a rewritten version in which every object carries the schema
that the service checked it against. This way a caller can't reach anything they couldn't also reach
object by object through OData; they're only spared fetching every row. A read-only rule allows only
reading statements. Callers who have the sql right or work under their own login still get unchecked
pass-through.
Large results are streamed. If a statement returns more than 500 rows, the response (JSON) starts
with the first rows while SQL Server is still producing the rest; smaller results are collected and
sent in one piece. Both forms are the same document—columns first, then data, then the counts. The
response therefore starts at about the time SQL Server delivers the first row instead of waiting for
the last one. If the statement fails after the response has started, the service drops the
connection: the 200 status has already been sent, and a properly closed but truncated document would
look complete. Every reader reports the aborted document as an error, and the cause is in the log
under the correlation ID. If the statement fails within the first 500 rows, the service responds with
the matching status. If the caller stops reading and closes the connection, the service cancels the
statement in SQL Server instead of reading and discarding the rest. The XML representation is still
collected in full.
The statement text is written to the audit stream before it runs, under the same correlation ID as the outcome of the request. Values are left out, because they're the data. The stream otherwise contains no statements, because the resource is enough there: it tells you what happened. Here, the text is the only description of what happened.
Deliberately not offered
The service deliberately doesn't offer three seemingly obvious options:
| Option | Why not |
|---|---|
| OData protocol versions before version 4 (Atom) | The service speaks the OData v4 interface. Version 4 describes itself with types, facets, and keys, and the Office clients read it |
| A connection taken from the address (database name or server in the URL) | It would build the connection from the URL, contradicting the alias model, whose whole point is that a caller can't choose the database |
| Database systems other than SQL Server without a provider | Every database system needs a provider; an alias whose provider isn't loaded stops the service from starting instead of being accepted as an empty shell |