All kinds of control flow happens in the context of some procedure, though we've already introduced examples of procedures let's now go over some of the additional aspects we have not yet illustrated.
Consider this procedure:
arg2 has been declared
out. CQL out parameters are very similar to "by reference" arguments in other langauges and
indeed they compile into a simple pointer reference in the generated C code. One notable difference is that, in CQL,
for reference types and nullable types are always set to NULL by default. This is another way that an otherwise non-null reference
variable can end up with a null in it.
Looking at the one line in the body of this procedure:
The input argument
arg1 is unconditionally stored in the output. Note that the
in keyword is entirely optional and does
nothing other than perhaps add some clarity. CQL also supports
inout arguments which are expected to contain non-garbage values on
entry. If the procedure is called from CQL, the compiler will arrange for this to be true.
inarguments contain a valid value
outarguments are assumed to contain garbage and are aggressively cleared on entry
inoutarguments contain a valid value
These invariants are very important when considering how reference types are handled.
inreference arguments are borrowed, CQL will not further retain unless they are stored elsewhere
outreference arguments are assumed to be garbage, they are not released on entry, but instead set to NULL
inoutreference arguments are assumed valid at entry
If CQL changes an
inout value it first releases the existing value and then retains the new value.
In all cases the caller will ultimately release any non-null out reference either because it was borrowed (
the caller now/still owns it (
out arguments normalizes pointer handling for all
call syntax is used to invoke a procedure. It returns no value but it can have any number of
The let's go over the most essential bits of control flow.
The IF statement
IF statement has no syntatic ambiguities at the expense of being somewhat more verbose than many other languages.
In CQL the
ELSE IF portion is baked into the
IF statement, so what you see below is logically a single statement.
The WHILE statement
What follows is a simple procedure that counts down its input argument.
WHILE loop has additional keywords that can be used within it to better control the loop. A more general
loop might look like this:
Let's go over this peculiar loop:
This is an immediate sign that there will be an unusual exit condition. The loop will never end without one because
1 will never be false.
Now here we've encoded our exit condition a bit strangely we might have done the equivalent job with a normal condition in the predicate
part of the
while statement but for illustration anyway, when x becomes negative
leave will cause us to exit the loop. This is like
break in C.
This bit says that on every 100th iteration we go back to the start of the loop. So the next bit will not run, which is the printing.
Finishing up the control flow, on every 10th iteration we print the value of the loop variable.
The SWITCH Statement
SWITCH is designed to map to the C
switch statement for better codegen and also to give us the opportunity to do better error checking.
SWITCH is statement like
IF not an expression like
CASE..WHEN..END so it combines with other statements. The general form looks like this:
- the switch-expression must be a not-null integral type (
integer not nullor
long integer not null)
WHENexpressions [expr1, expr2, etc.] are made from constant integer expressions (e.g.
WHENexpressions must be compatible with the switch expression (long constants cannot be used if the switch expression is an integer)
- the values in the
WHENclauses must be unique (after evaluation)
- within one of the interior statement lists the
LEAVEkeyword exits the
SWITCHprematurely, just like
LEAVEis not required before the next
- there are no fall-through semantics as you can find in
C, if fall-through ever comes to
SWITCHit will be explicit
- if the keyword
NOTHINGis used instead of
THENit means there is no code for that case, this is useful with
ALL VALUESsee below
ELSEclause is optional and works just like
C, covering any cases not otherwise explicitly listed
- If you add
- the expression be an from an enum type
WHENvalues must cover every value of the enum
- enum members that start with a leading
_are by convention considered pseudo values and do not need to be covered
- enum members that start with a leading
- there can be no extra
WHENvalues not in the enum
- there can be no
ELSEclause (it would defeat the point of listing
ALL VALUESwhich is to get an error if new values come along)
Some more complete examples:
THEN NOTHING allows the compiler to avoid emitting a useless
break in the C code. Hence that choice is better/clearer than
when brush then leave;
Note that the presence of
_count in the enum will not cause an error in the above because it starts with
C output for this statement will be a direct mapping to a
C switch statement.
The TRY, CATCH, and THROW Statements
This example illustrates catching an error from some DML, and recovering rather than letting the error cascade up. This is the common "upsert" pattern (insert or update)
Once again, let's go over this section by section:
Normally if the
insert statement fails, the procedure will exit with a failure result code. Here, instead,
we prepare to catch that error.
Now, having failed to insert, presumably because a row with the provided
id already exists, we try to update
that row instead. However that might also fail, so we wrap it in another try. If the update fails, then there is a final catch block:
Here we see a usage of the
@rc variable to observe the failed error code. In this case we simply print a diagnostic message and
then use the
throw keyword to rethrow the previous failure (exactly what is stored in
@rc). In general,
throw will create a
failure in the current block using the most recent failed result code from SQLite (
@rc) if it is an error, or else the general
SQLITE_ERROR result code if there is no such error. In this case the failure code for the
update statement will become the
result code of the current procedure.
This leaves only the closing markers:
If control flow reaches the normal end of the procedure it will return
Procedures as Functions: Motivation and Example
The calling convention for CQL stored procedures often (usually) requires that the procedure returns a result code from SQLite.
This makes it impossible to write a procedure that returns a result like a function, the result position is already used for
the error code. You can get around this problem by using
out arguments as your return codes. So for instance, this version
of the Fibonacci function is possible.
The above works, but the notation is very awkward.
CQL has a "procedures as functions" feature that tries to make this more pleasant by making it possible to use function call notation
on a procedure whose last argument is an
out variable. You simply call the procedure like it was a function and omit the last argument in the call.
A temporary variable is automatically created to hold the result and that temporary becomes the logical return of the function.
For semantic analysis, the result type of the function becomes the type of the
This form is allowed when:
- all but the last argument of the procedure was specified
- the formal parameter for that last argument was marked with
- the procedure does not return a result set using a
outstatement (more on these later)
If the procedure in question uses SQLite, or calls something that uses SQLite, then it might fail.
If that happens the result code will propagate just like it would have with a the usual
Any failures can be caught with
try/catch as usual.
This feature is really only syntatic sugar for the "awkward" form above, but it does allow for slightly better generated C code.