Files
five/hbrtl/json.go
CharlesKWON dd270d5d9d perf: RTL Go-native migration — 27 optimizations, DML up to 70-90x
Systematic pass through PRG hot paths, promoting them to Go RTL while
preserving Harbour/FiveSql2 semantics. Full log in
docs/RTL-Go-Native-Migration.md.

Bench (bench_sql) vs 2026-04-08 baseline
 - B1  SELECT *             2,192 → 114   µs   (19x)
 - B6  INNER JOIN           9,291 → 233   µs   (40x)
 - B7  CTE simple           8,037 → 129   µs   (62x)
 - B9  ROW_NUMBER           3,705 → 265   µs   (14x)
 - B10 RANK PARTITION       4,748 → 309   µs   (15x)
 - B12 INSERT (WA cache)    4,319 →  63   µs   (69x)
 - B13 UPDATE (WA cache)    6,144 →  68   µs   (90x)
 - B15 CTE+WIN+JOIN        18,395 → 1,873 µs   (10x)

Infrastructure
 - HbHash O(1) Index preserving insertion order (Harbour KEEPORDER)
 - HbDeepClone Go RTL (scalar-sharing, immutable hash keys)
 - MEMRDD auto-imported via gengo; all Five programs get mem:name driver
 - SQL plan + pcode caches (s_hPlanCache, s_hDmlPcodeCache)
 - Opt-in SqlWACacheEnable — dbUseArea/Close/Commit batched for DML

SQL engine
 - FiveSql2 lexer ported to Go (byte FSM) with combined automatic
   template parameterization (literals → ?, concat queries share plan)
 - Go RTL: SqlDistinct, SqlGroupRows, SqlWindowPartitions,
   SqlWindowSortPartition, SqlWindowAssignRank, SqlComputeAggSimple,
   SqlBulkInsert, SqlBulkUpdate, SqlExprHasAgg, SqlEvalHaving
 - CTE / subquery / driving-table materialize paths use MEMRDD
 - SqlCoerce/SqlCmp/SqlIsTrue helpers moved from PRG to Go
 - SqlBulkUpdate defers Flush when WA cache active (APFS fsync was
   dominant B13 cost — 1.6ms/call → gone)

Correctness fixes uncovered during migration
 - ASort default path now sorts dates/logicals/timestamps (was no-op)
 - ORDER BY default NULL placement matches PRG SqlRowCompare across
   Go fast path; explicit NULLS FIRST/LAST honored by both paths
 - SqlBulkUpdate respects EXCLUSIVE vs SHARED mode record locks
 - SqlCmp/SqlCmpEq normalize NumInt vs Double (caught by test 6b)

Verification
 - go test ./...              ALL PASS
 - FiveSql2 test_sql1999      43/43
 - tests/compat_harbour       56/56 (+5 new: ASort dates/logicals,
                              AScan int cross-type)
 - Regression test test_null_order.prg for ORDER BY NULL ordering

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:20:14 +09:00

