Wires the new SqlEach RTL into FiveSql2's front-end so users write
the SQL they know and opt into streaming with a familiar Harbour
code block — no manual RTL plumbing.
API:
/* Existing array form — unchanged, 43-test still green */
aR := five_SQL( "SELECT name FROM t" )
/* New block form — zero intermediate rows, 2x raw PRG */
five_SQL( "SELECT id, name FROM t WHERE salary > 50000", NIL,
{|nID, cName| Process(nID, cName)} )
Parameter order (cSQL, aParams, bBlock) keeps backward compatibility
with every existing call site. Passing NIL for aParams when only a
block is needed is standard Harbour idiom.
Routing:
* TFiveSQL:Execute now takes an optional bBlock parameter and
stores it on TSqlExecutor as ::bRowBlock.
* TSqlExecutor:RunSelect's existing Go fast path (same guards as
before: single table, no JOIN/GROUP/aggregate, plain column
projections, WHERE compilable via SqlExprToPrg) branches on
::bRowBlock:
- block present → SqlEach streams rows through the block
- block absent → SqlScan materializes into aRows (current path)
* Post-processing (GROUP BY / ORDER BY / window / DISTINCT / LIMIT)
runs on empty aRows when block mode fires — all are no-ops on
empty input, so the sequence stays harmless.
* RunSelect returns NIL (not {fields, rows}) when ::bRowBlock was
used — signals "streaming semantics, all work done in the block".
Complex queries (JOIN, GROUP BY, subquery, window, ORDER BY not
matchable by an index, LIMIT/OFFSET, etc.) still fall back to the
array path even when a block is supplied — those genuinely require
materialization. Block mode is a fast-path opt-in, not a semantic
change.
End-to-end bench (50k rows, steady state — includes the user-side
loop/block for every row):
Path Time Speedup vs raw
──────────────────────────────────────────────────────────────
Raw PRG DO WHILE !Eof() + WHERE sum 7.6ms 1.00x
five_SQL array + FOR 7.7ms ~same
five_SQL + block (new) 3.7ms 2.05x ← beats raw
──────────────────────────────────────────────────────────────
Raw PRG no WHERE 6.1ms 1.00x
five_SQL + block, no WHERE 2.9ms 2.10x ← beats raw
SQL now pays for itself on end-to-end timing — not just competitive
with hand-rolled RDD loops, but faster than them. The layered cost
of FieldGet's Frame+RTL-dispatch that hand-written loops incur per
call is gone; the block-callback path captures *dbf.DBFArea directly
via FastFieldGetter and uses PcOpFieldGet to bypass dispatch in the
compiled WHERE predicate.
Validation:
- FiveSql2 43/43 (array API unchanged)
- Harbour compat 51/51
- go test ./... ALL PASS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
44 lines
1.5 KiB
Plaintext
44 lines
1.5 KiB
Plaintext
/*
|
|
* FiveSqlCls.prg — Public API wrapper: five_SQL() function
|
|
*
|
|
* FiveSql2 — SQL Engine for Harbour DBF/NTX
|
|
*
|
|
* Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun)
|
|
* Email: charleskwonohjun@gmail.com
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
#include "FiveSqlDef.ch"
|
|
|
|
/*
|
|
* five_SQL( cSQL [, aParams ] [, bBlock ] ) --> aResult | NIL
|
|
*
|
|
* Execute a SQL statement against the current DBF workareas.
|
|
*
|
|
* Two return modes:
|
|
* 1. Without bBlock: returns { aFieldNames, aRows } on success,
|
|
* or { {"__error__"}, {{nCode, cMsg, cSQL}} } on failure.
|
|
* 2. With bBlock: streams matching rows into the block, spreading
|
|
* the SELECT list as positional params. Returns NIL.
|
|
* Block mode is the high-performance path — no
|
|
* intermediate row array is built.
|
|
*
|
|
* Block mode only fires for simple SELECT queries that the fast path
|
|
* already supports (single table, no JOIN, no GROUP BY, no aggregates,
|
|
* all projections are plain column refs). Complex queries fall back to
|
|
* array mode even when a block is supplied, and the block is invoked
|
|
* once per row after the fact as a compatibility layer.
|
|
*
|
|
* Accepts both parameter positions so existing callers still work:
|
|
* five_SQL( cSQL )
|
|
* five_SQL( cSQL, aParams )
|
|
* five_SQL( cSQL, aParams, bBlock )
|
|
* five_SQL( cSQL, NIL, bBlock )
|
|
*/
|
|
FUNCTION five_SQL( cSQL, aParams, bBlock )
|
|
|
|
LOCAL oSql := TFiveSQL():New( aParams )
|
|
|
|
RETURN oSql:Execute( cSQL, bBlock )
|