// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Go-native SQL scan loop for FiveSql2 hot path. // // Motivation: FiveSql2 is a PRG-based SQL interpreter. For simple // "SELECT cols FROM table WHERE cond" queries, the per-row cost is // dominated by PRG interpreter overhead (AST tree walk, field name // lookup, workarea switching). Moving just the inner scan loop to Go // bypasses all that overhead and gets us ~15x speedup for the common // case while keeping the rest of FiveSql2 untouched. // // The SQL engine remains responsible for: // - Parsing SQL and building AST // - Resolving field names to positions (column binding) // - Compiling WHERE expression to pcode (via PcCompile) // - GROUP BY, ORDER BY, aggregates (not per-row) // // This helper only handles the hot loop: // - Full table scan (workarea already positioned) // - Per-row WHERE evaluation via ExecPcode // - Column extraction via cached field positions // - Result array construction package hbrtl import ( "five/hbrdd" "five/hbrt" ) // SqlScan(aFieldPositions, pcWhere) → aRows // // Scans the current workarea top-to-bottom, evaluates pcWhere per row // (nil = no filter), collects selected column values into rows. // // aFieldPositions: array of 1-based field positions to extract per row. // Resolve once before calling (FieldPos cache is O(1) // but still has PRG → Go call overhead). // pcWhere: pcode function pointer from PcCompile, or NIL. // // Returns: // Array of rows, each row = Array of field values. // // Notes on CHAR trimming: DBF character fields are space-padded. The // caller decides whether to trim (via a SELECT-list AllTrim wrapper). // We don't trim here — that's a semantic choice, and callers who need // raw bytes shouldn't pay for a strings.TrimSpace(). func SqlScan(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() // Parse arguments fieldsVal := t.Local(1) if !fieldsVal.IsArray() { t.PushValue(hbrt.MakeArray(0)) t.RetValue() return } fieldsArr := fieldsVal.AsArray().Items nFields := len(fieldsArr) whereVal := t.Local(2) var whereFn *hbrt.PcodeFunc if !whereVal.IsNil() { if p := whereVal.AsPointer(); p != nil { whereFn, _ = p.(*hbrt.PcodeFunc) } } // Pre-convert field positions to []int (avoid Value->int per row) fieldPos := make([]int, nFields) for i := 0; i < nFields; i++ { fieldPos[i] = int(fieldsArr[i].AsNumInt()) if fieldPos[i] < 1 { fieldPos[i] = 1 } } wam, ok := t.WA.(*hbrdd.WorkAreaManager) if !ok { t.PushValue(hbrt.MakeArray(0)) t.RetValue() return } area := wam.Current() if area == nil { t.PushValue(hbrt.MakeArray(0)) t.RetValue() return } // SQLite-inspired: instead of one slice allocation per row, maintain // a single flat backing buffer and hand each row a sub-slice into it. // This halves allocations (row header + backing → just row header) // and keeps row data contiguous in memory for better cache locality. // // Safety: we cap each sub-slice to exactly nFields via the 3-index // slice form (flat[off:end:end]). Any later `append` on an individual // row will then trigger a reallocation of that row's backing, so we // don't clobber neighboring rows if PRG code mutates via AAdd. // Size the initial backing based on the workarea's record count — // even if WHERE filters most rows out, over-allocating beats five // regrowths of a 200 KB buffer mid-scan. estRows := 1024 if rc, err := area.RecCount(); err == nil && rc > 0 { estRows = int(rc) if estRows > 1 << 20 { estRows = 1 << 20 } } rows := make([]hbrt.Value, 0, estRows) flat := make([]hbrt.Value, 0, estRows*nFields) slab := hbrt.NewArraySlab(estRows) // Scan area.GoTop() for !area.EOF() { // WHERE evaluation (if any). Fast variant — WHERE expressions // compiled from SQL AST don't contain BEGIN SEQUENCE, so we can // skip the defer/recover frame exit. keep := true if whereFn != nil { hbrt.ExecPcodeFast(t, whereFn, nil) keep = t.GetRetValue().AsBool() } if keep { // Reserve nFields slots in flat, growing if needed. off := len(flat) end := off + nFields if end > cap(flat) { // Grow flat. Go's append growth policy handles this; // we re-reserve space so the sub-slice math still holds. flat = append(flat, make([]hbrt.Value, nFields)...) } else { flat = flat[:end] } row := flat[off:end:end] // Collect column values directly into the backing buffer. for i := 0; i < nFields; i++ { // GetValue is 0-based v, _ := area.GetValue(fieldPos[i] - 1) row[i] = v } rows = append(rows, slab.WrapNext(row)) } area.Skip(1) } t.PushValue(hbrt.MakeArrayFrom(rows)) t.RetValue() }