378 lines
8.4 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// JSON functions — Harbour compatible API + Go-native extensions.
//
// Harbour compatible:
// hb_jsonEncode(xValue [, lHumanReadable]) → cJSON
// hb_jsonDecode(cJSON) → xValue
//
// Five extensions (Go-native, beyond Harbour):
// JsonPretty(xValue [, cIndent]) → cPrettyJSON
// JsonTo(xValue, cFile) → lSuccess
// JsonFrom(cFile) → xValue
// JsonPath(xValue, cPath) → xResult — $.key.sub[0]
// JsonMerge(hDest, hSrc) → hMerged — deep merge
// JsonType(cJSON) → cType
// JsonValid(cJSON) → lValid
// JsonHttpGet(cURL [, nTimeout]) → hResult — HTTP GET + JSON
// JsonHttpPost(cURL, xBody [, nTimeout]) — HTTP POST + JSON
package hbrtl
import (
"encoding/json"
"five/hbrt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// === Harbour Compatible ===
// HB_JSONENCODE(xValue [, lHumanReadable]) → cJSON
func HbJsonEncode(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
v := t.Local(1)
pretty := false
if nParams >= 2 && !t.Local(2).IsNil() {
pretty = t.Local(2).AsBool()
}
goVal := valueToGo(v)
var data []byte
var err error
if pretty {
data, err = json.MarshalIndent(goVal, "", " ")
} else {
data, err = json.Marshal(goVal)
}
if err != nil {
t.RetString("")
return
}
t.RetString(string(data))
}
// HB_JSONDECODE(cJSON) → xValue
func HbJsonDecode(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
s := t.Local(1).AsString()
var raw interface{}
if err := json.Unmarshal([]byte(s), &raw); err != nil {
t.RetNil()
return
}
t.RetVal(goToValue(raw))
}
// === Five Extensions ===
// JSONPRETTY(xValue [, cIndent]) → cPrettyJSON
func JsonPretty(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
indent := " "
if nParams >= 2 && !t.Local(2).IsNil() {
indent = t.Local(2).AsString()
}
data, _ := json.MarshalIndent(valueToGo(t.Local(1)), "", indent)
t.RetString(string(data))
}
// JSONTO(xValue, cFile) → lSuccess
func JsonTo(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProc()
data, err := json.MarshalIndent(valueToGo(t.Local(1)), "", " ")
if err != nil {
t.RetBool(false)
return
}
t.RetBool(os.WriteFile(t.Local(2).AsString(), data, 0644) == nil)
}
// JSONFROM(cFile) → xValue
func JsonFrom(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
data, err := os.ReadFile(t.Local(1).AsString())
if err != nil {
t.RetNil()
return
}
var raw interface{}
if json.Unmarshal(data, &raw) != nil {
t.RetNil()
return
}
t.RetVal(goToValue(raw))
}
// JSONPATH(xValue, cPath) → xResult — $.key.sub[0]
func JsonPath(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProc()
root := t.Local(1)
path := strings.TrimPrefix(strings.TrimPrefix(t.Local(2).AsString(), "$."), "$")
if path == "" {
t.RetVal(root)
return
}
t.RetVal(navigatePath(root, path))
}
func navigatePath(v hbrt.Value, path string) hbrt.Value {
for _, part := range splitPath(path) {
if v.IsNil() {
return hbrt.MakeNil()
}
if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") {
idx, _ := strconv.Atoi(part[1 : len(part)-1])
if v.IsArray() {
arr := v.AsArray()
if idx >= 0 && idx < len(arr.Items) {
v = arr.Items[idx]
continue
}
}
return hbrt.MakeNil()
}
if v.IsHash() {
h := v.AsHash()
if i := h.Lookup(hbrt.MakeString(part)); i >= 0 {
v = h.Values[i]
} else {
return hbrt.MakeNil()
}
} else {
return hbrt.MakeNil()
}
}
return v
}
func splitPath(path string) []string {
var parts []string
cur := ""
for i := 0; i < len(path); i++ {
if path[i] == '.' {
if cur != "" {
parts = append(parts, cur)
cur = ""
}
} else if path[i] == '[' {
if cur != "" {
parts = append(parts, cur)
cur = ""
}
j := i
for j < len(path) && path[j] != ']' {
j++
}
parts = append(parts, path[i:j+1])
i = j
} else {
cur += string(path[i])
}
}
if cur != "" {
parts = append(parts, cur)
}
return parts
}
// JSONMERGE(hDest, hSrc) → hMerged
func JsonMerge(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProc()
dest, src := t.Local(1), t.Local(2)
if !dest.IsHash() || !src.IsHash() {
t.RetVal(src)
return
}
dh, sh := dest.AsHash(), src.AsHash()
result := &hbrt.HbHash{
Keys: make([]hbrt.Value, len(dh.Keys)),
Values: make([]hbrt.Value, len(dh.Values)),
}
copy(result.Keys, dh.Keys)
copy(result.Values, dh.Values)
for i, sk := range sh.Keys {
result.Set(sk, sh.Values[i])
}
t.RetVal(hbrt.MakeHashFrom(result))
}
// JSONTYPE(cJSON) → "object"|"array"|"string"|"number"|"boolean"|"null"|"invalid"
func JsonType(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
s := strings.TrimSpace(t.Local(1).AsString())
if s == "" {
t.RetString("invalid")
return
}
switch s[0] {
case '{':
t.RetString("object")
case '[':
t.RetString("array")
case '"':
t.RetString("string")
case 't', 'f':
t.RetString("boolean")
case 'n':
t.RetString("null")
default:
if (s[0] >= '0' && s[0] <= '9') || s[0] == '-' {
t.RetString("number")
} else {
t.RetString("invalid")
}
}
}
// JSONVALID(cJSON) → lValid
func JsonValid(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
t.RetBool(json.Valid([]byte(t.Local(1).AsString())))
}
// JSONHTTPGET(cURL [, nTimeout]) → {"status":n, "body":c, "error":c}
func JsonHttpGet(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
url := t.Local(1).AsString()
timeout := 30
if nParams >= 2 && !t.Local(2).IsNil() {
timeout = t.Local(2).AsInt()
}
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
resp, err := client.Get(url)
if err != nil {
t.RetVal(makeHttpResult(0, "", err.Error()))
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
t.RetVal(makeHttpResult(resp.StatusCode, string(body), ""))
}
// JSONHTTPPOST(cURL, xBody [, nTimeout]) → {"status":n, "body":c, "error":c}
func JsonHttpPost(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
url := t.Local(1).AsString()
bodyVal := t.Local(2)
timeout := 30
if nParams >= 3 && !t.Local(3).IsNil() {
timeout = t.Local(3).AsInt()
}
var bodyStr string
if bodyVal.IsString() {
bodyStr = bodyVal.AsString()
} else {
data, _ := json.Marshal(valueToGo(bodyVal))
bodyStr = string(data)
}
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
resp, err := client.Post(url, "application/json", strings.NewReader(bodyStr))
if err != nil {
t.RetVal(makeHttpResult(0, "", err.Error()))
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
t.RetVal(makeHttpResult(resp.StatusCode, string(body), ""))
}
func makeHttpResult(status int, body, errMsg string) hbrt.Value {
h := &hbrt.HbHash{
Keys: []hbrt.Value{hbrt.MakeString("status"), hbrt.MakeString("body"), hbrt.MakeString("error")},
Values: []hbrt.Value{hbrt.MakeInt(status), hbrt.MakeString(body), hbrt.MakeString(errMsg)},
}
return hbrt.MakeHashFrom(h)
}
// === Conversion: Five Value ↔ Go interface{} ===
func valueToGo(v hbrt.Value) interface{} {
if v.IsNil() {
return nil
}
if v.IsLogical() {
return v.AsBool()
}
if v.IsNumInt() {
return v.AsNumInt()
}
if v.IsNumeric() {
return v.AsNumDouble()
}
if v.IsString() {
return v.AsString()
}
if v.IsArray() {
arr := v.AsArray()
result := make([]interface{}, len(arr.Items))
for i, item := range arr.Items {
result[i] = valueToGo(item)
}
return result
}
if v.IsHash() {
h := v.AsHash()
result := make(map[string]interface{}, len(h.Keys))
for i := range h.Keys {
result[h.Keys[i].AsString()] = valueToGo(h.Values[i])
}
return result
}
return nil
}
func goToValue(v interface{}) hbrt.Value {
if v == nil {
return hbrt.MakeNil()
}
switch val := v.(type) {
case bool:
return hbrt.MakeBool(val)
case float64:
if val == float64(int64(val)) {
return hbrt.MakeNumInt(int64(val))
}
return hbrt.MakeDouble(val, 0, 0)
case string:
return hbrt.MakeString(val)
case []interface{}:
items := make([]hbrt.Value, len(val))
for i, item := range val {
items[i] = goToValue(item)
}
return hbrt.MakeArrayFrom(items)
case map[string]interface{}:
h := &hbrt.HbHash{
Keys: make([]hbrt.Value, 0, len(val)),
Values: make([]hbrt.Value, 0, len(val)),
}
for k, v := range val {
h.Keys = append(h.Keys, hbrt.MakeString(k))
h.Values = append(h.Values, goToValue(v))
}
return hbrt.MakeHashFrom(h)
}
return hbrt.MakeNil()
}