Parameters
A supplied value never becomes part of a statement, and being remote does not give that back.
A parameter is written $name and stands where a value stands:
DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE orders;
USE DATABASE orders;
DEFINE COLLECTION users;
CREATE users = { name: 'ada', email: 'ada@example.com' };
$ tessaridb ./data --param who="'ada'" -e 'SELECT * FROM users WHERE name = $who;'
1: { email: 'ada@example.com', name: 'ada' }
(1 record(s), via scan)
The value is bound to the name after the statement has been read. Nothing a
caller supplies can therefore become syntax: a $who holding
'ada' OR true is a string containing those characters, compared for equality
against a field, and it matches the record whose name is literally that.
A $name can also be bound inside the script itself, with LET — that is
Bindings, and it is the same mechanism seen from the
other side. This page is about the half that arrives from outside, which is the
half that carries the risk.
This is the whole of the injection story, and it is worth being explicit about why it is not merely "we escape it properly". Escaping is a transformation applied to text that is about to become syntax. Binding means the text never gets there: by the time a value exists, the statement's shape is already decided and cannot be added to.
Where a parameter may stand
| position | example |
|---|---|
| a value in a condition | WHERE name = $who |
| a value in a record | CREATE users = { name: $who } |
| a bound or a limit | LIMIT $howmany, START $from, CLAIM $n FROM jobs |
| a record's id | GET sessions:$token |
That last one is the exception worth knowing. A record is table:id, and the
two halves are different things: the table is a name, which a caller may
never supply, and the id is a value, which they may. So a parameter stands
in an identity's id and nowhere else in it.
That matters because GET sessions:$token is the shape a session lookup
actually has. Without it, every key-value read of a caller-supplied key would be
built by pasting text into a statement — which is exactly the construction
parameters exist to remove, and it would be the one place the language forced
you back into it.
Named values a database holds
DEFINE PARAM $grace VALUE 10m;
RETURN time::now() - $grace < time::now();
DEFINE PARAM OR REPLACE $grace VALUE 1h;
DROP PARAM $grace;
DROP PARAM IF EXISTS $grace;
DEFINE PARAM gives a database a named value that scripts, event bodies and views
read as $name (from 0.33.4-beta). A parameter bound to the statement wins over
one of the same name. OR REPLACE changes the value, IF NOT EXISTS keeps it,
and a second plain DEFINE PARAM is refused as ParamExists. INFO FOR DATABASE lists them under params; they replicate with the catalog and every
backup carries them.
What a parameter is not
It is not a name. A table, a field, an index or a database cannot be parameterised, and that is deliberate rather than unfinished: a statement whose shape depended on a supplied value could not be planned once and reused, and could not be reasoned about by reading it. A caller that needs to choose between two tables writes two statements and chooses between them in their own language, where the choice is visible.
Unbound
A parameter with no value is a refusal and never an empty value:
SELECT * FROM users WHERE name = $nobody;
UnboundParameter. Treating it as NONE would turn a wiring mistake — a name
misspelled on one side — into a read that quietly matches the records with no
such field, and answers.
From a client
Every client speaks this the same way, because binding happens in the node and not in the client's string handling. See The Rust client for the shape it takes there, and The HTTP surface for how a script and its bindings travel together in one request.
The search box on this site is one:
SELECT page FROM fragment WHERE text MATCHES $q;
The term you typed reached the store as a value. That is what makes it safe to take a search term from a query string — and it is the same mechanism whether the caller is across a socket or in the same process, because there is only one.