diff --git a/hbrtl/database.go b/hbrtl/database.go index 4bdce44..e9a8a41 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -12,6 +12,7 @@ import ( "os" "sort" "strings" + "sync/atomic" "five/hbrt" "five/hbrdd" @@ -771,6 +772,20 @@ func rtlDbAverage(t *hbrt.Thread) { t.RetDouble(sum/float64(n), 10, 2) } +// dbCmdTmpSeq generates a unique numeric suffix for temp aliases +// used by COPY/SORT/TOTAL/JOIN. Without this, a nested call (e.g., +// COPY inside a FOR clause that itself runs COPY) would collide on +// the same `__copytmp` alias and the inner Open would fail with +// "alias already in use". Atomic so concurrent goroutines (each +// owns its own WorkAreaManager but the counter is process-wide) +// don't hand out duplicates. +var dbCmdTmpSeq uint64 + +func nextTmpAlias(prefix string) string { + n := atomic.AddUint64(&dbCmdTmpSeq, 1) + return fmt.Sprintf("%s_%d", prefix, n) +} + // rtlDbNotImpl raises a runtime error explaining which xBase clause // the user invoked that Five doesn't yet implement. std.ch routes // SDF / DELIMITED / TO PRINTER / TO FILE variants here so they fail @@ -902,7 +917,7 @@ func rtlDbCopy(t *hbrt.Thread) { return } srcSel := wam.CurrentNum() - dstSel, err := wam.Open("DBFNTX", cFile, "__copytmp", false, false) + dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__copytmp"), false, false) if err != nil { // drv.Create wrote the file but Open failed (alias collision, // area-table full, ...). Without cleanup the user is left @@ -1175,7 +1190,7 @@ func rtlDbSort(t *hbrt.Thread) { return } srcSel := wam.CurrentNum() - dstSel, err := wam.Open("DBFNTX", cFile, "__sorttmp", false, false) + dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__sorttmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) @@ -1305,7 +1320,15 @@ func rtlDbList(t *hbrt.Thread) { if emit { parts := []string{} if !lOff { - parts = append(parts, fmt.Sprintf("%6d", srcArea.RecNo())) + // Deleted records get a `*` after the recno when shown + // (only happens with SET DELETED OFF — DELETED ON + // already skipped them at Area.Skip level). Matches + // Harbour LIST/DISPLAY convention. + marker := " " + if srcArea.Deleted() { + marker = "*" + } + parts = append(parts, fmt.Sprintf("%6d%s", srcArea.RecNo(), marker)) } if useAllFields { for i := 0; i < nFields; i++ { @@ -1319,7 +1342,10 @@ func rtlDbList(t *hbrt.Thread) { parts = append(parts, valueToDisplay(t.GetRetValue())) } } - fmt.Print("\r\n" + strings.Join(parts, " ")) + // Newline after the row, not before — avoids the spurious + // leading blank line at the top of the listing. `\n` only; + // terminals handle CR conversion themselves. + fmt.Println(strings.Join(parts, " ")) } srcArea.Skip(1) scanned++ @@ -1442,7 +1468,7 @@ func rtlDbTotal(t *hbrt.Thread) { return } srcSel := wam.CurrentNum() - dstSel, err := wam.Open("DBFNTX", cFile, "__totaltmp", false, false) + dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__totaltmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) @@ -1721,7 +1747,7 @@ func rtlDbJoin(t *hbrt.Thread) { t.RetBool(false) return } - dstSel, err := wam.Open("DBFNTX", cFile, "__jointmp", false, false) + dstSel, err := wam.Open("DBFNTX", cFile, nextTmpAlias("__jointmp"), false, false) if err != nil { _ = os.Remove(cFile) t.RetBool(false) diff --git a/tests/std_ch/run.sh b/tests/std_ch/run.sh new file mode 100755 index 0000000..acbf05a --- /dev/null +++ b/tests/std_ch/run.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# std.ch regression runner. Build each PRG against the current Five +# compiler (caller's PWD must be the repo root) and execute it; non-zero +# exit or "FAIL"/"NOT REJECTED" in stdout marks the run as failed. +# +# This deliberately runs in a temp scratch directory so the DBF/NTX +# artifacts don't collide with each other across tests. +set -e + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +FIVE="$ROOT/five" +if [ ! -x "$FIVE" ]; then + echo "five binary not found at $FIVE — run 'go build -o five ./cmd/five' first" >&2 + exit 2 +fi + +TESTS=( + test_pp_stdch + test_count + test_sum_avg + test_sum_multi + test_copy + test_sort + test_list + test_total + test_join + test_update + test_set_deleted + test_unsupported + test_block_comma +) + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +pass=0 +fail=0 +for name in "${TESTS[@]}"; do + src="$ROOT/tests/std_ch/${name}.prg" + bin="$work/${name}" + if ! "$FIVE" build "$src" -o "$bin" >/dev/null 2>"$work/${name}.err"; then + echo "FAIL build $name" + cat "$work/${name}.err" | sed 's/^/ /' + fail=$((fail+1)) + continue + fi + pushd "$work" >/dev/null + if ! out="$("$bin" 2>&1)"; then + echo "FAIL run $name" + echo "$out" | sed 's/^/ /' + fail=$((fail+1)) + popd >/dev/null + continue + fi + popd >/dev/null + if echo "$out" | grep -qE 'FAIL|NOT REJECTED|expect.*got'; then + echo "FAIL assert $name" + echo "$out" | sed 's/^/ /' + fail=$((fail+1)) + continue + fi + echo "PASS $name" + pass=$((pass+1)) +done + +echo +echo "================================================================" +echo " Results: $pass / $((pass+fail)) passed" +echo "================================================================" +[ $fail -eq 0 ] diff --git a/tests/std_ch/test_block_comma.prg b/tests/std_ch/test_block_comma.prg new file mode 100644 index 0000000..93cf20a --- /dev/null +++ b/tests/std_ch/test_block_comma.prg @@ -0,0 +1,12 @@ +PROCEDURE Main() + LOCAL a := 0, b := 0, c := 0, r + LOCAL b1 := {|| a := a + 1, b := b + 1, c := c + 1 } + r := Eval(b1) + ? "a=", a, "b=", b, "c=", c, "ret=", r + ? "(harbour: a=1, b=1, c=1, ret=1 — last expr)" + + /* Same block via inline expression */ + a := 0; b := 0; c := 0 + r := Eval({|| a := a + 1, b := b + 1, c := c + 1 }) + ? "inline: a=", a, "b=", b, "c=", c, "ret=", r + RETURN diff --git a/tests/std_ch/test_copy.prg b/tests/std_ch/test_copy.prg new file mode 100644 index 0000000..9bd1521 --- /dev/null +++ b/tests/std_ch/test_copy.prg @@ -0,0 +1,64 @@ +/* COPY TO via PP rule. */ + +PROCEDURE Main() + LOCAL aStruct, n + + FErase( "src.dbf" ) + FErase( "dst1.dbf" ) + FErase( "dst2.dbf" ) + FErase( "dst3.dbf" ) + + aStruct := { ; + { "ID", "N", 4, 0 }, ; + { "NAME", "C", 20, 0 }, ; + { "AGE", "N", 3, 0 } } + + dbCreate( "src.dbf", aStruct ) + USE src.dbf NEW EXCLUSIVE ALIAS s + dbAppend() ; FieldPut(1,1) ; FieldPut(2,"Alice") ; FieldPut(3,18) + dbAppend() ; FieldPut(1,2) ; FieldPut(2,"Bob") ; FieldPut(3,25) + dbAppend() ; FieldPut(1,3) ; FieldPut(2,"Carol") ; FieldPut(3,30) + dbAppend() ; FieldPut(1,4) ; FieldPut(2,"Dan") ; FieldPut(3,45) + dbAppend() ; FieldPut(1,5) ; FieldPut(2,"Eve") ; FieldPut(3,60) + dbCommit() + + /* 1. plain COPY TO — all rows */ + dbGoTop() + COPY TO dst1.dbf + dbCloseArea() + USE dst1.dbf NEW EXCLUSIVE + COUNT TO n + ? "1. COPY TO dst1.dbf rows =", n, "(expect 5)" + dbGoTop() + ? " first row id/name/age =", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbCloseArea() + + /* 2. COPY TO ... FOR */ + USE src.dbf NEW EXCLUSIVE ALIAS s + dbGoTop() + COPY TO dst2.dbf FOR s->age >= 30 + dbCloseArea() + USE dst2.dbf NEW EXCLUSIVE + COUNT TO n + ? "2. COPY TO dst2.dbf FOR age>=30 rows =", n, "(expect 3)" + dbCloseArea() + + /* 3. COPY TO ... FIELDS subset */ + USE src.dbf NEW EXCLUSIVE ALIAS s + dbGoTop() + COPY TO dst3.dbf FIELDS id, name + dbCloseArea() + USE dst3.dbf NEW EXCLUSIVE + COUNT TO n + ? "3. COPY TO dst3.dbf FIELDS id,name rows =", n, "(expect 5)" + ? " field count =", FCount(), "(expect 2)" + dbGoTop() + ? " row1 =", FieldGet(1), AllTrim(FieldGet(2)) + dbCloseArea() + + FErase( "src.dbf" ) + FErase( "dst1.dbf" ) + FErase( "dst2.dbf" ) + FErase( "dst3.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_count.prg b/tests/std_ch/test_count.prg new file mode 100644 index 0000000..5fa2d3c --- /dev/null +++ b/tests/std_ch/test_count.prg @@ -0,0 +1,40 @@ +/* COUNT via PP rule — uses alias-qualified fields. */ + +PROCEDURE Main() + LOCAL aStruct, n + + FErase( "people.dbf" ) + aStruct := { { "ID", "N", 4, 0 }, { "AGE", "N", 3, 0 } } + dbCreate( "people.dbf", aStruct ) + USE people.dbf NEW EXCLUSIVE ALIAS p + dbAppend() ; FieldPut(1, 1) ; FieldPut(2, 18) + dbAppend() ; FieldPut(1, 2) ; FieldPut(2, 25) + dbAppend() ; FieldPut(1, 3) ; FieldPut(2, 30) + dbAppend() ; FieldPut(1, 4) ; FieldPut(2, 45) + dbAppend() ; FieldPut(1, 5) ; FieldPut(2, 60) + dbCommit() + + n := -1 + dbGoTop() + COUNT TO n + ? "1. COUNT TO n =", n, "(expect 5)" + + n := -1 + dbGoTop() + COUNT TO n FOR p->age >= 30 + ? "2. COUNT TO n FOR p->age >= 30 =", n, "(expect 3)" + + n := -1 + dbGoTop() + COUNT TO n FOR p->age < 25 + ? "3. COUNT TO n FOR p->age < 25 =", n, "(expect 1)" + + n := -1 + dbGoTop() + COUNT TO n FOR p->age > 100 + ? "4. COUNT TO n FOR p->age > 100 =", n, "(expect 0)" + + dbCloseArea() + FErase( "people.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_join.prg b/tests/std_ch/test_join.prg new file mode 100644 index 0000000..60a79a0 --- /dev/null +++ b/tests/std_ch/test_join.prg @@ -0,0 +1,70 @@ +/* JOIN WITH ... TO ... FOR ... — uses non-reserved alias names. */ + +PROCEDURE Main() + LOCAL aStruct, n + + FErase( "cust.dbf" ) + FErase( "ord.dbf" ) + FErase( "out.dbf" ) + + /* Customers (master) */ + aStruct := { ; + { "CID", "N", 4, 0 }, ; + { "CNAME", "C", 12, 0 } } + dbCreate( "cust.dbf", aStruct ) + USE cust.dbf NEW EXCLUSIVE ALIAS cu + dbAppend() ; FieldPut(1, 1) ; FieldPut(2, "Alice") + dbAppend() ; FieldPut(1, 2) ; FieldPut(2, "Bob") + dbAppend() ; FieldPut(1, 3) ; FieldPut(2, "Carol") + dbCommit() + dbCloseArea() + + /* Orders (detail) */ + aStruct := { ; + { "OID", "N", 4, 0 }, ; + { "CID", "N", 4, 0 }, ; + { "AMT", "N", 8, 2 } } + dbCreate( "ord.dbf", aStruct ) + USE ord.dbf NEW EXCLUSIVE ALIAS od + dbAppend() ; FieldPut(1, 100) ; FieldPut(2, 1) ; FieldPut(3, 50) + dbAppend() ; FieldPut(1, 101) ; FieldPut(2, 1) ; FieldPut(3, 30) + dbAppend() ; FieldPut(1, 102) ; FieldPut(2, 2) ; FieldPut(3, 200) + dbAppend() ; FieldPut(1, 103) ; FieldPut(2, 4) ; FieldPut(3, 99) /* no-match cid */ + dbCommit() + dbCloseArea() + + /* Open both */ + USE cust.dbf NEW EXCLUSIVE ALIAS cu + USE ord.dbf NEW EXCLUSIVE ALIAS od + SELECT cu + + ? "--- JOIN WITH od TO out FIELDS cid, cname, oid, amt FOR cu->cid = od->cid ---" + JOIN WITH od TO out.dbf FIELDS cid, cname, oid, amt FOR cu->cid = od->cid + + /* Close both source areas */ + SELECT cu + dbCloseArea() + SELECT od + dbCloseArea() + + USE out.dbf NEW EXCLUSIVE + COUNT TO n + ? "Joined rows =", n, "(expect 3)" + + dbGoTop() + ? "Field count =", FCount(), "(expect 4: cid, cname, oid, amt)" + ? "" + ? "rows:" + DO WHILE !Eof() + ? " cid:", FieldGet(1), "name:", AllTrim(FieldGet(2)), "oid:", FieldGet(3), "amt:", FieldGet(4) + dbSkip() + ENDDO + ? "(expect: 1 Alice 100 50 / 1 Alice 101 30 / 2 Bob 102 200)" + + dbCloseArea() + + FErase( "cust.dbf" ) + FErase( "ord.dbf" ) + FErase( "out.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_list.prg b/tests/std_ch/test_list.prg new file mode 100644 index 0000000..7da36a9 --- /dev/null +++ b/tests/std_ch/test_list.prg @@ -0,0 +1,52 @@ +/* LIST and DISPLAY via PP rules — alias-qualified field references. */ + +PROCEDURE Main() + LOCAL aStruct + + FErase( "people.dbf" ) + aStruct := { ; + { "ID", "N", 4, 0 }, ; + { "NAME", "C", 10, 0 }, ; + { "AGE", "N", 3, 0 } } + + dbCreate( "people.dbf", aStruct ) + USE people.dbf NEW EXCLUSIVE ALIAS p + dbAppend() ; FieldPut(1,1) ; FieldPut(2,"Alice") ; FieldPut(3,18) + dbAppend() ; FieldPut(1,2) ; FieldPut(2,"Bob") ; FieldPut(3,25) + dbAppend() ; FieldPut(1,3) ; FieldPut(2,"Carol") ; FieldPut(3,30) + dbCommit() + + ? "--- 1. LIST (all rows, all fields) ---" + dbGoTop() + LIST + + ? "" + ? "--- 2. LIST p->id, p->name (selected) ---" + dbGoTop() + LIST p->id, p->name + + ? "" + ? "--- 3. LIST p->id, p->name FOR p->age >= 25 ---" + dbGoTop() + LIST p->id, p->name FOR p->age >= 25 + + ? "" + ? "--- 4. LIST p->id, p->name OFF ---" + dbGoTop() + LIST p->id, p->name OFF + + ? "" + ? "--- 5. DISPLAY (current record only) ---" + dbGoto(2) + DISPLAY + + ? "" + ? "--- 6. DISPLAY ALL ---" + dbGoTop() + DISPLAY ALL + + ? "" + dbCloseArea() + FErase( "people.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_pp_stdch.prg b/tests/std_ch/test_pp_stdch.prg new file mode 100644 index 0000000..f755f59 --- /dev/null +++ b/tests/std_ch/test_pp_stdch.prg @@ -0,0 +1,39 @@ +/* Test that std.ch #command rules expand correctly. */ + +PROCEDURE Main() + LOCAL cName := "/tmp/__pp_test_std_ch.tmp" + + /* Create a junk file so ERASE/RENAME have something to work with. */ + hb_MemoWrit( cName, "x" ) + ? "1. file created exists?", File( cName ), "(expect .T.)" + + /* ERASE <(f)> → FErase(<(f)>) */ + ERASE ( cName ) + ? "2. after ERASE exists?", File( cName ), "(expect .F.)" + + /* RENAME — recreate, rename, check both. */ + hb_MemoWrit( cName, "y" ) + RENAME ( cName ) TO ( cName + ".moved" ) + ? "3. after RENAME orig exists?", File( cName ), "(expect .F.)" + ? " moved exists?", File( cName + ".moved" ), "(expect .T.)" + FErase( cName + ".moved" ) + + /* DELETE FILE <(f)> alias for ERASE */ + hb_MemoWrit( cName, "z" ) + DELETE FILE ( cName ) + ? "4. after DELETE FILE exists?", File( cName ), "(expect .F.)" + + /* CLOSE / CLOSE ALL / CLOSE DATABASES — should compile without + hardcoded parser support. They each call DbCloseArea or + DbCloseAll. With no open workareas, both are no-ops. */ + CLOSE + CLOSE ALL + CLOSE DATABASES + ? "5. CLOSE / CLOSE ALL / CLOSE DATABASES compiled OK" + + /* COMMIT — DbCommit() with no open area is a no-op too. */ + COMMIT + ? "6. COMMIT compiled OK" + + ? "DONE" + RETURN diff --git a/tests/std_ch/test_set_deleted.prg b/tests/std_ch/test_set_deleted.prg new file mode 100644 index 0000000..999851f --- /dev/null +++ b/tests/std_ch/test_set_deleted.prg @@ -0,0 +1,31 @@ +PROCEDURE Main() + LOCAL n + + FErase("p.dbf"); FErase("c.dbf") + dbCreate("p.dbf", { {"X","N",4,0} }) + USE p.dbf NEW EXCLUSIVE ALIAS p + dbAppend(); FieldPut(1, 1) + dbAppend(); FieldPut(1, 2); dbDelete() + dbAppend(); FieldPut(1, 3) + dbCommit() + dbGoTop() + + /* SET DELETED OFF (default) — all 3 included */ + COUNT TO n + ? "DELETED OFF count =", n, "(expect 3)" + + SET DELETED ON + dbGoTop() + COUNT TO n + ? "DELETED ON count =", n, "(expect 2)" + + COPY TO c.dbf + dbCloseArea() + USE c.dbf NEW EXCLUSIVE + COUNT TO n + ? "COPY rows after DELETED ON =", n, "(expect 2)" + dbCloseArea() + + FErase("p.dbf"); FErase("c.dbf") + ? "DONE" + RETURN diff --git a/tests/std_ch/test_sort.prg b/tests/std_ch/test_sort.prg new file mode 100644 index 0000000..d3efe5d --- /dev/null +++ b/tests/std_ch/test_sort.prg @@ -0,0 +1,74 @@ +/* SORT TO via PP rule. */ + +PROCEDURE Main() + LOCAL aStruct + + FErase( "src.dbf" ) + FErase( "dst1.dbf" ) + FErase( "dst2.dbf" ) + FErase( "dst3.dbf" ) + + aStruct := { ; + { "ID", "N", 4, 0 }, ; + { "NAME", "C", 20, 0 }, ; + { "AGE", "N", 3, 0 } } + + dbCreate( "src.dbf", aStruct ) + USE src.dbf NEW EXCLUSIVE ALIAS s + dbAppend() ; FieldPut(1,1) ; FieldPut(2,"Carol") ; FieldPut(3,30) + dbAppend() ; FieldPut(1,2) ; FieldPut(2,"Alice") ; FieldPut(3,18) + dbAppend() ; FieldPut(1,3) ; FieldPut(2,"Eve") ; FieldPut(3,60) + dbAppend() ; FieldPut(1,4) ; FieldPut(2,"Bob") ; FieldPut(3,25) + dbAppend() ; FieldPut(1,5) ; FieldPut(2,"Dan") ; FieldPut(3,45) + dbCommit() + + /* 1. SORT ON name asc */ + dbGoTop() + SORT TO dst1.dbf ON name + dbCloseArea() + USE dst1.dbf NEW EXCLUSIVE + dbGoTop() + ? "1. name asc:" + DO WHILE !Eof() + ? " ", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbSkip() + ENDDO + ? " (expect: Alice, Bob, Carol, Dan, Eve)" + dbCloseArea() + + /* 2. SORT ON age/D */ + USE src.dbf NEW EXCLUSIVE ALIAS s + dbGoTop() + SORT TO dst2.dbf ON age/D + dbCloseArea() + USE dst2.dbf NEW EXCLUSIVE + dbGoTop() + ? "2. age desc:" + DO WHILE !Eof() + ? " ", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbSkip() + ENDDO + ? " (expect: 60, 45, 30, 25, 18)" + dbCloseArea() + + /* 3. SORT ON age FOR age >= 30 */ + USE src.dbf NEW EXCLUSIVE ALIAS s + dbGoTop() + SORT TO dst3.dbf ON age FOR s->age >= 30 + dbCloseArea() + USE dst3.dbf NEW EXCLUSIVE + dbGoTop() + ? "3. age asc, FOR age>=30:" + DO WHILE !Eof() + ? " ", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbSkip() + ENDDO + ? " (expect: Carol/30, Dan/45, Eve/60)" + dbCloseArea() + + FErase( "src.dbf" ) + FErase( "dst1.dbf" ) + FErase( "dst2.dbf" ) + FErase( "dst3.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_sum_avg.prg b/tests/std_ch/test_sum_avg.prg new file mode 100644 index 0000000..b3b3279 --- /dev/null +++ b/tests/std_ch/test_sum_avg.prg @@ -0,0 +1,45 @@ +/* SUM and AVERAGE via PP rule. */ + +PROCEDURE Main() + LOCAL aStruct, nSum, nAvg + + FErase( "people.dbf" ) + aStruct := { { "ID", "N", 4, 0 }, { "AGE", "N", 3, 0 } } + dbCreate( "people.dbf", aStruct ) + USE people.dbf NEW EXCLUSIVE ALIAS p + dbAppend() ; FieldPut(1, 1) ; FieldPut(2, 18) + dbAppend() ; FieldPut(1, 2) ; FieldPut(2, 25) + dbAppend() ; FieldPut(1, 3) ; FieldPut(2, 30) + dbAppend() ; FieldPut(1, 4) ; FieldPut(2, 45) + dbAppend() ; FieldPut(1, 5) ; FieldPut(2, 60) + dbCommit() + + /* SUM all ages */ + dbGoTop() + SUM p->age TO nSum + ? "1. SUM p->age =", nSum, "(expect 178 = 18+25+30+45+60)" + + /* SUM with FOR */ + dbGoTop() + SUM p->age TO nSum FOR p->age >= 30 + ? "2. SUM p->age FOR age>=30 =", nSum, "(expect 135 = 30+45+60)" + + /* AVERAGE all ages */ + dbGoTop() + AVERAGE p->age TO nAvg + ? "3. AVERAGE p->age =", nAvg, "(expect 35.6)" + + /* AVERAGE with FOR */ + dbGoTop() + AVERAGE p->age TO nAvg FOR p->age >= 30 + ? "4. AVERAGE p->age FOR age>=30 =", nAvg, "(expect 45)" + + /* AVERAGE with no matching rows */ + dbGoTop() + AVERAGE p->age TO nAvg FOR p->age > 200 + ? "5. AVERAGE p->age FOR age>200 =", nAvg, "(expect 0)" + + dbCloseArea() + FErase( "people.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_sum_multi.prg b/tests/std_ch/test_sum_multi.prg new file mode 100644 index 0000000..f45debd --- /dev/null +++ b/tests/std_ch/test_sum_multi.prg @@ -0,0 +1,23 @@ +PROCEDURE Main() + LOCAL aStruct, sx, sy, sz + + FErase("p.dbf") + aStruct := { {"X","N",6,0}, {"Y","N",6,0}, {"Z","N",6,0} } + dbCreate("p.dbf", aStruct) + USE p.dbf NEW EXCLUSIVE ALIAS p + dbAppend(); FieldPut(1, 1); FieldPut(2, 10); FieldPut(3, 100) + dbAppend(); FieldPut(1, 2); FieldPut(2, 20); FieldPut(3, 200) + dbAppend(); FieldPut(1, 3); FieldPut(2, 30); FieldPut(3, 300) + dbCommit() + + /* Multi-pair SUM */ + dbGoTop() + SUM p->x, p->y, p->z TO sx, sy, sz + ? "sx=", sx, "(expect 6)" + ? "sy=", sy, "(expect 60)" + ? "sz=", sz, "(expect 600)" + + dbCloseArea() + FErase("p.dbf") + ? "DONE" + RETURN diff --git a/tests/std_ch/test_total.prg b/tests/std_ch/test_total.prg new file mode 100644 index 0000000..00130e8 --- /dev/null +++ b/tests/std_ch/test_total.prg @@ -0,0 +1,49 @@ +/* TOTAL TO ... ON ... FIELDS ... via PP rule. */ + +PROCEDURE Main() + LOCAL aStruct, n + + FErase( "src.dbf" ) + FErase( "dst.dbf" ) + + /* Sales records, sorted by department. */ + aStruct := { ; + { "DEPT", "C", 12, 0 }, ; + { "EMP", "C", 12, 0 }, ; + { "AMT", "N", 8, 2 }, ; + { "QTY", "N", 4, 0 } } + + dbCreate( "src.dbf", aStruct ) + USE src.dbf NEW EXCLUSIVE ALIAS s + /* engineering: 100+200=300, qty 5+8=13 */ + dbAppend() ; FieldPut(1,"engineering"); FieldPut(2,"alice") ; FieldPut(3,100) ; FieldPut(4,5) + dbAppend() ; FieldPut(1,"engineering"); FieldPut(2,"bob") ; FieldPut(3,200) ; FieldPut(4,8) + /* sales: 50+150+25=225, qty 1+3+1=5 */ + dbAppend() ; FieldPut(1,"sales") ; FieldPut(2,"carol") ; FieldPut(3,50) ; FieldPut(4,1) + dbAppend() ; FieldPut(1,"sales") ; FieldPut(2,"dan") ; FieldPut(3,150) ; FieldPut(4,3) + dbAppend() ; FieldPut(1,"sales") ; FieldPut(2,"eve") ; FieldPut(3,25) ; FieldPut(4,1) + /* hr: 80, qty 2 */ + dbAppend() ; FieldPut(1,"hr") ; FieldPut(2,"frank") ; FieldPut(3,80) ; FieldPut(4,2) + dbCommit() + + ? "--- TOTAL TO dst ON dept FIELDS amt, qty ---" + dbGoTop() + TOTAL TO dst.dbf ON s->dept FIELDS amt, qty + dbCloseArea() + + USE dst.dbf NEW EXCLUSIVE + COUNT TO n + ? "Group rows =", n, "(expect 3 = engineering, sales, hr)" + + dbGoTop() + ? "1.", AllTrim(FieldGet(1)), "amt:", FieldGet(3), "qty:", FieldGet(4), "(expect engineering 300 13)" + dbSkip() + ? "2.", AllTrim(FieldGet(1)), "amt:", FieldGet(3), "qty:", FieldGet(4), "(expect sales 225 5)" + dbSkip() + ? "3.", AllTrim(FieldGet(1)), "amt:", FieldGet(3), "qty:", FieldGet(4), "(expect hr 80 2)" + dbCloseArea() + + FErase( "src.dbf" ) + FErase( "dst.dbf" ) + ? "DONE" + RETURN diff --git a/tests/std_ch/test_unsupported.prg b/tests/std_ch/test_unsupported.prg new file mode 100644 index 0000000..030116a --- /dev/null +++ b/tests/std_ch/test_unsupported.prg @@ -0,0 +1,29 @@ +PROCEDURE Main() + LOCAL caught := .F. + + FErase("d.dbf") + dbCreate("d.dbf", { {"X","N",4,0} }) + USE d.dbf NEW EXCLUSIVE + dbAppend(); FieldPut(1, 1) + dbCommit() + + /* Test that SDF / TO PRINTER raise instead of silent no-op */ + BEGIN SEQUENCE + COPY TO out.txt SDF + RECOVER + caught := .T. + END SEQUENCE + ? "SDF caught:", caught, "(expect .T.)" + + caught := .F. + BEGIN SEQUENCE + LIST TO PRINTER + RECOVER + caught := .T. + END SEQUENCE + ? "TO PRINTER caught:", caught, "(expect .T.)" + + dbCloseArea() + FErase("d.dbf") + ? "DONE" + RETURN diff --git a/tests/std_ch/test_update.prg b/tests/std_ch/test_update.prg new file mode 100644 index 0000000..a07ce70 --- /dev/null +++ b/tests/std_ch/test_update.prg @@ -0,0 +1,67 @@ +/* UPDATE FROM via PP rule. */ + +PROCEDURE Main() + LOCAL aStruct + + FErase( "mas.dbf" ) + FErase( "del.dbf" ) + + /* Master: id, total. Initially totals are zero. */ + aStruct := { ; + { "ID", "N", 4, 0 }, ; + { "NAME", "C", 8, 0 }, ; + { "TOTAL", "N", 8, 2 } } + dbCreate( "mas.dbf", aStruct ) + USE mas.dbf NEW EXCLUSIVE ALIAS mas + dbAppend() ; FieldPut(1, 1) ; FieldPut(2, "Alice") ; FieldPut(3, 0) + dbAppend() ; FieldPut(1, 2) ; FieldPut(2, "Bob") ; FieldPut(3, 0) + dbAppend() ; FieldPut(1, 3) ; FieldPut(2, "Carol") ; FieldPut(3, 0) + dbCommit() + dbCloseArea() + + /* Delta: same id, amount to apply. Sorted by id. */ + aStruct := { ; + { "ID", "N", 4, 0 }, ; + { "AMT", "N", 8, 2 } } + dbCreate( "del.dbf", aStruct ) + USE del.dbf NEW EXCLUSIVE ALIAS del + dbAppend() ; FieldPut(1, 1) ; FieldPut(2, 100) + dbAppend() ; FieldPut(1, 2) ; FieldPut(2, 200) + dbAppend() ; FieldPut(1, 3) ; FieldPut(2, 300) + dbCommit() + dbCloseArea() + + /* Re-open both, sorted on id. */ + USE mas.dbf NEW EXCLUSIVE ALIAS mas + USE del.dbf NEW EXCLUSIVE ALIAS del + SELECT mas + + ? "--- before UPDATE FROM ---" + dbGoTop() + DO WHILE !Eof() + ? " ", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbSkip() + ENDDO + + ? "" + ? "--- UPDATE FROM del ON id REPLACE total WITH del->amt ---" + dbGoTop() + UPDATE FROM del ON id REPLACE total WITH del->amt + + ? "" + ? "--- after UPDATE FROM ---" + dbGoTop() + DO WHILE !Eof() + ? " ", FieldGet(1), AllTrim(FieldGet(2)), FieldGet(3) + dbSkip() + ENDDO + ? "(expect: 1 Alice 100, 2 Bob 200, 3 Carol 300)" + + SELECT del + dbCloseArea() + SELECT mas + dbCloseArea() + FErase( "mas.dbf" ) + FErase( "del.dbf" ) + ? "DONE" + RETURN