This chapter builds a small notes application on the
sqlite library — the first rung of the libraries shelf.
You will copy three files into a fresh project, write a program against them,
and end with a binary that owns a real database. Along the way you meet the
two patterns the rung establishes and the third declared subprogram kind,
the callback.
Everything here runs in the
starter container; the library
lives in libraries/sqlite, and libsqlite3-dev is already installed.
The three files you copy
A shelf library is not a package you depend on — it is source you own:
sqlite.mica— the library unit: the whole surface, teaching-grade comments included, yours to read and change.sqlite.external— the contract naming the sqlite3 symbols the unit calls, with their exact C shapes.sqlite-shim.c— four small C functions for the entry points whose natural C shape has no Mica spelling.
mkdir notes && cd notes
cp ../mica-container/libraries/sqlite/{sqlite.mica,sqlite.external,sqlite-shim.c,mica.project} .The mica.project is for your editor: the language server reads it to resolve
the contracts, so everything analyzes clean while you work. Point its
sources at your own program as you go.
Opening and migrating
{
notes - a small notes application on the sqlite shelf library.
}
program notes;
imp
Open : sqlite;
Close : sqlite;
Exec : sqlite;
Query : sqlite;
BindInt64 : sqlite;
BindText : sqlite;
Rows : sqlite;
ColumnInt64 : sqlite;
ColumnText : sqlite;
LastInsertRowId : sqlite;
SqliteError : sqlite;
Sqlite : sqlite3;
WriteLn : std;
var
db : pointer Sqlite;
e : SqliteError;
begin
db := Open("notes.db") on fail e do WriteLn("cannot open notes.db");
Exec(db, "create table if not exists notes(title text, body text, stars integer)")
on fail e do WriteLn("migration failed");
Close(db);
end.Two things carry the whole chapter. First, failure travels the failure
channel: Open and Exec fail with the library’s own SqliteError domain,
and you consume trouble with the language’s forms — on fail e do at the
statement here, on fail leave where an enclosing channel should carry it on.
No return codes to remember, and forgetting a handler is a compile error,
never a silent nil.
Second, the handle is a borrow. sqlite3 owns the database cell; Close
returns it. The library’s borrowed pointer spelling tells the ownership
analysis exactly that, so your program owes no dispose for what it never
owned — and the analysis holds you to the rest.
Writing notes, with binds
Never build SQL out of user text — bind it:
procedure AddNote(handle : pointer Sqlite, title : string, body : string, stars : int64) fails SqliteError;
var
q : int64;
row : int64;
begin
q := Query(handle, "insert into notes(title, body, stars) values (?, ?, ?)") on fail leave;
BindText(q, 1, title) on fail leave;
BindText(q, 2, body) on fail leave;
BindInt64(q, 3, stars) on fail leave;
for row in Rows(q) do
begin
end
on fail leave;
end;Every fallible call carries its consumption form — the language admits no
bare one — and inside a fails procedure the honest form is on fail leave:
the failure forwards on this procedure’s own channel, and the caller decides.
Query compiles the statement and answers a plain ordinal — the query — that
every later call is keyed on. The binds fill the placeholders, positions
counted from one as sqlite counts them; the text crosses the boundary in
utf-8, whatever encoding your program targets. Draining Rows runs the
statement; an insert answers no rows, so the loop body is empty and the
generator’s own cleanup returns the statement. After the call,
LastInsertRowId(handle) answers the fresh note’s rowid.
Reading notes: the pull road
This is the daily idiom the library exists for:
procedure ListStarred(handle : pointer Sqlite, minimum : int64) fails SqliteError;
var
q : int64;
row : int64;
begin
q := Query(handle, "select title, stars from notes where stars >= ? order by stars desc") on fail leave;
BindInt64(q, 1, minimum) on fail leave;
for row in Rows(q) do
begin
WriteLn("%ls (%lld)", ColumnText(row, 0), ColumnInt64(row, 1));
end
on fail leave;
end;The imperative order stands on the page in the order it runs: prepare, bind,
pull, read. ColumnText answers a real Mica string — the library decodes
sqlite’s utf-8 bytes for you — and ColumnInt64 a real integer.
Rows deserves a pause, because it shows how the shelf is built. It is an
exported generator: it crosses the library’s contract as verbatim source
and compiles inside your program, which is why the loop reads like any
local for. Its body speaks only through the library’s exported surface —
StepQuery and FinalizeQuery, which are also yours whenever you want the
loop by hand — and a defer returns the statement however the loop ends. An
early leave from the loop leaks nothing, and neither does trouble: a step
refusal — a locked database, a table dropped under the open statement — rides
the stream’s own failure channel to your loop’s on fail form, the statement
returned first, so it can never masquerade as a clean end of rows.
That is the state-slot pattern: the statement handles live in the
library’s own table, Query answers a plain ordinal, and the generator steps
the ordinal. The handle never crosses into your frame, and the generator’s
input stays the plain value the language’s copy protocol asks for.
The callback road
sqlite also speaks its native shape: sqlite3_exec fires a function pointer
once per row. Mica has no function pointers — a routine is called with its
argument list, never read by name — and the boundary answer is the third
declared subprogram kind:
type
Tally = record
rows : int64;
end;
callback CountRow(context : pointer Tally, columns : int32, values : pointer int64, names : pointer int64) : int32;
begin
context.rows := context.rows + 1;
CountRow := 0;
end;
rc := SqliteExecRaw(db, "select title from notes", CountRow, address tally, address sink);A callback is never called from Mica and never a value: sqlite performs
every firing, while the exec call itself runs. Its whole environment is the
context record whose address you pass — everything the callback can touch is
on that one line. Use the pull road daily; the callback road is there when a
C API’s own shape is the honest one.
One Makefile, C and Mica together
The build is two steps, and the library’s Makefile is the whole story:
gcc -O2 -c -o build/sqlite-shim.o sqlite-shim.c
mica --compile --link --output-modifier archive --emit-contract \
--external-contract sqlite.external --external static \
--source sqlite.mica --build build/sqlite
mica --compile --link \
--external-contract build/sqlite/mica.external,sqlite.external --external static \
--source notes.mica --build buildThe C shim compiles like any C file. The library compiles once into an archive and emits its own contract — and that emitted contract carries the archive and the C link flags transitively, so your program’s link line composes itself from the two contracts you name. C and Mica bake together in one Makefile, each side speaking its own language, the contracts telling the truth between them.
Where to go next
The libraries shelf grows rung by rung, and every rung ships this same shape: a library you copy and own, its contract, a shim only where the C shape demands one, and a demo as first caller. The contracts reference states every field the contract files use, callback slots included; callbacks at the boundary is the kind’s own chapter.