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, writes two
documents with a vector, asks for the nearest 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, embed vector<3> @hnsw(cosine))")
db.query("put docs {title: $1, embed: $2}", ["Night at the oasis", [0.1, 0.2, 0.3]])
db.query("put docs {title: $1, embed: $2}", ["Dunes", [0.9, 0.1, 0.0]])
rows = db.query("get docs select title near embed $1 limit $2", [[0.1, 0.2, 0.3], 5])
# [{"title": "Night at the oasis", "_score": 0.99...}, {"title": "Dunes", ...}]
try:
db.query("get nowhere")
except FenecError as e:
print(e.status, e) # 404 collection `nowhere`Or let the query builder write the statement: the same text, every value a parameter, every name checked.
docs = db.collection("docs")
docs.insert({"title": "Night at the oasis", "embed": [0.1, 0.2, 0.3]})
rows = docs.select("title").near("embed", [0.1, 0.2, 0.3]).limit(5).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, embed vector<3> @hnsw(cosine))');
await query('put docs {title: $1, embed: $2}', ['Night at the oasis', [0.1, 0.2, 0.3]]);
await query('put docs {title: $1, embed: $2}', ['Dunes', [0.9, 0.1, 0.0]]);
const rows = await query('get docs select title near embed $1 limit $2', [[0.1, 0.2, 0.3], 5]);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', embed: [0.1, 0.2, 0.3] });
const rows = await db.from('docs').select('title').near('embed', [0.1, 0.2, 0.3]).limit(5).rows();
// rows: { 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/fenec/integrations/go # the standard library aloneimport fenecdb "github.com/fenecdb/fenec/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, embed vector<3> @hnsw(cosine))")
db.Exec(ctx, "put docs {title: $1, embed: $2}", "Night at the oasis", []float32{0.1, 0.2, 0.3})
db.Exec(ctx, "put docs {title: $1, embed: $2}", "Dunes", []float32{0.9, 0.1, 0.0})
type Hit struct {
Title string `json:"title"`
Score float32 `json:"_score"`
}
hits, err := fenecdb.QueryAs[Hit](ctx, db, "get docs select title near embed $1 limit $2", []float32{0.1, 0.2, 0.3}, 5)
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", "embed", []float32{0.1, 0.2, 0.3}))
query := docs.Select("title").Near("embed", []float32{0.1, 0.2, 0.3}).Limit(5)
hits, err := fenecdb.RowsAs[Hit](ctx, query)
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 (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, embed vector<3> @hnsw(cosine))")
query(nil, "put docs {title: $1, embed: $2}", "Night at the oasis", []float32{0.1, 0.2, 0.3})
query(nil, "put docs {title: $1, embed: $2}", "Dunes", []float32{0.9, 0.1, 0.0})
var rows []struct {
Title string `json:"title"`
Score float32 `json:"_score"`
}
err := query(&rows, "get docs select title near embed $1 limit $2", []float32{0.1, 0.2, 0.3}, 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 --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, embed vector<3> @hnsw(cosine))", json!([]))?;
query("put docs {title: $1, embed: $2}", json!(["Night at the oasis", [0.1, 0.2, 0.3]]))?;
query("put docs {title: $1, embed: $2}", json!(["Dunes", [0.9, 0.1, 0.0]]))?;
let rows = query("get docs select title near embed $1 limit $2", json!([[0.1, 0.2, 0.3], 5]))?;
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, embed vector<3> @hnsw(cosine))");
await db.ExecAsync("put docs {title: $1, embed: $2}", ["Night at the oasis", new[] { 0.1f, 0.2f, 0.3f }]);
await db.ExecAsync("put docs {title: $1, embed: $2}", ["Dunes", new[] { 0.9f, 0.1f, 0.0f }]);
var hits = await db.QueryAsync<Hit>("get docs select title near embed $1 limit $2", [new[] { 0.1f, 0.2f, 0.3f }, 5]);
try { await db.QueryAsync("get nowhere"); }
catch (FenecException e) { Console.WriteLine($"{e.Status} {e.Code} {e.Message}"); } // 404 not_found collection `nowhere`
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", embed = new[] { 0.1f, 0.2f, 0.3f } });
var query = docs.Select("title").Near("embed", new[] { 0.1f, 0.2f, 0.3f }).Limit(5);
var hits = await query.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
(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, embed vector<3> @hnsw(cosine))");
await Query("put docs {title: $1, embed: $2}", "Night at the oasis", new[] { 0.1f, 0.2f, 0.3f });
await Query("put docs {title: $1, embed: $2}", "Dunes", new[] { 0.9f, 0.1f, 0.0f });
var rows = await Query("get docs select title near embed $1 limit $2", new[] { 0.1f, 0.2f, 0.3f }, 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, embed vector<3> @hnsw(cosine))"}""");
query("""
{"query": "put docs {title: $1, embed: $2}", "params": ["Night at the oasis", [0.1, 0.2, 0.3]]}""");
query("""
{"query": "put docs {title: $1, embed: $2}", "params": ["Dunes", [0.9, 0.1, 0.0]]}""");
String rows = query("""
{"query": "get docs select title near embed $1 limit $2", "params": [[0.1, 0.2, 0.3], 5]}""").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 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, embed vector<3> @hnsw(cosine))');
query('put docs {title: $1, embed: $2}', ['Night at the oasis', [0.1, 0.2, 0.3]]);
query('put docs {title: $1, embed: $2}', ['Dunes', [0.9, 0.1, 0.0]]);
$rows = query('get docs select title near embed $1 limit $2', [[0.1, 0.2, 0.3], 5]);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, embed vector<3> @hnsw(cosine))")
query("put docs {title: $1, embed: $2}", ["Night at the oasis", [0.1, 0.2, 0.3]])
query("put docs {title: $1, embed: $2}", ["Dunes", [0.9, 0.1, 0.0]])
rows = query("get docs select title near embed $1 limit $2", [[0.1, 0.2, 0.3], 5]).package(url: "https://github.com/fenecdb/fenec", from: "0.1.7") // 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, embed vector<3> @hnsw(cosine))")
try await db.execute("put docs {title: $1, embed: $2}", "Night at the oasis", [Float(0.1), 0.2, 0.3])
try await db.execute("put docs {title: $1, embed: $2}", "Dunes", [Float(0.9), 0.1, 0.0])
let rows = try await db.query("get docs select title near embed $1 limit $2", [Float(0.1), 0.2, 0.3], 5)
// rows[0]["title"] == "Night at the oasis", rows[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", "embed": .floats([0.1, 0.2, 0.3])] as Value)
let near = try await docs.select("title").near("embed", [Float(0.1), 0.2, 0.3]).limit(5).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, embed: $2}", "Dunes", [Float(0.9), 0.1, 0.0])
let hits = try await remote.from("docs").select("title").near("embed", [Float(0.1), 0.2, 0.3]).limit(5).rows()A [Float] goes to the library as its bytes; rows decode as
any Decodable. 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.7") // 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, embed vector<3> @hnsw(cosine))")
db.execute("put docs {title: \$1, embed: \$2}", "Night at the oasis", floatArrayOf(0.1f, 0.2f, 0.3f))
db.execute("put docs {title: \$1, embed: \$2}", "Dunes", floatArrayOf(0.9f, 0.1f, 0f))
val rows = db.query("get docs select title near embed \$1 limit \$2", floatArrayOf(0.1f, 0.2f, 0.3f), 5)
// rows[0].string("title") == "Night at the oasis", rows[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", "embed" to floatArrayOf(0.1f, 0.2f, 0.3f)))
val near = docs.select("title").near("embed", floatArrayOf(0.1f, 0.2f, 0.3f)).limit(5).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, embed: \$2}", "Dunes", floatArrayOf(0.9f, 0.1f, 0f))
val hits = remote.from("docs").select("title").near("embed", floatArrayOf(0.1f, 0.2f, 0.3f)).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, embed vector<3> @hnsw(cosine))');
await db.execute(r'put docs {title: $1, embed: $2}', ['Night at the oasis', Float32List.fromList([0.1, 0.2, 0.3])]);
await db.execute(r'put docs {title: $1, embed: $2}', ['Dunes', Float32List.fromList([0.9, 0.1, 0.0])]);
final rows = await db.query(r'get docs select title near embed $1 limit $2', [Float32List.fromList([0.1, 0.2, 0.3]), 5]);
// rows[0]['title'] == 'Night at the oasis', rows[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', 'embed': Float32List.fromList([0.1, 0.2, 0.3])});
final near = await docs.select(['title']).near('embed', Float32List.fromList([0.1, 0.2, 0.3])).limit(5).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, embed: $2}', ['Dunes', Float32List.fromList([0.9, 0.1, 0.0])]);
final hits = await remote.from('docs').select(['title']).near('embed', Float32List.fromList([0.1, 0.2, 0.3])).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.
| Python | JavaScript | Go | .NET |
|---|---|---|---|
db.collection("docs") | db.from('docs') | db.From("docs") | db.From("docs") |
.where("year", ">=", 2024) | .where('year', '>=', 2024) | .Where("year", ">=", 2024) | .Where("year", ">=", 2024) |
.where(or_({"lang": "tr"}, {"tags": {"has": "rust"}})) | .where(or({ lang: 'tr' }, { tags: { has: 'rust' } })) | .WhereCond(fenecdb.Or(fenecdb.Fields("lang", "tr"), fenecdb.Fields("tags", fenecdb.Ops("has", "rust")))) | .Where(Cond.Or(Cond.Cmp("lang", "=", "tr"), Cond.Cmp("tags", "has", "rust"))) |
.near("embed", v, ef=64) | .near('embed', v, { ef: 64 }) | .Near("embed", v, fenecdb.Ef(64)) | .Near("embed", v, ef: 64) |
.lookup("reviews", on="product_id", limit=3) | .lookup('reviews', { on: 'product_id', limit: 3 }) | .Lookup("reviews", fenecdb.On("product_id"), fenecdb.Limit(3)) | .Lookup("reviews", on: "product_id", limit: 3) |
.order("title", "desc", collate="tr") | .order('title', 'desc', { collate: 'tr' }) | .Order("title", "desc", fenecdb.Collate("tr")) | .Order("title", "desc", collate: "tr") |
.rows(), .first(), .count() | .rows(), .first(), .count() | .Rows(ctx), .First(ctx), .Count(ctx), RowsAs[T] | .RowsAsync(), .FirstAsync(), .CountAsync(), .RowsAsync<T>() |
.insert(doc), .update(patch), .delete() | .insert(doc), .update(patch), .delete() | .Insert(ctx, doc), .Update(ctx, patch), .Delete(ctx) | .InsertAsync(doc), .UpdateAsync(patch), .DeleteAsync() |
.update({"n": inc(1)}), .insert(doc, if_absent=True) | .update({ n: inc(1) }), .insert(doc, { ifAbsent: true }) | .Update(ctx, fenecdb.D("n", fenecdb.Inc(1))), .Insert(ctx, doc, fenecdb.IfAbsent()) | .UpdateAsync(new { n = Computed.Inc(1) }), .InsertAsync(doc, ifAbsent: true) |
| Swift | Kotlin | Dart |
|---|---|---|
try db.from("docs") | db.from("docs") | db.from('docs') |
.where("year", ">=", 2024) | .where("year", ">=", 2024) | .where('year', '>=', 2024) |
.where(.or(["lang": "tr"], ["tags": ["has": "rust"]])) | .where(Cond.or(mapOf("lang" to "tr"), mapOf("tags" to mapOf("has" to "rust")))) | .where(Cond.or([{'lang': 'tr'}, {'tags': {'has': 'rust'}}])) |
.near("embed", v, ef: 64) | .near("embed", v, ef = 64) | .near('embed', v, ef: 64) |
.lookup("reviews", on: "product_id", limit: 3) | .lookup("reviews", on = "product_id", limit = 3) | .lookup('reviews', on: 'product_id', limit: 3) |
.order("title", "desc", collate: "tr") | .order("title", "desc", collate = "tr") | .order('title', 'desc', 'tr') |
.rows(), .first(), .count(), .rows(as: T.self) | .rows(), .first(), .count(), .rowsBlocking() | .rows(), .first(), .count() |
.insert(doc), .update(patch), .delete() | .insert(doc), .update(patch), .delete() | .insert(doc), .update(patch), .delete() |
.update(["n": .inc(1)]), .insert(doc, ifAbsent: true) | .update(mapOf("n" to Computed.inc(1))), .insert(doc, ifAbsent = true) | .update({'n': Computed.inc(1)}), .insert(doc, ifAbsent: true) |
Each has select with aggregates and group,
where with every operator, in, null,
paths into a json field and or, and,
not and raw; near, match,
fuse and rerank; 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.
Details: one golden file
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.
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 near embed $1 limit $2","params":[[0.1,0.2,0.3],5]}'
# [{"title":"Night at the oasis","_score":0.9999999},{"title":"Dunes","_score":0.3246547}]The REST routes, batches, subscriptions and the change stream are on HTTP endpoint.
Details: what the examples are held to
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 TypeScript tab's programs run there as the JavaScript they are
without their types, and 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 vector search's rows
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.