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 build

The 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.