-
Notifications
You must be signed in to change notification settings - Fork 566
DQL (DOM Query Language)
This proposal aims to introduce DQL (DOM Query Language) into XGo, a universal DOM query syntax. DQL provides a unified query interface applicable to various structured data formats and tree structures.
DQL is designed as a universal query language, applicable to but not limited to:
- JSON
- YAML
- HTML
- XML
- Go AST
- XGo AST
- File System
- Other tree-structured data
The language involves the following core concepts:
-
NodeSet: Node set (
ns NodeSet). Both input and output of Query operations are NodeSet. The initial query node set typically contains only the root node, referred to asdoc NodeSet. -
Child: Child nodes. Nodes form a tree structure through parent-child relationships. Child nodes can be accessed via
ns.namesyntax. Node names may contain-, accessed vians."elem-name". Wildcard syntax (ns.*) selects any child node. Usens.**.nameto query nodes namednameat any level within thenssubtree. Index accessns[index]for list/array nodes (e.g.,users[0]for the first user). -
Select: Node filtering. Syntax:
ns@cond. Example:user@($age < 18)filters all users under 18 years old. Conditions can be:-
fn(args)// Function call returning bool -
(condExpr)// Parenthesized expression -
nameor"elem-name"// Pass node name to select only nodes with that name
-
-
Attr: Attributes (data carried by the node itself). Two versions available (similar to
map[index]):-
Dual-value:
val, err := ns.$name- explicit error handling -
Single-value:
val := ns.$name- defaults to zero value on error (equivalent to$name?:zeroValue) - Also supports error operators:
$attr!(panic if err),$attr?:defVal(custom default) - Returns the attribute value from the first node in the NodeSet
- For multiple nodes, use list comprehension:
[n.$attr for n in ns] - Attribute names may contain
-, accessed vians.$"attr-name"
-
Dual-value:
-
Method: Methods (general mechanism for accessing node data beyond attributes). Two versions typically available:
-
Dual-value:
result, err := ns.method(args)- explicit error handling -
Single-value:
result := ns.method(args)- defaults to zero value on error -
Exception: Methods returning numeric types or other values where defaulting to zero could cause subtle bugs (e.g.,
age < 18might incorrectly pass whenageerrors to0) provide only the dual-value version - General syntax:
ns._method(args)orns.method(args)(in domain-specific contexts) - Methods operate on the first node in the NodeSet
- For multiple nodes, use list comprehension:
[n.method(args) for n in ns] - Examples include
text,value,count,int, etc. - If a child node name starts with
_, use quotes:node."_text"(child node) vsnode._text(method)
-
Dual-value:
-
Cache and Performance: Standard cache control methods for optimizing lazy query evaluation:
-
_all/all: Materialize and cache all nodes. Use when the NodeSet will be accessed multiple times. -
_one/one: Assume single-result query, stop at first match. Returns NodeSet marked withErrNotFoundif no match. Use when you expect one result and want early termination. -
_single/single: Validate exactly one match. Returns NodeSet marked withErrNotFound(0 matches) orErrMultipleEntities(>1 matches). Use for uniqueness validation.
-
Definition: ns NodeSet
- Both input and output of Query operations are NodeSet
- Initially, the node set typically contains only the root node (referred to as
doc NodeSet) - All query operations are performed on node sets
Syntax:
-
Basic access:
ns.name- Access child nodes through parent-child relationships
-
Names with hyphens:
ns."elem-name"- Use quotes when node names contain
-
- Use quotes when node names contain
-
Index access:
ns[index]- Access child nodes by index for list/array nodes using bracket notation
- Example:
users[0]represents the first user (0-based indexing) - Similar semantics to name-based access, but for ordered collections
-
Wildcard:
ns.*- Select any child node
-
Deep query:
ns.**.name- Query nodes named
nameat any level within thenssubtree
- Query nodes named
Notes:
- Nodes form a tree structure through parent-child relationships
- Node names may contain
-characters - List/array nodes use index-based access with bracket notation and 0-based indexing
Syntax: ns@cond
Example:
user@($age < 18)
Filters all users under 18 years old
Condition Types:
-
Function call:
fn(args)- Function call that returns bool
-
Expression:
(condExpr)- Conditional expression wrapped in parentheses
-
Name matching:
nameor"elem-name"- Pass node name instead of a bool expression
- Selects only nodes with the corresponding name
Syntax: ns.$name
Two-Version Design (similar to map[index]):
DQL provides two versions for attribute access:
-
Dual-value version:
val, err := ns.$attr- Explicitly returns both value and error
- Allows manual error handling
-
Single-value version:
val := ns.$attr- Returns only the value (equivalent to
ns.$attr?:zeroValue) - Defaults to the zero value of the attribute's type if error occurs
- Convenient for cases where errors can be safely ignored
- Returns only the value (equivalent to
Semantics:
- Attribute access operates on the first node in the NodeSet
ns - If
nscontains multiple nodes, only the first node is considered - To get attributes from all nodes in the set, use list comprehension
Error Handling Operators: In addition to the two standard versions, XGo's error handling operators can be used:
-
$attr!- Panic if error occurs -
$attr?:defVal- Return custom default value if error occurs
Examples:
// Single-value version (defaults to zero value on error)
name := doc.users.*.$name
// Dual-value version (explicit error handling)
name, err := doc.users.*.$name
if err != nil {
// handle error
}
// With error operators
name := doc.users.*.$name! // Panics on error
name := doc.users.*.$name?:"Unknown" // Custom default value
// Get name attributes of all users
names := [user.$name for user in doc.users.*] // Uses zero value as default
names := [user.$name! for user in doc.users.*] // Panics on any error
names := [user.$name?:"N/A" for user in doc.users.*] // Custom default for errorsNotes:
- Access data carried by the node itself
- Attribute names may contain
-, use quotes in this case:ns.$"attr-name" - Attributes are node metadata
- Always operates on the first node in the NodeSet
- The single-value version provides convenience similar to Go's
map[key]access pattern
Syntax: ns._method(args) (general form) or ns.method(args) (in specific DOMs)
Two-Version Design:
Most methods provide two versions for result access:
-
Dual-value version:
result, err := ns.method(args)- Explicitly returns both result and error
- Allows manual error handling
-
Single-value version:
result := ns.method(args)- Returns only the result (equivalent to
ns.method(args)?:zeroValue) - Defaults to the zero value of the result's type if error occurs
- Convenient for cases where errors can be safely ignored
- Returns only the result (equivalent to
Important Exception - Methods Returning Numeric or Sensitive Types:
Some methods only provide the dual-value version to prevent subtle bugs:
// Methods returning integers, floats, or other types where zero-default is dangerous
val, err := ns.int // ✓ Correct - explicit error handling required
val := ns.int // ✗ Error - single-value version not available
val := ns.int?:0 // ✓ Correct - explicit default value required
// Why? To prevent silent failures like:
age := ns.int // If this silently returned 0 on error...
if age < 18 { // ...this condition would incorrectly pass!
// Wrong logic executed
}
// Correct approaches:
age, err := ns.int
if err != nil {
// handle missing/invalid age
}
age := ns.int?:100 // Or use explicit safe default
if age < 18 {
// Now logic is correct
}Semantics:
- Methods operate on the first node in the NodeSet
ns - If
nscontains multiple nodes, only the first node's method is called - To call methods on all nodes, use list comprehension:
[n.method(args) for n in ns]
Notes:
- Methods are a general mechanism for accessing node data beyond attributes
- In the general form, all methods are accessed with the
_methodsyntax pattern - Names starting with
_are reserved for method access in the general case -
Domain-specific optimization: In specific DOMs where method names don't conflict with child node names, the
_prefix can be omitted- Example: In HTML DOM, use
node.textinstead ofnode._textto access text content - This makes queries more natural and concise when the context is clear
- Example: In HTML DOM, use
- Combined with XGo's feature of omitting
()in function calls,textor_textbecomes a special case (equivalent toText()orXGo_text()) - Common methods:
text,value,count,int,float,bool, etc. (or_text,_value,_count,_int,_float,_boolin general form)
Special Cases:
- If a child node name starts with
_, quotes are required for access - Example:
node."_text"represents a child node named_text - While
node._textrepresents calling the_textmethod on the node - Method calls can omit parentheses:
node.textis equivalent tonode.Text() - The choice between
methodand_methoddepends on the specific DOM implementation
NodeSet Error State:
Before discussing cache methods, it's important to understand that NodeSet carries error state internally:
- A NodeSet can be marked with an error (e.g.,
ErrNotFound,ErrMultipleEntities) - When a NodeSet has an internal error, all
$attrand method calls on it return that error - This design eliminates the need for dual-value versions of cache methods (
_all,_one,_single) - Errors propagate through the query pipeline naturally
// NodeSet with internal error
ns := doc.users.*@($id == 999)._one // Returns NodeSet marked with ErrNotFound
// Error propagates to attribute access
name, err := ns.$name // err == ErrNotFound
age := ns.$age! // Panics with ErrNotFound
// Can still use the NodeSet in normal ways
for user in ns { // Loop doesn't execute (empty due to error)
// ...
}Lazy Evaluation Background:
DQL queries use lazy evaluation - a NodeSet doesn't immediately execute the query operations applied to it. Instead, it stores a query plan that is executed only when the nodes are actually accessed (e.g., when iterating, accessing attributes, or calling methods).
Benefits of Lazy Evaluation:
- Memory efficient: NodeSets are lightweight query descriptors, not materialized node collections
- Composition friendly: Complex queries can be built incrementally without overhead
- Optimization potential: Query engines can analyze and optimize the entire query plan before execution
The Trade-off:
When a NodeSet is used multiple times, the entire query chain is re-executed each time:
users := doc.users.*@($age > 18)
// Each of these causes a full re-execution of the query
names := [u.$name for u in users] // Execution #1: scan, filter, iterate
ages := [u.$age for u in users] // Execution #2: scan, filter, iterate
emails := [u.$email for u in users] // Execution #3: scan, filter, iterateFor complex queries (especially deep queries with **) or large datasets, this repeated execution can become a performance bottleneck.
Standard Cache Control Methods:
To optimize query performance, DQL provides three standard methods for cache control and query termination. These methods are available in two forms:
- General form:
_all,_one,_single - Domain-specific form:
all,one,single(when no naming conflicts exist)
cached := ns._all // or ns.all in domain-specific contextsPurpose: Execute the query immediately, materialize all matching nodes, and cache the results in the returned NodeSet.
Semantics:
- Returns a new NodeSet containing all nodes that match the query
- The returned NodeSet is fully materialized - subsequent operations on it don't re-execute the original query
- The original NodeSet
nsremains lazy; only the returned NodeSet is cached - No dual-value version needed - NodeSet carries error state internally
When to use:
- The NodeSet will be traversed or accessed multiple times
- Multiple attributes/methods will be extracted from the same set of nodes
- The query is expensive (e.g., deep queries with
**) and results are reused - You're working with a reasonably sized result set that fits comfortably in memory
Examples:
// Without cache - inefficient for multiple accesses
users := doc.users.*@($active == true)
for name in [u.$name for u in users] { // Query execution #1
echo(name)
}
for email in [u.$email for u in users] { // Query execution #2
sendEmail(email)
}
// With cache - query executes once
users := doc.users.*@($active == true)._all
for name in [u.$name for u in users] { // Uses cached nodes
echo(name)
}
for email in [u.$email for u in users] { // Uses cached nodes
sendEmail(email)
}
// Cache expensive deep queries
targets := doc.**.*@hasClass("widget")._all
// Now can efficiently access multiple attributes
ids := [t.$id for t in targets]
values := [t.$value for t in targets]result := ns._one // or ns.one in domain-specific contextsPurpose: Optimize queries when you have prior knowledge that the result should contain only one node, allowing early termination after finding the first match.
Semantics:
- Returns a NodeSet (not just the first node) that can be used like any other NodeSet
- Acts as a cache operation similar to
_all, but stops query evaluation after finding the first match - Important: This is based on the assumption/expectation that there is exactly one matching node
- If no nodes match: returns a NodeSet marked internally with
ErrNotFounderror - If multiple nodes match: returns the first match (does not validate uniqueness - that's
_single's job) - The returned NodeSet can be used in all normal ways (attribute access, method calls, iteration, etc.)
-
Error propagation: If the NodeSet has an internal error (like
ErrNotFound), all$attrand method calls on it will return that error
When to use:
- You have prior knowledge/expectation that only one node matches
- Performance optimization for queries where you know the result should be singular
- Early exit from expensive deep queries once a match is found
- Existence checks where you expect at most one match
Examples:
// Assumption: there's only one admin (or maybe zero)
admin := doc.users.*@($role == "admin")._one
name := admin.$name // If _one returned ErrNotFound, this returns that error
email := admin.$email // Same - error propagates through
// Error handling through attribute access
name, err := admin.$name
if err == ErrNotFound {
echo("No admin user found")
}
// Or using error operators
name := admin.$name! // Panics if ErrNotFound
name := admin.$name?:"Default" // Uses default if ErrNotFound
// Existence check
target := doc.**.*@hasClass("error")._one
location := target.$location // Returns ErrNotFound if no error element found
if location, err := target.$location; err == nil {
echo("First error found at:", location)
}
// Performance benefit: stops searching after first match
config := doc.**.settings@($key == "api_url")._one
value := config.$value // Only scanned until first match, not entire treeKey Point - Not a Validator:
_one is a performance optimization based on your expectation, not a validator:
// If there are actually 3 admin users:
admin := doc.users.*@($role == "admin")._one
// Returns the first admin (stops after finding it)
// Does NOT error for the other 2 admins
// Use _single if you need to validate uniquenessunique := ns._single // or ns.single in domain-specific contextsPurpose: Validate that the query matches exactly one node, returning a NodeSet marked with an error if the uniqueness constraint is violated.
Semantics:
- Returns a NodeSet (not just a node) that can be used like any other NodeSet
- Acts as both a validator and cache operation
- Validates the uniqueness constraint by scanning enough to determine if there are 0, 1, or >1 matches
- If query matches exactly one node: returns a NodeSet containing that node
- If query matches zero nodes: returns a NodeSet marked internally with
ErrNotFounderror - If query matches multiple nodes: returns a NodeSet marked internally with
ErrMultipleEntitieserror - The returned NodeSet can still be used (attributes, methods, iteration), but carries the error state
-
Error propagation: If the NodeSet has an internal error, all
$attrand method calls on it will return that error
When to use:
- You require exactly one match and want to validate this constraint
- Working with unique identifiers (IDs, primary keys, etc.)
- Validating data integrity and consistency
- Preventing silent logic errors from unexpected multiple matches
- Configuration or settings where exactly one value must exist
Examples:
// Database-like unique ID lookup
user := doc.users.*@($id == 12345)._single
name := user.$name // If _single found 0 or >1 users, this returns the error
// Error handling through attribute access
name, err := user.$name
if err == ErrNotFound {
return errors.New("user not found")
} else if err == ErrMultipleEntities {
return errors.New("data corruption: duplicate user IDs")
}
// Using panic operator for cases where violation is a programming error
config := doc.config.*@($key == "api_endpoint")._single
endpoint := config.$value! // Panics if ErrNotFound or ErrMultipleEntities
// Ensure exactly one primary configuration exists
primary := doc.settings.*@($primary == true)._single
value, err := primary.$value
if err != nil {
log.Fatal("Configuration error: ", err)
}
// Validation in data processing
func getUniqueOwner(doc NodeSet) (owner string, err error) {
ownerNode := doc.metadata.*@($role == "owner")._single
return ownerNode.$name // Returns (name, err) - err could be ErrNotFound or ErrMultipleEntities
}Comparison 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 == ErrMultipleEntitiesWhy it returns NodeSet, not just error: The returned NodeSet can be used directly in pipelines:
// Can chain operations - errors propagate through
value := doc.config.*@($key == key)._single.$value
// If _single found 0 or >1 configs, $value returns that error
// Or handle errors at the end
value, err := doc.config.*@($key == key)._single.$value
if err != nil {
// Handle ErrNotFound or ErrMultipleEntities
}
// Using error operators
value := doc.config.*@($key == key)._single.$value!
value := doc.config.*@($key == key)._single.$value?:"default"Performance and Usage Guidelines:
-
Choose based on your expectations and needs:
// Expect/assume single result, want performance → _one admin := doc.users.*@($role == "admin")._one name := admin.$name // Stops at first match // Require exactly one result, need validation → _single admin := doc.users.*@($role == "admin")._single! name := admin.$name // Errors if 0 or multiple found // Need all results, will use multiple times → _all admins := doc.users.*@($role == "admin")._all processNames([a.$name for a in admins]) processEmails([a.$email for a in admins])
-
Understanding the differences:
// Scenario: 3 admin users exist in the data // No cache: repeated query execution admins := doc.users.*@($role == "admin") // Uses first admin each time, but re-executes query // _one: assumes single, stops early, returns first admin := doc.users.*@($role == "admin")._one // Returns first admin, no error (doesn't check for others) // _single: validates uniqueness admin := doc.users.*@($role == "admin")._single // Returns NodeSet with ErrMultipleEntities // _all: caches all results admins := doc.users.*@($role == "admin")._all // Caches all 3 admins
-
Error handling patterns:
// Error propagates through NodeSet to attribute/method access result := doc.config.*@($key == key)._one value, err := result.$value if err == ErrNotFound { // Handle missing case } // Or handle at the attribute access point value, err := doc.config.*@($key == key)._one.$value if err == ErrNotFound { // Handle missing case } // _single with full validation result := doc.config.*@($key == key)._single value, err := result.$value if err == ErrNotFound { // Handle missing case } else if err == ErrMultipleEntities { // Handle duplicate case (data integrity issue) } // Using error operators when violations are bugs config := doc.config.*@($key == key)._single value := config.$value! // Panics if ErrNotFound or ErrMultipleEntities
-
Chain cache methods at the end of query pipeline:
// ✓ Good - apply filters first, then cache results := doc.users.*@($age > 18)@($country == "US")._all results := doc.users.*@($age > 18)@($country == "US")._one results := doc.users.*@($age > 18)@($country == "US")._single // ✗ Less efficient - caches too early results := doc.users.*._all@($age > 18)@($country == "US")
-
Memory vs CPU trade-offs:
// Small result sets: _all is usually fine admins := doc.users.*@($role == "admin")._all // Probably <10 users // Large result sets with uniqueness requirement: _single user := doc.users.*@($id == userId)._single! // Validates, doesn't cache all // Large result sets, need first: _one firstMatch := doc.products.*@($price < 100)._one // Stops early // For large sets used once: no cache needed for user in doc.users.* { // Stream processing, no cache process(user) }
Performance Summary:
| Scenario | Without Cache | With _all
|
With _one
|
With _single
|
|---|---|---|---|---|
| Single access | O(n) | O(n) | O(1) avg | O(n)* |
| Multiple accesses | O(k×n) | O(n) | O(1) avg | O(n)* |
| Memory usage | O(1) | O(n) | O(1) | O(1) |
| Early termination | No | No | Yes | Partial** |
| Validates uniqueness | No | No | No | Yes |
*Scans enough to verify uniqueness (finds at most 2 nodes to distinguish between 1 and >1)
**Stops after finding 2 nodes (to detect multiple), doesn't scan entire set
Best Practices:
- Use lazy evaluation (no cache) for single-pass processing
- Use
_allwhen the same NodeSet is accessed multiple times - Use
_onefor existence checks and first-match scenarios - Use
_singlewhen uniqueness is a requirement, not just an optimization - Consider memory constraints when caching large result sets
- Profile your queries to identify performance bottlenecks before optimizing
| Operation | Syntax | Description |
|---|---|---|
| Child node access | ns.name |
Access normal child nodes |
| Index access | ns[index] |
Access child nodes by index (0-based, bracket notation) |
| Special character nodes | ns."elem-name" |
Node names with - or other special characters |
| Wildcard | ns.* |
Select all child nodes |
| Deep query | ns.**.name |
Query at any level |
| Node filtering | ns@cond |
Conditional filtering of nodes |
| Attribute (single) | val := ns.$name |
Access attribute (defaults to zero value on error) |
| Attribute (dual) | val, err := ns.$name |
Access attribute with explicit error handling |
| Attribute with panic | ns.$name! |
Access attribute, panic if error |
| Attribute with default | ns.$name?:defVal |
Access attribute, use defVal if error |
| Special attributes | ns.$"attr-name" |
Attributes with special characters |
| Method (single)* | result := ns.method(args) |
Call method (defaults to zero value on error, *not available for numeric returns) |
| Method (dual) | result, err := ns.method(args) |
Call method with explicit error handling |
| Method with panic | ns.method(args)! |
Call method, panic if error |
| Method with default | ns.method(args)?:defVal |
Call method, use defVal if error |
| Cache all nodes |
ns._all or ns.all
|
Materialize and cache all matching nodes |
| Get first node |
ns._one or ns.one
|
Return first node with early termination |
| Get single node |
ns._single or ns.single
|
Return exactly one node (error if 0 or >1) |
| Special child nodes | ns."_text" |
Access child node named _text
|
*Note: Single-value method syntax is not available for methods returning numeric or other sensitive types to prevent subtle bugs.
// Select all users
doc.users.*
// Select first user by index
doc.users[0]
// Get first user's name
name := doc.users.*.$name // Single-value (defaults to zero value)
name, err := doc.users.*.$name // Dual-value (explicit error)
name := doc.users.*.$name! // Panic on error
name := doc.users.*.$name?:"Unknown" // Custom default
// Get third user's name specifically
name := doc.users[2].$name
// Get all users' names (list comprehension)
names := [user.$name for user in doc.users.*] // Uses zero value as default
names := [user.$name! for user in doc.users.*] // Panics on any error
names := [user.$name?:"N/A" for user in doc.users.*] // Custom default
// Get user age (numeric - must use dual-value or error operator)
age, err := doc.users.*.$age.int // ✓ Correct
age := doc.users.*.$age.int // ✗ Error - not available
age := doc.users.*.$age.int?:0 // ✓ Correct - explicit default
// Select users under 18 (safe because we use explicit default)
young_users := doc.users.*@($age.int?:100 < 18)
// Select users named john
johns := doc.users.*@($name == "john")
// Get ages of all young users
ages := [user.$age.int?:0 for user in doc.users.*@($age.int?:100 < 18)]// Select all div elements
doc.**.div
// Get text content via method
text := doc.body.div.text // Single-value (first div only)
text, err := doc.body.div.text // Dual-value (first div only)
texts := [div.text for div in doc.body.div] // All divs (with zero value default)
// Get attribute from first matching element
href := doc.**.a.$href // Single-value (defaults to zero value)
href, err := doc.**.a.$href // Dual-value (explicit error)
href := doc.**.a.$href! // Panic on error
href := doc.**.a.$href?:"#" // Custom default
// Get attributes from all matching elements
hrefs := [a.$href for a in doc.**.a] // Uses zero value as default
hrefs := [a.$href! for a in doc.**.a] // Panics on any error
hrefs := [a.$href?:"#" for a in doc.**.a] // Uses "#" as default
// Call method (operates on first node only)
parent := doc.**.a.parentN(3)
// Count method (numeric - must use dual-value or error operator)
count, err := doc.**.div.count // ✓ Correct
count := doc.**.div.count // ✗ Error - not available
count := doc.**.div.count?:0 // ✓ Correct - explicit default
// In HTML DOM, these work without _ prefix since there's no conflict// List all files
root.**.file
// List all directories
root.**.dir
// Find all .go files
root.**.file@match("*.go", $name)
// Get names of all .go files
names := [f.$name for f in root.**.file@match("*.go", $name)]
names := [f.$name! for f in root.**.file@match("*.go", $name)]
names := [f.$name?:"unknown" for f in root.**.file@match("*.go", $name)]
// Get file sizes (numeric - requires explicit handling)
size, err := root.**.file.$size.int
size := root.**.file.$size.int?:0
sizes := [f.$size.int?:0 for f in root.**.file]- Type Safety: Ensure type safety of query results
-
Performance Optimization:
- Implement lazy evaluation for memory efficiency
- Optimize deep queries and large node sets
- Provide cache control methods (
_all,_one,_single) for performance tuning - Support early termination in
_onequeries - Consider query plan optimization before execution
-
Error Handling:
- Provide two versions for most attribute and method accesses
- For methods returning numeric or sensitive types, provide only dual-value version to prevent subtle bugs
- Integrate seamlessly with XGo's error handling operators (
!and?:) - Provide clear error messages and debugging support
- Define standard error types (
ErrNotFound,ErrMultipleEntities) for_single
- Extensibility: Support custom functions and operators
- Compatibility: Integration with existing XGo syntax and toolchain
-
Domain-Specific Adaptations: Allow DOM implementations to optimize method syntax (e.g., using
textinstead of_textin HTML DOM when there's no naming conflict) - First-node Semantics: All data access operations (attributes and methods) operate exclusively on the first node in the NodeSet. This provides consistent, predictable behavior and requires explicit iteration (e.g., list comprehension) when multi-node access is needed.
- Error Propagation: Ensure proper error propagation in list comprehensions and nested queries
- Version Safety: Clearly document which methods provide single-value versions and which don't, to prevent runtime errors and subtle bugs
-
Cache Strategy:
- Implement efficient caching mechanisms for
_all - Ensure
_onetruly terminates early in query execution - Validate uniqueness constraints efficiently in
_single - Consider memory limits and provide guidelines for large datasets
- Implement efficient caching mechanisms for
DQL provides XGo with a unified, powerful, and flexible query interface that simplifies operations on various structured data, improving development efficiency and code readability.
Key design features:
-
Two-version design for attributes and methods (similar to Go's
map[index]) provides both convenience and safety, while selective availability of single-value versions for numeric methods prevents subtle bugs - First-node semantics for all data access operations ensures predictable behavior and encourages explicit handling of multi-node scenarios through list comprehensions
- Lazy evaluation minimizes memory overhead while enabling efficient query composition
-
Cache control methods (
_all,_one,_single) give developers fine-grained control over performance trade-offs, supporting use cases from streaming processing to batch operations with validation
The combination of expressive syntax, performance optimization options, and safety-focused error handling makes DQL suitable for a wide range of structured data processing tasks across different domains.