DOCUMENTATION

SQLite Database

DOCUMENTATION

XCX 4.4.1 Database

XCX 4.4.1 has native relational database support via the database: type and a set of built-in methods. Version 4.4.1 supports SQLite only.

Table of Contents

1. Connection Declaration (database:)

2. Table Column Attributes

3. Named Arguments

4. Result Capture (as)

5. API Reference

6. Type Mapping (XCX ↔ SQL)

7. Security Constraints

8. Full Example

1. Connection Declaration (database:)

$ xcx
_ ⛶ ×
1database: app {
2 engine = "sqlite",
3 path = "app.db"
4};

Fields

FieldTypeRequiredDefaultDescription
enginesyes-Database engine. Currently only "sqlite".
pathsyes-Path to the .db file (relative to project root).
timeoutino5000Operation timeout in ms.
readonlybnofalseRead-only mode.

Behavior

  • Connections are lazy - opened on first use.
  • On connection failure (e.g. missing permissions) halt.error is triggered automatically.
  • SQLite creates the database file if it does not exist (unless readonly = true).
  • Multiple simultaneous connections are allowed - each has its own name.
$ xcx
_ ⛶ ×
1database: main { engine = "sqlite", path = "main.db" };
2database: logs { engine = "sqlite", path = "logs.db" };
3
4yield main.sync(users);
5yield logs.sync(events);

I/O Model

All DB operations that touch disk or the driver are I/O operations:

ContextBehavior
Inside a fiberrequire yield
Outside a fiberblock synchronously

This applies to read (fetch, query, queryRaw), write (push, save, insert, exec), delete (remove, exec), and DDL (sync, drop) operations.

Exception - methods without yield. Methods that only operate on transaction state or metadata do not require yield in any context:

  • db.has(), db.close(), db.isOpen()
  • db.begin(), db.commit(), db.rollback()

begin(), commit() and rollback() do not touch data - they only manage transaction state in the driver.

2. Table Column Attributes

These attributes extend the table: block declaration and are used by the database module.

Attributes

AttributeDescriptionSQL Equivalent
@pkPrimary key. Can be combined with @auto.PRIMARY KEY
@uniqueValue must be unique within the column.UNIQUE
@optionalColumn may be NULL in SQL. XCX returns the type's default value on read.NULL
@default(v)Default column value in SQL.DEFAULT v
@fk(t.col)Foreign key pointing to column col of table t.REFERENCES t(col)

Example

$ xcx
_ ⛶ ×
1table: users {
2 columns = [
3 id :: i @auto @pk,
4 name :: s @unique,
5 age :: i,
6 phone :: s @optional,
7 role :: s @default("user")
8 ]
9 rows = [EMPTY]
10};
11
12table: posts {
13 columns = [
14 id :: i @auto @pk,
15 user_id :: i @fk(users.id),
16 title :: s,
17 body :: s @optional
18 ]
19 rows = [EMPTY]
20};

@optional Behavior

XCX has no null as a standalone type. @optional columns in SQL may store NULL, but when read into XCX the value is automatically replaced with the type's default:

XCX TypeValue when SQL NULL
i0
f0.0
s""
bfalse

Note: @optional only signals NULL acceptability on the SQL side. XCX does not preserve the distinction between NULL and the default value - that information is lost on read. If your application logic requires distinguishing these cases, store a flag in a separate column.

Column Omission Rules for add() and insert()

Column AttributeCan be omitted?What goes into SQL
@autoyes (always)auto-generated value
@default(v)yesvalue v from DEFAULT
@optionalyesNULL
@optional @default(v)yesvalue v from DEFAULT
no attributesnocompile error

@auto columns can never be provided explicitly - neither positionally nor via named argument. Attempting to do so is a compile error.

3. Named Arguments

Named arguments allow passing values to table.add(), table.insert(), and db.insert() by column name rather than by position. They are an optional, additive feature - all existing positional calls remain fully valid.

