mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 06:16:24 +00:00
feat(storage): store mongo values as native BSON documents
Implement mongodb_value_codec to encode portfolio/gold/stock values as native BSON documents instead of JSON-serialized byte strings. Reduces storage size, improves query efficiency, and eliminates unmarshal overhead. Native BSON encoding preserves int64 fidelity and enables future native-format queries. Add NativeValueRepresentation and Int64Fidelity tests validating lossless encoding. Update migration tool comments. Document native-value behavior in deploy guide. Update phase-01 plan with implementation notes.
This commit is contained in:
1 parent
6bc5f334bb
commit
e8a6dbb4de
5 files changed
+137
-10
No files matched your search
@@ -0,0 +1,116 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// This file converts between the KVStore's opaque JSON []byte contract and a
|
||||
// native BSON representation, so values render as expandable documents in the
|
||||
// Atlas/Compass UI (and are queryable) rather than as opaque blobs.
|
||||
//
|
||||
// Fidelity: JSON is decoded with json.Number so integral numbers persist as
|
||||
// BSON int64 (NOT float64) — a struct's int64 field round-trips without
|
||||
// precision loss. Only JSON objects and arrays become native; bare scalars and
|
||||
// non-JSON values (e.g. a plain date string) fall back to a BSON string.
|
||||
|
||||
// encodeValue produces the stored BSON representation of a value. A JSON object
|
||||
// or array → native (expandable/queryable); anything else → a string (the
|
||||
// human-readable fallback, also covers non-UTF-8-free callers identically to
|
||||
// the previous string encoding).
|
||||
func encodeValue(val []byte) any {
|
||||
if native, ok := jsonToNative(val); ok {
|
||||
return native
|
||||
}
|
||||
return string(val)
|
||||
}
|
||||
|
||||
// jsonToNative decodes val into a native BSON value (bson.M / bson.A) when it is
|
||||
// a JSON object or array, preserving integers as int64. Returns ok=false for
|
||||
// scalars, non-JSON, or decode errors (caller stores those as a string).
|
||||
func jsonToNative(val []byte) (any, bool) {
|
||||
trimmed := bytes.TrimSpace(val)
|
||||
if len(trimmed) == 0 || (trimmed[0] != '{' && trimmed[0] != '[') {
|
||||
return nil, false
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(val))
|
||||
dec.UseNumber()
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
// Reject trailing garbage after the JSON value.
|
||||
if dec.More() {
|
||||
return nil, false
|
||||
}
|
||||
return numbersToBSON(v), true
|
||||
}
|
||||
|
||||
// numbersToBSON walks a json.Unmarshal(UseNumber) tree, converting maps/slices
|
||||
// to bson.M/bson.A and json.Number to int64 (when integral) else float64.
|
||||
func numbersToBSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
m := make(bson.M, len(t))
|
||||
for k, val := range t {
|
||||
m[k] = numbersToBSON(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
a := make(bson.A, len(t))
|
||||
for i, val := range t {
|
||||
a[i] = numbersToBSON(val)
|
||||
}
|
||||
return a
|
||||
case json.Number:
|
||||
if i, err := t.Int64(); err == nil {
|
||||
return i
|
||||
}
|
||||
if f, err := t.Float64(); err == nil {
|
||||
return f
|
||||
}
|
||||
return t.String()
|
||||
default:
|
||||
return v // string, bool, nil
|
||||
}
|
||||
}
|
||||
|
||||
// nativeToJSON re-serializes a natively-stored value (bson.M / bson.A / bson.D
|
||||
// / scalar) back to JSON bytes. bson.M is map[string]any and bson.A is []any,
|
||||
// both of which json.Marshal handles directly; bson.D (ordered) is normalized
|
||||
// to a map first so it marshals as an object, not an array of key/value pairs.
|
||||
func nativeToJSON(raw any) ([]byte, error) {
|
||||
out, err := json.Marshal(normalizeBSON(raw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mongo: re-encode native value: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// normalizeBSON converts bson.D to bson.M recursively so json.Marshal emits an
|
||||
// object. bson.M/bson.A children are recursed; scalars pass through.
|
||||
func normalizeBSON(v any) any {
|
||||
switch t := v.(type) {
|
||||
case bson.D:
|
||||
m := make(bson.M, len(t))
|
||||
for _, e := range t {
|
||||
m[e.Key] = normalizeBSON(e.Value)
|
||||
}
|
||||
return m
|
||||
case bson.M:
|
||||
for k, val := range t {
|
||||
t[k] = normalizeBSON(val)
|
||||
}
|
||||
return t
|
||||
case bson.A:
|
||||
for i, val := range t {
|
||||
t[i] = normalizeBSON(val)
|
||||
}
|
||||
return t
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user