// 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 } // Pre-allocate result: 50k × small-row header pressure matters rows := make([]hbrt.Value, 0, 1024) // Scan area.GoTop() for !area.EOF() { // WHERE evaluation (if any) keep := true if whereFn != nil { hbrt.ExecPcode(t, whereFn, nil) keep = t.GetRetValue().AsBool() } if keep { // Collect column values row := make([]hbrt.Value, nFields) for i := 0; i < nFields; i++ { // GetValue is 0-based v, _ := area.GetValue(fieldPos[i] - 1) row[i] = v } rows = append(rows, hbrt.MakeArrayFrom(row)) } area.Skip(1) } t.PushValue(hbrt.MakeArrayFrom(rows)) t.RetValue() }