Named arguments work only for table insert operations. They do not apply to user-defined functions, fibers, or other built-in methods. Column names are statically known to the compiler from the table: schema.

Syntax

$ xcx
_ ⛶ ×
1--- Positional (backward compatible)
2users.add("Alice", 25, "", "user");
3
4--- Named
5users.add(name = "Alice", age = 25, phone = "", role = "user");
6
7--- With db.insert()
8yield app.insert(users, name = "Alice", age = 25) as saved;

Namespace separation. The left side of = is always the column name. The right side is an expression from the local scope. These are two independent namespaces - no conflict:

$ xcx
_ ⛶ ×
1s: name = "Alice";
2users.add(name = name, age = 25);
3--- left "name" = column users.name
4--- right "name" = local variable

Evaluation order. Right-hand expressions are evaluated left to right in the order written:

$ xcx
_ ⛶ ×
1users.add(name = get_name(), age = get_age());
2--- get_name() is called before get_age()

Mixing Positional and Named

Positional and named arguments can be mixed. Positional arguments must precede named arguments.

$ xcx
_ ⛶ ×
1--- OK - positional before named
2users.add("Alice", age = 25, role = "admin");
3
4--- COMPILE ERROR - named before positional
5users.add(name = "Alice", 25, "admin");

Assignment rule. Positional arguments are assigned to columns left-to-right in declaration order, skipping @auto, until they run out. Named arguments fill the remaining columns by name.

$ xcx
_ ⛶ ×
1--- "Alice" → name, rest named
2users.add("Alice", age = 25, role = "admin");
3--- phone omitted → NULL (@optional)
4
5--- "Alice", 25 → name, age; role named, phone omitted
6users.add("Alice", 25, role = "mod");

Positional arguments assign sequentially without skipping. Skipping a middle column is only possible via a named argument or by omitting it entirely (if optional):

$ xcx
_ ⛶ ×
1--- COMPILE ERROR - compiler assigns: name="Alice", age="" - type mismatch
2--- there is no way to "skip" age positionally
3users.add("Alice", "", "user");

Providing the same column both positionally and by name is a compile error:

$ xcx
_ ⛶ ×
1--- COMPILE ERROR - name provided twice
2users.add("Alice", name = "Bob", age = 25);

Compile-Time Rules

RuleBehavior
@auto column providedcompile error
Unknown column namecompile error
Duplicate column namecompile error
Required column omittedcompile error
Named arg before positional argcompile error

Completeness table:

Column AttributeCan be omitted?Value when omitted
@autoyes (always)auto-generated value
@default(v)yesvalue v from DEFAULT
@optionalyesNULL (→ XCX default on read)
@optional @default(v)yesvalue v from DEFAULT
no attributesnocompile error
$ xcx
_ ⛶ ×
1--- OK - role has @default("user"), phone is @optional
2users.add(name = "Alice", age = 25);
3
4--- COMPILE ERROR - age is required
5users.add(name = "Alice");

4. Result Capture (as)

as is a general XCX mechanism for capturing the result of a block operation:

$ xcx
_ ⛶ ×
1--- HTTP request
2net.request { method = "GET", url = "..." } as resp;
3
4--- DB operations
5yield app.insert(users, name = "Alice", age = 25) as saved;
6yield app.exec("DELETE FROM users WHERE id = ?", [5]) as res;

Write/Delete Result Object

Operations db.insert(), db.save(), db.push(), and db.exec() return an object with two fields:

FieldTypeDescription
affectediNumber of rows changed / inserted by the operation
insertIdiID of the last inserted record (@auto @pk); 0 if no insert
$ xcx
_ ⛶ ×
1yield app.insert(users, name = "Alice", age = 25) as saved;
2>! "New user ID: " + s(saved.insertId);
3
4yield app.exec("DELETE FROM users WHERE id = ?", [5]) as res;
5>! "Deleted: " + s(res.affected);

