Writing data
put, insert, set and del, and how a write says what it expects to find.
A write names a collection and answers how many rows it wrote:
put and insert add documents, set changes
the rows a where keeps, and del removes them.
Run in the shell, answers and all; the timings are from one run.
fenec> create collection stock (sku text @unique, qty int);
collection `stock` created
fenec> put stock [{sku: "tea", qty: 10}, {sku: "jam", qty: 2}];
2 records (152.00µs)
fenec> insert stock {sku: "tea", qty: 1};
error: duplicate: `stock.sku` is unique, and document 1 holds "tea" already
fenec> put stock {sku: "tea", qty: 1} if absent;
0 records (94.58µs)
fenec> put stock {sku: "tea", qty: 5} if absent else set {qty: qty + new.qty} returning sku, qty;
sku qty
--- ---
tea 15
(1 rows, 157.00µs)
fenec> set stock {qty: qty - 3} where sku = "tea" and qty >= 3 require 1 returning sku, qty;
sku qty
--- ---
tea 12
(1 rows, 194.00µs)
fenec> set stock {qty: qty - 3} where sku = "jam" and qty >= 3 require 1;
error: unmet: `set stock` wrote 0 rows, and requires 1
fenec> del stock where qty < 3 returning sku;
sku
---
jam
(1 rows, 115.71µs)
fenec> get stock select sku, qty
sku qty
--- ---
tea 12
(1 rows, 85.21µs)insert never writes over a row and is refused when a
@unique value or an id is taken; put … if absent passes
over such a row and says it wrote none, and else set changes it
instead, an upsert in one statement. A set can read the row it
writes, require N makes the count a condition and refuses the
statement, and the batch it is in, when it is not met, and
returning answers the rows written or removed. The sections
below take each in turn, and the last ones put several statements in one
batch.
Writes that read the row
A value in a set can be an expression over the row it
writes: its fields, parameters and functions, with +
- * /. Every write answers how many
rows it changed, so a write that names what it expects to find says
whether it found it.
set hits {n: n + 1, seen: now()} where key = "ip1" -- an increment
set hits {n: coalesce(n, 0) + $1} where key = $2 -- from nothing, by a parameter
set docs {body: $1, version: version + 1}
where id = $2 and version = $3 -- compare-and-set: 1 or 0
put locks {name: "nightly", owner: $1, at: now()} if absent -- 1 if taken, 0 if held
insert journal {entry: $1 + ":dr", amount: 0 - $2} -- two texts joined- An increment is atomic. The row is read and written under the write lock, and there is one writer, so increments from any number of clients all land.
- A field read is the row as it was. In
{n: n + 1, m: n}both read the oldn; a name on the value side is a field, a quoted string a string. nullstaysnull.n + 1over a row with nonisnull, as in SQL; writecoalesce(n, 0) + 1to count from nothing.- Every write answers its count —
{"affected": n}over HTTP, thecountof the browser module's answer, and the write result of every SDK.
How set reads the row, and its types, precedence and cost
Types. The result goes through the field's type check as a
literal would. Two ints make an int, and a result
past 64 bits is refused, never wrapped; / between
ints divides whole, toward zero, as PostgreSQL's does
(7 / 2 is 3), and a division by zero is refused.
An int and a float make a float,
which an int field takes when it is whole and refuses when it
is not — n * 2.0 fits, n + 0.5 does not, as the
literal 2.5 does not. A float that is no longer finite is
refused. A timestamp plus or minus an int is a
timestamp that many milliseconds on (now() + 30000), and two
timestamps apart are the milliseconds between them. + joins two
texts ($1 + ":dr"), and only two texts. Anything else — text
and a number, either way round, or - between texts — is a type
error: a number turned into text without asking would hide the mistake a
typed field is there to catch. An error refuses the statement whole, a
block of one, so nothing of it is written; over no rows there is no error,
as there is no row to work it out over.
Precedence. * and / bind tighter than
+ and -, and all four tighter than a comparison:
where n + 1 > 2 * m. -1 where a value starts is
a number; after a value, n-1 and n - 1 are a
subtraction.
Time. now() is worked out once a statement, so every
row a set writes holds the same time. In the browser it is the
time the page hands the module with every statement, which has no clock of
its own.
What travels. The record holds the document as written, the value
worked out, never the expression: a replica, /_changes, an
archive and a subscriber see the result, and a restore replays it as it
was. A scoped token's rules test the document a set writes, as
they test a literal's (Scoped
access). A replica of Sync works the
expression out over its own row at once and sends the statement, which the
server works out again over its own: two replicas' offline increments both
land, and both replicas come to the server's sum.
How it runs. The pairs are bound to the collection once a
statement, as a filter is: each field written and read by its position, and
a value that reads no field — a literal, a parameter, now() —
worked out once rather than a row. Each row is read once: a
set read it twice before, once for the values and once for the
indexes it takes out.
if absent passes over a document whose
id, or whose @unique value, a live row holds,
where insert refuses the statement: the others are written,
and the answer counts those alone. That is Redis's SET NX in
one statement, and it neither aborts a /batch nor makes an
error a client has to catch. A row past its @ttl holds
nothing — it is out of every read — so a write takes its place.
What it costs, measured (make counters-bench, in
process over 100 000 rows of a text, an int and a note). A
set of a constant over every row took 65.0 ms against 85.5
before expressions — the row read once instead of twice — and
{n: n + 1} 66.6 ms; one row by id 1.07 us against 1.26, and
1.18 for n + 1. Sixteen threads incrementing one key 10 000
times each ended at 160 000, 320 000 increments a second, and sixteen HTTP
clients the same against fenec-server, 102 000 a second.
Writes that must find their rows
A write that matches no row is no error: it answers
{"affected": 0}, and a /batch goes on to the next
statement. require N after a set, a
del, a put or an insert makes the
count a condition: unless the statement wrote exactly N rows
it is refused, and the block it is in — a /batch, a run of
several in the page, a keyed write — is put back whole, as on any other
error.
set accounts {balance: balance - $1} where name = $2 and balance >= $1 require 1
del holds where order_ref = $1 and state = "held" require 1
put locks {name: "nightly", owner: $1} if absent require 1 -- refused while it is held- Refused over HTTP with 412, the error naming the statement:
`set accounts` wrote 0 rows, and requires 1. A/batchalso says which statement stopped it ("at", from 0). Every SDK has its own kind for it (unmet), and every builder an option:.update(patch, { require: 1 }). - The count is the write's own, taken under the write lock that made it, so nothing can come between the check and the write. A scoped token's count is of the rows its rules let it write.
require 0asserts that nothing matches.
A read that must find its rows
require N after a get guards a read the same
way: unless the get answers exactly N rows it is
refused (412, "at" naming it) and its batch put back. In a
/batch it reads under the batch's write lock, after the
statements before it and before those after, so what it saw is what the
writes around it see: "the price is still 12", "the coupon is still
good".
get products where sku = $1 and price = $2 and stock > 0 require 1
get coupons where code = $1 and uses < max_uses limit 1 require 1 -- one is enough
get holds where order_ref = $1 and state = "held" require 0 -- none may be heldawait db.from('products')
.where('sku', 'tee')
.where('price', 1200)
.where('stock', '>', 0)
.require(1)
.rows();
await db.from('coupons')
.where('code', 'X1')
.where(raw('uses < max_uses'))
.limit(1)
.require(1)
.rows();
await db.from('holds')
.where('order_ref', 'o-17')
.where('state', 'held')
.require(0)
.rows();(db.collection("products")
.where("sku", "tee")
.where("price", 1200)
.where("stock", ">", 0)
.require(1)
.rows())
(db.collection("coupons")
.where("code", "X1")
.where(raw("uses < max_uses"))
.limit(1)
.require(1)
.rows())
(db.collection("holds")
.where("order_ref", "o-17")
.where("state", "held")
.require(0)
.rows())db.From("products").
Where("sku", "=", "tee").
Where("price", "=", 1200).
Where("stock", ">", 0).
Require(1).
Rows(ctx)
db.From("coupons").
Where("code", "=", "X1").
WhereCond(fenecdb.Raw("uses < max_uses")).
Limit(1).
Require(1).
Rows(ctx)
db.From("holds").
Where("order_ref", "=", "o-17").
Where("state", "=", "held").
Require(0).
Rows(ctx)await db.From("products")
.Where("sku", "tee")
.Where("price", 1200)
.Where("stock", ">", 0)
.Require(1)
.RowsAsync();
await db.From("coupons")
.Where("code", "X1")
.Where(Cond.Raw("uses < max_uses"))
.Limit(1)
.Require(1)
.RowsAsync();
await db.From("holds")
.Where("order_ref", "o-17")
.Where("state", "held")
.Require(0)
.RowsAsync();try await db.from("products")
.where("sku", "tee")
.where("price", 1200)
.where("stock", ">", 0)
.require(1)
.rows()
try await db.from("coupons")
.where("code", "X1")
.where(.raw("uses < max_uses"))
.limit(1)
.require(1)
.rows()
try await db.from("holds")
.where("order_ref", "o-17")
.where("state", "held")
.require(0)
.rows()db.from("products")
.where("sku", "tee")
.where("price", 1200)
.where("stock", ">", 0)
.require(1)
.rows()
db.from("coupons")
.where("code", "X1")
.where(Cond.raw("uses < max_uses"))
.limit(1)
.require(1)
.rows()
db.from("holds")
.where("order_ref", "o-17")
.where("state", "held")
.require(0)
.rows()await db.from('products')
.where('sku', 'tee')
.where('price', 1200)
.where('stock', '>', 0)
.require(1)
.rows();
await db.from('coupons')
.where('code', 'X1')
.where(Cond.raw('uses < max_uses'))
.limit(1)
.require(1)
.rows();
await db.from('holds')
.where('order_ref', 'o-17')
.where('state', 'held')
.require(0)
.rows();get products where sku = $1 and price = $2 and stock > $3 require 1
-- ["tee", 1200, 0]
get coupons where code = $1 and uses < max_uses limit $2 require 1
-- ["X1", 1]
get holds where order_ref = $1 and state = $2 require 0
-- ["o-17", "held"]- It counts the rows the
getanswers, afteroffsetandlimit:limit 1 require 1asks that one exists, and reads no further. Alookup's children are not counted, andrequiregoes before thelookup, whose clauses are the children's. - Not with
countor an aggregate withoutgroup, which answer one row whatever matched; over groups it counts the groups. An innergetofin (get …)takes none. - On its own it is a read like any other, refused with 412 when the count is not met. A scoped token's count is of the rows it may read.
- Every builder has it as a step,
.require(1); a write takes it as its option instead. A replica's sync refuses one in a batch of synced writes: it would count the replica's rows, and the server, which lands the batch, would never see it — send a guarded batch to the server itself.
Transfers
Moving money between two accounts is a debit where the money is, a
credit where the account is, and a journal of both, as one
/batch. Without require an overdraft's debit
matched nothing, answered 0, and the credit went ahead: the sum of the
balances grew with every refused transfer. With it, a debit that finds too
little money or a credit that finds no account puts the whole transfer
back, journal and all:
create collection accounts (name text @unique, balance int)
create collection journal (tx text @hash, account text @hash, amount int)
set accounts {balance: balance - $3} where name = $1 and balance >= $3 require 1
set accounts {balance: balance + $3} where name = $2 require 1
insert journal [{tx: $4, account: $1, amount: 0 - $3}, {tx: $4, account: $2, amount: $3}]printf '%s\n' \
'{"query":"set accounts {balance: balance - $3} where name = $1 and balance >= $3 require 1","params":["ada","bob",30,"t1"]}' \
'{"query":"set accounts {balance: balance + $3} where name = $2 require 1","params":["ada","bob",30,"t1"]}' \
'{"query":"insert journal [{tx: $4, account: $1, amount: 0 - $3}, {tx: $4, account: $2, amount: $3}]","params":["ada","bob",30,"t1"]}' |
curl -s -X POST localhost:8080/batch -H "Idempotency-Key: t1" --data-binary @-
# an overdraft: 412 {"error":"unmet: `set accounts` wrote 0 rows, and requires 1","completed":0,"at":0}- Amounts are whole minor units in an
int, so a sum is exact. - An
Idempotency-Keymakes a retry after a timeout answer what the first try did, rather than move the money twice (Retrying a write). - Who may move what is the app's. The credit is another user's row, which a scoped token may not write: transfers go through an app server holding the server's token.
- A read can guard it too. A client that showed a balance and
moves money only while it is still that puts
get accounts where name = $1 and balance = $5 require 1first; changed meanwhile, the transfer stops there, 412 with"at":0. - Held to it: eight threads making 4 800 random transfers among
six accounts in process, and eight clients making 2 400 as
/batches over HTTP, an eighth of them to an account that is not there, leave the sum where it started, no balance below zero, and a journal that accounts for every balance (crates/fenec-core/tests/require.rs,crates/fenec-http/tests/require.rs). - A whole ledger built this way — refunds, holds, an append-only journal, reconciliation, a change-stream sink, scoped tokens — is examples/ledger.
Checkout
An order is placed at the price the shopper saw, against stock that is
there, with a coupon that is still good — or not at all. Each condition is
a require: the price and the coupon read, the stock taken, the
coupon's use counted and the order written, as one /batch.
create collection products (sku text @unique, price int, stock int)
create collection coupons (code text @unique, off int, uses int, max_uses int)
create collection orders (ref text @unique, sku text @hash, paid int)
get products where sku = $1 and price = $2 require 1
get coupons where code = $3 and uses < max_uses require 1
set products {stock: stock - 1} where sku = $1 and stock > 0 require 1
set coupons {uses: uses + 1} where code = $3 require 1
insert orders {ref: $4, sku: $1, paid: $2 - $5}- A price that moved stops the batch at its first line, 412 with
"at":0: the shopper is shown the new price, and nothing was written. - A coupon used up stops it at the second, no stock at the
third; the order's
@uniquerefrefuses the same checkout twice (409), and anIdempotency-Keyanswers a retry what the first try did. - Under the write lock, every line sees the ones before it: two shoppers taking the last one cannot both pass the stock line.
Writes that pick their rows
order and limit after a set or a
del pick the rows it writes: the page a get with
the same where, order and limit would
answer, found and written under the one writer's lock.
returning answers them rather than their count — a
set's rows as it wrote them, a del's as they were,
in the order they were picked. A job queue's claim is one statement:
create collection jobs (payload json, run_at timestamp @sorted, owner text, attempts int)put jobs [{payload: {to: "ada"}, run_at: now(), attempts: 0}, {payload: {to: "bo"}, run_at: now(), attempts: 0}]
set jobs {owner: "w1", run_at: now() + 30000, attempts: attempts + 1} where run_at <= now() order run_at asc limit 10 returning *
del jobs where id = 1 and owner = "w1" require 1
del jobs where run_at <= now() order run_at asc limit 10 returning id, payload.toawait db.from('jobs')
.insert([{ payload: { to: 'ada' }, run_at: expr('now()'), attempts: 0 }, { payload: { to: 'bo' }, run_at: expr('now()'), attempts: 0 }]);
await db.from('jobs')
.where(raw('run_at <= now()'))
.order('run_at')
.limit(10)
.update(
{ owner: 'w1', run_at: expr('now() + ?', 30000), attempts: expr('attempts + 1') },
{ returning: true }
);
await db.from('jobs')
.where('id', 1)
.where('owner', 'w1')
.delete({ require: 1 });
await db.from('jobs')
.where(raw('run_at <= now()'))
.order('run_at')
.limit(10)
.delete({ returning: ['id', 'payload.to'] });(db.collection("jobs")
.insert([{"payload": {"to": "ada"}, "run_at": expr("now()"), "attempts": 0}, {"payload": {"to": "bo"}, "run_at": expr("now()"), "attempts": 0}]))
(db.collection("jobs")
.where(raw("run_at <= now()"))
.order("run_at")
.limit(10)
.update(
{"owner": "w1", "run_at": expr("now() + ?", 30000), "attempts": expr("attempts + 1")},
returning=True
))
(db.collection("jobs")
.where("id", 1)
.where("owner", "w1")
.delete(require=1))
(db.collection("jobs")
.where(raw("run_at <= now()"))
.order("run_at")
.limit(10)
.delete(returning=["id", "payload.to"]))db.From("jobs").
Insert(
ctx,
fenecdb.D("payload", fenecdb.D("to", "ada"), "run_at", fenecdb.Expr("now()"), "attempts", 0),
fenecdb.D("payload", fenecdb.D("to", "bo"), "run_at", fenecdb.Expr("now()"), "attempts", 0),
)
db.From("jobs").
WhereCond(fenecdb.Raw("run_at <= now()")).
Order("run_at", "asc").
Limit(10).
Update(
ctx,
fenecdb.D("owner", "w1", "run_at", fenecdb.Expr("now() + ?", 30000), "attempts", fenecdb.Expr("attempts + 1")),
fenecdb.Returning("*"),
)
db.From("jobs").
Where("id", "=", 1).
Where("owner", "=", "w1").
Delete(ctx, fenecdb.Require(1))
db.From("jobs").
WhereCond(fenecdb.Raw("run_at <= now()")).
Order("run_at", "asc").
Limit(10).
Delete(ctx, fenecdb.Returning("id", "payload.to"))await db.From("jobs")
.InsertAsync(new object[] { new { payload = new { to = "ada" }, run_at = Computed.Expr("now()"), attempts = 0 }, new { payload = new { to = "bo" }, run_at = Computed.Expr("now()"), attempts = 0 } });
await db.From("jobs")
.Where(Cond.Raw("run_at <= now()"))
.Order("run_at")
.Limit(10)
.UpdateAsync(
new { owner = "w1", run_at = Computed.Expr("now() + ?", 30000), attempts = Computed.Expr("attempts + 1") },
returning: ["*"]
);
await db.From("jobs")
.Where("id", 1)
.Where("owner", "w1")
.DeleteAsync(require: 1);
await db.From("jobs")
.Where(Cond.Raw("run_at <= now()"))
.Order("run_at")
.Limit(10)
.DeleteAsync(returning: ["id", "payload.to"]);try await db.from("jobs")
.insert([["payload": ["to": "ada"], "run_at": .expr("now()"), "attempts": 0], ["payload": ["to": "bo"], "run_at": .expr("now()"), "attempts": 0]] as Value)
try await db.from("jobs")
.where(.raw("run_at <= now()"))
.order("run_at")
.limit(10)
.updateReturning(["owner": "w1", "run_at": .expr("now() + ?", 30000), "attempts": .expr("attempts + 1")] as Value)
try await db.from("jobs")
.where("id", 1)
.where("owner", "w1")
.delete(require: 1)
try await db.from("jobs")
.where(.raw("run_at <= now()"))
.order("run_at")
.limit(10)
.deleteReturning(returning: ["id", "payload.to"])db.from("jobs")
.insert(listOf(mapOf("payload" to mapOf("to" to "ada"), "run_at" to Computed.expr("now()"), "attempts" to 0), mapOf("payload" to mapOf("to" to "bo"), "run_at" to Computed.expr("now()"), "attempts" to 0)))
db.from("jobs")
.where(Cond.raw("run_at <= now()"))
.order("run_at")
.limit(10)
.updateReturning(mapOf("owner" to "w1", "run_at" to Computed.expr("now() + ?", 30000), "attempts" to Computed.expr("attempts + 1")))
db.from("jobs").where("id", 1).where("owner", "w1").delete(require = 1)
db.from("jobs")
.where(Cond.raw("run_at <= now()"))
.order("run_at")
.limit(10)
.deleteReturning(returning = listOf("id", "payload.to"))await db.from('jobs')
.insert([{'payload': {'to': 'ada'}, 'run_at': Computed.expr('now()'), 'attempts': 0}, {'payload': {'to': 'bo'}, 'run_at': Computed.expr('now()'), 'attempts': 0}]);
await db.from('jobs')
.where(Cond.raw('run_at <= now()'))
.order('run_at')
.limit(10)
.updateReturning({'owner': 'w1', 'run_at': Computed.expr('now() + ?', [30000]), 'attempts': Computed.expr('attempts + 1')});
await db.from('jobs')
.where('id', 1)
.where('owner', 'w1')
.delete(require: 1);
await db.from('jobs')
.where(Cond.raw('run_at <= now()'))
.order('run_at')
.limit(10)
.deleteReturning(returning: ['id', 'payload.to']);put jobs [{payload: $1, run_at: now(), attempts: $2}, {payload: $3, run_at: now(), attempts: $4}]
-- [{"to": "ada"}, 0, {"to": "bo"}, 0]
set jobs {owner: $1, run_at: now() + $2, attempts: attempts + 1} where run_at <= now() order run_at asc limit $3 returning *
-- ["w1", 30000, 10]
del jobs where id = $1 and owner = $2 require 1
-- [1, "w1"]
del jobs where run_at <= now() order run_at asc limit $1 returning id, payload.to
-- [10]- The page is a
get's.orderover an@sortedfield walks its index and stops at the page;limitwithoutordertakes the first matches byid;orderwithoutlimitwrites every match, in that order.offsetis a read's alone. - Two writers never pick the same row. The rows are found and
written under the write lock, where PostgreSQL's
UPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED) RETURNING *locks rows and skips those another holds. A client that reads the page and then writes it loses the race whenever another read the same page. returning *answers the id and every field, asselect *does; a list answers those named, paths among them. Over HTTP the answer is the rows, as aget's.require Ncounts the rows answered:limit 1 require 1is one row or a refusal.- A scoped token picks among the rows its rules let it update or
delete, and what it answers is held to its read rules as well — a
set's rows as it writes them, so a worker that reads only the jobs it holds is answered the job it claims, and asetthat would leave a row it may not read is refused whole (403). Every row it writes is held to its rules after the write, its fields to anupdate(…)grant's and its change to a transition's (Scoped access). A row answered shows the fields of the read rules admitting it, and a value, awhereor anorderreads a field a rule withholds only as a read may (Fields a read shows). - A sync replica refuses one on a synced collection: it would pick and answer the replica's rows, and the server, sent the text, its own.
- Every builder writes it:
orderandlimitbefore anupdateor adelete, and{ returning: true }— or the fields — as its option. See Job queues for the whole queue: ack, extend, backoff, dead letters, priorities. - An
orderkey may be an expression, so the nearest ready job is one claim too:set jobs {owner: $1} where run_at <= now() order distance(pickup, $2) limit 1 returning *(Ordering by an expression). A write takes nonear: the distance is a key oforderthere, or an innerget'snearpicks the id —where id in (get jobs select id where run_at <= now() near pickup $2 limit 1), run first under the same lock.
The rows an insert made
returning after a put or an
insert answers the rows it wrote rather than their count,
ids and all, in the order of its documents — what a client needs to know
the id of the row it made, without reading it back by a key of its
own:
create collection orders (item text, customer text, n int)
insert orders {item: "tea", customer: "ada"} returning id
insert orders [{item: "pie", customer: "ada"}, {item: "jam", customer: "bo", n: 2}] returning *
put orders {item: "tea", customer: "ada"} if absent else set {n: coalesce(n, 0) + 1} returning id, n- Each row as the statement left it: a row
if absentpassed over is not answered, the rowelse setset is, and a document written twice in one statement is answered twice, as it ended.require Ncounts the rows answered. - Over HTTP the answer is the rows, with the change it left the
database at in
Fenec-Seq;POST /orders?returning=idanswers a REST insert's rows (201) where it otherwise answers{"inserted": n}. - A scoped token is answered only rows its read rules let it
read, as a
set's are: each row written is held to them as written — a row made as an insert, a row set as an update — and one they leave out refuses the write, put back whole (403). An append to a stream the token may write and not read is written withoutreturning. - Every builder takes it:
insert(docs, { returning: ['id'] })andupsert(docs, patch, { returning: true }), Python'sreturning=, Go'sReturning(...)among the documents, .NET'sreturning:, Swift's, Kotlin's and Dart'sinsertReturningandupsertReturning. A sync replica refuses it on a synced collection, as a claim: its ids are the replica's until the server answers.
Bulk loading
For a bulk load, build the index afterwards. The write path becomes a pure append and the graph is built in one parallel pass.
create collection docs (title text, embed vector<768>)
put docs [ ... 100 000 documents ... ]
create index on docs (embed) @hnsw(cosine)Update and delete
set articles {year: 2025} where id = 42
set articles {views: coalesce(views, 0) + 1} where id = 42 -- an increment
del articles where year < 2000await db.from('articles').where('id', 42).update({ year: 2025 });
await db.from('articles').where('id', 42).update({ views: inc(1) });
await db.from('articles').where('year', '<', 2000).delete();db.collection("articles").where("id", 42).update({"year": 2025})
db.collection("articles").where("id", 42).update({"views": inc(1)})
db.collection("articles").where("year", "<", 2000).delete()db.From("articles").
Where("id", "=", 42).
Update(ctx, fenecdb.D("year", 2025))
db.From("articles").
Where("id", "=", 42).
Update(ctx, fenecdb.D("views", fenecdb.Inc(1)))
db.From("articles").Where("year", "<", 2000).Delete(ctx)await db.From("articles")
.Where("id", 42)
.UpdateAsync(new { year = 2025 });
await db.From("articles")
.Where("id", 42)
.UpdateAsync(new { views = Computed.Inc(1) });
await db.From("articles").Where("year", "<", 2000).DeleteAsync();try await db.from("articles")
.where("id", 42)
.update(["year": 2025] as Value)
try await db.from("articles")
.where("id", 42)
.update(["views": .inc(1)] as Value)
try await db.from("articles").where("year", "<", 2000).delete()db.from("articles").where("id", 42).update(mapOf("year" to 2025))
db.from("articles")
.where("id", 42)
.update(mapOf("views" to Computed.inc(1)))
db.from("articles").where("year", "<", 2000).delete()await db.from('articles').where('id', 42).update({'year': 2025});
await db.from('articles')
.where('id', 42)
.update({'views': Computed.inc(1)});
await db.from('articles').where('year', '<', 2000).delete();set articles {year: $1} where id = $2
-- [2025, 42]
set articles {views: coalesce(views, 0) + $1} where id = $2
-- [1, 42]
del articles where year < $1
-- [2000]