-
Notifications
You must be signed in to change notification settings - Fork 566
DQL NodeSet Implementation Specification
This document specifies the implementation requirements for NodeSet types to support DQL (DOM Query Language) syntax in XGo. A NodeSet implementation must provide a set of specially-named methods that the XGo compiler recognizes and translates from the declarative DQL syntax.
Any type implementing NodeSet must provide the following core methods:
Purpose: Enable iteration over the NodeSet
Semantics:
- Returns an iterator sequence over the NodeSet
- Each yielded value is a NodeSet (typically containing a single node), not a raw node
- This preserves NodeSet syntax capabilities within iteration variables
DQL Usage:
// for..in loop
for user in doc.users.* { ... }
// List comprehension
names := [user.$name for user in doc.users.*]Compiler Translation:
// for user in doc.users.* { ... }
// Compiles to:
for user in doc.XGo_Elem("users").XGo_Child().XGo_Enum() { ... }Requirements:
- Must return
iter.Seq[NodeSet], notiter.Seq[Node] - Each yielded NodeSet typically contains a single node
- Should support lazy iteration where possible
- Must handle empty NodeSets gracefully (yields nothing)
- Must respect early termination (when iterator consumer returns false)
Purpose: Extract the first node from a NodeSet
DQL Syntax: ns._first
Semantics:
- Returns the first (or only) node from the NodeSet
- Returns error if NodeSet is empty or has internal error state
- Used both by users directly and internally by compiler in select operation transformations
DQL Usage:
// User can call directly
node, err := ns._first
// Also used by compiler in conditional select transformationsCompiler Translation:
// DQL: node, err := doc.users.*._first
// Compiles to: node, err := doc.XGo_Elem("users").XGo_Child().XGo_first()
// Used internally in conditional select:
if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
if !__xgo_yield(__xgo_val) {
return false
}
}Requirements:
- Must return the first node in the set
- Must return
dql.ErrNotFoundif NodeSet is empty - Must propagate internal error state if present
-
NodeTypeis domain-specific (the actual node type for this implementation) - Should be efficient (O(1) if possible)
- Should provide both single-value and dual-value versions like other methods, if appropriate for the domain
Purpose: Enable XGo type conversion syntax for constructing NodeSet from sequence functions
XGo Type Conversion Syntax:
In XGo, if a function named TypeName_Cast(SourceType) TypeName exists, users can use the type conversion syntax:
result := TypeName(sourceValue) // Equivalent to TypeName_Cast(sourceValue)For NodeSet, this means:
// If this function exists:
func NodeSet_Cast(seq func(func(NodeType) bool)) NodeSet
// Users can write:
ns := NodeSet(mySequence) // Converts sequence to NodeSetSemantics:
- Takes a sequence function that yields nodes via a callback
- Returns a NodeSet containing the yielded nodes
- This is a public API that users can directly use
- Also used internally by the compiler for conditional select operations
User-Facing Usage:
// User creates a sequence function
mySeq := func(yield func(NodeType) bool) {
// Generate nodes...
for _, node := range someNodes {
if !yield(node) {
return // Early termination
}
}
}
// User converts it to NodeSet using type conversion syntax
ns := NodeSet(mySeq)
// Now can use DQL syntax on ns
name := ns.$name
for child in ns.* { ... }Compiler-Generated Usage:
// DQL: users@($age < 18)
// Compiles to:
NodeSet(func(__xgo_yield func(NodeType) bool) {
users.XGo_Enum()(func(self NodeSet) bool {
if self.XGo_Attr__0("age") < 18 {
if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
if !__xgo_yield(__xgo_val) {
return false
}
}
}
return true
})
})Requirements:
- Must construct a valid NodeSet from the sequence function
- Should preserve lazy evaluation if possible
- Must handle early termination correctly (when yield returns false)
-
NodeTypeis domain-specific - Must be exported (public) so users can leverage the type conversion syntax
When Users Might Use This:
- Integrating external iterators or generators with DQL
- Building custom NodeSet sources from application logic
- Converting between different sequence representations
- Implementing adapters for third-party tree structures
DQL Syntax: ns.name or ns."elem-name"
Semantics:
- Returns a NodeSet containing all direct children of nodes in
nswith the specified name - For quoted names like
ns."elem-name", the compiler passes"elem-name"as the name parameter
Examples:
// DQL: doc.users.profile
// Compiles to: doc.XGo_Elem("users").XGo_Elem("profile")
// DQL: doc."user-data".profile
// Compiles to: doc.XGo_Elem("user-data").XGo_Elem("profile")Requirements:
- Must handle empty NodeSets gracefully (return empty NodeSet)
- Should support lazy evaluation where possible
- Must preserve internal error state from parent NodeSet
DQL Syntax: ns.*
Semantics:
- Returns a NodeSet containing all direct children of all nodes in
ns - Wildcard selector - does not filter by name
Examples:
// DQL: doc.users.*
// Compiles to: doc.XGo_Elem("users").XGo_Child()Requirements:
- Must include all child nodes regardless of name
- Order may be significant depending on the domain (e.g., document order in XML/HTML)
- Should support lazy evaluation
DQL Syntax: ns[index]
Semantics:
- Returns a NodeSet containing the child node at the specified index
- Uses 0-based indexing
-
IndexTypeis typicallyintbut may be domain-specific
Examples:
// DQL: doc.users[0]
// Compiles to: doc.XGo_Elem("users").XGo_Index(0)
// DQL: doc.users[2].name
// Compiles to: doc.XGo_Elem("users").XGo_Index(2).XGo_Elem("name")
// DQL: doc.users[0].$name
// Compiles to: doc.XGo_Elem("users").XGo_Index(0).XGo_Attr__0("name")Requirements:
- Must handle out-of-bounds indices gracefully (return empty NodeSet or NodeSet with error state)
- Index semantics should match the domain (e.g., array index for JSON, child position for XML)
- Should work on both single nodes and multi-node sets (applies index to each node's children)
DQL Syntax: ns.**.name, ns.**."elem-name", or ns.**.*
Semantics:
- Deep/recursive query: searches for nodes at any depth within
ns - When
nameis non-empty: returns all descendant nodes with that name - When
nameis empty string"": returns all descendant nodes (forns.**.*syntax)
Examples:
// DQL: doc.**.div
// Compiles to: doc.XGo_Any("div")
// DQL: doc.**."elem-name"
// Compiles to: doc.XGo_Any("elem-name")
// DQL: doc.**.*
// Compiles to: doc.XGo_Any("")Requirements:
- Must search recursively through entire subtree
- Should support lazy evaluation to avoid materializing entire tree
- May use depth-first or breadth-first traversal (domain-specific choice)
- Empty string parameter means "match all descendants"
DQL Syntax: ns@name or ns@"elem-name"
Semantics:
- Filters nodes in
nsto only those with the specified name - Simplified form of select operation for name-based filtering
Examples:
// DQL: doc.users.*@admin
// Compiles to: doc.XGo_Elem("users").XGo_Child().XGo_Select("admin")Requirements:
- Should filter to nodes matching the given name
- Must maintain NodeSet semantics (return empty set if no matches)
- Should support lazy evaluation
DQL Syntax: ns@(condExpr) or ns@fn(args)
Semantics:
- Filters nodes based on arbitrary boolean condition
- The compiler transforms this into NodeSet type conversion with generated filter logic
Compiler Transformation:
// DQL: users@($age < 18)
// Compiles to:
NodeSet(func(__xgo_yield func(NodeType) bool) {
users.XGo_Enum()(func(self NodeSet) bool {
// Condition evaluation depends on available version
if self.XGo_Attr__0("age") < 18 { // Single-value version
if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
if !__xgo_yield(__xgo_val) {
return false
}
}
}
return true
})
})Requirements:
-
NodeSet_Castmust be implemented to enable theNodeSet(seq)conversion syntax -
XGo_first()must extract the first node from a NodeSet - The pattern ensures each node in the iteration is still a NodeSet (single-element), preserving DQL syntax capabilities
DQL provides two versions for attribute access, similar to Go's map[key] pattern:
-
Single-value version:
XGo_Attr__0(name string) ValueType- Returns only the value
- Defaults to zero value if error occurs
- Convenient when errors can be safely ignored
-
Dual-value version:
XGo_Attr__1(name string) (ValueType, error)- Returns both value and error
- Allows explicit error handling
DQL Syntax: ns.$name or ns.$"attr-name" (single-value version)
Semantics:
- Returns the attribute value from the first node in the NodeSet
- If error occurs (empty NodeSet, missing attribute, internal error state), returns zero value of
ValueType - Equivalent to
ns.XGo_Attr__1(name)?:zeroValue
Examples:
// DQL: user.$name
// Compiles to: user.XGo_Attr__0("name")
// DQL: user.$"attr-name"
// Compiles to: user.XGo_Attr__0("attr-name")
// DQL: names := [user.$name for user in doc.users.*]
// Single-value version in list comprehensionRequirements:
- Must operate on first node only
- Must return zero value on any error (node not found, attribute missing, type conversion failure, internal error state)
-
ValueTypeis domain-specific (oftenstring,any, or specific type)
DQL Syntax: val, err := ns.$name or val, err := ns.$"attr-name" (dual-value version)
Semantics:
- Returns the attribute value and error from the first node in the NodeSet
- Allows explicit error handling
Examples:
// DQL: name, err := user.$name
// Compiles to: name, err := user.XGo_Attr__1("name")
// DQL: names := [user.$name! for user in doc.users.*]
// With error operator (panics if error)
// DQL: names := [user.$name?:"N/A" for user in doc.users.*]
// With default value operatorRequirements:
- Must operate on first node only
- Should return appropriate errors for:
- Empty NodeSet
- Missing attribute
- Type conversion failure
- Internal error state (e.g., from
XGo_single())
-
ValueTypeis domain-specific
DQL Syntax: ns.method(args)
Semantics:
- Regular XGo method calls on NodeSet
- Method name is used as-is
- Typically operates on first node in NodeSet (implementation should document this)
Examples:
// DQL: div.text
// Compiles to: div.Text() (assuming parentheses can be omitted)
// DQL: div.text(true)
// Compiles to: div.Text(true)
// DQL: node.count
// Compiles to: node.Count()Two-Version Design:
Most methods should provide two versions:
-
Single-value version:
Method(args) ResultType- Returns only the result
- Defaults to zero value if error occurs
- Exception: Methods returning numeric or sensitive types may provide only dual-value version to prevent subtle bugs
-
Dual-value version:
Method(args) (ResultType, error)- Returns both result and error
- Allows explicit error handling
Requirements:
- Follow standard XGo method conventions
- Most methods should provide both single-value and dual-value versions
- Methods returning numeric types (where zero-default could cause bugs) should provide only dual-value version
- Should document whether they operate on first node or entire NodeSet
- Can integrate with XGo's error handling operators (
!and?:)
DQL Syntax: ns._method (without parentheses) or ns.XGo_method() (with parentheses)
Semantics:
- Method calls with
_prefix map toXGo_methodfunction - Used for general-form method access or to avoid naming conflicts with child nodes
- When using the
_methodsyntax (without parentheses), XGo applies the function call omission feature - When using parentheses, the full method name
XGo_method()must be used
Examples:
// DQL: node._text (without parentheses)
// Compiles to: node.XGo_text()
// DQL: node.XGo_text() (with parentheses)
// Compiles to: node.XGo_text()
// DQL: node._count
// Compiles to: node.XGo_count()
// DQL: node._all
// Compiles to: node.XGo_all()
// DQL: node._first
// Compiles to: node.XGo_first()Naming Pattern:
- Function name is
XGo_+ method name - Example:
_text→XGo_text(),_count→XGo_count(),_first→XGo_first()
Requirements:
- Otherwise follows same conventions as regular methods
- Should provide two-version design where appropriate
- Useful when child node names might conflict with method names
Special Case - Child Nodes Starting with _:
// DQL: node."_text"
// Compiles to: node.XGo_Elem("_text")
// Accesses child node named "_text"
// DQL: node._text
// Compiles to: node.XGo_text()
// Calls XGo_text() methodThese methods provide fine-grained control over query evaluation and caching. The specific method names used depend on the NodeSet implementation's choice.
Implementations may provide cache control methods using either:
- General form:
XGo_all(),XGo_one(),XGo_single() - Domain-specific form:
All(),One(),Single()(or other names)
The choice depends on:
- Whether the domain has existing methods with these names
- Domain-specific naming conventions
- Whether there are potential naming conflicts
DQL Syntax Mapping:
- If implementation provides
XGo_all(): usens._allin DQL - If implementation provides
All(): usens.allin DQL - Similarly for
_one/oneand_single/single
DQL Syntax: ns.all or ns._all (depending on implementation)
Method Signature:
// Domain-specific form
func (ns NodeSet) All() NodeSet
// Or general form
func (ns NodeSet) XGo_all() NodeSetSemantics:
- Executes the query immediately and materializes all matching nodes
- Returns a new NodeSet containing cached results
- Subsequent operations on returned NodeSet don't re-execute the original query
- The original NodeSet remains unchanged (still lazy)
Examples:
// If implementation provides All():
// DQL: users := doc.users.*@($active == true).all
// Compiles to: users := doc.XGo_Elem("users").XGo_Child().[filter].All()
// If implementation provides XGo_all():
// DQL: users := doc.users.*@($active == true)._all
// Compiles to: users := doc.XGo_Elem("users").XGo_Child().[filter].XGo_all()Requirements:
- Must fully evaluate the lazy query
- Must cache results in the returned NodeSet
- Original NodeSet should remain lazy
- Should handle large result sets efficiently
- Must preserve error state if present
When to Use:
- NodeSet will be accessed multiple times
- Multiple attributes/methods will be extracted from same set
- Query is expensive and results are reused
- Working with reasonably sized result set that fits in memory
DQL Syntax: ns.one or ns._one (depending on implementation)
Method Signature:
// Domain-specific form
func (ns NodeSet) One() NodeSet
// Or general form
func (ns NodeSet) XGo_one() NodeSetSemantics:
- Optimizes queries when expecting only one node, allowing early termination
- Returns a NodeSet containing the first match
- If no nodes match: returns NodeSet marked internally with
dql.ErrNotFounderror - If multiple nodes match: returns first match (does NOT validate uniqueness)
- The returned NodeSet can be used normally (attributes, methods, iteration)
Examples:
// If implementation provides One():
// DQL: admin := doc.users.*@($role == "admin").one
// Compiles to: admin := doc.XGo_Elem("users").XGo_Child().[filter].One()
// If implementation provides XGo_one():
// DQL: admin := doc.users.*@($role == "admin")._one
// Compiles to: admin := doc.XGo_Elem("users").XGo_Child().[filter].XGo_one()Requirements:
- Must stop query execution after finding first match
- If no matches, must return NodeSet with
dql.ErrNotFoundinternal error - Returned NodeSet should be cached (single node)
- Should NOT check for multiple matches (that's
single's job) - Internal error must propagate to all attribute/method calls on returned NodeSet
When to Use:
- Expecting only one node match
- Performance optimization for single-result queries
- Early exit from expensive deep queries
- Existence checks where expecting at most one match
Error Propagation:
// Error propagates through attribute access
name := admin.$name // Returns zero value if dql.ErrNotFound
name, err := admin.$name // err == dql.ErrNotFound if no match
// Or using error operators
name := admin.$name! // Panics if dql.ErrNotFound
name := admin.$name?:"Default" // Uses default if dql.ErrNotFoundDQL Syntax: ns.single or ns._single (depending on implementation)
Method Signature:
// Domain-specific form
func (ns NodeSet) Single() NodeSet
// Or general form
func (ns NodeSet) XGo_single() NodeSetSemantics:
- Validates that query matches exactly one node
- Returns NodeSet containing the single matching node if exactly one match
- If 0 matches: returns NodeSet marked internally with
dql.ErrNotFounderror - If >1 matches: returns NodeSet marked internally with
dql.ErrMultipleEntitieserror - The returned NodeSet can still be used (attributes, methods, iteration) but carries error state
Examples:
// If implementation provides Single():
// DQL: user := doc.users.*@($id == 12345).single
// Compiles to: user := doc.XGo_Elem("users").XGo_Child().[filter].Single()
// If implementation provides XGo_single():
// DQL: user := doc.users.*@($id == 12345)._single
// Compiles to: user := doc.XGo_Elem("users").XGo_Child().[filter].XGo_single()Requirements:
- Must validate uniqueness constraint (scan enough to determine 0, 1, or >1 matches)
- If zero matches, must return NodeSet with
dql.ErrNotFoundinternal error - If multiple matches, must return NodeSet with
dql.ErrMultipleEntitiesinternal error - If exactly one match, returned NodeSet should be cached
- Internal error must propagate to all attribute/method calls
- Should optimize to stop after finding 2 nodes (sufficient to detect multiplicity)
When to Use:
- Require exactly one match and need validation
- Working with unique identifiers (IDs, primary keys)
- Validating data integrity and consistency
- Preventing silent logic errors from unexpected multiple matches
- Configuration where exactly one value must exist
Error Handling:
// Error propagates to attribute access
name, err := user.$name
if err == dql.ErrNotFound {
// Handle missing case
} else if err == dql.ErrMultipleEntities {
// Handle duplicate case (data integrity issue)
}
// Using panic operator when violations are bugs
config := doc.config.*@($key == "api_endpoint")._single
endpoint := config.$value! // Panics if dql.ErrNotFound or dql.ErrMultipleEntitiesComparison with One:
// one: assumes single result, stops at first match (no validation)
admin := doc.users.*@($role == "admin").one
name := admin.$name // If 3 admins exist: returns first admin's name
// single: validates single result, checks for multiple
admin := doc.users.*@($role == "admin").single
name, err := admin.$name // If 3 admins exist: err == dql.ErrMultipleEntitiesThe DQL package provides standard error types that implementations should use:
package dql
var (
ErrNotFound error = errors.New("node not found")
ErrMultipleEntities error = errors.New("multiple entities found, expected single")
)These errors are used by:
-
XGo_first(): returnsdql.ErrNotFoundwhen no matches -
XGo_one()/One(): returnsdql.ErrNotFoundwhen no matches -
XGo_single()/Single(): returnsdql.ErrNotFound(0 matches) ordql.ErrMultipleEntities(>1 matches)
NodeSet implementations should support internal error state:
Concept:
- A NodeSet can carry an internal error (e.g., from
one,single) - When NodeSet has internal error, all attribute and method calls return that error
Pattern:
// NodeSet with internal error
ns := doc.users.*@($id == 999)._one // Returns NodeSet marked with dql.ErrNotFound
// Error propagates automatically
name, err := ns.$name // err == dql.ErrNotFound
age := ns.$age! // Panics with dql.ErrNotFound
// Can still use NodeSet normally
for user in ns { // Loop doesn't execute (empty due to error)
// ...
}Benefits:
- Eliminates need for dual-value versions of cache methods
- Errors propagate naturally through query pipeline
- Consistent error handling pattern
The IndexType in XGo_Index is typically int but can be customized:
// Typical: integer indexing
func (ns NodeSet) XGo_Index(index int) NodeSet
// Alternative: string keys for map-like structures
func (ns NodeSet) XGo_Index(index string) NodeSetThe ValueType in attribute methods is domain-specific:
// String-centric
func (ns NodeSet) XGo_Attr__0(name string) string
// Generic
func (ns NodeSet) XGo_Attr__0(name string) any
// Typed
func (ns NodeSet) XGo_Attr__0(name string) ValueTypeThe NodeType in XGo_first() and NodeSet_Cast is domain-specific:
// HTML DOM
func (ns NodeSet) XGo_first() (*HTMLNode, error)
func NodeSet_Cast(seq func(func(*HTMLNode) bool)) NodeSet
// JSON DOM
func (ns NodeSet) XGo_first() (*JSONNode, error)
func NodeSet_Cast(seq func(func(*JSONNode) bool)) NodeSetMost operations should use lazy evaluation:
- Return lightweight query descriptors, not materialized results
- Defer actual tree traversal until iteration or attribute access
- Enable query optimization through plan analysis
Support early termination in iterators:
- Respect the boolean return value from yield functions
- Stop processing when consumer returns
false - Optimize
oneto stop after first match - Optimize
singleto stop after finding 2 nodes
Implement caching efficiently:
-
all: Materialize and store all nodes -
one: Stop after first match, cache single node -
single: Stop after finding 2 nodes (sufficient to detect multiplicity), cache if exactly one
| DQL Syntax | Method Signature | Notes |
|---|---|---|
ns.name |
XGo_Elem(name string) NodeSet |
Access child nodes |
ns."elem-name" |
XGo_Elem("elem-name") NodeSet |
Special characters in name |
ns.* |
XGo_Child() NodeSet |
All child nodes |
ns[index] |
XGo_Index(index IndexType) NodeSet |
Index access (0-based) |
ns.**.name |
XGo_Any("name") NodeSet |
Deep query |
ns.**.* |
XGo_Any("") NodeSet |
All descendants |
ns@name |
XGo_Select(name string) NodeSet |
Filter by name |
ns@(cond) |
Compiler-generated + NodeSet(seq)
|
Conditional filter via type conversion |
ns.$name |
XGo_Attr__0(name string) ValueType |
Attribute (single-value) |
val, err := ns.$name |
XGo_Attr__1(name string) (ValueType, error) |
Attribute (dual-value) |
ns.method(args) |
Method(args) or XGo_method(args)
|
Regular method call |
ns._method |
XGo_method() |
Prefixed method (no parens) |
ns.XGo_method() |
XGo_method() |
Prefixed method (with parens) |
ns._first |
XGo_first() (NodeType, error) |
Extract first node (user-accessible, no parens) |
ns.XGo_first() |
XGo_first() (NodeType, error) |
Extract first node (with parens) |
ns.all |
All() NodeSet |
Cache all (if provided) |
ns._all |
XGo_all() NodeSet |
Cache all (general form) |
ns.one |
One() NodeSet |
First match (if provided) |
ns._one |
XGo_one() NodeSet |
First match (general form) |
ns.single |
Single() NodeSet |
Unique match (if provided) |
ns._single |
XGo_single() NodeSet |
Unique match (general form) |
for x in ns |
XGo_Enum() iter.Seq[NodeSet] |
Iteration support |
NodeSet(seq) |
NodeSet_Cast(func(func(NodeType) bool)) NodeSet |
Type conversion from sequence |
This specification defines the contract for NodeSet implementations in DQL:
-
Core Infrastructure:
XGo_Enum(),XGo_first(),NodeSet_Cast()(for type conversion) -
Child Access:
XGo_Elem(),XGo_Child(),XGo_Index(),XGo_Any() -
Filtering:
XGo_Select()and compiler-generated conditional filters usingNodeSet(seq)conversion -
Attributes: Two-version design with
XGo_Attr__0()andXGo_Attr__1() -
Methods: Regular methods and
XGo_method()prefixed form (includingXGo_first()which is user-accessible via_firstsyntax) -
Cache Control: Implementation chooses between general form (
XGo_all/one/single) or domain-specific form (All/One/Single) -
Error Handling: Internal error state with standard
dql.ErrNotFoundanddql.ErrMultipleEntitiesfrom thedqlpackage -
Type Customization: Domain-specific
NodeType,ValueType,IndexType
Implementations should adapt type parameters to their specific domain while maintaining these core semantics and providing efficient lazy evaluation with appropriate caching strategies.