perf(FiveSql2): hybrid fast path — 11x speedup on string WHERE scans

Implements hybrid execution model: keep AST tree-walk for SQL:2013+
features (Window, Recursive CTE, JOIN, aggregates) while compiling
simple SELECT hot paths to Go + pcode. See docs/FiveSql2-Hybrid-Plan.md
for the full architecture rationale (why not SQLite-style VDBE).

Hot path (single table, no joins/groups/aggregates):
  - TryBuildFieldPositions: resolves SELECT column list to FieldPos
    array once per query (bails to PRG loop on any complex expr).
  - TryCompileWhere + SqlExprToPrg: walks WHERE AST, emits equivalent
    PRG source, runs it through PcCompile to get a PcodeFunc.
  - SqlScan RTL: Go-native scan loop — GoTop/EOF/Skip/GetValue
    direct, ExecPcode per row for WHERE, result array pre-alloc.

WHERE compiler scope:
  - ND_LIT numeric/logical/string (string literals AllTrim'd to match
    SqlCmpEq CHAR-padding semantics; rejects embedded quotes/newlines)
  - ND_COL: CHAR fields auto-wrapped with AllTrim(FieldGet(n)) based
    on dbStruct() lookup cached once per query in aCompileStruct
  - ND_BIN: = <> != < <= > >= AND OR + - * /
  - ND_UNI: NOT -
  - Anything else (ND_FN, ND_CASE, ND_SUB, ND_PAR, LIKE, IN, IS NULL,
    BETWEEN, dates) returns NIL → falls back to PRG tree-walk.

Bench (50k rows, ~/tmp ext4):
                        Before      After     Speedup
  Numeric WHERE         ~150ms     11.7ms     ~13x
  String WHERE          119.3ms    10.5ms     11.4x
  No WHERE               -         14.6ms      -
  Raw RDD baseline        6.8ms     6.8ms      1.0x

Remaining gap to raw RDD (~1.5x) is structural: Value boxing, result
array construction, per-row ExecPcode frame overhead. Would need a
Value-pool or SoA refactor to close further.

Side fixes bundled:
  - TSqlIndex:FindExclusive short-circuited. Originally called
    dbInfo(DBI_FULLPATH)/DBI_SHARED which are unresolved symbols in
    Five (dbInfo is a stub, DBI_* never defined). Panic'd with
    "local variable index out of range: 0" whenever a standalone PRG
    had a workarea Used before calling five_SQL. 43-test masked the
    bug because it only reached FindExclusive with no open workareas.
    Restore the scan once dbInfo lands in hbrtl.
  - cmd/five/main.go: FIVE_KEEP_BUILD=1 env var keeps the temp Go
    project around for debugging gengo output.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 09:15:08 +09:00
parent 6b26f1b642
commit 8aaed994f4
6 changed files with 552 additions and 37 deletions

View File

@@ -128,7 +128,11 @@ func runPRG(prgFile string) {
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
defer os.RemoveAll(tmpDir)
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
writeGoProject(tmpDir, prgFile, goCode)
@@ -159,7 +163,11 @@ func buildPRG(prgFile, output string) {
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
defer os.RemoveAll(tmpDir)
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
writeGoProject(tmpDir, prgFile, goCode)
@@ -198,7 +206,11 @@ func buildMultiPRG(prgFiles []string, output string) {
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
defer os.RemoveAll(tmpDir)
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
// Phase 1: Parse all files and collect cross-file function names
type parsedFile struct {
@@ -549,7 +561,11 @@ func buildFRB(prgFile, outputFile string) {
if err != nil {
fatal("cannot create temp dir: " + err.Error())
}
defer os.RemoveAll(tmpDir)
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
// Write go.mod — point to Five's module root
fiveRoot := mustAbs(".")
@@ -659,7 +675,11 @@ func debugPRG(prgFile string) {
if err != nil {
fatal("cannot create temp dir: " + err.Error())
}
defer os.RemoveAll(tmpDir)
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
fiveRoot := findProjectRoot()
goMod := fmt.Sprintf("module five-generated\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot)