Documentation
¶
Overview ¶
Package pbpath provides functionality for representing a sequence of protobuf reflection operations on a message, including parsing human-readable path strings and traversing messages along a path to collect values.
pbpath extends the standard protopath.Step with additional step kinds for list range slicing ([start:end], [start:], [:end]) and list wildcards ([*], [:]) that fan out during value traversal.
Index ¶
- func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string
- func ParseStrptime(format, value string) (time.Time, error)
- func PathValuesMulti(md protoreflect.MessageDescriptor, m proto.Message, paths ...PlanPathSpec) ([][]Values, error)
- type EntryOption
- type Expr
- func CondWithAutoPromote(on bool, predicate, then, els Expr) Expr
- func FilterPathRef(relPath string, fields ...protoreflect.FieldDescriptor) Expr
- func FuncAbs(child Expr) Expr
- func FuncAdd(a, b Expr) Expr
- func FuncAge(children ...Expr) Expr
- func FuncAnd(a, b Expr) Expr
- func FuncBucket(child Expr, size int) Expr
- func FuncCastFloat(child Expr) Expr
- func FuncCastInt(child Expr) Expr
- func FuncCastString(child Expr) Expr
- func FuncCeil(child Expr) Expr
- func FuncCoalesce(children ...Expr) Expr
- func FuncCoerce(child Expr, ifTrue, ifFalse Value) Expr
- func FuncConcat(sep string, children ...Expr) Expr
- func FuncCond(predicate, then, els Expr) Expr
- func FuncDatePart(part string, child Expr) Expr
- func FuncDefault(child Expr, literal Value) Expr
- func FuncDistinct(child Expr) Expr
- func FuncDiv(a, b Expr) Expr
- func FuncEnumName(child Expr) Expr
- func FuncEpochToDate(child Expr) Expr
- func FuncEq(a, b Expr) Expr
- func FuncExtractDay(child Expr) Expr
- func FuncExtractHour(child Expr) Expr
- func FuncExtractMinute(child Expr) Expr
- func FuncExtractMonth(child Expr) Expr
- func FuncExtractSecond(child Expr) Expr
- func FuncExtractYear(child Expr) Expr
- func FuncFloor(child Expr) Expr
- func FuncGe(a, b Expr) Expr
- func FuncGt(a, b Expr) Expr
- func FuncHas(child Expr) Expr
- func FuncHash(children ...Expr) Expr
- func FuncLe(a, b Expr) Expr
- func FuncLen(child Expr) Expr
- func FuncListConcat(child Expr, sep string) Expr
- func FuncLower(child Expr) Expr
- func FuncLt(a, b Expr) Expr
- func FuncMask(child Expr, keepFirst, keepLast int, maskChar string) Expr
- func FuncMax(a, b Expr) Expr
- func FuncMin(a, b Expr) Expr
- func FuncMod(a, b Expr) Expr
- func FuncMul(a, b Expr) Expr
- func FuncNe(a, b Expr) Expr
- func FuncNot(child Expr) Expr
- func FuncOr(a, b Expr) Expr
- func FuncRound(child Expr) Expr
- func FuncSelect(predicate, input Expr) Expr
- func FuncStrptime(format string, child Expr) Expr
- func FuncSub(a, b Expr) Expr
- func FuncSum(child Expr) Expr
- func FuncTrim(child Expr) Expr
- func FuncTrimPrefix(child Expr, prefix string) Expr
- func FuncTrimSuffix(child Expr, suffix string) Expr
- func FuncTryStrptime(format string, child Expr) Expr
- func FuncUpper(child Expr) Expr
- func Literal(val Value, kind protoreflect.Kind) Expr
- func PathRef(path string) Expr
- type ObjectEntry
- type Path
- type PathOption
- type PipeContext
- type PipeExpr
- type Pipeline
- type Plan
- func (p *Plan) Clone() *Plan
- func (p *Plan) Entries() []PlanEntry
- func (p *Plan) Eval(m proto.Message) ([][]Values, error)
- func (p *Plan) EvalLeaves(m proto.Message) ([][]Value, error)
- func (p *Plan) EvalLeavesConcurrent(m proto.Message) ([][]Value, error)
- func (p *Plan) ExprInputEntries(idx int) []int
- func (p *Plan) InternalPath(idx int) Path
- type PlanEntry
- type PlanOption
- type PlanPathSpec
- type Query
- type Result
- func (r Result) Bool() bool
- func (r Result) Bools() []bool
- func (r Result) Bytes() []byte
- func (r Result) BytesSlice() [][]byte
- func (r Result) Float64() float64
- func (r Result) Float64s() []float64
- func (r Result) Int64() int64
- func (r Result) Int64s() []int64
- func (r Result) IsEmpty() bool
- func (r Result) Len() int
- func (r Result) Message() protoreflect.Message
- func (r Result) Messages() []protoreflect.Message
- func (r Result) ProtoValues() []protoreflect.Value
- func (r Result) String() string
- func (r Result) Strings() []string
- func (r Result) Uint64() uint64
- func (r Result) Uint64s() []uint64
- func (r Result) Value() Value
- func (r Result) Values() []Value
- type ResultSet
- type Step
- func AnyExpand(md protoreflect.MessageDescriptor) Step
- func FieldAccess(fd protoreflect.FieldDescriptor) Step
- func Filter(predicate Expr) Step
- func ListIndex(i int) Step
- func ListRange(start, end int) Step
- func ListRangeFrom(start int) Step
- func ListRangeStep3(start, end, step int, startOmitted, endOmitted bool) Step
- func ListWildcard() Step
- func MapIndex(k protoreflect.MapKey) Step
- func MapWildcard() Step
- func Root(md protoreflect.MessageDescriptor) Step
- func (s Step) EndOmitted() bool
- func (s Step) FieldDescriptor() protoreflect.FieldDescriptor
- func (s Step) Kind() StepKind
- func (s Step) ListIndex() int
- func (s Step) MapIndex() protoreflect.MapKey
- func (s Step) MessageDescriptor() protoreflect.MessageDescriptor
- func (s Step) Predicate() Expr
- func (s Step) ProtoStep() protopath.Step
- func (s Step) RangeEnd() int
- func (s Step) RangeOpen() bool
- func (s Step) RangeStart() int
- func (s Step) RangeStep() int
- func (s Step) StartOmitted() bool
- type StepKind
- type Value
- func FromProtoValue(pv protoreflect.Value) Value
- func ListVal(items []Value) Value
- func MessageVal(m protoreflect.Message) Value
- func Null() Value
- func ObjectVal(entries []ObjectEntry) Value
- func Scalar(pv protoreflect.Value) Value
- func ScalarBool(b bool) Value
- func ScalarFloat64(f float64) Value
- func ScalarInt32(n int32) Value
- func ScalarInt64(n int64) Value
- func ScalarString(s string) Value
- func (v Value) Entries() []ObjectEntry
- func (v Value) Get(fd protoreflect.FieldDescriptor) Value
- func (v Value) Index(i int) Value
- func (v Value) IsNonZero() bool
- func (v Value) IsNull() bool
- func (v Value) Kind() ValueKind
- func (v Value) Len() int
- func (v Value) List() []Value
- func (v Value) Message() protoreflect.Message
- func (v Value) ProtoValue() protoreflect.Value
- func (v Value) String() string
- func (v Value) ToProtoValue() protoreflect.Value
- type ValueKind
- type Values
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FormatValue ¶
func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string
FormatValue returns a human-readable string representation of a protoreflect.Value.
func ParseStrptime ¶ added in v0.2.0
ParseStrptime parses a date/time string using the given format.
Format auto-detection:
- If the format contains a '%' character, it is interpreted as a DuckDB strptime format (e.g. "%Y-%m-%d %H:%M:%S").
- Otherwise it is treated as a Go time.Parse layout (e.g. "2006-01-02 15:04:05").
Supported DuckDB format specifiers:
%Y — 4-digit year %y — 2-digit year (00–99 → 2000–2099) %m — month (01–12) %-m — month without leading zero %d — day (01–31) %-d — day without leading zero %H — hour 24h (00–23) %-H — hour without leading zero %I — hour 12h (01–12) %-I — hour 12h without leading zero %M — minute (00–59) %-M — minute without leading zero %S — second (00–59) %-S — second without leading zero %f — microseconds (up to 6 digits, zero-padded right) %p — AM/PM %z — UTC offset (+HHMM / -HHMM / Z) %Z — timezone name (parsed but forced to UTC for portability) %j — day of year (001–366) %a — abbreviated weekday name (Mon, Tue, …) — consumed but ignored %A — full weekday name (Monday, Tuesday, …) — consumed but ignored %b — abbreviated month name (Jan, Feb, …) %B — full month name (January, February, …) %% — literal '%' %n — any whitespace (at least one character) %t — any whitespace (at least one character; same as %n)
The parser is intentionally simple and lenient: it does not validate day-of-week consistency, and %Z is treated as informational (the result is always in UTC unless %z provides an offset).
func PathValuesMulti ¶
func PathValuesMulti(md protoreflect.MessageDescriptor, m proto.Message, paths ...PlanPathSpec) ([][]Values, error)
PathValuesMulti is a convenience wrapper that compiles a Plan from the given path specs and immediately evaluates it against msg. For repeated evaluation of the same paths against many messages, prefer NewPlan + Plan.Eval.
Example ¶
ExamplePathValuesMulti demonstrates the convenience wrapper for one-shot multi-path evaluation. This is useful for tests and ad-hoc extractions. For repeated evaluation of the same paths across many messages, prefer NewPlan + EvalLeaves.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("hello"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
results, err := pbpath.PathValuesMulti(testMD, msg,
pbpath.PlanPath("nested.stringfield", pbpath.Alias("greeting")),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(results[0][0].Index(-1).Value.Interface())
}
Output: hello
Types ¶
type EntryOption ¶ added in v0.2.0
type EntryOption func(*planEntryOpts)
EntryOption configures a single path entry inside a Plan.
func Alias ¶
func Alias(name string) EntryOption
Alias returns an EntryOption that gives this path entry a human-readable name. The alias is returned by Plan.Entries and is useful for mapping paths to output column names.
func StrictPath ¶
func StrictPath() EntryOption
StrictPath returns an EntryOption that makes this path's evaluation return an error when a range or index is clamped due to the list being shorter than the requested bound. Without StrictPath, out-of-bounds accesses are silently clamped or skipped.
Example ¶
ExampleStrictPath demonstrates using StrictPath to detect when a range or index was clamped because the list is shorter than expected. Without StrictPath, out-of-bounds accesses are silently skipped.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD, nil,
// This path expects at least 10 elements — will error if fewer exist.
pbpath.PlanPath("repeats[0:10].nested.stringfield", pbpath.StrictPath()),
)
if err != nil {
log.Fatal(err)
}
// Build a message with only 2 repeats — the range [0:10] will be clamped.
msg := dynamicpb.NewMessage(testMD)
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"a", "b"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
_, err = plan.Eval(msg)
fmt.Printf("strict error: %v\n", err != nil)
}
Output: strict error: true
func WithExpr ¶ added in v0.2.0
func WithExpr(expr Expr) EntryOption
WithExpr returns an EntryOption that attaches a composable Expr to a path entry. The expr tree's leaf PathRef nodes are resolved against the plan's trie during compilation; at evaluation time the function tree is applied to the resolved leaf values.
When WithExpr is set the path string passed to PlanPath is ignored for traversal purposes — all paths come from the Expr tree's leaves. The PlanPath path string (or Alias) is still used as the entry name.
type Expr ¶ added in v0.2.0
type Expr interface {
// contains filtered or unexported methods
}
Expr represents a composable expression in a Plan.
An Expr tree is built from leaf PathRef nodes (referencing protobuf field paths) and interior function nodes (created via Func* constructors). The tree is validated and resolved at NewPlan time; evaluation happens inside Plan.EvalLeaves.
Expr is intentionally an opaque interface — construct instances via PathRef, FuncCoalesce, FuncAdd, and friends.
func CondWithAutoPromote ¶ added in v0.2.0
CondWithAutoPromote returns a FuncCond with an explicit auto-promote override. When on is true and the then/else branches have different output kinds, the result is promoted to the wider type.
func FilterPathRef ¶ added in v0.3.0
func FilterPathRef(relPath string, fields ...protoreflect.FieldDescriptor) Expr
FilterPathRef creates a leaf Expr referencing a field path relative to the current message cursor. Used inside filter predicates. The fields parameter is the resolved chain of field descriptors from the cursor message to the target field.
func FuncAbs ¶ added in v0.2.0
FuncAbs returns the absolute value of a numeric child. Preserves int vs float kind.
func FuncAdd ¶ added in v0.2.0
FuncAdd returns the sum of two numeric children (a + b). Go-style type promotion: mixed int/float promotes to float.
func FuncAge ¶ added in v0.2.0
FuncAge computes the duration in milliseconds between timestamps.
- 1 argument: now − child (age of the timestamp).
- 2 arguments: child[0] − child[1] (difference between two timestamps).
Children must be Int64 Unix-millisecond values (e.g. from Strptime). Output kind: Int64Kind.
func FuncAnd ¶ added in v0.3.0
FuncAnd returns the logical AND of two boolean children. Both children should evaluate to boolean-like values; the result is true only when both are truthy (non-null, non-zero). Output kind: BoolKind.
func FuncBucket ¶ added in v0.2.0
FuncBucket floors the integer child value to the nearest multiple of size. result = value − value%size. Output kind: pass-through.
func FuncCastFloat ¶ added in v0.2.0
FuncCastFloat casts the child to Float64 (Double). int→float64, string→float64 (parse), bool→0.0/1.0. Output kind: DoubleKind.
func FuncCastInt ¶ added in v0.2.0
FuncCastInt casts the child to Int64. float→int64 (truncate), string→int64 (parse), bool→0/1. Output kind: Int64Kind.
func FuncCastString ¶ added in v0.2.0
FuncCastString casts the child to String using [valueToString]. Output kind: StringKind.
func FuncCeil ¶ added in v0.2.0
FuncCeil returns the ceiling of a float child. No-op for integers. Preserves int vs float kind.
func FuncCoalesce ¶ added in v0.2.0
FuncCoalesce returns the first non-zero child value. All children must resolve to the same protoreflect.Kind.
func FuncCoerce ¶ added in v0.2.0
FuncCoerce maps a boolean child to one of two literal values. If the child is non-zero (true), ifTrue is returned; otherwise ifFalse. Output kind: StringKind.
func FuncConcat ¶ added in v0.2.0
FuncConcat joins the string representations of all children with sep. Output kind: StringKind.
func FuncCond ¶ added in v0.2.0
FuncCond evaluates predicate (child 0); if its value is non-zero, returns child 1 (then), otherwise child 2 (else). Use CondWithAutoPromote to override the plan-level auto-promote setting.
func FuncDatePart ¶ added in v0.2.0
FuncDatePart extracts a calendar component from a Unix-epoch-second timestamp. Supported parts: "year", "month", "day", "hour", "minute", "second". The part name is stored in the separator field. Output kind: Int64Kind.
func FuncDefault ¶ added in v0.2.0
FuncDefault returns the child value if non-zero, otherwise the literal.
func FuncDistinct ¶ added in v0.2.0
FuncDistinct is an aggregate that counts the number of distinct values across all fan-out branches of the child. Output kind: Int64Kind.
func FuncDiv ¶ added in v0.2.0
FuncDiv returns the quotient of two numeric children (a / b). Integer division truncates toward zero. Division by zero returns zero.
func FuncEnumName ¶ added in v0.2.0
FuncEnumName maps an enum-typed field to its string name. The protoreflect.EnumDescriptor is resolved at NewPlan time by inspecting the child leaf's terminal field descriptor. Output kind: StringKind.
func FuncEpochToDate ¶ added in v0.2.0
FuncEpochToDate converts a Unix-epoch-second timestamp (Int64) to a day-offset (Int32) by dividing by 86 400. Useful for date-only columns. Output kind: Int32Kind. TODO: ideally maps to Arrow Date32 in typemap.go — deferred.
func FuncEq ¶ added in v0.2.0
FuncEq returns a Bool indicating whether a == b. Numeric operands use Go-style int→float promotion; strings use lexicographic comparison.
func FuncExtractDay ¶ added in v0.2.0
FuncExtractDay extracts the day of month (1–31) from a Unix-millisecond timestamp. Output kind: Int64Kind.
func FuncExtractHour ¶ added in v0.2.0
FuncExtractHour extracts the hour (0–23) from a Unix-millisecond timestamp. Output kind: Int64Kind.
func FuncExtractMinute ¶ added in v0.2.0
FuncExtractMinute extracts the minute (0–59) from a Unix-millisecond timestamp. Output kind: Int64Kind.
func FuncExtractMonth ¶ added in v0.2.0
FuncExtractMonth extracts the month (1–12) from a Unix-millisecond timestamp. Output kind: Int64Kind.
func FuncExtractSecond ¶ added in v0.2.0
FuncExtractSecond extracts the second (0–59) from a Unix-millisecond timestamp. Output kind: Int64Kind.
func FuncExtractYear ¶ added in v0.2.0
FuncExtractYear extracts the year from a Unix-millisecond timestamp (Int64). Output kind: Int64Kind.
func FuncFloor ¶ added in v0.2.0
FuncFloor returns the floor of a float child. No-op for integers. Preserves int vs float kind.
func FuncHas ¶ added in v0.2.0
FuncHas returns a Bool indicating whether the child path is set (non-zero). Output kind: BoolKind.
func FuncHash ¶ added in v0.2.0
FuncHash returns an FNV-1a 64-bit hash of the child's string representation. Output kind: Int64Kind.
func FuncLen ¶ added in v0.2.0
FuncLen returns the length of a repeated/map/bytes/string field as Int64. Output kind: Int64Kind.
func FuncListConcat ¶ added in v0.2.0
FuncListConcat is an aggregate that joins the string representation of all fan-out branches of the child with the given separator. Output kind: StringKind.
func FuncLower ¶ added in v0.2.0
FuncLower returns the string value converted to lower case. Output kind: StringKind.
func FuncMask ¶ added in v0.2.0
FuncMask redacts the interior of a string, keeping keepFirst leading characters and keepLast trailing characters. The interior is replaced with repetitions of maskChar (default "*"). Output kind: StringKind.
func FuncMax ¶ added in v0.2.0
FuncMax returns the larger of two numeric children. Go-style int→float promotion for mixed types.
func FuncMin ¶ added in v0.2.0
FuncMin returns the smaller of two numeric children. Go-style int→float promotion for mixed types.
func FuncMod ¶ added in v0.2.0
FuncMod returns the remainder of two integer children (a % b). Mod by zero returns zero.
func FuncNot ¶ added in v0.3.0
FuncNot returns the logical NOT of a boolean child. The result is true when the child is falsy (null, zero, false, empty). Output kind: BoolKind.
func FuncOr ¶ added in v0.3.0
FuncOr returns the logical OR of two boolean children. The result is true when at least one child is truthy. Output kind: BoolKind.
func FuncRound ¶ added in v0.2.0
FuncRound returns the nearest integer value of a float child (banker's rounding). No-op for integers. Preserves int vs float kind.
func FuncSelect ¶ added in v0.3.0
FuncSelect evaluates a predicate child and, if truthy, returns the value of the second child (the "input" being filtered). If the predicate is falsy, returns null. This gives jq-style `| select(pred)` semantics.
Usage: FuncSelect(predicate, inputPathRef) The first child is the boolean predicate; the second is the value to pass through (typically the same path being filtered). Output kind: pass-through (same as the input child).
func FuncStrptime ¶ added in v0.2.0
FuncStrptime parses a string child into a Unix-millisecond timestamp (Int64) using the given format. If the format contains '%' specifiers, it is interpreted as a DuckDB strptime format; otherwise it is treated as a Go time.Parse layout.
Returns an invalid value on parse failure. The format is stored in the separator field. Output kind: Int64Kind.
func FuncSum ¶ added in v0.2.0
FuncSum is an aggregate that sums all fan-out branches of the child. The result is the same for every branch. Output kind: pass-through.
func FuncTrim ¶ added in v0.2.0
FuncTrim returns the string with leading and trailing whitespace removed. Output kind: StringKind.
func FuncTrimPrefix ¶ added in v0.2.0
FuncTrimPrefix returns the string with the given prefix removed (if present). The prefix is stored as the separator field. Output kind: StringKind.
func FuncTrimSuffix ¶ added in v0.2.0
FuncTrimSuffix returns the string with the given suffix removed (if present). The suffix is stored as the separator field. Output kind: StringKind.
func FuncTryStrptime ¶ added in v0.2.0
FuncTryStrptime is like FuncStrptime but returns zero (epoch) instead of an invalid value on parse failure. Output kind: Int64Kind.
func FuncUpper ¶ added in v0.2.0
FuncUpper returns the string value converted to upper case. Output kind: StringKind.
type ObjectEntry ¶ added in v0.3.0
ObjectEntry is a single key-value pair in an ObjectKind Value.
type Path ¶
type Path []Step
Path is a sequence of protobuf reflection steps applied to some root protobuf message value to arrive at the current value. The first step must be a Root step.
func ParsePath ¶
func ParsePath(md protoreflect.MessageDescriptor, path string) (Path, error)
ParsePath translates a human-readable representation of a path into a Path.
An empty path is an empty string. A field access step is path '.' identifier A map index step is path '[' natural ']' A list index step is path '[' integer ']' (negative indices allowed) A list range step is path '[' start ':' end ']' or '[' start ':' ']' A list slice step is path '[' start ':' end ':' step ']' (Python-style slice)
- Any of start, end, step may be omitted: [::2], [1::], [::-1], [::]
- step=0 is an error.
- Both [:] and [::] produce a wildcard.
A list wildcard step is path '[' '*' ']' or '[' ':' ']' or '[' '::' ']' A root step is '(' msg.Descriptor().String() ')'
If the path does not start with '(' then the root step is implicitly for the given message. The parser is "type aware" to distinguish lists and maps keyed by numbers.
Example ¶
ExampleParsePath demonstrates parsing a path string against a message descriptor. The returned Path is a slice of Step values that can be inspected or passed to PathValues for traversal.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, _ := exampleDescriptors()
// Parse a simple field access path.
path, err := pbpath.ParsePath(testMD, "nested.stringfield")
if err != nil {
log.Fatal(err)
}
fmt.Printf("steps: %d\n", len(path))
fmt.Printf("path: %s\n", path)
// Parse a wildcard path — fans out across all elements of "repeats".
path2, err := pbpath.ParsePath(testMD, "repeats[*].nested.stringfield")
if err != nil {
log.Fatal(err)
}
fmt.Printf("fan-out path: %s\n", path2)
}
Output: steps: 3 path: (example.Test).nested.stringfield fan-out path: (example.Test).repeats[*].nested.stringfield
type PathOption ¶
type PathOption func(*pathOpts)
PathOption configures the behaviour of PathValues.
func Strict ¶
func Strict() PathOption
Strict returns a PathOption that makes PathValues return an error when a negative index or range bound resolves to an out-of-bounds position. Without Strict, out-of-bounds accesses are silently clamped or skipped.
type PipeContext ¶ added in v0.3.0
type PipeContext struct {
// contains filtered or unexported fields
}
PipeContext carries execution state through a pipeline. It holds the root message descriptor for schema-aware operations and variable bindings from `as $name` expressions.
type PipeExpr ¶ added in v0.3.0
type PipeExpr interface {
// contains filtered or unexported methods
}
PipeExpr is a node in a pipeline expression tree. Each node receives one input Value and produces zero or more output Values.
Implementations include path access, iteration, collection, comparisons, boolean logic, built-in functions, and literal constants.
type Pipeline ¶ added in v0.3.0
type Pipeline struct {
// contains filtered or unexported fields
}
Pipeline is a compiled sequence of pipe-separated expressions. Each expression receives every value from the current stream and produces zero or more output values for the next expression.
Create a Pipeline via ParsePipeline.
func ParsePipeline ¶ added in v0.3.0
func ParsePipeline(md protoreflect.MessageDescriptor, input string) (*Pipeline, error)
ParsePipeline parses a jq-style pipeline string against a message descriptor and returns a compiled Pipeline ready for execution.
Grammar:
pipeline = comma_expr [ "as" "$" ident "|" pipeline ] { "|" comma_expr [ "as" "$" ident "|" pipeline ] }
comma_expr = alt_expr { "," alt_expr }
alt_expr = or_expr { "//" or_expr }
or_expr = and_expr { "or" and_expr }
and_expr = compare_expr { "and" compare_expr }
compare_expr = add_expr [ ("==" | "!=" | "<" | "<=" | ">" | ">=") add_expr ]
add_expr = mul_expr { ("+" | "-") mul_expr }
mul_expr = postfix_expr { ("*" | "/" | "%") postfix_expr }
postfix_expr = primary { suffix } [ "?" ]
suffix = "." ident // field access
| "[" "]" // iterate
| "[" integer "]" // index
primary = "." // identity (or start of path/iterate/index)
| "." ident // field access on input
| "." "[" "]" // iterate on input
| "." "[" int "]" // index on input
| "[" pipeline "]" // collect
| "(" pipeline ")" // grouping
| ident [ "(" pipeline { ";" pipeline } ")" ] // function call
| "$" ident // variable reference
| "-" primary // unary negation
| "!" primary // unary not
| "@" ident // format string (@base64, @csv, @text, etc.)
| "if" expr "then" pipeline {"elif" expr "then" pipeline} ["else" pipeline] "end"
| "try" primary ["catch" primary]
| "reduce" postfix_expr "as" "$" ident "(" pipeline ";" pipeline ")"
| "foreach" postfix_expr "as" "$" ident "(" pipeline ";" pipeline [";" pipeline] ")"
| "label" "$" ident "|" pipeline
| "break" "$" ident
| "def" ident [ "(" ident { ";" ident } ")" ] ":" pipeline ";" pipeline // user-defined function
| "{" [ obj_entry { "," obj_entry } [","] ] "}" // object construction
| string_interp // string interpolation
| literal
string_interp = strbegin { pipeline strmid } pipeline strend // "text \(expr) text"
obj_entry = ident ":" alt_expr // static key
| string ":" alt_expr // string key
| "(" pipeline ")" ":" alt_expr // dynamic key
| ident // shorthand for ident: .ident
literal = string | integer | float | "true" | "false" | "null"
func (*Pipeline) Exec ¶ added in v0.3.0
Exec runs the pipeline against the given input stream. Each expression is applied to every value in the current stream; results are concatenated to form the input for the next expression.
func (*Pipeline) ExecMessage ¶ added in v0.3.0
func (p *Pipeline) ExecMessage(msg protoreflect.Message) ([]Value, error)
ExecMessage runs the pipeline against a protobuf message.
type Plan ¶
type Plan struct {
// contains filtered or unexported fields
}
Plan is an immutable, pre-compiled bundle of Path values ready for repeated evaluation against messages of a single type.
Paths that share a common prefix are traversed once through the shared segment, then forked — giving an O(1) cost per shared step rather than O(P) where P is the number of paths.
Plan.Eval and Plan.EvalLeavesConcurrent are safe for concurrent use by multiple goroutines.
Plan.EvalLeaves reuses internal scratch buffers and is NOT safe for concurrent use; it is the preferred method when called from a single goroutine (e.g. inside [Transcoder.AppendDenorm]).
func NewPlan ¶
func NewPlan(md protoreflect.MessageDescriptor, opts []PlanOption, paths ...PlanPathSpec) (*Plan, error)
NewPlan compiles one or more path strings against md into an immutable Plan. Parsing and trie construction happen once; Plan.Eval is the hot path.
opts may be nil when no plan-level options are needed. All paths must be rooted at the same message descriptor md. Returns an error that bundles all parse failures.
Example ¶
ExampleNewPlan demonstrates compiling multiple paths into a Plan for repeated evaluation against messages of the same type.
The Plan API is the recommended approach for hot paths because:
- Paths sharing a common prefix are traversed only once (trie merging).
- EvalLeaves reuses scratch buffers, eliminating per-call allocations.
- The Plan is immutable and safe for concurrent use (with EvalLeavesConcurrent).
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
// Compile two paths — they share the "repeats[*].nested" prefix so
// the Plan traverses it only once.
plan, err := pbpath.NewPlan(testMD, nil,
pbpath.PlanPath("repeats[*].nested.stringfield", pbpath.Alias("name")),
pbpath.PlanPath("nested.stringfield", pbpath.Alias("top")),
)
if err != nil {
log.Fatal(err)
}
// Build a message:
// Test { nested: { stringfield: "root" }, repeats: [
// Test{ nested: { stringfield: "a" } },
// Test{ nested: { stringfield: "b" } },
// ]}
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("root"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"a", "b"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
// Evaluate.
results, err := plan.Eval(msg)
if err != nil {
log.Fatal(err)
}
// results[0] = "name" path (repeats[*].nested.stringfield) → 2 branches
fmt.Printf("name: %d branches\n", len(results[0]))
for _, v := range results[0] {
fmt.Printf(" %v\n", v.Index(-1).Value.Interface())
}
// results[1] = "top" path (nested.stringfield) → 1 branch
fmt.Printf("top: %v\n", results[1][0].Index(-1).Value.Interface())
}
Output: name: 2 branches a b top: root
func (*Plan) Clone ¶ added in v0.3.0
Clone returns a shallow copy of p with its own scratch buffer. The trie, entries, and schema are shared (all immutable after construction); only the mutable [scratch] field is reset so that the clone's Plan.EvalLeaves calls do not race with the original or other clones.
Use Clone when creating independent workers (e.g. [Transcoder.Clone]) that each need to call Plan.EvalLeaves without synchronisation.
func (*Plan) Entries ¶
Entries returns metadata for each compiled path, in the order they were provided to NewPlan.
Example ¶
ExamplePlan_Entries shows how to inspect a Plan's compiled entries. This is useful for mapping result slots to output column names — each entry's Name is the alias (if provided) or the raw path string.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, _ := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD, nil,
pbpath.PlanPath("nested.stringfield", pbpath.Alias("col_a")),
pbpath.PlanPath("repeats[*].nested.stringfield"),
)
if err != nil {
log.Fatal(err)
}
for _, e := range plan.Entries() {
fmt.Printf("%-30s path: %s\n", e.Name, e.Path)
}
}
Output: col_a path: (example.Test).nested.stringfield repeats[*].nested.stringfield path: (example.Test).repeats[*].nested.stringfield
func (*Plan) Eval ¶
Eval traverses msg along all compiled paths simultaneously. Paths sharing a prefix are traversed once through the shared segment.
Returns a slice of []Values indexed by entry position (matching the order paths were provided to NewPlan). Each []Values may contain multiple entries when the path fans out via wildcards, ranges, or slices.
Per-path StrictPath options are checked at leaf nodes: if any branch was clamped during traversal and the path is strict, an error is returned.
Example ¶
ExamplePlan_Eval shows how fan-out paths produce multiple Values branches. The [0:2] range slice selects the first two elements of the repeated field, even though three elements exist.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD, nil,
pbpath.PlanPath("repeats[0:2].nested.stringfield"),
)
if err != nil {
log.Fatal(err)
}
// Three-element repeated field — the [0:2] slice selects the first two.
msg := dynamicpb.NewMessage(testMD)
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"x", "y", "z"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
results, err := plan.Eval(msg)
if err != nil {
log.Fatal(err)
}
for _, v := range results[0] {
fmt.Println(v.Index(-1).Value.Interface())
}
}
Output: x y
func (*Plan) EvalLeaves ¶ added in v0.2.0
EvalLeaves traverses msg along all compiled paths simultaneously, returning only the leaf (last) value for each branch — not the full path/values chain.
This is significantly cheaper than Plan.Eval because it avoids clonePath/cloneValues allocations entirely, tracking only a single cursor value per branch per trie step.
The returned slice is indexed by entry position (matching NewPlan order). Each inner slice contains one Value per fan-out branch. An empty inner slice means the path produced no values for this message (left-join null).
EvalLeaves reuses internal scratch buffers and is NOT safe for concurrent use. Use Plan.EvalLeavesConcurrent when calling from multiple goroutines.
Example ¶
ExamplePlan_EvalLeaves demonstrates the high-performance EvalLeaves method, which returns only leaf values (not full path chains) and reuses internal scratch buffers to minimize allocations.
Use EvalLeaves for hot paths where you process thousands of messages per second. For concurrent access from multiple goroutines, use EvalLeavesConcurrent instead.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD, nil,
pbpath.PlanPath("nested.stringfield", pbpath.Alias("top_name")),
pbpath.PlanPath("repeats[*].nested.stringfield", pbpath.Alias("child_name")),
)
if err != nil {
log.Fatal(err)
}
// Build a message with nested value and two repeats.
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("root"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"x", "y"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
// EvalLeaves returns [][]protoreflect.Value — just the leaf values.
// No full path chains, no per-call allocations (scratch buffers reused).
leaves, err := plan.EvalLeaves(msg)
if err != nil {
log.Fatal(err)
}
// leaves[0] = top_name → 1 value
fmt.Printf("top_name: %s\n", leaves[0][0].ToProtoValue().String())
// leaves[1] = child_name → 2 values (one per repeat)
fmt.Printf("child_names: %d values\n", len(leaves[1]))
for _, v := range leaves[1] {
fmt.Printf(" %s\n", v.ToProtoValue().String())
}
}
Output: top_name: root child_names: 2 values x y
func (*Plan) EvalLeavesConcurrent ¶ added in v0.2.0
EvalLeavesConcurrent is like Plan.EvalLeaves but allocates fresh buffers per call, making it safe for concurrent use by multiple goroutines.
func (*Plan) ExprInputEntries ¶ added in v0.2.0
ExprInputEntries returns the plan-entry indices of all leaf PathRef nodes referenced by the Expr on user-visible entry idx. These are indices into the full internal entries slice (which may include hidden leaf paths appended after the user-visible entries).
Returns nil when the entry has no Expr. Callers can use Plan.InternalPath to retrieve the compiled path for each returned index.
func (*Plan) InternalPath ¶ added in v0.2.0
InternalPath returns the compiled Path for the given internal entry index. Unlike [Entries] (which exposes only user-visible entries), this method can access hidden leaf paths that were added to the trie for Expr evaluation. Returns nil if idx is out of range.
type PlanEntry ¶
type PlanEntry struct {
// Name is the alias if one was set via [Alias], otherwise the original
// path string.
Name string
// Path is the compiled [Path]. Nil for Expr-only entries.
Path Path
// OutputKind is the protoreflect.Kind of the expression's result,
// or zero when no [WithExpr] was set (raw path leaf kind) or when the
// Expr is pass-through (same kind as input leaf).
OutputKind protoreflect.Kind
// HasExpr is true when this entry was created with [WithExpr].
HasExpr bool
}
PlanEntry exposes metadata about one compiled path inside a Plan.
type PlanOption ¶
type PlanOption func(*planConfig)
PlanOption configures plan-level behaviour for NewPlan.
func AutoPromote ¶ added in v0.2.0
func AutoPromote(on bool) PlanOption
AutoPromote returns a PlanOption that sets the default auto-promotion behaviour for [Cond] expressions. When true, Cond branches with mismatched output kinds are automatically promoted to a common type. Individual Cond expressions can override this setting.
type PlanPathSpec ¶
type PlanPathSpec struct {
// contains filtered or unexported fields
}
PlanPathSpec pairs a raw path string with per-path options. Create one with PlanPath.
func PlanPath ¶
func PlanPath(path string, opts ...EntryOption) PlanPathSpec
PlanPath creates a PlanPathSpec pairing a path string with per-path options.
type Query ¶ added in v0.3.0
type Query struct {
// contains filtered or unexported fields
}
Query is a pre-compiled, reusable query object built from a Plan. It wraps the plan and presents evaluation results through the typed ResultSet / Result API instead of raw protoreflect.Value slices.
Use NewQuery to create a Query from an existing plan, or QueryEval for a one-shot evaluation without pre-compilation.
func NewQuery ¶ added in v0.3.0
NewQuery creates a Query that wraps plan. The plan must already be compiled via NewPlan. The Query does not own the plan; the caller is responsible for ensuring the plan outlives the query.
func (*Query) Run ¶ added in v0.3.0
Run evaluates the query against msg and returns a ResultSet keyed by each path entry's name (alias or path string).
Run reuses the plan's internal scratch buffers (via Plan.EvalLeaves) and is therefore NOT safe for concurrent use. Use [RunConcurrent] when calling from multiple goroutines.
type Result ¶ added in v0.3.0
type Result struct {
// contains filtered or unexported fields
}
Result wraps a slice of Value representing the fan-out output of a single path entry. It provides typed accessor methods for convenient consumption of query results.
The zero value is an empty result (no values).
Accessor naming convention:
- Plural methods (Float64s, Strings, …) return a slice of all branches.
- Singular methods (Float64, String, …) return the first branch value or the Go zero value when the result is empty.
func NewResult ¶ added in v0.3.0
NewResult creates a Result from a slice of Value. The slice is referenced, not copied.
func QueryEval ¶ added in v0.3.0
func QueryEval(md protoreflect.MessageDescriptor, path string, msg proto.Message) (Result, error)
QueryEval is a convenience function that compiles a single path against md, evaluates it against msg, and returns the typed Result.
For repeated evaluation of the same path against many messages, prefer NewPlan + NewQuery + Query.Run.
func (Result) Bool ¶ added in v0.3.0
Bool returns the first branch as bool. Returns false if the result is empty or the value is not boolean.
func (Result) Bools ¶ added in v0.3.0
Bools returns all branches as bool values. Non-boolean branches are silently skipped.
func (Result) Bytes ¶ added in v0.3.0
Bytes returns the first branch as []byte. Returns nil if the result is empty or the value is not bytes.
func (Result) BytesSlice ¶ added in v0.3.0
BytesSlice returns all branches as []byte values. Non-bytes branches are silently skipped.
func (Result) Float64 ¶ added in v0.3.0
Float64 returns the first branch as float64. Returns 0 if the result is empty or the value is not numeric.
func (Result) Float64s ¶ added in v0.3.0
Float64s returns all branches as float64 values. Non-numeric branches are silently skipped. The returned slice has length ≤ Result.Len.
func (Result) Int64 ¶ added in v0.3.0
Int64 returns the first branch as int64. Returns 0 if the result is empty or the value is not an integer type.
func (Result) Int64s ¶ added in v0.3.0
Int64s returns all branches as int64 values. Non-integer branches are silently skipped.
func (Result) Message ¶ added in v0.3.0
func (r Result) Message() protoreflect.Message
Message returns the first branch as protoreflect.Message. Returns nil if the result is empty or the value is not a message.
func (Result) Messages ¶ added in v0.3.0
func (r Result) Messages() []protoreflect.Message
Messages returns all branches as protoreflect.Message values. Non-message branches are silently skipped.
func (Result) ProtoValues ¶ added in v0.3.0
func (r Result) ProtoValues() []protoreflect.Value
ProtoValues converts all branches back to protoreflect.Value for interoperability with existing code that expects the legacy representation. Null values become the zero protoreflect.Value.
func (Result) String ¶ added in v0.3.0
String returns the first branch as string. Returns "" if the result is empty. Non-string values are converted via [valueToStringValue].
func (Result) Strings ¶ added in v0.3.0
Strings returns all branches as string values. Non-string values are converted via [valueToStringValue].
func (Result) Uint64 ¶ added in v0.3.0
Uint64 returns the first branch as uint64. Returns 0 if the result is empty or the value is not an unsigned integer type.
func (Result) Uint64s ¶ added in v0.3.0
Uint64s returns all branches as uint64 values. Non-unsigned-integer branches are silently skipped.
type ResultSet ¶ added in v0.3.0
type ResultSet struct {
// contains filtered or unexported fields
}
ResultSet is an ordered collection of named Result values returned by a Query.Run call. Results can be accessed by name or iterated in order.
The zero value is a valid, empty result set.
func (ResultSet) All ¶ added in v0.3.0
All returns an iterator over all (name, result) pairs in order. Intended for use with Go range-over-func.
func (ResultSet) At ¶ added in v0.3.0
At returns the Result and name at position i. Panics if i is out of range.
func (ResultSet) Get ¶ added in v0.3.0
Get returns the Result for the given entry name. If the name does not exist, an empty Result is returned.
func (ResultSet) Has ¶ added in v0.3.0
Has reports whether the result set contains an entry with the given name.
type Step ¶
type Step struct {
// contains filtered or unexported fields
}
Step is a single operation in a Path. It wraps a protopath.Step for the standard step kinds and adds ListRangeStep and ListWildcardStep.
func AnyExpand ¶
func AnyExpand(md protoreflect.MessageDescriptor) Step
AnyExpand returns an AnyExpandStep for the given message descriptor.
func FieldAccess ¶
func FieldAccess(fd protoreflect.FieldDescriptor) Step
FieldAccess returns a FieldAccessStep for the given field descriptor.
func Filter ¶ added in v0.3.0
Filter returns a FilterStep that evaluates predicate against the current cursor value and keeps only branches where the predicate is truthy. The predicate is an Expr whose leaf PathRef nodes are relative to the cursor's message descriptor; resolution happens at NewPlan time.
func ListIndex ¶
ListIndex returns a ListIndexStep for the given index. Negative indices are allowed and resolved at traversal time. For non-negative indices, the underlying protopath.ListIndex is used. For negative indices, only the raw value is stored (since protopath panics on negatives).
func ListRange ¶
ListRange returns a ListRangeStep representing the half-open range [start, end) with stride 1. Both start and end may be negative (resolved at traversal time).
func ListRangeFrom ¶
ListRangeFrom returns a ListRangeStep with only a start bound (open end) and stride 1. This represents [start:] syntax.
func ListRangeStep3 ¶
ListRangeStep3 returns a ListRangeStep with explicit start, end, and step. Panics if step is 0.
Use startOmitted/endOmitted to indicate that the respective bound should be defaulted at traversal time based on the step sign (Python semantics).
func ListWildcard ¶
func ListWildcard() Step
ListWildcard returns a ListWildcardStep that selects every element.
func MapIndex ¶
func MapIndex(k protoreflect.MapKey) Step
MapIndex returns a MapIndexStep for the given map key.
func MapWildcard ¶ added in v0.3.0
func MapWildcard() Step
MapWildcard returns a MapWildcardStep that selects every value from a map field, iterating over all key-value pairs.
func Root ¶
func Root(md protoreflect.MessageDescriptor) Step
Root returns a RootStep for the given message descriptor.
func (Step) EndOmitted ¶
EndOmitted reports whether the end bound was omitted in the source syntax. When true, the effective end is computed at traversal time based on the step sign: len for positive step, -(len+1) for negative step.
func (Step) FieldDescriptor ¶
func (s Step) FieldDescriptor() protoreflect.FieldDescriptor
FieldDescriptor returns the field descriptor for a FieldAccessStep.
func (Step) ListIndex ¶
ListIndex returns the list index for a ListIndexStep. May be negative (resolved at traversal time).
func (Step) MapIndex ¶
func (s Step) MapIndex() protoreflect.MapKey
MapIndex returns the map key for a MapIndexStep.
func (Step) MessageDescriptor ¶
func (s Step) MessageDescriptor() protoreflect.MessageDescriptor
MessageDescriptor returns the message descriptor for RootStep and AnyExpandStep.
func (Step) Predicate ¶ added in v0.3.0
Predicate returns the predicate Expr for a FilterStep. Returns nil for all other step kinds.
func (Step) ProtoStep ¶
ProtoStep returns the underlying protopath.Step. It panics if the step kind is ListRangeStep, ListWildcardStep, FilterStep, or MapWildcardStep.
func (Step) RangeEnd ¶
RangeEnd returns the end bound of a ListRangeStep. The value is 0 when Step.EndOmitted is true.
func (Step) RangeOpen ¶
RangeOpen reports whether the range has no explicit end bound. Deprecated: use Step.EndOmitted instead.
func (Step) RangeStart ¶
RangeStart returns the start bound of a ListRangeStep. The value is 0 when Step.StartOmitted is true.
func (Step) RangeStep ¶
RangeStep returns the stride of a ListRangeStep. Defaults to 1 when not explicitly provided. Never 0.
func (Step) StartOmitted ¶
StartOmitted reports whether the start bound was omitted in the source syntax. When true, the effective start is computed at traversal time based on the step sign: 0 for positive step, len-1 for negative step.
type StepKind ¶
type StepKind int
StepKind enumerates the kinds of steps in a Path. The first six values mirror protopath.StepKind.
const ( // RootStep identifies a [Step] as a root message. RootStep StepKind = iota // FieldAccessStep identifies a [Step] as accessing a message field by name. FieldAccessStep // UnknownAccessStep identifies a [Step] as accessing unknown fields. UnknownAccessStep // ListIndexStep identifies a [Step] as indexing into a list by position. // Negative indices are allowed and resolved at traversal time. ListIndexStep // MapIndexStep identifies a [Step] as indexing into a map by key. MapIndexStep // AnyExpandStep identifies a [Step] as expanding an Any message. AnyExpandStep // ListRangeStep identifies a [Step] as a Python-style slice // [start:end:step] applied to a repeated field during value traversal. // Any of start, end, step may be omitted; omitted bounds are resolved // at traversal time based on the step sign (see Python slice semantics). ListRangeStep // ListWildcardStep identifies a [Step] as selecting all elements of a // repeated field. Equivalent to [*], [:], or [::] in the path syntax. ListWildcardStep // FilterStep identifies a [Step] as a mid-traversal predicate filter. // It evaluates a predicate expression against the current cursor value // (which must be a message) and keeps only branches where the predicate // is truthy. Syntax: [?(.field == "value")]. // // The predicate is compiled at [NewPlan] time into a sub-[Plan] rooted // at the cursor's message descriptor. At traversal time the predicate // is evaluated per-branch with zero additional compilation cost. FilterStep // MapWildcardStep identifies a [Step] as selecting all values from a // map field. Equivalent to [*] applied to a map-typed field. MapWildcardStep )
type Value ¶ added in v0.3.0
type Value struct {
// contains filtered or unexported fields
}
Value is the universal intermediate representation for pbpath expressions.
It is intentionally a small struct (≤ 48 bytes on 64-bit) that can be passed by value without heap allocation in hot-path expression evaluation. The zero value is a null (kind == NullKind).
Design rationale:
- scalar is stored as protoreflect.Value (interface{} wrapper) so every proto primitive type is supported without boxing again.
- list is a slice header (24 bytes) stored directly in the struct. For small fan-out counts, Go 1.26 stack-allocated append backing stores keep these off the heap entirely.
- msg is a protoreflect.Message interface — one word + one pointer.
- kind occupies a single int. Keeping it first enables branch prediction on the hot-path switch.
func FromProtoValue ¶ added in v0.3.0
func FromProtoValue(pv protoreflect.Value) Value
FromProtoValue converts a protoreflect.Value to a Value. Messages are wrapped as MessageKind; Lists/Maps are wrapped as ScalarKind (preserving the proto List/Map interface for downstream expression functions). Invalid values become null.
func ListVal ¶ added in v0.3.0
ListVal creates a ListKind Value from an existing slice of Value. If items is nil or empty, the result is an empty list (not null).
func MessageVal ¶ added in v0.3.0
func MessageVal(m protoreflect.Message) Value
MessageVal wraps a protoreflect.Message into a Value with kind MessageKind. If m is nil, the result is null.
func Null ¶ added in v0.3.0
func Null() Value
Null returns the null Value. Equivalent to the zero value.
func ObjectVal ¶ added in v0.3.0
func ObjectVal(entries []ObjectEntry) Value
ObjectVal creates an ObjectKind Value from key-value pairs. If entries is nil, the result is an empty object (not null).
func Scalar ¶ added in v0.3.0
func Scalar(pv protoreflect.Value) Value
Scalar wraps a single protoreflect.Value into a Value with kind ScalarKind. If pv is not valid (zero), the result is null.
func ScalarBool ¶ added in v0.3.0
ScalarBool creates a ScalarKind Value holding a bool.
func ScalarFloat64 ¶ added in v0.3.0
ScalarFloat64 creates a ScalarKind Value holding a float64.
func ScalarInt32 ¶ added in v0.3.0
ScalarInt32 creates a ScalarKind Value holding an int32.
func ScalarInt64 ¶ added in v0.3.0
ScalarInt64 creates a ScalarKind Value holding an int64.
func ScalarString ¶ added in v0.3.0
ScalarString creates a ScalarKind Value holding a string.
func (Value) Entries ¶ added in v0.3.0
func (v Value) Entries() []ObjectEntry
Entries returns the key-value pairs for an ObjectKind value. Returns nil for non-object kinds.
func (Value) Get ¶ added in v0.3.0
func (v Value) Get(fd protoreflect.FieldDescriptor) Value
Get returns the value of field fd on a MessageKind value. Returns null for non-message values.
func (Value) Index ¶ added in v0.3.0
Index returns the i-th element of a ListKind value. Returns null for non-list kinds or out-of-bounds indices. Negative indices are supported (Python-style).
func (Value) IsNonZero ¶ added in v0.3.0
IsNonZero reports whether the value is non-null and not the protobuf zero value for its kind. For scalars this means non-zero/non-empty; for lists, non-empty; for messages, non-nil.
func (Value) Len ¶ added in v0.3.0
Len returns the number of elements for a ListKind value, or the number of entries for an ObjectKind value, or 0 otherwise.
func (Value) List ¶ added in v0.3.0
List returns the child elements for a ListKind value. Returns nil for non-list kinds.
func (Value) Message ¶ added in v0.3.0
func (v Value) Message() protoreflect.Message
Message returns the protoreflect.Message for a MessageKind value. Returns nil for non-message kinds.
func (Value) ProtoValue ¶ added in v0.3.0
func (v Value) ProtoValue() protoreflect.Value
ProtoValue returns the underlying protoreflect.Value for a scalar. For non-scalar kinds, it returns the zero protoreflect.Value.
func (Value) String ¶ added in v0.3.0
String returns a human-readable representation of the value for debugging.
func (Value) ToProtoValue ¶ added in v0.3.0
func (v Value) ToProtoValue() protoreflect.Value
ToProtoValue converts a Value back to a protoreflect.Value. For ScalarKind, the wrapped value is returned directly. For MessageKind, the message is wrapped via protoreflect.ValueOfMessage. For ListKind and NullKind, the zero protoreflect.Value is returned.
type ValueKind ¶ added in v0.3.0
type ValueKind int
ValueKind identifies the category of data held by a Value. A Value is always exactly one of these kinds.
const ( // NullKind indicates the [Value] carries no data. // This is the zero-value kind for [Value]. NullKind ValueKind = iota // ScalarKind indicates the [Value] wraps a single [protoreflect.Value]. ScalarKind // ListKind indicates the [Value] holds an ordered collection of child // [Value] elements — the result of a collect or fan-out operation. ListKind // MessageKind indicates the [Value] wraps a live [protoreflect.Message] // that can be traversed further by subsequent path steps. MessageKind // ObjectKind indicates the [Value] holds a constructed object — an // ordered sequence of key-value pairs produced by {key: expr, ...} // syntax. Unlike [MessageKind], objects are schema-free and can hold // arbitrary string keys and [Value] values. ObjectKind )
type Values ¶
type Values struct {
Path Path
Values []protoreflect.Value
// contains filtered or unexported fields
}
Values is a Path paired with a sequence of values at each step. The lengths of [Values.Path] and [Values.Values] must be identical. The first step must be a Root step and the first value must be a concrete message value.
func PathValues ¶
PathValues returns the values along a path in message m.
When the path contains a ListWildcardStep or ListRangeStep the function fans out: one Values is produced for every matching list element (cartesian product when multiple fan-out steps are nested).
A single ListIndexStep with a negative index is resolved relative to the list length (e.g. -1 → last element).
Example ¶
ExamplePathValues demonstrates traversing a path through a live message to extract values. For scalar paths, exactly one Values is returned.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
// Build a message: Test { nested: { stringfield: "hello" } }
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("hello"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
// Parse and evaluate.
path, _ := pbpath.ParsePath(testMD, "nested.stringfield")
results, err := pbpath.PathValues(path, msg)
if err != nil {
log.Fatal(err)
}
// Scalar path → exactly one result.
fmt.Printf("branches: %d\n", len(results))
// Index(-1) returns the last (step, value) pair — the leaf value.
leaf := results[0].Index(-1)
fmt.Printf("value: %s\n", leaf.Value.String())
}
Output: branches: 1 value: hello
Example (Fanout) ¶
ExamplePathValues_fanout demonstrates how wildcards cause PathValues to produce multiple result branches — one per matching list element.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats". This models a self-referential proto
// message:
//
// message Test {
// Nested nested = 1;
// repeated Test repeats = 2;
// message Nested {
// string stringfield = 1;
// }
// }
//
// Self-referential (recursive) messages are fully supported by pbpath.
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
// Build a message with 3 elements in the "repeats" list.
msg := dynamicpb.NewMessage(testMD)
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"alpha", "beta", "gamma"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
path, _ := pbpath.ParsePath(testMD, "repeats[*].nested.stringfield")
results, err := pbpath.PathValues(path, msg)
if err != nil {
log.Fatal(err)
}
// One branch per list element, each with the concrete index resolved.
fmt.Printf("branches: %d\n", len(results))
for _, r := range results {
// ListIndices() extracts the concrete list indices visited.
indices := r.ListIndices()
fmt.Printf(" repeats[%d] = %s\n", indices[0], r.Index(-1).Value.String())
}
}
Output: branches: 3 repeats[0] = alpha repeats[1] = beta repeats[2] = gamma
func (Values) Index ¶
func (p Values) Index(i int) (out struct { Step Step Value protoreflect.Value }, )
Index returns the ith step and value and supports negative indexing. A negative index starts counting from the tail of the Values such that -1 refers to the last pair, -2 refers to the second-to-last pair, and so on.
func (Values) Len ¶
Len reports the length of the path and values. If the path and values have differing length, it returns the minimum length.
func (Values) ListIndices ¶
ListIndices returns the concrete list indices visited along this Values path. It collects the index from every ListIndexStep in order.