Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra

Embedding columns

A vector column the database keeps in step with a text column. Write the text; the embedding follows in the background, through the durable queue SQL Anywhere already ships.

Most applications that search by meaning keep two things in step by hand: the text a user edits and the embedding computed from it. Embedding inline makes every write wait on a model. Embedding later means building a job system, and it opens three quiet failure modes: a vector computed from text that changed while the model ran, an edit that never gets re-embedded, and a column that mixes vectors from two models after an upgrade. An embedding column handles all three.

Runnable from the docs of sqlanywhere::embedding_column. Verified: tests/embedding_column.rs.

How

use sqlanywhere::{embedding_column::EmbeddingColumn, params};

// docs(id INTEGER PRIMARY KEY, body TEXT, emb FLOAT32(384)), indexed with
// sqlanywhere_vector_idx as usual.
let model = MyModel::load()?;               // any Embedder
let column = EmbeddingColumn::new("docs", "body", "emb");
column.install(&conn, &model).await?;       // enqueues the rows that exist

// Application code just writes text.
conn.execute("UPDATE docs SET body = ? WHERE id = ?", params![text, id]).await?;

// A worker, anywhere that can open the database, on a loop:
let report = column.process(&conn, &model, 100).await?;
Method Does
install(conn, embedder) Create the queue tables if needed, record the column and its model, add the triggers, enqueue every existing row with text.
process(conn, embedder, max_jobs) Claim and embed up to max_jobs rows. Safe to run from several workers at once.
status(conn) The model, the backlog (pending, ready, oldest_seconds), and failed jobs. is_current() when nothing is pending.
reembed(conn, embedder) Switch to another model and enqueue every row.
uninstall(conn) Remove the triggers and pending jobs. Vectors already written stay.

What it guarantees

Writes never wait for a model. Triggers on the text column enqueue a job; the write commits at the speed of an insert.

The queue is the queue contract. Jobs go into askr_jobs on the queue sqlanywhere.embed:<table>.<column>, claimed, acked, released and dead-lettered through sqlanywhere::contracts::queue, the API compiled from the queue contract, so the statements are the contract's own and cannot drift from it. That makes the backlog durable, replicated, and visible to whatever already watches queue depth, and a crashed worker's job comes back when its reservation lapses.

A vector always matches the text it sits next to. A worker embeds without holding a transaction, so a slow model never blocks writers, and then writes the vector only if the row still has the text it embedded. If the text changed in the meantime, the stale vector is dropped; the edit queued its own job, which embeds the new text.

Edits collapse. Ten edits before a worker gets to a row are one job, which embeds the latest text. Writing the same text again, or changing another column, queues nothing. Setting the text to NULL clears the vector.

One model per column. The column records Embedder::model_id(), and a worker whose embedder reports another model, or another dimensionality, is refused instead of writing. Implement model_id on your embedder with a name that changes when the model does; the default only names the dimensionality. reembed is the explicit way to change models: it records the new one and enqueues every row, and status().is_current() says when the migration is done. Until then the column holds vectors from both models.

Failures back off, then stop. An embedder that returns the wrong number of dimensions, or text that is not text, puts the job back with the contract's reference backoff (1 s doubling to an hour). On its last attempt it moves to askr_failed_jobs with the reason, and status().failed counts it.

Limits

  • The table needs a rowid; WITHOUT ROWID tables are refused.
  • Embedder::embed is synchronous, so a hosted model blocks the worker task while it runs. Run workers on their own task or thread.
  • Embedding happens in the application process that calls process. A sqld-side worker, and an embed() SQL function, are not built yet.