Using as is optional - if the result is not needed it can be omitted:

$ xcx
_ ⛶ ×
1yield app.save(users);

affected and insertId Semantics

OperationaffectedinsertId
insert() - single row1ID of inserted row
push() - multiple rowsnumber of inserted rowsID of last inserted row
save() - upsert (insert)1ID of inserted row
save() - upsert (update)10
exec(DELETE ...)number of deleted rows0
exec(UPDATE ...)number of updated rows0

On operation failure (constraint violation, no connection, readonly) halt.error is triggered and execution does not reach the code using the as result. The result object is always valid if the code can see it.

5. API Reference

5.1 DDL

$ xcx
_ ⛶ ×
1--- Creates the SQL table if it does not exist (based on XCX table schema)
2yield app.sync(users);
3
4--- Drops the SQL table
5yield app.drop(users);
6
7--- Checks if table exists in the database (no data touched - no yield)
8b: exists = app.has(users);

5.2 Write Operations

$ xcx
_ ⛶ ×
1--- INSERT all rows from a local XCX table into SQL
2yield app.push(users);
3
4--- INSERT OR UPDATE (upsert) - requires @pk; no @pk = compile error
5--- Implementation: INSERT INTO ... ON CONFLICT(pk) DO UPDATE SET ...
6yield app.save(users);
7
8--- INSERT a single row - positional or named
9yield app.insert(users, "Alice", 25) as saved;
10yield app.insert(users, name = "Alice", age = 25) as saved;

On error (violation of @unique, @fk, no connection) the operation triggers halt.error.

5.3 Read Operations

$ xcx
_ ⛶ ×
1--- SELECT * - returns a native XCX table
2table: all_users = yield app.fetch(users);
3
4--- SELECT with XCX-style filter (compiles to SQL WHERE)
5table: adults = yield app.fetch(users).where(age > 18);
6
7--- SELECT with raw SQL - requires schema hint as first argument
8table: result = yield app.query(users, "SELECT * FROM users WHERE age > ?", [18]);
9
10--- Raw SQL without schema - always returns a JSON array
11json: raw = yield app.queryRaw("SELECT COUNT(*) as n FROM users");
12--- raw = [{"n": 42}]
13
14--- Get the first row
15json: row = yield app.queryRaw("SELECT COUNT(*) as n FROM users").first();
16--- row = {"n": 42}
17--- halt.error if result is empty

#### .first() on queryRaw Result

.first() returns the first element of the JSON array as a single json object. It triggers halt.error if the query returned no rows. Typical use is for aggregate queries (COUNT, SUM, MAX) and lookups that return at most one row.

$ xcx
_ ⛶ ×
1json: row = yield app.queryRaw("SELECT COUNT(*) as n FROM users").first();
2i: total; row.bind("n", total);
3>! "Users: " + s(total);

If you are not certain the result will be non-empty, check the size before using .first():

$ xcx
_ ⛶ ×
1json: results = yield app.queryRaw("SELECT * FROM users WHERE age > ?", [100]);
2if (results.size() > 0) then;
3 json: row = results.get(0);
4end;

#### Supported Operators in .where()

The .where() filter compiles to a SQL WHERE clause. A limited subset of XCX expressions is supported:

CategorySupported
Comparisons==, !=, <, <=, >, >=
LogicalAND, OR, NOT
Valuesliterals, local variables
Left operandscolumn names only

Function calls, string methods, and nested expressions are not supported in .where() with db.fetch(). Using an unsupported expression is a compile error.

$ xcx
_ ⛶ ×
1--- OK
2table: r = yield app.fetch(users).where(age > 18 AND role == "admin");
3
4--- COMPILE ERROR - string method not allowed in where() with fetch
5table: r = yield app.fetch(users).where(name.lower() == "alice");

Chaining .where() joins conditions with AND:

$ xcx
_ ⛶ ×
1table: r = yield app.fetch(users)
2 .where(age > 18)
3 .where(role == "admin");
4--- SQL equivalent: WHERE age > 18 AND role = 'admin'

#### db.fetch() and Local rows

db.fetch() reads only data from the SQL database - local rows declared in the table: block are ignored. The local table definition serves only as a schema hint.

$ xcx
_ ⛶ ×
1table: users {
2 columns = [ id :: i @auto @pk, name :: s ]
3 rows = [ ("Alice") ] --- these rows do NOT go into fetch
4};
5
6yield app.sync(users);
7
8--- Returns only rows stored in app.db
9table: all = yield app.fetch(users);

To insert local rows into the database, use db.push(users) before db.fetch().

5.4 Delete Operations

$ xcx
_ ⛶ ×
1--- DELETE with filter - .where() is required (D401)
2yield app.remove(users).where(age < 18);
3
4--- Delete all rows
5yield app.truncate(users);
6
7--- Raw DELETE with parameters
8yield app.exec("DELETE FROM users WHERE id = ?", [5]) as res;

db.remove() without .where() is a compile error (D401). To delete all rows, use db.truncate(users) explicitly.

5.5 Transactions

$ xcx
_ ⛶ ×
1app.begin();
2yield app.save(users);
3yield app.save(posts);
4app.commit();

If any operation inside the transaction triggers halt.error, the transaction is automatically rolled back before the frame is aborted. The connection returns to the state before begin().

rollback() is for explicitly cancelling a transaction in conditional logic before an error occurs:

$ xcx
_ ⛶ ×
1app.begin();
2b: valid = validate_users(users);
3if (NOT valid) then;
4 app.rollback();
5end;
6yield app.save(users);
7app.commit();

5.6 Other

$ xcx
_ ⛶ ×
1app.close();
2b: alive = app.isOpen();

6. Type Mapping (XCX ↔ SQL)

XCX TypeSQL Type (SQLite)
iINTEGER
fREAL
sTEXT
bINTEGER (0 / 1)
dateTEXT (YYYY-MM-DD HH:mm:ss)

7. Security Constraints

ConstraintBehavior
Path containing ..halt.fatal - path traversal
Absolute pathhalt.fatal
Raw SQL with unvalidated inputDeveloper is responsible for sanitization - use ? params
readonly = true + DML operationhalt.error
save() on table without @pkcompile error
remove() without .where()compile error (D401)

Always use prepared statements with ?:

$ xcx
_ ⛶ ×
1--- WRONG
2app.exec("DELETE FROM users WHERE name = '" + name + "'");
3
4--- CORRECT
5app.exec("DELETE FROM users WHERE name = ?", [name]);

8. Full Example

$ xcx
_ ⛶ ×
1database: app {
2 engine = "sqlite",
3 path = "data.db"
4};
5
6table: users {
7 columns = [
8 id :: i @auto @pk,
9 name :: s @unique,
10 email :: s @unique,
11 age :: i,
12 phone :: s @optional
13 ]
14 rows = [EMPTY]
15};
16
17yield app.sync(users);
18
19fiber handle_get_users(json: req -> json) {
20 table: all = yield app.fetch(users);
21 yield net.respond(200, all.toJson());
22};
23
24fiber handle_create_user(json: req -> json) {
25 json: body;
26 req.bind("body", body);
27
28 s: name; body.bind("name", name);
29 s: email; body.bind("email", email);
30 i: age; body.bind("age", age);
31
32 --- Named arguments - readable and order-independent
33 yield app.insert(users, name = name, email = email, age = age) as saved;
34
35 json: resp <<< {"ok": true, "id": 0} >>>;
36 resp.set("id", saved.insertId);
37 yield net.respond(201, resp);
38};
39
40serve: api {
41 port = 8080,
42 routes = [
43 "GET /users" :: handle_get_users,
44 "POST /users" :: handle_create_user
45 ]
46};