Your language
fenecdb speaks HTTP and JSON, so every language reaches it with the HTTP client it already has. No driver to install.
Every example on this page is a program that runs against a real server
on each change to fenecdb (make languages-test). Python,
JavaScript, Go and .NET have official clients, each its standard library
alone; in the other languages these few lines are the whole client. Swift,
Kotlin and Dart embed the database in the app, a file on the device
(Mobile and native apps), and reach a server with
Fenec.connect; their tabs run the same flow in their own
tests. Each tab ends with a full example: one app, Notes, in every
language, a folder of a few files to clone and run
(examples),
and beside them a whole shop in Next.js
(shop),
a double-entry payments ledger on Node
(ledger)
and courier dispatch across tenants
(dispatch).
Start a server
fenec-server --file app.fenec --http 127.0.0.1:8080 --http-token secretThen pick your language. Each example makes a collection of a text and
a number, each indexed, writes two rows, reads them back by a filter and an
order, searches the words with a typo in them and handles a refusal. Each
sends one FenecQL statement a request to POST /query, its
values as $1, $2, ... in params, and
the token as Authorization: Bearer.
Connect and search
pip install fenecdb # the standard library alonefrom fenecdb import Client, FenecError
db = Client("http://127.0.0.1:8080", token="secret")
db.query("create collection if not exists docs "
"(title text @text, year int @sorted)")
db.query("put docs {title: $1, year: $2}", ["Dunes", 2021])
db.query("put docs {title: $1, year: $2}", ["Night at the oasis", 2024])
rows = db.query("get docs select title, year where year >= $1 order year desc limit $2", [2020, 5])
# [{"title": "Night at the oasis", "year": 2024}, {"title": "Dunes", "year": 2021}]
hits = db.query("get docs select title match title $1 fuzzy", ["oasys"])
# [{"title": "Night at the oasis", "_score": 0.62...}]
try:
db.query("get nowhere")
except FenecError as e:
print(e.status, e) # 404 collection `nowhere`Or let the query builder write the statement,
every value a parameter, every name checked: the read's text to the byte,
and the search as match title $1 fuzzy 2, two edits at most,
which is what a plain fuzzy allows a word anyway, so the rows
are the same.
docs = db.collection("docs")
docs.insert({"title": "Night at the oasis", "year": 2024})
rows = docs.select("title", "year").where("year", ">=", 2020).order("year", "desc").limit(5).rows()
hits = docs.select("title").match("title", "oasys", fuzzy=True).rows()
n = docs.where("title", "~", "Dunes").count()batch([(text, params), ...]) runs statements as one block,
all or none -- a FenecError that stops it says which
(e.at, from 0); idempotency_key= on
query and batch, or
with_idempotency_key(key) for every write of a copy, makes a
write once however often it is sent (db.replayed), and
db.seq is the change the last write left the database at.
AsyncClient does the same in an event loop, the endpoints of
its collection(...) awaited; LangChain and LlamaIndex have
stores of their own (Integrations).
Full example: Notes, a CLI over the server with the builder, match, fuse and the change stream.
async function query(q: string, params: unknown[] = []) {
const res = await fetch('http://127.0.0.1:8080/query', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: 'Bearer secret' },
body: JSON.stringify({ query: q, params }),
});
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error), { status: res.status });
return body;
}
await query('create collection if not exists docs (title text @text, year int @sorted)');
await query('put docs {title: $1, year: $2}', ['Dunes', 2021]);
await query('put docs {title: $1, year: $2}', ['Night at the oasis', 2024]);
const rows = await query('get docs select title, year where year >= $1 order year desc limit $2', [2020, 5]);
const hits = await query('get docs select title match title $1 fuzzy', ['oasys']);Or npm install @fenecdb/web: its connect sends
the query builder's statements to a server, the same
code that runs in the page.
import { connect } from '@fenecdb/web';
import type { FenecSchema } from './fenec-schema.js'; // fenec types app.fenec
const db = connect<FenecSchema>('http://127.0.0.1:8080', { token: 'secret' });
await db.from('docs').insert({ title: 'Night at the oasis', year: 2024 });
const rows = await db.from('docs').select('title', 'year').where('year', '>=', 2020).order('year', 'desc').limit(5).rows();
// rows: { title: string | null; year: number | null }[]
const hits = await db.from('docs').select('title').match('title', 'oasys', { fuzzy: true }).rows();
// hits: { title: string | null; _score: number }[]The same code runs as plain JavaScript, without the types. In the browser there is no server at all: the same database runs in the page (JavaScript and TypeScript).
Full example: Notes from Node through @fenecdb/web/client, its live queries run on the server; in the page, web-local and react.
go get github.com/fenecdb/fenecdb/integrations/go # the standard library aloneimport fenecdb "github.com/fenecdb/fenecdb/integrations/go"
db := fenecdb.New("http://127.0.0.1:8080", fenecdb.WithToken("secret"))
db.Exec(ctx, "create collection if not exists docs (title text @text, year int @sorted)")
db.Exec(ctx, "put docs {title: $1, year: $2}", "Dunes", 2021)
db.Exec(ctx, "put docs {title: $1, year: $2}", "Night at the oasis", 2024)
type Doc struct {
Title string `json:"title"`
Year int `json:"year"`
}
rows, err := fenecdb.QueryAs[Doc](ctx, db, "get docs select title, year where year >= $1 order year desc limit $2", 2020, 5)
hits, err := db.Query(ctx, "get docs select title match title $1 fuzzy", "oasys")
var e *fenecdb.Error
if _, err := db.Query(ctx, "get nowhere"); errors.As(err, &e) {
fmt.Println(e.Status, e.Code, e.Message) // 404 not_found collection `nowhere`
}Or let the query builder write the statement, immutable, each step a copy:
docs := db.From("docs")
docs.Insert(ctx, fenecdb.D("title", "Night at the oasis", "year", 2024))
query := docs.Select("title", "year").Where("year", ">=", 2020).Order("year", "desc").Limit(5)
rows, err := fenecdb.RowsAs[Doc](ctx, query)
hits, err := docs.Select("title").Match("title", "oasys", fenecdb.Fuzzy(2)).Rows(ctx)
n, err := docs.Where("title", "~", "Dunes").Count(ctx)Query gives rows as []map[string]any;
Batch, IdempotencyKey, After for a
replica, Subscribe, Changes and
WithTenant cover the rest of the HTTP
endpoint and its reference (integrations/go);
a refusal is an *fenecdb.Error, and a batch's says which
statement stopped it (At, from 0).
A []float32 goes out as the decimals that read back as each
f32, so a vector round-trips to the bit.
Without the SDK, net/http and encoding/json:
// query runs one FenecQL statement and decodes its answer into out.
func query(out any, q string, params ...any) error {
body, _ := json.Marshal(map[string]any{"query": q, "params": params})
req, _ := http.NewRequest("POST", "http://127.0.0.1:8080/query", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer secret")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
e := &FenecError{Status: res.StatusCode}
json.NewDecoder(res.Body).Decode(e)
return e
}
if out == nil {
return nil
}
return json.NewDecoder(res.Body).Decode(out)
}
query(nil, "create collection if not exists docs (title text @text, year int @sorted)")
query(nil, "put docs {title: $1, year: $2}", "Dunes", 2021)
query(nil, "put docs {title: $1, year: $2}", "Night at the oasis", 2024)
var rows []struct {
Title string `json:"title"`
Year int `json:"year"`
}
err := query(&rows, "get docs select title, year where year >= $1 order year desc limit $2", 2020, 5)FenecError is a struct of the status and the body's
error.
Full example: Notes, a CLI over the server with the builder, typed rows and a subscription.
cargo add ureq@2 --features json
cargo add serde_jsonuse serde_json::{json, Value};
fn query(q: &str, params: Value) -> Result<Value, ureq::Error> {
Ok(ureq::post("http://127.0.0.1:8080/query")
.set("Authorization", "Bearer secret")
.send_json(json!({"query": q, "params": params}))?
.into_json()?)
}
query("create collection if not exists docs (title text @text, year int @sorted)", json!([]))?;
query("put docs {title: $1, year: $2}", json!(["Dunes", 2021]))?;
query("put docs {title: $1, year: $2}", json!(["Night at the oasis", 2024]))?;
let rows = query("get docs select title, year where year >= $1 order year desc limit $2", json!([2020, 5]))?;
let hits = query("get docs select title match title $1 fuzzy", json!(["oasys"]))?;
match query("get nowhere", json!([])) {
Err(ureq::Error::Status(404, res)) => { /* res.into_json()? holds {"error": ...} */ }
other => { /* ... */ }
}Or embed the engine in your process, with no server (Embedded Rust).
Full example: Notes with fenec-core embedded, a file and no server.
dotnet add package FenecDb # HttpClient and System.Text.Json aloneusing FenecDb;
using var db = new FenecClient("http://127.0.0.1:8080", new() { Token = "secret" });
await db.ExecAsync("create collection if not exists docs (title text @text, year int @sorted)");
await db.ExecAsync("put docs {title: $1, year: $2}", ["Dunes", 2021]);
await db.ExecAsync("put docs {title: $1, year: $2}", ["Night at the oasis", 2024]);
var rows = await db.QueryAsync<Doc>("get docs select title, year where year >= $1 order year desc limit $2", [2020, 5]);
var hits = await db.QueryAsync<Hit>("get docs select title match title $1 fuzzy", ["oasys"]);
try { await db.QueryAsync("get nowhere"); }
catch (FenecException e) { Console.WriteLine($"{e.Status} {e.Code} {e.Message}"); } // 404 not_found collection `nowhere`
record Doc(string Title, int Year);
record Hit(string Title, [property: JsonPropertyName("_score")] float Score);Or let the query builder write the statement, immutable, each call a new query:
var docs = db.From("docs");
await docs.InsertAsync(new { title = "Night at the oasis", year = 2024 });
var query = docs.Select("title", "year").Where("year", ">=", 2020).Order("year", "desc").Limit(5);
var rows = await query.RowsAsync<Doc>();
var hits = await docs.Select("title").Match("title", "oasys", fuzzy: 2).RowsAsync<Hit>();
var n = await docs.Where("title", "~", "Dunes").CountAsync();.NET 8 and newer, async throughout with a CancellationToken
on every call. QueryAsync<T> maps rows to records or
classes by property name, any case; BatchAsync,
WithIdempotencyKey, After for a replica,
SubscribeAsync as an IAsyncEnumerable,
ChangesAsync and a Tenant option cover the rest
of the HTTP endpoint and its
reference
(integrations/dotnet);
a refusal is a FenecException, and a batch's says which
statement stopped it (At, from 0).
A float[] or ReadOnlyMemory<float> goes out
as the decimals that read back as each float.
Without the SDK, HttpClient and System.Text.Json:
var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8080") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "secret");
async Task<JsonElement> Query(string query, params object[] args)
{
var json = JsonSerializer.Serialize(new { query, @params = args });
var res = await http.PostAsync("/query", new StringContent(json, Encoding.UTF8, "application/json"));
var body = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new HttpRequestException(body.GetProperty("error").GetString(), null, res.StatusCode);
return body;
}
await Query("create collection if not exists docs (title text @text, year int @sorted)");
await Query("put docs {title: $1, year: $2}", "Dunes", 2021);
await Query("put docs {title: $1, year: $2}", "Night at the oasis", 2024);
var rows = await Query("get docs select title, year where year >= $1 order year desc limit $2", 2020, 5);
var titles = rows.EnumerateArray().Select(r => r.GetProperty("title").GetString());The body is serialised whole: PostAsJsonAsync streams it chunked,
and the server wants a Content-Length.
Full example: Notes, a console app over the server with the builder, records and SubscribeAsync.
static final HttpClient HTTP = HttpClient.newHttpClient();
static HttpResponse<String> query(String json) throws Exception {
var req = HttpRequest.newBuilder(URI.create("http://127.0.0.1:8080/query"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer secret")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString());
}
query("""
{"query": "create collection if not exists docs (title text @text, year int @sorted)"}""");
query("""
{"query": "put docs {title: $1, year: $2}", "params": ["Dunes", 2021]}""");
query("""
{"query": "put docs {title: $1, year: $2}", "params": ["Night at the oasis", 2024]}""");
String rows = query("""
{"query": "get docs select title, year where year >= $1 order year desc limit $2", "params": [2020, 5]}""").body();
String hits = query("""
{"query": "get docs select title match title $1 fuzzy", "params": ["oasys"]}""").body();java.net.http alone, Java 11 and newer (text blocks need 15),
run as java Search.java. The standard library has no JSON: the
test compares the answer's text and reads the titles with a pattern, and a
project reads the rows with Jackson or Gson.
function query(string $q, array $params = []) {
$ch = curl_init('http://127.0.0.1:8080/query');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer secret'],
CURLOPT_POSTFIELDS => json_encode(['query' => $q, 'params' => $params]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 300) {
throw new RuntimeException($body['error'], $status);
}
return $body;
}
query('create collection if not exists docs (title text @text, year int @sorted)');
query('put docs {title: $1, year: $2}', ['Dunes', 2021]);
query('put docs {title: $1, year: $2}', ['Night at the oasis', 2024]);
$rows = query('get docs select title, year where year >= $1 order year desc limit $2', [2020, 5]);
$hits = query('get docs select title match title $1 fuzzy', ['oasys']);The curl extension and json_encode; Guzzle sends the same
request.
require "json"
require "net/http"
URL = URI("http://127.0.0.1:8080")
def query(q, params = [])
req = Net::HTTP::Post.new(URI.join(URL, "/query"), "Content-Type" => "application/json")
req["Authorization"] = "Bearer secret"
req.body = JSON.generate(query: q, params: params)
res = Net::HTTP.start(URL.host, URL.port) { |http| http.request(req) }
body = JSON.parse(res.body)
raise "#{res.code}: #{body["error"]}" unless res.is_a?(Net::HTTPSuccess)
body
end
query("create collection if not exists docs (title text @text, year int @sorted)")
query("put docs {title: $1, year: $2}", ["Dunes", 2021])
query("put docs {title: $1, year: $2}", ["Night at the oasis", 2024])
rows = query("get docs select title, year where year >= $1 order year desc limit $2", [2020, 5])
hits = query("get docs select title match title $1 fuzzy", ["oasys"]).package(url: "https://github.com/fenecdb/fenecdb", from: "0.1.12") // product "FenecDB"import FenecDB
// Embedded: the database is a file the app holds, no server.
let db = try await Fenec.open(path: path)
try await db.execute("create collection if not exists docs (title text @text, year int @sorted)")
try await db.execute("put docs {title: $1, year: $2}", "Dunes", 2021)
try await db.execute("put docs {title: $1, year: $2}", "Night at the oasis", 2024)
let rows = try await db.query("get docs select title, year where year >= $1 order year desc limit $2", 2020, 5)
// rows[0]["title"] == "Night at the oasis", rows[0]["year"] == 2024
let hits = try await db.query("get docs select title match title $1 fuzzy", "oasys")
// hits[0]["title"] == "Night at the oasis", hits[0]["_score"] its score
do {
_ = try await db.query("get nowhere")
} catch let e as FenecError {
print(e.code, e) // notFound: collection `nowhere` ...
}let docs = try db.from("docs")
try await docs.insert(["title": "Night at the oasis", "year": 2024] as Value)
let recent = try await docs.select("title", "year").where("year", ">=", 2020).order("year", "desc").limit(5).rows()
let found = try await docs.select("title").match("title", "oasys", fuzzy: 2).rows()
let n = try await docs.where("title", "~", "Dunes").count()// No file: every query goes to the server, through the same builder.
let remote = Fenec.connect(url: "http://127.0.0.1:8080", token: "secret")
try await remote.execute("put docs {title: $1, year: $2}", "Dunes", 2021)
let recent = try await remote.from("docs").select("title", "year").where("year", ">=", 2020).order("year", "desc").limit(5).rows()Rows decode as any Decodable, and a [Float]
goes to the library as its bytes. A live query for SwiftUI, where the file goes
and a replica a server keeps in step are on
Mobile and native apps. A device reaches a
server through TLS: URLSession makes the requests; over it,
remote.batch([...], idempotencyKey:) lands statements whole
and withIdempotencyKey makes a write once
(Without a file).
Full example: Notes, a SwiftUI app over a file, its list a LiveQuery, and a CLI.
implementation("com.fenecdb:fenecdb-android:0.1.12") // the AAR; the JVM: com.fenecdb:fenecdbimport com.fenecdb.*
// Embedded: the database is a file the app holds, no server.
val db = Fenec.openAsync(path)
db.execute("create collection if not exists docs (title text @text, year int @sorted)")
db.execute("put docs {title: \$1, year: \$2}", "Dunes", 2021)
db.execute("put docs {title: \$1, year: \$2}", "Night at the oasis", 2024)
val rows = db.query("get docs select title, year where year >= \$1 order year desc limit \$2", 2020, 5)
// rows[0].string("title") == "Night at the oasis", rows[0].int("year") == 2024
val hits = db.query("get docs select title match title \$1 fuzzy", "oasys")
// hits[0].string("title") == "Night at the oasis", hits[0].double("_score") its score
try {
db.query("get nowhere")
} catch (e: FenecException) {
println("${e.code} ${e.message}") // NOT_FOUND collection `nowhere` ...
}val docs = db.from("docs")
docs.insert(mapOf("title" to "Night at the oasis", "year" to 2024))
val recent = docs.select("title", "year").where("year", ">=", 2020).order("year", "desc").limit(5).rows()
val found = docs.select("title").match("title", "oasys", fuzzy = 2).rows()
val n = docs.where("title", "~", "Dunes").count()// No file: every query goes to the server, through the same builder.
val remote = Fenec.connect("http://127.0.0.1:8080", token = "secret")
remote.execute("put docs {title: \$1, year: \$2}", "Dunes", 2021)
val recent = remote.from("docs").select("title", "year").where("year", ">=", 2020).order("year", "desc").limit(5).rows()The calls suspend, on Dispatchers.IO; Java calls the
...Blocking ones. Live queries are Flows, and a
replica a server keeps in step is Fenec.sync
(Mobile and native apps). A device reaches a
server through TLS: HttpURLConnection makes the requests; over
it, remote.batch(listOf(...), idempotencyKey = id) lands
statements whole and withIdempotencyKey makes a write once
(Without a file).
Full example: Notes, a Compose app over a file, its list a live Flow, and a JVM CLI.
flutter pub add fenecdb_flutter # Dart alone: dart pub add fenecdbimport 'package:fenecdb/fenecdb.dart';
// Embedded: the database is a file the app holds, no server.
final db = await Fenec.open(path);
await db.execute('create collection if not exists docs (title text @text, year int @sorted)');
await db.execute(r'put docs {title: $1, year: $2}', ['Dunes', 2021]);
await db.execute(r'put docs {title: $1, year: $2}', ['Night at the oasis', 2024]);
final rows = await db.query(r'get docs select title, year where year >= $1 order year desc limit $2', [2020, 5]);
// rows[0]['title'] == 'Night at the oasis', rows[0]['year'] == 2024
final hits = await db.query(r'get docs select title match title $1 fuzzy', ['oasys']);
// hits[0]['title'] == 'Night at the oasis', hits[0]['_score'] its score
try {
await db.query('get nowhere');
} on FenecException catch (e) {
print('${e.code.name} ${e.message}'); // notFound collection `nowhere` ...
}final docs = db.from('docs');
await docs.insert({'title': 'Night at the oasis', 'year': 2024});
final recent = await docs.select(['title', 'year']).where('year', '>=', 2020).order('year', 'desc').limit(5).rows();
final found = await docs.select(['title']).match('title', 'oasys', fuzzy: true).rows();
final n = await docs.where('title', '~', 'Dunes').count();// No file: every query goes to the server, through the same builder.
final remote = Fenec.connect('http://127.0.0.1:8080', token: 'secret');
await remote.execute(r'put docs {title: $1, year: $2}', ['Dunes', 2021]);
final recent = await remote.from('docs').select(['title', 'year']).where('year', '>=', 2020).order('year', 'desc').limit(5).rows();Every call runs on an isolate the database keeps, so none blocks the UI.
Live queries are Streams, and a replica a server keeps in step
is Fenec.openSynced (Mobile and native
apps). A device reaches a server through TLS: dart:io's
HttpClient makes the requests; over it,
remote.batch([...], idempotencyKey: id) lands statements whole
and withIdempotencyKey makes a write once
(Without a file).
Full example: Notes, a Flutter app over a file, its list a StreamBuilder over a live query.
The builder
Python, JavaScript, Go, .NET, Swift, Kotlin and Dart each have a query
builder: a chain of calls that writes the FenecQL statement, every value a
parameter and every name checked against FenecQL's own rule, which is where
an injection would have to get through. All seven build the same text from
the same chain, to the byte, and refuse the same mistakes with the same
message. Python's builder starts with collection, since
from is a keyword there.
A short tour, the same in every tab, over two collections:
create collection docs (slug text @unique, title text @text, year int,
lang text, tags [text], views int)
create collection comments (doc_id int @hash, body text)A page of documents, filtered by a comparison and an or, in
Turkish order, each with its first three comments; a search that forgives a
misspelt word; one document and a count; then the writes — an insert, an
insert passed over because its slug is held (slug is
@unique), a counter moved and a delete by a filter.
db is the client each language's section above connects, and
Go's ctx a context. The FenecQL tab is what each chain writes;
the block under the tabs is what it sends, its parameters apart.
get docs where year >= 2024 and (lang = "tr" or tags has "rust")
order title collate tr asc limit 20
lookup comments on doc_id limit 3
get docs where year >= 2024 match title "databse" fuzzy 2 limit 10
get docs where slug = "intro" limit 1
get docs where year >= 2024 count
put docs {slug: "intro", title: "Ownership in Rust", year: 2026}
put docs {slug: "intro", title: "Ownership in Rust"} if absent
set docs {views: coalesce(views, 0) + 1} where slug = "intro"
del docs where year < 2000await db.from('docs')
.where('year', '>=', 2024)
.where(or({ lang: 'tr' }, { tags: { has: 'rust' } }))
.order('title', 'asc', { collate: 'tr' })
.limit(20)
.lookup('comments', { on: 'doc_id', limit: 3 })
.rows();
await db.from('docs')
.where('year', '>=', 2024)
.match('title', 'databse', { fuzzy: true })
.limit(10)
.rows();
await db.from('docs').where('slug', 'intro').first();
await db.from('docs').where('year', '>=', 2024).count();
await db.from('docs')
.insert({ slug: 'intro', title: 'Ownership in Rust', year: 2026 });
await db.from('docs')
.insert({ slug: 'intro', title: 'Ownership in Rust' }, { ifAbsent: true });
await db.from('docs').where('slug', 'intro').update({ views: inc(1) });
await db.from('docs').where('year', '<', 2000).delete();(db.collection("docs")
.where("year", ">=", 2024)
.where(or_({"lang": "tr"}, {"tags": {"has": "rust"}}))
.order("title", "asc", collate="tr")
.limit(20)
.lookup("comments", on="doc_id", limit=3)
.rows())
(db.collection("docs")
.where("year", ">=", 2024)
.match("title", "databse", fuzzy=True)
.limit(10)
.rows())
db.collection("docs").where("slug", "intro").first()
db.collection("docs").where("year", ">=", 2024).count()
(db.collection("docs")
.insert({"slug": "intro", "title": "Ownership in Rust", "year": 2026}))
(db.collection("docs")
.insert({"slug": "intro", "title": "Ownership in Rust"}, if_absent=True))
db.collection("docs").where("slug", "intro").update({"views": inc(1)})
db.collection("docs").where("year", "<", 2000).delete()db.From("docs").
Where("year", ">=", 2024).
WhereCond(fenecdb.Or(fenecdb.Fields("lang", "tr"), fenecdb.Cmp("tags", "has", "rust"))).
Order("title", "asc", fenecdb.Collate("tr")).
Limit(20).
Lookup("comments", fenecdb.On("doc_id"), fenecdb.Limit(3)).
Rows(ctx)
db.From("docs").
Where("year", ">=", 2024).
Match("title", "databse", fenecdb.Fuzzy(2)).
Limit(10).
Rows(ctx)
db.From("docs").Where("slug", "=", "intro").First(ctx)
db.From("docs").Where("year", ">=", 2024).Count(ctx)
db.From("docs").
Insert(
ctx,
fenecdb.D("slug", "intro", "title", "Ownership in Rust", "year", 2026),
)
db.From("docs").
Insert(
ctx,
fenecdb.D("slug", "intro", "title", "Ownership in Rust"),
fenecdb.IfAbsent(),
)
db.From("docs").
Where("slug", "=", "intro").
Update(ctx, fenecdb.D("views", fenecdb.Inc(1)))
db.From("docs").Where("year", "<", 2000).Delete(ctx)await db.From("docs")
.Where("year", ">=", 2024)
.Where(Cond.Or(Cond.Cmp("lang", "=", "tr"), Cond.Cmp("tags", "has", "rust")))
.Order("title", "asc", collate: "tr")
.Limit(20)
.Lookup("comments", on: "doc_id", limit: 3)
.RowsAsync();
await db.From("docs")
.Where("year", ">=", 2024)
.Match("title", "databse", fuzzy: 2)
.Limit(10)
.RowsAsync();
await db.From("docs").Where("slug", "intro").FirstAsync();
await db.From("docs").Where("year", ">=", 2024).CountAsync();
await db.From("docs")
.InsertAsync(new { slug = "intro", title = "Ownership in Rust", year = 2026 });
await db.From("docs")
.InsertAsync(
new { slug = "intro", title = "Ownership in Rust" },
ifAbsent: true
);
await db.From("docs")
.Where("slug", "intro")
.UpdateAsync(new { views = Computed.Inc(1) });
await db.From("docs").Where("year", "<", 2000).DeleteAsync();try await db.from("docs")
.where("year", ">=", 2024)
.where(.or(["lang": "tr"], .cmp("tags", "has", "rust")))
.order("title", "asc", collate: "tr")
.limit(20)
.lookup("comments", on: "doc_id", limit: 3)
.rows()
try await db.from("docs")
.where("year", ">=", 2024)
.match("title", "databse", fuzzy: 2)
.limit(10)
.rows()
try await db.from("docs").where("slug", "intro").first()
try await db.from("docs").where("year", ">=", 2024).count()
try await db.from("docs")
.insert(["slug": "intro", "title": "Ownership in Rust", "year": 2026] as Value)
try await db.from("docs")
.insert(
["slug": "intro", "title": "Ownership in Rust"] as Value,
ifAbsent: true
)
try await db.from("docs")
.where("slug", "intro")
.update(["views": .inc(1)] as Value)
try await db.from("docs").where("year", "<", 2000).delete()db.from("docs")
.where("year", ">=", 2024)
.where(Cond.or(mapOf("lang" to "tr"), Cond.cmp("tags", "has", "rust")))
.order("title", "asc", collate = "tr")
.limit(20)
.lookup("comments", on = "doc_id", limit = 3)
.rows()
db.from("docs")
.where("year", ">=", 2024)
.match("title", "databse", fuzzy = 2)
.limit(10)
.rows()
db.from("docs").where("slug", "intro").first()
db.from("docs").where("year", ">=", 2024).count()
db.from("docs")
.insert(mapOf("slug" to "intro", "title" to "Ownership in Rust", "year" to 2026))
db.from("docs")
.insert(
mapOf("slug" to "intro", "title" to "Ownership in Rust"),
ifAbsent = true
)
db.from("docs")
.where("slug", "intro")
.update(mapOf("views" to Computed.inc(1)))
db.from("docs").where("year", "<", 2000).delete()await db.from('docs')
.where('year', '>=', 2024)
.where(Cond.or([{'lang': 'tr'}, Cond.cmp('tags', 'has', 'rust')]))
.order('title', 'asc', 'tr')
.limit(20)
.lookup('comments', on: 'doc_id', limit: 3)
.rows();
await db.from('docs')
.where('year', '>=', 2024)
.match('title', 'databse', fuzzy: true)
.limit(10)
.rows();
await db.from('docs').where('slug', 'intro').first();
await db.from('docs').where('year', '>=', 2024).count();
await db.from('docs')
.insert({'slug': 'intro', 'title': 'Ownership in Rust', 'year': 2026});
await db.from('docs')
.insert({'slug': 'intro', 'title': 'Ownership in Rust'}, ifAbsent: true);
await db.from('docs')
.where('slug', 'intro')
.update({'views': Computed.inc(1)});
await db.from('docs').where('year', '<', 2000).delete();get docs where year >= $1 and (lang = $2 or tags has $3) order title collate tr asc limit $4 lookup comments on doc_id limit $5
-- [2024, "tr", "rust", 20, 3]
get docs where year >= $1 match title $2 fuzzy 2 limit $3
-- [2024, "databse", 10]
get docs where slug = $1 limit $2
-- ["intro", 1]
get docs where year >= $1 count
-- [2024]
put docs {slug: $1, title: $2, year: $3}
-- ["intro", "Ownership in Rust", 2026]
put docs {slug: $1, title: $2} if absent
-- ["intro", "Ownership in Rust"]
set docs {views: coalesce(views, 0) + $1} where slug = $2
-- [1, "intro"]
del docs where year < $1
-- [2000]Each has select with aggregates and group,
where with every operator, in, null,
paths into a json field and or, and,
not and raw; match; near,
a vector's nearest rows (near('embed', v, { ef: 64 }) in
JavaScript, Vector search), and
fuse and rerank, which rank by the words and a
vector together; highlight,
snippet and facet, the facets beside the rows;
lookup and its chains; order with a collation; limit and
offset; and the writes, an update or
delete with no filter refused unless it says all,
a value worked out over the row with inc and expr
(Writes that read the row), and an
insert if absent.
A query is immutable, so a base query can be kept and branched from.
Rows come back as each language holds a document, and three builders
also decode them into a type of the app's: Go's
fenecdb.RowsAs[T](ctx, q), .NET's RowsAsync<T>()
and Swift's rows(as: T.self). Go's endpoints take the
ctx that bounds the request, .NET's are …Async,
and Kotlin's suspend, with rowsBlocking() and
countBlocking() for code outside a coroutine.
How one golden file holds every builder
integrations/builder-golden.json is the contract: each case a
chain of steps ({op, args}, the JavaScript builder's names) and
the statement it must make, its text and parameters, or the refusal and its
message. The JavaScript builder is the reference: make
builder-golden runs every chain through it and writes what it made,
and its own tests run the file again, so the two cannot drift. The Python,
Go, .NET, Swift, Kotlin and Dart tests each read every case, call their own
builder, and compare the text and parameters exactly. The cases cover every method and option,
each operator and kind of value, parameter numbering across
where, near, match and the levels of
a lookup chain, and the names and combinations each builder refuses. Each
SDK's tests also run builder queries against a real server and hold their
answers to the same statements sent as text. The tour above and the tabs
of the FenecQL reference's examples are cases of the file written in each
language (Text search has
some): the JavaScript and Python tabs run as the page shows them, and each
other language's tests hold a copy of its tabs' code, so a tab is code that
sends the statement beside it.
What a language has no word for is written its own way: Go's object
conditions are Fields and Ops, names and values
in turn, since a Go map has no order and the order is the text's; a Go
document keeps its order as a fenecdb.D(...), and a map's
fields go in sorted. Options are keyword arguments in Python, named
arguments in C# and functional options in Go, where a struct could not tell
limit 0 from no limit. A refusal is a FenecError
with status 0 in Python, a *fenecdb.QueryError in Go and a
FenecQueryException in .NET. Swift's dictionaries have no
order either, so a document or an object condition is a Value
or Row literal, which keeps the order written, or a
Codable type, its fields in the order it encodes them; Kotlin's
and Dart's maps keep their order. A refusal there is a
FenecError, a FenecException and a
FenecException, each with the code builder, and
every builder method throws as it is called, so a Swift chain takes one
try.
From the shell
The same request from curl:
curl -X POST localhost:8080/query -H 'authorization: Bearer secret' \
-d '{"query":"get docs select title, year where year >= $1 order year desc limit $2","params":[2020,5]}'
# [{"title":"Night at the oasis","year":2024},{"title":"Dunes","year":2021}]The REST routes, subscriptions and the change stream are on HTTP reference, and batches on HTTP endpoint.
What the examples and SDK tests run against
The programs under integrations/languages are the examples
above with their asserts: make languages-test starts a
fenec-server over a new file for each language and runs Python,
Java, PHP and Ruby from their official images in Docker, and JavaScript, Go,
C# and Rust with the toolchains on the machine — Go and C# both through
their SDK and without it, and each SDK's program through its query builder
too. The SDKs' own tests (make go-test,
make dotnet-test) start a primary, a replica of it and a node
of tenants and cover the rest: every type of parameter, the errors, a batch
put back whole, an idempotency key, reading a write on the replica, a
subscription, the change stream, a tenant and a scoped token. CI runs it on every change,
so a change to the HTTP endpoint that would break one of them is caught
before it ships.
The JavaScript tab's programs are TypeScript, and run there as the
JavaScript they are without their types; the tab's code itself is type-checked under
tsc --strict against the client's declarations, as every
TypeScript example in these docs is (make docs-types).
Every answer is JSON: a read is an array of rows, a ranked search's rows
(match, near) carry their _score, a
write is {"affected": n}, and
a refusal is a status of 400 and up with {"error": "..."} — 401
for a missing or wrong token, 404 for a collection that is not there, 409
for an id insert finds taken. A vector goes in as a JSON array
of numbers and comes out as the shortest text that reads back as each
f32. Values always go in params, never into the
statement's text.
The server keeps a connection open between requests (HTTP/1.1
keep-alive) for a client that reuses it, as Go's, .NET's, Java's and
ureq's do. A body is read by its Content-Length; a chunked one
is refused.