Skip to Content
Getting StartedSQL Queries

Querying & DML

KalamClient.query(...) executes SQL over HTTP. For Flutter lists, prefer Kalam Sync watch() so the UI reads the local cache.

DART
final response = await client.query('SELECT 1');print(response.success);

Parameterized queries

Use $1, $2, … placeholders in SQL and pass params as a Dart list.

DART
final res = await client.query(  r'SELECT * FROM app.messages WHERE conversation_id = $1 AND is_deleted = $2',  params: ['conv_42', false],);

params is encoded to JSON and sent to the server, so values should be JSON-compatible (strings, numbers, booleans, null, lists, and maps).

If you need richer server-side typing (timestamps, decimals, JSON columns), keep your SQL explicit (e.g. CAST(...)) and validate the returned values.

Namespaces

KalamDB supports per-tenant namespaces. The SDK exposes this as an optional namespace argument.

DART
final res = await client.query(  'SELECT * FROM messages ORDER BY created_at DESC LIMIT 10',  namespace: 'alice',);

This is most useful when your SQL references unqualified tables (e.g. messages instead of alice.messages).

For background, see: SQL Reference: Namespaces.

Reading results

A QueryResponse can contain multiple QueryResult values (one per statement). The most common pattern is to read the first one.

Primary row access (QueryResult.rows)

DART
if (!res.success) {  print(res.error);  return;} final first = res.results.first;print(first.columns.map((c) => c.name).toList()); for (final row in first.rows) {  final id = row['id']?.asInt();  final name = row['name']?.asString();  final active = row['active']?.asBool();  print('id=$id name=$name active=$active');}

rows is List<Map<String, KalamCellValue>> (named columns), not List<List<dynamic>>.

Multiple statements

DART
final res = await client.query('SELECT 1; SELECT 2;'); for (final (i, r) in res.results.indexed) {  print('statement[$i] columns=${r.columns.length} rows=${r.rowCount}');}

DML: INSERT / UPDATE / DELETE

All SQL goes through query(...):

DART
await client.query(  r"INSERT INTO app.messages (conversation_id, role, content) VALUES ($1, $2, $3)",  params: [42, 'user', 'hello'],); await client.query(  r"UPDATE app.messages SET content = $1 WHERE message_id = $2",  params: ['edited', 'msg_123'],); await client.query(  r"DELETE FROM app.messages WHERE message_id = $1",  params: ['msg_123'],);

FILE refs: insert and read

Insert a FILE reference value

If you already have file metadata (for example from a previous read), insert it as JSON:

DART
final ref = KalamFileRef(  id: '1234567890123456789',  sub: 'f0001',  name: 'avatar.png',  size: 20480,  mime: 'image/png',  sha256: 'abc123...',); await client.query(  r'INSERT INTO app.users (id, avatar) VALUES ($1, $2)',  params: ['user_1', ref.toMap()],);

Read a FILE reference

DART
final res = await client.query('SELECT id, avatar FROM app.users WHERE id = $1', params: ['user_1']);final row = res.rows.first; final fileRef = row['avatar']?.asFile();if (fileRef != null) {  final url = fileRef.getDownloadUrl('http://localhost:18080', 'app', 'users');  print('${fileRef.name} ${fileRef.formatSize()} $url');}

getDownloadUrl(...) delegates to the shared Rust link-common FileRef model, the same source used by the TypeScript SDK. The returned path is:

TEXT
/v1/files/{namespace}/{table}/{sub}/{stored_name}

Fetch the URL with the authenticated user’s bearer token. For user tables, SDK helpers generate an owner-scoped URL: regular users can download only their own files. Service clients can upload/write through authorized EXECUTE AS USER flows, but direct cross-user FILE byte downloads require a dba or system token and an authorized user_id query parameter.

Error handling

When success is false, the server returns an error code/message:

DART
final res = await client.query('SELECT * FROM table_that_does_not_exist');if (!res.success) {  throw StateError('${res.error}');}

For user-facing apps, prefer mapping error.code to a stable UI message and logging error.details for debugging.

Procedures

Typed procedure clients come from kalam schema gen --languages dart, not from a generic method on KalamClient. Generated KalamFunctions wraps query('CALL ...') the same way TypeScript createKalam(client) does:

DART
final db = KalamFunctions(client);final user = await db.chat.createMessage(  ChatCreateMessageRequest(userId: 'u1', body: 'hello'),);

JSON parameters and returns are Map<String, Object?>. Named SQL types reuse the generated row classes. REST POST /v1/functions/{namespace}/{procedure} remains a server HTTP path for non-SDK callers.

Next

Generated typed remote queries

After kalam schema gen --languages dart, reuse the generated table codecs and primary key directly:

DART
final inventory = KalamTables.inventory.bind(client);final query = inventory.query()    .filter(InventoryColumns.sku.eq('sku-1'))    .limit(20);final rows = await query.get();final updates = query.live();

InventoryColumns.sku is KalamColumn<String>, so passing a number fails Dart analysis. Existing .where((c) => c.col('sku').eq(value)) remains available for dynamic queries. eq(null) and neq(null) produce IS NULL and IS NOT NULL when the column type permits null. Values use bound parameters.

orderBy on a remote live query sorts the matching rows locally before applying limit. The limit controls the displayed rows, not retained memory. Use a narrow filter for large datasets. Scalar string ordering is local and does not promise server collation equivalence. Use kalam_sync for durable local projections and offline actions; bind(client) uses remote transport and does not add persistence.

Generated Dart table specs require one declared primary key. Keyless tables still get row codecs, but no sync spec or guessed key. Composite primary keys are rejected by schema compilation to match current server support. Array codecs preserve null arrays and encode/decode each supported scalar, enum, composite, and timestamp element; null array elements are not represented by the current contract.