feat: FK restrict on delete + employee sample runs; group-by parked (9b)
- FK restrict: deleting a row a non-nullable `ref` still points at traps WO_T_FK (11), catchable. The compiler now records a `ref` field's target class in the class-table field_class metadata; the engine (wo_row_has_referrers) scans referencing scalar columns before a delete. Correctness-first full scan; the backlink-index optimization is recorded for later - docs/examples/employee now COMPILES AND RUNS all six modes against a WAL-durable database: seed (+@unique trap across restart), report (per-dept aggregates + payroll), staff (unique probe + backlink + ref nav), raise (update-through-row), drop (FK restrict), and persistence via replay - group-by SYNTAX parked to a future iteration (user decision): the report mode is hand-rolled from the shipped primitives meanwhile (same numbers). "table relations and FK" is complete - scripts/employee-accept.sh (8 checks) + a `just employee` module; manifest parser tolerates iteration 9c's [share]/[[share.clients]] sections so `woc .` builds the sample on this branch - fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch; oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0, employee-accept 8/0 - 9b story + status board updated Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa4b282934
commit
65e5739492
17 changed files with 282 additions and 16 deletions
|
|
@ -600,10 +600,23 @@ let manifest_parse (path : string) : (string * string) list =
|
|||
if line = "" || (String.length line >= 1 && line.[0] = '#') then ()
|
||||
else if line.[0] = '[' then begin
|
||||
if line.[String.length line - 1] <> ']' then fail !lineno "malformed section header";
|
||||
section := String.sub line 1 (String.length line - 2);
|
||||
if !section <> "runtime" && !section <> "build" then
|
||||
fail !lineno (Printf.sprintf "unknown section [%s] (runtime and build exist)" !section)
|
||||
(* accept `[[table.array]]` headers too (iteration 9c's
|
||||
[[share.clients]]) by trimming the doubled brackets *)
|
||||
let inner = String.sub line 1 (String.length line - 2) in
|
||||
let inner =
|
||||
if String.length inner >= 2 && inner.[0] = '[' && inner.[String.length inner - 1] = ']'
|
||||
then String.sub inner 1 (String.length inner - 2)
|
||||
else inner
|
||||
in
|
||||
section := inner;
|
||||
if !section <> "runtime" && !section <> "build" && !section <> "share"
|
||||
&& !section <> "share.clients"
|
||||
then
|
||||
fail !lineno
|
||||
(Printf.sprintf "unknown section [%s] (runtime and build exist)" !section)
|
||||
end
|
||||
else if !section = "share" || !section = "share.clients" then
|
||||
() (* iteration 9c manifest keys — parsed by the attach feature, ignored here *)
|
||||
else
|
||||
match String.index_opt line '=' with
|
||||
| None -> fail !lineno "expected `key = \"value\"`"
|
||||
|
|
|
|||
|
|
@ -1426,7 +1426,8 @@ let field_class_meta (p : pctx) (ty : Ast.field_ty) : int =
|
|||
match name_of (Ast.Scalar e) with
|
||||
| Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||
| None -> wob_none)
|
||||
| Ast.Ref _ | Ast.Backlink _ | Ast.Nullable _ -> wob_none
|
||||
| Ast.Ref n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||
| Ast.Backlink _ | Ast.Nullable _ -> wob_none
|
||||
|
||||
let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||
match unwrap ty with
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ int wo_builtin_db(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
case WO_B_DB_DELETE: {
|
||||
uint32_t cid = (uint32_t)R[B];
|
||||
uint64_t id = R[B + 1];
|
||||
/* FK restrict: refuse if another row still references this one
|
||||
(iteration 9b) — nothing is removed, the statement traps */
|
||||
if (wo_row_has_referrers(db, cid, id)) {
|
||||
*msg = "row is still referenced (restrict)";
|
||||
return WO_T_FK;
|
||||
}
|
||||
if (wo_row_remove(db, cid, id) != 0) {
|
||||
*msg = "no such row";
|
||||
return WO_T_DB;
|
||||
|
|
|
|||
|
|
@ -703,6 +703,28 @@ int wo_row_update_field(wo_db *db, uint32_t class_id, uint64_t id, uint32_t fiel
|
|||
return 0;
|
||||
}
|
||||
|
||||
int wo_row_has_referrers(wo_db *db, uint32_t class_id, uint64_t id) {
|
||||
if (!id) return 0;
|
||||
for (uint32_t c = 0; c < db->class_cnt; c++) {
|
||||
const wo_classdesc *cd = &db->classes[c];
|
||||
db_table *t = &db->tables[c];
|
||||
if (!t->row_size || !cd->field_class) continue;
|
||||
for (uint32_t fld = 0; fld < cd->field_cnt; fld++) {
|
||||
/* a scalar column whose recorded field_class is our target is a
|
||||
`ref` to it (WOB_NONE / JSON_RAW / NIL_SCALAR are not class ids) */
|
||||
if (cd->kinds[fld] != WO_K_SCALAR || cd->field_class[fld] != class_id) continue;
|
||||
uint32_t total = t->slab_cnt * DB_SLAB_ROWS;
|
||||
for (uint32_t g = 0; g < total; g++) {
|
||||
if (!(t->bitmap[g >> 6] & (1ull << (g & 63)))) continue;
|
||||
db_row *r = (db_row *)(t->slabs[g / DB_SLAB_ROWS] +
|
||||
(size_t)(g % DB_SLAB_ROWS) * t->row_size);
|
||||
if (r->slots[fld] == id) return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id) {
|
||||
if (class_id >= db->class_cnt) return -1;
|
||||
db_table *t = &db->tables[class_id];
|
||||
|
|
|
|||
|
|
@ -150,6 +150,14 @@ int wo_row_read(wo_db *db, wo_rt *rt, uint32_t class_id, uint64_t id,
|
|||
* it. 0 ok, -1 no such row. */
|
||||
int wo_row_remove(wo_db *db, uint32_t class_id, uint64_t id);
|
||||
|
||||
/* iteration 9b FK restrict: 1 if some row in some class holds a non-nullable
|
||||
* `ref` to [class_id] equal to [id] — i.e. deleting this row would dangle a
|
||||
* reference. The compiler records a ref field's target class in the class
|
||||
* table's field_class metadata; this scans those columns. Correctness-first
|
||||
* (a full scan of referencing tables); the backlink index is the later
|
||||
* optimization the spec records. */
|
||||
int wo_row_has_referrers(wo_db *db, uint32_t class_id, uint64_t id);
|
||||
|
||||
/* Update one field in place (iteration 9 Task 5): encode the VM value,
|
||||
* swap it into the slot, keep every index containing that column honest —
|
||||
* remove-old/add-new with the unique re-check running BEFORE anything
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⏸ off the workload's path (no `@gc`) |
|
||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
||||
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b |
|
||||
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ spec + plan ready (2026-08-15) |
|
||||
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | 🔄 query surface + relations + FK done (branch query-surface); group-by parked |
|
||||
| 9c | [Cross-program tables](stories/language-runtime-database/09c-cross-program-tables.md) | 🔄 channel done (branch ipc-attach); manifest+binding pending |
|
||||
| 9d | [Keypair attach auth](stories/language-runtime-database/09d-keypair-attach-auth.md) | 🔄 crypto+handshake done (branch keypair-auth); manifest pending |
|
||||
| 9e | [Durability, throughput, scale](stories/language-runtime-database/09e-durability-throughput-scale.md) | ⬜ needs a spec first |
|
||||
|
|
|
|||
14
docs/examples/employee/justfile
Normal file
14
docs/examples/employee/justfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# docs/examples/employee — the database track's acceptance workload.
|
||||
# `just employee::<recipe>` from the repo root, or plain `just <recipe>` here.
|
||||
ROOT := source_directory() / "../../.."
|
||||
|
||||
default: accept
|
||||
|
||||
# build the standalone binary from wo.toml (needs woc-build + wovm-build once)
|
||||
build:
|
||||
{{ROOT}}/compiler/_build/default/bin/woc .
|
||||
@ls -la target/employee
|
||||
|
||||
# the acceptance: compile + every mode against a WAL-durable database
|
||||
accept:
|
||||
{{ROOT}}/scripts/employee-accept.sh
|
||||
|
|
@ -48,19 +48,34 @@ fn seed() -> Int {
|
|||
return 0;
|
||||
}
|
||||
|
||||
-- One GROUP BY after another: group-and-reduce lowers to a single hash pass
|
||||
-- (no group objects), avg/min/max are ?Int because an empty group is data.
|
||||
-- Per-department aggregates. The group-by SYNTAX
|
||||
-- from e in Employee group e by e.dept into g order by avg(g.salary) desc
|
||||
-- select { dept: g.key.name, headcount: count(g), avg_salary: avg(g.salary), ... }
|
||||
-- is PARKED for a future iteration (compile-time group-and-reduce + projection
|
||||
-- records). Until it lands, the same report is hand-rolled from the primitives
|
||||
-- that DO exist — a scan of departments, a backlink scan of each one's staff,
|
||||
-- and plain scalar accumulation. Same numbers, more lines; the group-by
|
||||
-- version is the ergonomic upgrade, not a new capability.
|
||||
fn report() -> Int {
|
||||
let rows = from e in Employee
|
||||
group e by e.dept into g
|
||||
order by avg(g.salary) desc
|
||||
select { dept: g.key.name, headcount: count(g),
|
||||
avg_salary: avg(g.salary), min_salary: min(g.salary),
|
||||
max_salary: max(g.salary) };
|
||||
for r in rows {
|
||||
print("DEPT ${r.dept} headcount=${r.headcount} avg=${r.avg_salary} min=${r.min_salary} max=${r.max_salary}");
|
||||
let payroll = 0;
|
||||
for d in from x in Department order by x.name select x {
|
||||
let headcount = 0;
|
||||
let total = 0;
|
||||
let smin = -1;
|
||||
let smax = -1;
|
||||
for e in from s in d.staff select s {
|
||||
headcount = headcount + 1;
|
||||
total = total + e.salary;
|
||||
payroll = payroll + e.salary;
|
||||
if smin == -1 or e.salary < smin { smin = e.salary; }
|
||||
if smax == -1 or e.salary > smax { smax = e.salary; }
|
||||
}
|
||||
if headcount == 0 {
|
||||
print("DEPT ${d.name} headcount=0 avg=nil min=nil max=nil");
|
||||
} else {
|
||||
print("DEPT ${d.name} headcount=${headcount} avg=${total / headcount} min=${smin} max=${smax}");
|
||||
}
|
||||
}
|
||||
let payroll = sum(from e in Employee select e.salary);
|
||||
print("PAYROLL ${payroll}");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
BIN
docs/examples/employee/target/employee
Executable file
BIN
docs/examples/employee/target/employee
Executable file
Binary file not shown.
|
|
@ -132,6 +132,35 @@ validation (`WO-E102`), the Rust runtime already ships secondary indexes and
|
|||
id rather than a pointer — so the relational vocabulary partly exists and this
|
||||
iteration makes it mean something in the C stack.
|
||||
|
||||
## Query surface landed (2026-08-16, branch `query-surface`)
|
||||
|
||||
The compiler-checked query surface runs end to end, proven by
|
||||
`docs/examples/employee` (8-check acceptance, `scripts/employee-accept.sh`):
|
||||
|
||||
- **Queries**: `from <v> in <table|nav> where* [order by <k> [desc]] [take n]
|
||||
select <v|v.field>`, lowered to bytecode loops over engine cursor builtins
|
||||
(DB_SCAN / DB_GET_FIELD / DB_PROBE) — no SQL text, disassembly-provable.
|
||||
- **Relations**: `ref C` forward navigation (`e.dept.name`, a point read),
|
||||
`backlink C.f` reverse navigation (`d.staff`, an index probe); backlink
|
||||
fields are virtual (no stored column).
|
||||
- **Mutation**: update-through-row (`e.salary = v` → DB_UPDATE_FIELD),
|
||||
`delete <row>`, and **FK restrict** — deleting a row a `ref` still points at
|
||||
traps `WO_T_FK` (the compiler records the ref target in the class table's
|
||||
field_class metadata; the engine scans referencing columns).
|
||||
- **`@unique`** violations trap and are catchable; everything is WAL-durable
|
||||
and survives a process restart (proven in the acceptance).
|
||||
|
||||
**PARKED to a future iteration (2026-08-16, user decision):** **group-by
|
||||
aggregation** — the `group … by … into g … select { count(g), avg(g.salary),
|
||||
… }` syntax, which needs projection-record synthesis (anonymous record types),
|
||||
aggregate clause-functions, and two-phase hash aggregation. The employee
|
||||
sample's `report` mode is hand-rolled from the shipped primitives meanwhile
|
||||
(a scan of departments × a backlink scan of each one's staff × scalar
|
||||
accumulation) — same numbers, and the group-by version is the ergonomic
|
||||
upgrade, not a new capability. The relational vocabulary these queries used
|
||||
(`ref`/`backlink`/`@unique`/restrict) is the "table relations and FK" half,
|
||||
now complete.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
- ~~Brainstorm a spec first~~ — **done 2026-08-15**; the spec settles all
|
||||
|
|
|
|||
5
justfile
5
justfile
|
|
@ -50,6 +50,11 @@ wovm-test:
|
|||
# pass; the corpus below gates the individual behaviors underneath it.
|
||||
mod log-watcher "docs/examples/log-watcher"
|
||||
|
||||
# the database track's acceptance workload (iteration 9/9b): @table storage,
|
||||
# ref/backlink relations + FK restrict, and the compiler-checked query surface
|
||||
# (scan/where/select/order/take, update, delete). `just employee` runs it.
|
||||
mod employee "docs/examples/employee"
|
||||
|
||||
# conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap},
|
||||
# exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md.
|
||||
# Fails loudly (and names the recipe to run) if woc or wovm isn't built.
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ enum {
|
|||
raised by the engine at the row choke point, catchable like any
|
||||
trap (the employee sample's SEED-DUP line) */
|
||||
WO_T_UNIQUE = 10,
|
||||
/* iteration 9b: deleting a row still referenced by a `ref` traps here
|
||||
(restrict) — the employee sample's DROP-of-a-department-with-staff */
|
||||
WO_T_FK = 11,
|
||||
};
|
||||
|
||||
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
|
||||
|
|
|
|||
106
scripts/employee-accept.sh
Executable file
106
scripts/employee-accept.sh
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/employee-accept.sh — the database track's acceptance workload.
|
||||
#
|
||||
# docs/examples/employee must compile via `woc <dir>` and run all its modes
|
||||
# against a real WAL-durable database: insert + @unique trap, per-department
|
||||
# aggregates, ref/backlink navigation, update-through-row, FK restrict on
|
||||
# delete, and persistence across a process restart. This is iteration 9/9b's
|
||||
# acceptance the way log-watcher is iterations 1-7's.
|
||||
#
|
||||
# Group-by SYNTAX is parked (a future iteration); report is hand-rolled from
|
||||
# the primitives, so the numbers below exercise the shipped query surface.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||
WOVM="$ROOT/runtime/wovm"
|
||||
SAMPLE="$ROOT/docs/examples/employee"
|
||||
|
||||
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 "employee-accept: build woc and wovm first (just woc-build; just wovm-build)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/emp-accept.XXXXXX")"
|
||||
DATA="$WORK/data"
|
||||
mkdir -p "$DATA"
|
||||
IMG="$WORK/employee.wob"
|
||||
cleanup() { [[ -n "${EMP_ACCEPT_KEEP:-}" ]] && echo "kept $WORK" || rm -rf "$WORK"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# ---- 1. compile ------------------------------------------------------
|
||||
if "$WOC" --emit "$SAMPLE" -o "$IMG" >"$WORK/compile.out" 2>&1; then
|
||||
ok "compile ($(stat -c%s "$IMG") bytes)"
|
||||
else
|
||||
bad "compile" "$(head -1 "$WORK/compile.out")"
|
||||
echo; printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"; exit 1
|
||||
fi
|
||||
|
||||
run() { WO_DATA="$DATA" "$WOVM" "$IMG" "$@"; }
|
||||
|
||||
# ---- 2. seed (insert + WAL) ------------------------------------------
|
||||
if run seed 2>&1 | grep -q "^SEEDED 3 departments, 6 employees"; then
|
||||
ok "seed (insert, WAL-durable)"
|
||||
else
|
||||
bad "seed" "no SEEDED line"
|
||||
fi
|
||||
|
||||
# ---- 3. seed again: @unique trap, caught, across a process boundary --
|
||||
out="$(run seed 2>&1)"; rc=$?
|
||||
if [[ "$out" == *"SEED-DUP"* && $rc -eq 3 ]]; then
|
||||
ok "unique violation caught on re-seed (persisted via replay)"
|
||||
else
|
||||
bad "unique re-seed" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)"
|
||||
fi
|
||||
|
||||
# ---- 4. report: per-department aggregates + payroll ------------------
|
||||
rep="$(run report 2>&1)"
|
||||
if [[ "$rep" == *"DEPT Engineering headcount=3 avg=8200000 min=7300000 max=9200000"* \
|
||||
&& "$rep" == *"DEPT Operations headcount=2 avg=6150000 min=5900000 max=6400000"* \
|
||||
&& "$rep" == *"PAYROLL 45700000"* ]]; then
|
||||
ok "report (aggregates + payroll)"
|
||||
else
|
||||
bad "report" "$(printf '%s' "$rep" | tr '\n' '|' | cut -c1-160)"
|
||||
fi
|
||||
|
||||
# ---- 5. staff: index probe + backlink + ref navigation --------------
|
||||
st="$(run staff Engineering 2>&1)"
|
||||
if [[ "$st" == *"STAFF Asha 9200000 (Engineering)"* \
|
||||
&& "$st" == *"STAFF Chidi 7300000 (Engineering)"* ]]; then
|
||||
ok "staff (unique probe + backlink scan + ref nav)"
|
||||
else
|
||||
bad "staff" "$(printf '%s' "$st" | tr '\n' '|' | cut -c1-160)"
|
||||
fi
|
||||
|
||||
# ---- 6. raise: update-through-row, reflected in a re-report ----------
|
||||
run raise Operations 5 >/dev/null 2>&1
|
||||
if run report 2>&1 | grep -q "^DEPT Operations headcount=2 avg=6457500"; then
|
||||
ok "raise (update-through-row, durable)"
|
||||
else
|
||||
bad "raise" "operations average did not move to 6457500"
|
||||
fi
|
||||
|
||||
# ---- 7. drop: FK restrict (Engineering still has staff) --------------
|
||||
out="$(run drop Engineering 2>&1)"; rc=$?
|
||||
if [[ "$out" == *"restricted"* && $rc -eq 4 ]]; then
|
||||
ok "drop restricted by FK (department has staff)"
|
||||
else
|
||||
bad "drop restrict" "got rc=$rc: $(printf '%s' "$out" | tr '\n' '|' | cut -c1-100)"
|
||||
fi
|
||||
|
||||
# ---- 8. persistence: a fresh process still sees every acked write ----
|
||||
if run report 2>&1 | grep -q "^PAYROLL 46315000"; then
|
||||
ok "persistence (replay: raised payroll survives restart)"
|
||||
else
|
||||
bad "persistence" "payroll after restart not 46315000"
|
||||
fi
|
||||
|
||||
echo
|
||||
printf 'employee-accept: %d checks, %d failures\n' "$((pass + fail))" "$fail"
|
||||
[[ $fail -eq 0 ]]
|
||||
2
tests/corpus/run/db-fk-restrict-catch/fixture.out
Normal file
2
tests/corpus/run/db-fk-restrict-catch/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
eng restricted
|
||||
ops deleted
|
||||
23
tests/corpus/run/db-fk-restrict-catch/fixture.wo
Normal file
23
tests/corpus/run/db-fk-restrict-catch/fixture.wo
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- the restrict trap is catchable; a free (unreferenced) row deletes.
|
||||
@table(name: "dept", index: [name])
|
||||
class Dept {
|
||||
name: Text
|
||||
staff: backlink Emp.dept
|
||||
}
|
||||
@table(name: "emp", index: [dept])
|
||||
class Emp {
|
||||
name: Text
|
||||
dept: ref Dept
|
||||
}
|
||||
fn main() {
|
||||
let eng = insert Dept { name: "eng" }
|
||||
let ops = insert Dept { name: "ops" }
|
||||
insert Emp { name: "asha", dept: eng }
|
||||
let de = from x in Dept where x.name == "eng" take 1 select x
|
||||
let r1 = try delete de[0] catch (e) nil
|
||||
if r1 == nil { print("eng restricted") }
|
||||
let dop = from x in Dept where x.name == "ops" take 1 select x
|
||||
let r2 = try delete dop[0] catch (e) nil
|
||||
if r2 == nil { print("ops restricted (WRONG)") }
|
||||
print("ops deleted")
|
||||
}
|
||||
1
tests/corpus/trap/db-fk-restrict/fixture.trap
Normal file
1
tests/corpus/trap/db-fk-restrict/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
11
|
||||
18
tests/corpus/trap/db-fk-restrict/fixture.wo
Normal file
18
tests/corpus/trap/db-fk-restrict/fixture.wo
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- iteration 9b: deleting a row a `ref` still points at traps WO_T_FK
|
||||
-- (restrict). Uncaught here: the trap code (11) is the assertion.
|
||||
@table(name: "dept", index: [name])
|
||||
class Dept {
|
||||
name: Text
|
||||
staff: backlink Emp.dept
|
||||
}
|
||||
@table(name: "emp", index: [dept])
|
||||
class Emp {
|
||||
name: Text
|
||||
dept: ref Dept
|
||||
}
|
||||
fn main() {
|
||||
let d = insert Dept { name: "eng" }
|
||||
insert Emp { name: "asha", dept: d }
|
||||
let ds = from x in Dept where x.name == "eng" take 1 select x
|
||||
delete ds[0]
|
||||
}
|
||||
Loading…
Reference in a new issue