feat(db): durable:false skips the WAL append and replay
Task 4 of docs/superpowers/plans/2026-08-26-table-residency.md — the first behavioural change in the iteration. - db.c: one predicate, `table_is_durable`, gating the three EXISTING mutation sites. Kept as a function rather than an inlined condition so database/src/CODE-LOGIC.md's "nothing else may mutate storage" claim keeps holding — the choke points stayed three - the ack contract is untouched for durable tables: RAM applied, record staged, one commit before the ack, and a failed commit still removes the row - replay: a log holding records for a class the image now declares volatile is a real migration case, not corruption. apply_record returns -2 (distinct from -1), wo_wal_replay_ex reports the class id, and main.c names it and exits 2. `wo_wal_replay` stays as the NULL wrapper, so all 156 WAL unit checks are untouched - measured, not asserted: 50 inserts wrote 1500 WAL bytes into a durable table and ZERO into a volatile one. The file's SIZE proves nothing (it is fallocate'd to 1 MiB up front), so the gate measures the non-zero prefix BUG I INTRODUCED AND CAUGHT: the mismatch message first printed the class name with %s, but wo_str.data is `char data[]` with NO NUL terminator (obj.h) — a buffer over-read. Now %.*s with the explicit length, and re-verified under ASan. New gate `just residency` (8 checks), because everything above was otherwise a one-off manual measurement: restart behaviour, the zero-byte write path, the mismatch refusal (exit 2, names the class, NOT reported as corruption), and both compile-time refusals. Its own first run failed two checks for a bug in the script rather than the feature — `woc | grep` under `set -o pipefail` returns woc's exit 1 even when grep matches, since woc exits 1 whenever it reports diagnostics. Captures first now, with the reason noted inline. Also new: corpus run/table-volatile-inprocess pins that a volatile table is a FULL table in-process — same @unique enforcement, same index probe, same query surface. Only survival differs, and that is unobservable from inside one process. Gates: woc-test 557/0, 18 runtime suites 0 fail, cli_smoke OK, oop-e2e 119/0 (was 118), residency 8/0, employee 8/0, db-actor 8/0, site 21/0, ASan clean on the new replay path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
477f8d1b2b
commit
b74e13d21e
9 changed files with 266 additions and 6 deletions
|
|
@ -23,6 +23,21 @@ VM values ──copy──▶ row slots (engine-owned malloc) ──copy──
|
|||
the id hash maps id → slot. Ids are never reused (per-table counter,
|
||||
shard-interleaved `S+1, S+1+N, …`), which is also what makes the hash's
|
||||
tombstone sentinel safe.
|
||||
- **Storage is per-table since databasev2 2.** `@table(durable: false)` sets
|
||||
`WO_CLASSF_VOLATILE` in the class descriptor (`.wob` v7), and `db.c`'s
|
||||
`table_is_durable` gates all three mutation sites: a volatile table stages
|
||||
nothing, so it pays none of the fsync cost and is empty after a restart.
|
||||
Measured: 50 inserts wrote 1500 WAL bytes durable, **0** volatile. The three
|
||||
sites stayed three — the predicate is one function, not an inlined condition,
|
||||
precisely so this file's "nothing else may mutate storage" claim keeps
|
||||
holding.
|
||||
- **A mode mismatch refuses, it does not convert.** If the log holds records
|
||||
for a class the loaded image now declares volatile, `apply_record` returns
|
||||
**-2** (distinct from -1 corruption) and `wo_wal_replay_ex` reports the class
|
||||
id so `main.c` can name it. Silently skipping those records would resurrect
|
||||
nothing but would also hide a real migration; silently applying them would
|
||||
load rows into a table declared not to have any. `wo_wal_replay` remains as
|
||||
the NULL-out-param wrapper so the 156 WAL unit checks are untouched.
|
||||
- **Choke points**: `wo_row_insert` / `wo_row_remove` carry the `INDEX HOOK`
|
||||
comments where Task 4's secondary indexes attach and Task 2's WAL stages
|
||||
its record. Nothing else may mutate storage.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@
|
|||
#include "table.h"
|
||||
#include "wal.h"
|
||||
|
||||
/* databasev2 2: is this table's storage durable? A `@table(durable: false)`
|
||||
* class carries WO_CLASSF_VOLATILE and is never staged to the WAL — no
|
||||
* record, no fsync, ack straight from RAM. One predicate for all three
|
||||
* mutation sites below: `database/src/CODE-LOGIC.md` names those as the only
|
||||
* places storage may be staged, and that invariant is worth more than the
|
||||
* convenience of inlining this. cid is always loader-validated by the time a
|
||||
* mutation has succeeded, so no bounds check is added here. */
|
||||
static int table_is_durable(const wo_db *db, uint32_t cid) {
|
||||
return (db->classes[cid].flags & WO_CLASSF_VOLATILE) == 0u;
|
||||
}
|
||||
|
||||
int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
||||
uint32_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
|
||||
wo_db *db = (wo_db *)vm->rt.db;
|
||||
|
|
@ -24,7 +35,7 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
: ek == DB_ERR_OOM ? WO_T_OOM
|
||||
: WO_T_DB;
|
||||
wo_wal *w = (wo_wal *)vm->rt.wal;
|
||||
if (w) {
|
||||
if (w && table_is_durable(db, cid)) {
|
||||
/* RAM applied, record staged, ONE commit before the ack (the
|
||||
* builtin's return). A failed commit is a failed write: the
|
||||
* row is removed again so RAM never claims what disk never
|
||||
|
|
@ -46,7 +57,7 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
if (wo_row_update_field(db, cid, id, field, R[B + 3], msg, &ek) != 0)
|
||||
return ek == DB_ERR_UNIQUE ? WO_T_UNIQUE : ek == DB_ERR_OOM ? WO_T_OOM : WO_T_DB;
|
||||
wo_wal *w = (wo_wal *)vm->rt.wal;
|
||||
if (w) {
|
||||
if (w && table_is_durable(db, cid)) {
|
||||
if (wo_wal_append_update(w, db, cid, id) != 0 || wo_wal_commit(w) != 0) {
|
||||
*msg = "wal commit failed"; /* RAM ahead of disk: trap, do not ack */
|
||||
return WO_T_IO;
|
||||
|
|
@ -69,7 +80,7 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
return WO_T_DB;
|
||||
}
|
||||
wo_wal *w = (wo_wal *)vm->rt.wal;
|
||||
if (w) {
|
||||
if (w && table_is_durable(db, cid)) {
|
||||
if (wo_wal_append_remove(w, cid, id) != 0 || wo_wal_commit(w) != 0) {
|
||||
*msg = "wal commit failed";
|
||||
return WO_T_IO;
|
||||
|
|
|
|||
|
|
@ -412,6 +412,12 @@ static int apply_record(wo_db *db, const uint8_t *payload, uint32_t len) {
|
|||
uint32_t cid = rd_u32(&r);
|
||||
uint64_t id = rd_u64(&r);
|
||||
if (r.bad || cid >= db->class_cnt) return -1;
|
||||
/* databasev2 2: this log holds records for a class the CURRENT source
|
||||
* declares `durable: false`. Not corruption — a real migration case (the
|
||||
* table used to be durable). Refuse rather than convert, and refuse
|
||||
* rather than silently resurrect rows into a table declared not to have
|
||||
* any. -2 so the caller can say which of the two it is. */
|
||||
if (db->classes[cid].flags & WO_CLASSF_VOLATILE) return -2;
|
||||
if (kind == WO_WAL_REMOVE) return wo_row_remove(db, cid, id);
|
||||
if (kind != WO_WAL_INSERT && kind != WO_WAL_UPDATE) return -1;
|
||||
if (kind == WO_WAL_UPDATE) {
|
||||
|
|
@ -444,7 +450,7 @@ static int apply_record(wo_db *db, const uint8_t *payload, uint32_t len) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int64_t wo_wal_replay(const char *path, wo_db *db) {
|
||||
int64_t wo_wal_replay_ex(const char *path, wo_db *db, uint32_t *volatile_cid) {
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return errno == ENOENT ? 0 : -1; /* no WAL yet = fresh boot */
|
||||
uint64_t off = 0;
|
||||
|
|
@ -453,11 +459,17 @@ int64_t wo_wal_replay(const char *path, wo_db *db) {
|
|||
uint32_t len;
|
||||
uint8_t *payload;
|
||||
if (scan_record(fd, off, &len, &payload) != 0) break; /* intact prefix ends */
|
||||
/* peek the class id before applying, so a -2 can name it */
|
||||
uint32_t rec_cid = len >= 5u ? (uint32_t)payload[1] | ((uint32_t)payload[2] << 8)
|
||||
| ((uint32_t)payload[3] << 16)
|
||||
| ((uint32_t)payload[4] << 24)
|
||||
: 0u;
|
||||
int rc = apply_record(db, payload, len);
|
||||
free(payload);
|
||||
if (rc != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
if (rc == -2 && volatile_cid) *volatile_cid = rec_cid;
|
||||
return rc == -2 ? -2 : -1;
|
||||
}
|
||||
off += 8u + len + 4u;
|
||||
applied++;
|
||||
|
|
@ -466,6 +478,10 @@ int64_t wo_wal_replay(const char *path, wo_db *db) {
|
|||
return applied;
|
||||
}
|
||||
|
||||
int64_t wo_wal_replay(const char *path, wo_db *db) {
|
||||
return wo_wal_replay_ex(path, db, NULL);
|
||||
}
|
||||
|
||||
int64_t wo_wal_check(const char *path, uint64_t *intact_bytes) {
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ int wo_wal_commit(wo_wal *w);
|
|||
* the intact prefix and reports it. */
|
||||
int64_t wo_wal_replay(const char *path, wo_db *db);
|
||||
|
||||
/* databasev2 2: as wo_wal_replay, but distinguishes the two failure kinds.
|
||||
* Returns the applied count on success; -1 on corruption beyond a torn tail;
|
||||
* -2 when the log holds records for a class the loaded image declares
|
||||
* `durable: false`, writing that class id through [volatile_cid] if non-NULL.
|
||||
* The plain wo_wal_replay above is this with NULL, kept so the existing
|
||||
* callers and the 156 WAL unit checks are untouched. */
|
||||
int64_t wo_wal_replay_ex(const char *path, wo_db *db, uint32_t *volatile_cid);
|
||||
|
||||
/* Offline verification (no engine): scan [path], count intact records.
|
||||
* *intact_bytes (optional) = where the intact prefix ends. -1 = open
|
||||
* failure. */
|
||||
|
|
|
|||
9
justfile
9
justfile
|
|
@ -66,6 +66,15 @@ fibers:
|
|||
db-actor:
|
||||
./scripts/db-actor-accept.sh
|
||||
|
||||
# residency: databasev2 2's gate — per-table storage. The corpus covers the
|
||||
# in-process half; this covers what one process cannot see: a volatile table
|
||||
# empty after restart while its durable sibling replays, volatile inserts
|
||||
# writing ZERO WAL bytes (measured against the fallocate'd file's non-zero
|
||||
# prefix, since its size proves nothing), the mode-mismatch startup refusal,
|
||||
# and the two compile-time refusals.
|
||||
residency:
|
||||
./scripts/residency-accept.sh
|
||||
|
||||
# db-bench: iteration 22's campaign (docs/examples/db-bench) — OFF the
|
||||
# fast path, minutes long: ram+durable x 1/N shards, durability legs,
|
||||
# gates vs bench/baseline.json. quick = seconds, floors only.
|
||||
|
|
|
|||
|
|
@ -200,7 +200,36 @@ int main(int argc, char **argv) {
|
|||
if (data_dir && data_dir[0]) {
|
||||
char wal_path[512];
|
||||
snprintf(wal_path, sizeof wal_path, "%s/shard-0.wal", data_dir);
|
||||
if (wo_wal_replay(wal_path, &DB) < 0) {
|
||||
uint32_t vol_cid = 0;
|
||||
int64_t replayed = wo_wal_replay_ex(wal_path, &DB, &vol_cid);
|
||||
if (replayed == -2) {
|
||||
/* databasev2 2: not corruption — this log was written when the
|
||||
* table was durable and the source now says `durable: false`.
|
||||
* Refusing beats converting, and beats resurrecting rows into a
|
||||
* table declared not to have any. Name the class so the fix is
|
||||
* obvious. */
|
||||
/* wo_str.data is NOT NUL-terminated (obj.h), so the name must be
|
||||
* printed with an explicit length — %s here would over-read. */
|
||||
const char *cname = "?";
|
||||
int cnlen = 1;
|
||||
if (vol_cid < mod.class_cnt) {
|
||||
uint32_t k = mod.classes[vol_cid].name;
|
||||
if (k < mod.const_cnt && mod.consts[k].s) {
|
||||
cname = mod.consts[k].s->data;
|
||||
cnlen = (int)mod.consts[k].s->len;
|
||||
}
|
||||
}
|
||||
fprintf(stderr,
|
||||
"wovm: %s: holds records for `%.*s`, which this program declares "
|
||||
"`@table(durable: false)` — refusing to start. Either restore "
|
||||
"`durable: true` for that table, or remove the data directory.\n",
|
||||
wal_path, cnlen, cname);
|
||||
wo_db_destroy(&DB);
|
||||
wo_vm_destroy(&VM);
|
||||
wo_module_free(&mod);
|
||||
return 2;
|
||||
}
|
||||
if (replayed < 0) {
|
||||
fprintf(stderr, "wovm: %s: replay found corruption beyond a torn tail\n", wal_path);
|
||||
wo_db_destroy(&DB);
|
||||
wo_vm_destroy(&VM);
|
||||
|
|
|
|||
138
scripts/residency-accept.sh
Executable file
138
scripts/residency-accept.sh
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/residency-accept.sh — databasev2 2's gate: per-table storage.
|
||||
#
|
||||
# The corpus proves the in-process half (tests/corpus/run/table-volatile-inprocess,
|
||||
# .../table-residency-legal, .../compile-fail/table-durable-ref-volatile). This
|
||||
# script proves the half a single process cannot observe:
|
||||
# * a volatile table is EMPTY after a restart while its durable sibling replays
|
||||
# * volatile inserts write ZERO bytes to the WAL — measured, not asserted,
|
||||
# because the file is fallocate'd to 1 MiB up front so its SIZE proves
|
||||
# nothing; what is measured is the non-zero prefix actually written
|
||||
# * a mode mismatch (log holds records for a table the source now declares
|
||||
# volatile) REFUSES to start, exits 2, and names the class
|
||||
# * the compile-time refusals still fire
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
WOC="compiler/_build/default/bin/woc"
|
||||
WOVM="runtime/wovm"
|
||||
pass=0; fail=0
|
||||
ok() { echo "ok $1"; pass=$((pass + 1)); }
|
||||
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
|
||||
|
||||
if [[ ! -x "$WOC" || ! -x "$WOVM" ]]; then
|
||||
echo "residency-accept: build woc and wovm first (just woc-build && just wovm-build)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/residency-accept.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# non-zero prefix of a fallocate'd WAL = bytes actually written
|
||||
wal_bytes() {
|
||||
python3 -c "import sys;d=open(sys.argv[1],'rb').read();print(len(d.rstrip(b'\x00')))" "$1"
|
||||
}
|
||||
|
||||
# ---- 1. restart: durable replays, volatile does not -----------------------
|
||||
cat > "$WORK/mix.wo" <<'EOF'
|
||||
@table(name: "kept", index: [k])
|
||||
class Kept { k: Text }
|
||||
@table(name: "scratch", durable: false, index: [k])
|
||||
class Scratch { k: Text }
|
||||
fn main(args: multi Text) -> Int {
|
||||
if len(args) > 0 and args[0] == "seed" {
|
||||
insert Kept { k: "a" };
|
||||
insert Scratch { k: "b" };
|
||||
return 0;
|
||||
}
|
||||
let nk = 0;
|
||||
for x in from r in Kept select r { nk = nk + 1; }
|
||||
let ns = 0;
|
||||
for x in from r in Scratch select r { ns = ns + 1; }
|
||||
print("kept=${nk} scratch=${ns}");
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
if "$WOC" --emit "$WORK/mix.wo" -o "$WORK/mix.wob" 2>"$WORK/e"; then
|
||||
mkdir -p "$WORK/d1"
|
||||
WO_DATA="$WORK/d1" "$WOVM" "$WORK/mix.wob" seed >/dev/null 2>&1
|
||||
got="$(WO_DATA="$WORK/d1" "$WOVM" "$WORK/mix.wob" 2>&1)"
|
||||
[[ "$got" == "kept=1 scratch=0" ]] \
|
||||
&& ok "restart: durable row replays, volatile row is gone" \
|
||||
|| bad "restart: durable replays, volatile gone" "got: $got"
|
||||
else
|
||||
bad "restart fixture compiles" "$(head -1 "$WORK/e")"
|
||||
fi
|
||||
|
||||
# ---- 2. the write-path saving, measured ------------------------------------
|
||||
cat > "$WORK/only.wo" <<'EOF'
|
||||
@table(name: "dur", index: [k])
|
||||
class Dur { k: Text }
|
||||
@table(name: "vol", durable: false, index: [k])
|
||||
class Vol { k: Text }
|
||||
fn main(args: multi Text) -> Int {
|
||||
let n = 0;
|
||||
while n < 50 {
|
||||
if args[0] == "dur" { insert Dur { k: "x" }; } else { insert Vol { k: "x" }; }
|
||||
n = n + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
if "$WOC" --emit "$WORK/only.wo" -o "$WORK/only.wob" 2>"$WORK/e"; then
|
||||
for m in dur vol; do
|
||||
mkdir -p "$WORK/w_$m"
|
||||
WO_DATA="$WORK/w_$m" "$WOVM" "$WORK/only.wob" "$m" >/dev/null 2>&1
|
||||
done
|
||||
db="$(wal_bytes "$WORK/w_dur/shard-0.wal")"
|
||||
vb="$(wal_bytes "$WORK/w_vol/shard-0.wal")"
|
||||
[[ "$vb" -eq 0 ]] \
|
||||
&& ok "50 volatile inserts write 0 WAL bytes (durable wrote $db)" \
|
||||
|| bad "volatile inserts write nothing" "volatile wrote $vb bytes"
|
||||
[[ "$db" -gt 0 ]] \
|
||||
&& ok "50 durable inserts do write to the WAL ($db bytes)" \
|
||||
|| bad "durable inserts still log" "durable wrote $db bytes"
|
||||
else
|
||||
bad "measurement fixture compiles" "$(head -1 "$WORK/e")"
|
||||
fi
|
||||
|
||||
# ---- 3. mode mismatch refuses, exits 2, names the class --------------------
|
||||
printf '@table(name: "orders", index: [k])\nclass Orders { k: Text }\nfn main() -> Int { insert Orders { k: "a" }; return 0; }\n' > "$WORK/wasdur.wo"
|
||||
printf '@table(name: "orders", durable: false, index: [k])\nclass Orders { k: Text }\nfn main() -> Int { print("started"); return 0; }\n' > "$WORK/nowvol.wo"
|
||||
if "$WOC" --emit "$WORK/wasdur.wo" -o "$WORK/a.wob" 2>/dev/null \
|
||||
&& "$WOC" --emit "$WORK/nowvol.wo" -o "$WORK/b.wob" 2>/dev/null; then
|
||||
mkdir -p "$WORK/d3"
|
||||
WO_DATA="$WORK/d3" "$WOVM" "$WORK/a.wob" >/dev/null 2>&1
|
||||
out="$(WO_DATA="$WORK/d3" "$WOVM" "$WORK/b.wob" 2>&1)"; rc=$?
|
||||
[[ $rc -eq 2 ]] \
|
||||
&& ok "mode mismatch exits 2 (refuses to start)" \
|
||||
|| bad "mode mismatch exits 2" "exit=$rc"
|
||||
grep -q 'Orders' <<<"$out" \
|
||||
&& ok "mode mismatch names the offending class" \
|
||||
|| bad "mode mismatch names the class" "got: $out"
|
||||
grep -q 'corruption' <<<"$out" \
|
||||
&& bad "mismatch is not reported as corruption" "got: $out" \
|
||||
|| ok "mode mismatch is not misreported as corruption"
|
||||
else
|
||||
bad "mismatch fixtures compile" "compile failed"
|
||||
fi
|
||||
|
||||
# ---- 4. the compile-time refusals still fire ------------------------------
|
||||
printf '@table(name: "x", durable: false, resident: keys)\nclass X { k: Text }\nfn main() -> Int { return 0; }\n' > "$WORK/combo.wo"
|
||||
# NOTE: capture, then grep. `woc | grep` under `set -o pipefail` returns
|
||||
# woc's exit 1 (it reports diagnostics) even when grep matched, which made
|
||||
# both of these checks fail while the compiler was behaving correctly.
|
||||
combo_out="$("$WOC" "$WORK/combo.wo" 2>&1)"
|
||||
grep -q 'WO-E102' <<<"$combo_out" \
|
||||
&& ok "durable:false + resident:keys is WO-E102" \
|
||||
|| bad "combination refused" "got: $combo_out"
|
||||
|
||||
printf '@table(name: "s", durable: false)\nclass S { t: Text }\n@table(name: "o")\nclass O { s: ref S }\nfn main() -> Int { return 0; }\n' > "$WORK/dref.wo"
|
||||
dref_out="$("$WOC" "$WORK/dref.wo" 2>&1)"
|
||||
grep -q 'WO-E224' <<<"$dref_out" \
|
||||
&& ok "durable ref into a volatile table is WO-E224" \
|
||||
|| bad "dangling ref refused" "got: $dref_out"
|
||||
|
||||
echo
|
||||
echo "residency-accept: $((pass + fail)) checks, $fail failures"
|
||||
[[ $fail -eq 0 ]] || exit 1
|
||||
4
tests/corpus/run/table-volatile-inprocess/fixture.out
Normal file
4
tests/corpus/run/table-volatile-inprocess/fixture.out
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
rows 2
|
||||
unique refused
|
||||
who asha token t1
|
||||
who asha token t2
|
||||
30
tests/corpus/run/table-volatile-inprocess/fixture.wo
Normal file
30
tests/corpus/run/table-volatile-inprocess/fixture.wo
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-- databasev2 2: a `durable: false` table is a FULL table for the life of the
|
||||
-- process — same indexes, same @unique enforcement, same FK restrict, same
|
||||
-- query surface. Only its survival differs, and that cannot be observed from
|
||||
-- inside one process, so this fixture pins the in-process half. The restart
|
||||
-- half is scripts/residency-accept.sh.
|
||||
|
||||
@table(name: "sessions", durable: false, index: [who])
|
||||
class Session {
|
||||
token: Text @unique
|
||||
who: Text
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
insert Session { token: "t1", who: "asha" };
|
||||
insert Session { token: "t2", who: "asha" };
|
||||
|
||||
let n = 0;
|
||||
for s in from x in Session select x { n = n + 1; }
|
||||
print("rows ${n}");
|
||||
|
||||
-- @unique still bites on a volatile table
|
||||
let dup = try insert Session { token: "t1", who: "eve" } catch (e) nil;
|
||||
if dup == nil { print("unique refused"); }
|
||||
|
||||
-- the index-backed probe still works
|
||||
for s in from x in Session where x.who == "asha" order by x.token select x {
|
||||
print("who ${s.who} token ${s.token}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue