traverse

package module
v0.22.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: MIT Imports: 21 Imported by: 6

README

Traverse

A declarative OData v2/v4 client for Go.

Go Version CI Tests Coverage CodeQL Trivy Release pkg.go.dev Go Report Card


Documentation - pkg.go.dev - Quick Start - Features - Extensions

Overview

Traverse is a Go library for consuming OData v2 and v4 services. It handles all protocol details - pagination, CSRF tokens, ETag concurrency control, delta sync, batch requests, async long-running operations, actions, functions, and schema validation - so you can focus on the data.

Built on relay for HTTP transport. Well-suited for SAP environments (ABAP Gateway / OData v2, S/4HANA / OData v4), Microsoft Graph, and any standards-compliant OData service.

go get github.com/jhonsferg/traverse

Requires Go 1.25 or later.


Why traverse?

The verb to traverse means to walk through something large, complex, or extended - one step at a time, without needing to hold the whole thing in memory. In computer science, tree traversal and graph traversal describe exactly that: visiting every node of a structure incrementally rather than materialising it all at once.

That is the problem this library solves:

other clients:  GET /MaterialSet → load 1 000 000 records into RAM → out of memory
traverse:       GET /MaterialSet → visit each record one by one   → constant memory

The difference is not what you fetch - it is how you move through it. Traverse treats a remote OData collection the way a graph traversal treats a tree: as a path to walk, not a payload to download.

Three principles follow naturally from the name:

The path matters more than the destination. You do not wait to have all the data before you start working. Each record is actionable the moment it arrives - that is exactly the for result := range client.From("MaterialSet").Stream(ctx) pattern at the core of the library.

Respect for the terrain. A careful traversal does not tear up the ground beneath it. Traverse is deliberately gentle on the services it talks to: rate limiting and circuit breaking are inherited from relay, page size follows the server's own nextLink rhythm, and CSRF tokens are managed transparently without extra round-trips.

The map is not the territory. A tree traversal does not require the full tree in memory - only the current node and a pointer to the next. Traverse does the same with OData: you can walk a million SAP materials without keeping a million structs alive simultaneously.

The name also has an intentional honesty to it: it does not promise to be an OData library or a SAP library. It promises to help you move through large, remote datasets. Today that means OData. Tomorrow it could mean any cursor-based protocol - the name stays valid.


Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jhonsferg/traverse"
)

type Product struct {
    ID    int     `json:"ProductID"`
    Name  string  `json:"ProductName"`
    Price float64 `json:"UnitPrice"`
}

func main() {
    client, err := traverse.New(
        traverse.WithBaseURL("https://services.odata.org/V4/Northwind/Northwind.svc/"),
    )
    if err != nil {
        log.Fatal(err)
    }

    products := traverse.From[Product](client, "Products")

    results, err := products.
        Filter("UnitPrice lt 20").
        OrderBy("ProductName").
        Top(10).
        List(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    for _, p := range results {
        fmt.Printf("%s - $%.2f\n", p.Name, p.Price)
    }
}

Features

Feature Description
Typed query builder From[T]() with .Filter(), .Select(), .Expand(), .OrderBy(), .Top(), .Skip()
Type-safe filter builder F("Field").Eq(value), And(), Or(), Not() - no raw strings
CRUD operations .Get(), .List(), .Create(), .Update(), .Delete(), .Upsert()
ETag & concurrency Automatic ETag tracking for optimistic concurrency on updates
Entity change tracking Track and PATCH only modified fields
Typed pagination Paginator[T] with .NextPage() and .Stream()
Async operations Automatic polling for 202 Accepted long-running operations
Streaming Channel-based streaming via json.Decoder - constant memory
Batch requests $batch with transactional changesets; OData 4.01 JSON batch format
Delta sync $deltatoken tracking for incremental data sync
Lambda filters any() / all() on collection navigation properties
Deep insert Create entity graphs in a single request
Deep update PATCH nested entity graphs in a single round-trip
BulkUpdate PATCH /EntitySet?$filter=... for mass updates (OData 4.01)
Singletons First-class singleton access: client.Singleton("me").Page(ctx)
Type casting AsType("Model.Manager") path segments; IsOf() / Cast() filter helpers
$expand $levels Expand("Children", traverse.WithExpandLevels(traverse.LevelsMax))
Geospatial GeographyPoint, GeometryPoint, GeoDistance, GeoIntersects filter functions
$ref link operations LinkTo() / UnlinkFrom() for managing navigation property references
Actions & Functions ActionBuilder / FunctionBuilder - bound and unbound
Schema validation Client-side field name validation on $filter / $orderby
Prefer headers PreferHandlingStrict, ReturnMinimal, ReturnRepresentation, PreferTrackChanges
$schemaversion WithSchemaVersion("2.0") at client or per-query level
Atom/XML responses OData v2 application/atom+xml streaming parser (auto-detected)
OData v2 $inlinecount $inlinecount=allpages emitted for v2 services; d.__count parsed
SAP TLS WithSAPTLSConfig(cfg) for custom CA bundles and self-signed certs
SAP property path FetchPropertyAs[T] for scalar/complex property fetch by key and path
Code generation traverse-gen generates typed clients from $metadata
SAP compatibility CSRF tokens, X-Requested-With, SAP sap:* metadata attributes

Full feature documentation: jhonsferg.github.io/traverse


SAP CSRF Token Management (Automatic)

Traverse now handles SAP CSRF tokens completely transparently, including session cookie management. This fixes a critical architectural issue where CSRF tokens and session cookies must be paired atomically:

import (
    "context"
    "github.com/jhonsferg/traverse"
    "github.com/jhonsferg/traverse/ext/sap"
)

// CSRF token and session cookie are fetched and managed automatically
// No explicit token management needed
client, _ := traverse.New(
    traverse.WithBaseURL("https://sap.example.com/sap/opu/odata/"),
    sap.WithCSRFMiddleware(), // Automatic token fetch + session persistence
)

// Create operation - token is reused, session cookies are automatic
newOrder := Order{ID: "1001", Amount: 99.99}
created, err := sap.CreateJsonAs[Order](
    client.From("Orders"),
    context.Background(),
    newOrder,
)

// If token expires, middleware automatically handles 403 recovery
// No retry logic needed in application code

What's Fixed (v0.19.0+):

  • ✅ Session cookies are now captured and reused (via relay v0.4.0 CookieJar support)
  • ✅ CSRF tokens are reused for their full validity window (no preventive invalidation)
  • ✅ URL construction handles edge cases (no multiple slashes)
  • ✅ Better error diagnostics distinguish between CSRF, auth, config, and network failures
  • ✅ Automatic 403 recovery when tokens expire in-flight

CSDL JSON Support

Traverse can parse both EDMX/XML and CSDL JSON (the OData v4.01 JSON format used by Microsoft Graph). The client auto-detects the format by Content-Type when fetching $metadata:

// Auto-detected - no code change needed when the service returns JSON metadata
client, _ := traverse.New(traverse.WithBaseURL("https://api.example.com/odata/v4/"))
meta, err := client.Metadata(ctx)

For direct parsing:

import "github.com/jhonsferg/traverse"

// From bytes
meta, err := traverse.ParseCSDLJSON(data)

// From a reader (e.g. http.Response.Body)
meta, err := traverse.ParseCSDLJSONReader(resp.Body)

XML Support

Traverse supports both JSON and XML response formats. Some OData backends (particularly SAP) may return XML instead of JSON, even when JSON is requested. Use the explicit format methods to handle both:

type Product struct {
    ID    int    `json:"ProductID" xml:"ProductID"`
    Name  string `json:"ProductName" xml:"ProductName"`
}

client, _ := traverse.New(traverse.WithBaseURL("https://api.example.com/odata/"))

ctx := context.Background()

qb := client.From("Products").Filter("Price lt 100")

products, err := traverse.CollectJsonAs[Product](qb, ctx)

if err != nil {
    products, err = traverse.CollectXmlAs[Product](qb, ctx)
    if err != nil {
        log.Fatal(err)
    }
}

for _, p := range products {
    fmt.Printf("%s\n", p.Name)
}

Streaming with XML:

type Order struct {
    OrderID string `json:"OrderID" xml:"OrderID"`
    Amount  float64 `json:"Amount" xml:"Amount"`
}

qb := client.From("Orders").Top(1000)

for result := range traverse.StreamXmlAs[Order](qb, context.Background()) {
    if result.Err != nil {
        log.Printf("stream error: %v", result.Err)
        break
    }
    fmt.Printf("Order %s: $%.2f\n", result.Value.OrderID, result.Value.Amount)
}

All CRUD and query operations have explicit JSON/XML variants:

Operation JSON XML
Create CreateJsonAs[T]() CreateXmlAs[T]()
Collect (list) CollectJsonAs[T]() CollectXmlAs[T]()
Stream (paginated) StreamJsonAs[T]() StreamXmlAs[T]()
First (single) FirstJsonAs[T]() FirstXmlAs[T]()
FindByKey (get) FindByKeyJsonAs[T]() FindByKeyXmlAs[T]()
Functions ExecuteFunctionJsonAs[T]() ExecuteFunctionXmlAs[T]()
Actions ExecuteActionJsonAs[T]() ExecuteActionXmlAs[T]()
Delta sync DeltaSyncJsonAs[T] DeltaSyncXmlAs[T]

Struct tags determine marshaling behavior:

type Material struct {
    ID    string `json:"MatID" xml:"MatID"`
    Name  string `json:"Name" xml:"Name"`
    Stock int    `json:"StockQty" xml:"StockQty"`
}

json_mats, _ := traverse.CollectJsonAs[Material](qb, ctx)

xml_mats, _ := traverse.CollectXmlAs[Material](qb, ctx)

OpenAPI 3.1 Export

Convert OData metadata to an OpenAPI 3.1 document:

import (
    "encoding/json"
    "github.com/jhonsferg/traverse"
    "github.com/jhonsferg/traverse/ext/openapi"
)

meta, _ := client.Metadata(ctx)
doc, err := openapi.Export(meta,
    openapi.WithTitle("My OData API"),
    openapi.WithVersion("1.0.0"),
    openapi.WithServerURL("https://api.example.com/odata/v4/"),
)

out, _ := json.MarshalIndent(doc, "", "  ")
fmt.Println(string(out))
go get github.com/jhonsferg/traverse/ext/openapi

OData Vocabularies

Properties carry parsed Core and Validation vocabulary annotations:

meta, _ := client.Metadata(ctx)
for _, et := range meta.EntityTypes {
    for _, prop := range et.Properties {
        core := traverse.ParseCoreVocabulary(prop.Annotations)
        val  := traverse.ParseValidationVocabulary(prop.Annotations)

        fmt.Printf("%s: %s", prop.Name, core.Description)
        if val.Pattern != "" {
            fmt.Printf(" (pattern: %s)", val.Pattern)
        }
        if val.Required {
            fmt.Print(" [required]")
        }
        fmt.Println()
    }
}

Available types: CoreVocabulary (Description, LongDescription, Immutable, Computed, Permissions, …) and ValidationVocabulary (Minimum, Maximum, Pattern, AllowedValues, Required).


Tools

traverse-gen

traverse-gen generates type-safe Go clients from an OData $metadata endpoint:

go run github.com/jhonsferg/traverse/cmd/traverse-gen \
  -metadata https://services.odata.org/V4/Northwind/Northwind.svc/$metadata \
  -out ./northwind
traverse-tui

Interactive terminal UI for exploring OData endpoints, building queries, and inspecting results:

go run github.com/jhonsferg/traverse/cmd/traverse-tui
SAP OData Mock Server

A local SAP NetWeaver OData v2 simulator for integration testing without a real SAP system:

go run github.com/jhonsferg/traverse/cmd/sap-mock

Simulates CSRF token lifecycle, Basic Auth, $metadata responses, entity-set queries, key-predicate lookups, and property-path navigation. Logs all incoming requests with headers, query parameters, and body for inspection.

SAP OData Mock Server
  Listen: http://localhost:44300
  Auth:   enabled (user=sapuser pass=sappass)

Extensions

Module Import path Description
ext/sap github.com/jhonsferg/traverse/ext/sap SAP Gateway CSRF, session handling, and Fiori UI annotations
ext/openapi github.com/jhonsferg/traverse/ext/openapi OpenAPI 3.1 export from OData metadata
ext/oauth2 github.com/jhonsferg/traverse/ext/oauth2 OAuth2 token provider
ext/prometheus github.com/jhonsferg/traverse/ext/prometheus Prometheus metrics
ext/tracing github.com/jhonsferg/traverse/ext/tracing OpenTelemetry tracing
ext/graphql github.com/jhonsferg/traverse/ext/graphql GraphQL-to-OData bridge
ext/cache github.com/jhonsferg/traverse/ext/cache HTTP response and metadata caching
ext/offline github.com/jhonsferg/traverse/ext/offline Persistent offline store with JSON cache
ext/dataverse github.com/jhonsferg/traverse/ext/dataverse Microsoft Dataverse adapter
ext/azure github.com/jhonsferg/traverse/ext/azure Azure Event Grid change events
ext/webhooks github.com/jhonsferg/traverse/ext/webhooks OData webhook subscriptions
ext/audit github.com/jhonsferg/traverse/ext/audit Audit trail middleware

Extension documentation: jhonsferg.github.io/traverse/extensions

SAP Fiori UI Annotations

ext/sap includes support for SAP UI annotations parsed from EDMX attributes (sap:label, sap:sortable, sap:filterable, etc.):

import "github.com/jhonsferg/traverse/ext/sap"

ann := sap.ParseSAPUIAnnotation(property.RawAttributes)
fmt.Printf("Label: %s, Filterable: %v\n", ann.Label, ann.Filterable)

// Get all annotated properties from an entity type
props := sap.AnnotatedProperties(entityType, meta)
for _, p := range props {
    fmt.Printf("%s → label=%s sortable=%v\n", p.Property.Name, p.Annotation.Label, p.Annotation.Sortable)
}

Microsoft Graph

rc := relay.New(relay.WithBearerToken(token))
gc := traverse.NewGraphClient(rc, traverse.GraphConfig{
    AccessToken: token,
})

type User struct {
    ID          string `json:"id"`
    DisplayName string `json:"displayName"`
}

users, err := traverse.From[User](gc, "users").
    Filter("department eq 'Engineering'").
    Select("id", "displayName").
    List(ctx)

Microsoft Graph guide


Documentation

The full documentation is at jhonsferg.github.io/traverse:


License

MIT - see LICENSE.

Documentation

Overview

Package traverse provides a production-grade OData v2/v4 client library for Go.

Traverse is designed for high-performance querying and manipulation of OData services, with special optimizations for SAP systems. It offers:

  • Streaming-first architecture for processing large datasets without memory overhead

- Ultra-low memory allocations (-81% vs baseline) through object pooling and zero-allocation patterns - Fluent query builder API for ergonomic OData query construction - Support for OData batch operations, delta queries, functions, and actions - Thread-safe client with concurrent goroutine support - Extensive metadata caching and query hooks for extensibility

Quick Start:

import "github.com/jhonsferg/traverse"

client, _ := traverse.New(
	traverse.WithBaseURL("https://odata.example.com/v4"),
	traverse.WithODataVersion(traverse.ODataV4),
)
defer client.Close()

results, _ := client.From("Products").
	Filter("Price gt 100").
	OrderBy("Name").
	Collect(context.Background())

For streaming large result sets:

stream := client.From("Orders").
	Stream(context.Background())
for result := range stream {
	processOrder(result.Value)
}

The Client type is the main entry point and is safe for concurrent use across multiple goroutines. Use From() to create a QueryBuilder for constructing queries.

Package traverse provides a production-grade OData v2/v4 client for Go, built on top of github.com/jhonsferg/relay.

traverse is designed to consume OData services with millions of records without running out of memory, using server-side pagination, streaming JSON decoding, and Go channels for backpressure.

It is compatible with SAP OData services (both classic ABAP Gateway using OData v2 and S/4HANA using OData v4).

Quick start:

client, err := traverse.New(
    traverse.WithBaseURL("https://sap.example.com/sap/opu/odata/sap/MY_SRV"),
    traverse.WithBasicAuth("user", "pass"),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

for result := range client.From("MaterialSet").Stream(ctx) {
    if result.Err != nil {
        log.Fatal(result.Err)
    }
    fmt.Println(result.Value) // map[string]any for each record
}

See https://github.com/jhonsferg/traverse for full documentation.

Index

Constants

View Source
const (
	// PreferHandlingStrict instructs the server to fail if it encounters unknown
	// query options (handling=strict).
	PreferHandlingStrict = "handling=strict"
	// PreferHandlingLenient instructs the server to silently ignore unknown
	// query options (handling=lenient).
	PreferHandlingLenient = "handling=lenient"
	// PreferReturnRepresentation instructs the server to return the full entity
	// in the response body after a create or update operation (return=representation).
	PreferReturnRepresentation = "return=representation"
	// PreferReturnMinimal instructs the server to return 204 No Content after a
	// create or update operation instead of the full entity (return=minimal).
	PreferReturnMinimal = "return=minimal"
	// PreferTrackChanges requests that the server include a deltaLink in the
	// response, enabling incremental sync on the next request.
	PreferTrackChanges = "odata.track-changes"
	// PreferRespondAsync requests that the server process the request asynchronously
	// and return a 202 Accepted with a status monitor URL.
	PreferRespondAsync = "respond-async"
)

Prefer header constants for OData Prefer request header values. See OData v4.0 spec section 8.2.8.

View Source
const DefaultMaxPolls = 60

DefaultMaxPolls is the default maximum number of poll attempts.

View Source
const DefaultPollInterval = 5 * time.Second

DefaultPollInterval is the default interval between async operation polls.

View Source
const LevelsMax = -1

LevelsMax is a sentinel value for WithExpandLevels that instructs the server to expand recursively to the maximum supported depth ($levels=max).

Variables

View Source
var (
	// ErrEntityNotFound is returned when a requested entity is not found (HTTP 404).
	ErrEntityNotFound = errors.New("traverse: entity not found (404)")

	// ErrConcurrencyConflict is returned when an ETag mismatch occurs (HTTP 412).
	// This indicates the entity was modified by another client.
	ErrConcurrencyConflict = errors.New("traverse: ETag mismatch (412)")

	// ErrServiceUnavailable is returned when the OData service is unavailable.
	ErrServiceUnavailable = errors.New("traverse: OData service unavailable")

	// ErrCSRFTokenRequired is returned when a CSRF token is required (SAP-specific).
	// This is common in SAP systems; include a CSRF token in requests.
	ErrCSRFTokenRequired = errors.New("traverse: CSRF token required (SAP)")

	// ErrInvalidFilter is returned when a $filter expression is invalid.
	ErrInvalidFilter = errors.New("traverse: invalid $filter expression")

	// ErrMetadataInvalid is returned when $metadata parsing fails.
	ErrMetadataInvalid = errors.New("traverse: failed to parse $metadata")

	// ErrPageSizeExceeded is returned when server rejects $top value.
	ErrPageSizeExceeded = errors.New("traverse: server rejected $top value")

	// ErrStreamClosed is returned when stream is closed or context cancelled.
	ErrStreamClosed = errors.New("traverse: stream closed or context cancelled")

	// ErrClientClosed is returned when client is closed.
	ErrClientClosed = errors.New("traverse: client closed")

	// ErrUnauthorized is returned when authentication fails (HTTP 401).
	ErrUnauthorized = errors.New("traverse: authentication required (401)")

	// ErrForbidden is returned when authorization fails (HTTP 403).
	ErrForbidden = errors.New("traverse: authorization denied (403)")

	// ErrBatchFailed is returned when a batch request fails.
	ErrBatchFailed = errors.New("traverse: batch request failed")
)

Sentinel errors define common error conditions in OData operations.

These are error values that can be compared directly with errors.Is() for categorizing failures in OData interactions.

View Source
var ErrAsyncOpFailed = errors.New("traverse: async operation failed")

ErrAsyncOpFailed is returned when the async operation completes with a failure status.

View Source
var ErrAsyncOpTimeout = errors.New("traverse: async operation timed out after max polls")

ErrAsyncOpTimeout is returned when maxPolls is exhausted before the operation completes.

Functions

func Cast added in v0.6.0

func Cast(args ...string) string

Cast returns an OData filter expression string that casts a value to the given type (cast() function).

Cast implements the OData v4.0 type system function defined in spec section 5.1.1.6.3. Two forms are supported:

  • cast(TypeName) - casts the entity itself to the given type
  • cast(expression, TypeName) - casts an expression to the given type

The result can be used in filter expressions, typically with a comparison operator.

Example:

// $filter=cast(Budget,Edm.Decimal) gt 1000
client.From("Projects").
    Filter(traverse.Cast("Budget", "Edm.Decimal") + " gt 1000").
    Collect(ctx)

func ClearGlobalCache

func ClearGlobalCache()

ClearGlobalCache clears the global string interning cache.

ClearGlobalCache removes all cached strings and reinitializes the cache. Useful for testing or when you want to reset memory state in long-running processes.

func CollectAs

func CollectAs[T any](qb *QueryBuilder, ctx context.Context) ([]T, error)

CollectAs iterates through the query result stream, converts each result to type T, and collects them into a slice. All results are loaded into memory before returning.

⚠️ Warning: For large datasets (millions of records), this method loads all results into memory at once, which can cause significant memory pressure and GC overhead. For large result sets, prefer StreamAs which processes results incrementally without materializing the entire collection.

The buffer size parameter is passed to the underlying QueryBuilder.Stream call to control the buffering of results. Default is adaptive buffering (see [Stream]).

Returns a slice of T with all query results, or an error if streaming or conversion fails.

Example:

type Product struct {
	ID    int     `json:"id"`
	Price float64 `json:"price"`
}

// Load all products (⚠️ use with caution for large datasets)
products, err := CollectAs[Product](qb, ctx)

CollectAs is an alias for CollectJsonAs for backward compatibility. Deprecated: Use CollectJsonAs or CollectXmlAs instead.

func CollectJsonAs added in v0.16.0

func CollectJsonAs[T any](qb *QueryBuilder, ctx context.Context) ([]T, error)

CollectJsonAs materializes all results from the query into a slice of type T using JSON.

CollectJsonAs iterates through the query result stream, converts each result to type T, and collects them into a slice. All results are loaded into memory before returning.

⚠️ Warning: For large datasets (millions of records), this method loads all results into memory at once, which can cause significant memory pressure and GC overhead. For large result sets, prefer StreamJsonAs which processes results incrementally without materializing the entire collection.

Returns a slice of T with all query results, or an error if streaming or conversion fails.

Example:

type Product struct {
	ID    int     `json:"id"`
	Price float64 `json:"price"`
}

// Load all products (⚠️ use with caution for large datasets)
products, err := CollectJsonAs[Product](qb, ctx)

func CollectXmlAs added in v0.16.0

func CollectXmlAs[T any](qb *QueryBuilder, ctx context.Context) ([]T, error)

CollectXmlAs materializes all results from the query into a slice of type T with XML struct tags.

CollectXmlAs is the XML-tag variant of CollectAs. It iterates through the query result stream, converts each result to type T, and collects them into a slice. All results are loaded into memory before returning.

The "XmlAs" suffix indicates that the target struct T has xml:"..." tags for field mapping, not that the server returns XML responses. The server response is always JSON.

⚠️ Warning: For large datasets (millions of records), this method loads all results into memory at once, which can cause significant memory pressure and GC overhead. For large result sets, prefer StreamXmlAs which processes results incrementally without materializing the entire collection.

Returns a slice of T with all query results, or an error if streaming or conversion fails.

Example:

type Product struct {
	ID    int     `xml:"id"`
	Price float64 `xml:"price"`
}

// Load all products (⚠️ use with caution for large datasets)
products, err := CollectXmlAs[Product](qb, ctx)

func ComputeExpr added in v0.2.27

func ComputeExpr(parts ...string) string

ComputeExpr joins the provided parts with spaces to form a $compute expression.

This is a convenience helper for constructing compute expressions from individual tokens without manual string concatenation.

Example:

ComputeExpr("Price", "mul", "Quantity", "as", "Total") // "Price mul Quantity as Total"

func CreateAtomXmlAs added in v0.21.3

func CreateAtomXmlAs[T any](c *Client, ctx context.Context, entitySet string, data interface{}) (T, error)

CreateAtomXmlAs creates a new entity and unmarshals the OData v2 Atom XML response directly to type T.

CreateAtomXmlAs is specialized for SAP OData v2 services that return Atom+XML format (RFC 5023). Unlike CreateXmlAs which converts through a map[string]interface{}, this method:

  • Sends Accept: application/atom+xml header to request Atom format
  • Gets raw XML bytes from the response
  • Unmarshals directly to the target struct T using xml.Unmarshal
  • Preserves the full Atom structure including namespaces and metadata

The target struct T must have xml:"..." tags with proper namespace handling for Atom elements. For OData v2 Atom, the response contains an <entry> element with <content><m:properties> children.

This is the preferred method for SAP OData v2 integrations when you need true XML unmarshaling without intermediate JSON conversion.

Returns the created entity as type T, or an error if creation fails.

Example:

type SAPNotification struct {
	XMLName  xml.Name `xml:"entry"`
	ID       string   `xml:"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata id"`
	Content  struct {
		Properties struct {
			NotifID string `xml:"NotificationID"`
		} `xml:"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata properties"`
	} `xml:"http://www.w3.org/2005/Atom content"`
}

notif, err := CreateAtomXmlAs[SAPNotification](client, ctx, "MaintenanceNotifications", newNotif)

func CreateJsonAs added in v0.16.0

func CreateJsonAs[T any](c *Client, ctx context.Context, entitySet string, data interface{}) (T, error)

CreateJsonAs creates a new entity using JSON payload and decodes the response to type T.

CreateJsonAs is the generic version of Client.Create. It sends a POST request to create a new entity in the specified entity set, then unmarshals the response into a typed value of type T.

The created entity data is marshaled to JSON automatically. The server response typically contains the new entity with generated fields (ID, timestamps, etc.).

Returns the created entity (with server-assigned fields) as type T, or an error if creation fails.

Example:

type Order struct {
	ID    int    `json:"id"`
	Total float64 `json:"total"`
}

newOrder := map[string]interface{}{"total": 99.99}
order, err := CreateJsonAs[Order](client, ctx, "Orders", newOrder)
// order.ID is now set by the server

func CreateRawAs added in v0.20.0

func CreateRawAs(c *Client, ctx context.Context, entitySet string, data interface{}) ([]byte, error)

CreateRawAs creates a new entity and returns the raw response bytes.

CreateRawAs sends a POST request to create a new entity in the specified entity set and returns the raw response body as bytes. This is useful for debugging, testing, or handling responses that don't fit standard patterns (e.g., custom XML, mixed formats).

Returns the raw response bytes, or an error if creation fails. The caller is responsible for parsing or validating the response content.

Example:

rawData, err := CreateRawAs(client, ctx, "Orders", newOrder)
if err != nil {
	// handle error
}
fmt.Println(string(rawData)) // Print raw response

This is commonly used for:

  • Testing and debugging OData responses
  • Working with non-standard response formats
  • Validating SAP backend behavior
  • Capturing both JSON and XML responses transparently

func CreateXmlAs added in v0.16.0

func CreateXmlAs[T any](c *Client, ctx context.Context, entitySet string, data interface{}) (T, error)

CreateXmlAs creates a new entity and decodes the response to type T with XML struct tags.

CreateXmlAs is the generic version of Client.Create. It sends a POST request to create a new entity in the specified entity set, then unmarshals the response into a typed value of type T.

The target struct T must have xml:"..." tags for proper field mapping. The method handles responses that are returned as JSON (converted via mapToXmlStruct) or for direct XML unmarshaling, use the lower-level Client methods with rawMessageToXmlStruct.

Returns the created entity (with server-assigned fields) as type T, or an error if creation fails.

Example:

type Order struct {
	ID    int    `xml:"id"`
	Total float64 `xml:"total"`
}

newOrder := map[string]interface{}{"total": 99.99}
order, err := CreateXmlAs[Order](client, ctx, "Orders", newOrder)
// order.ID is now set by the server

func DateTimeOffsetValue

func DateTimeOffsetValue(t time.Time) string

DateTimeOffsetValue produces an OData DateTimeOffset literal for use in filter expressions.

DateTimeOffsetValue generates the OData v4 DateTimeOffset format (ISO 8601/RFC 3339) suitable for $filter expressions on DateTimeOffset fields.

Returns string in RFC 3339 format: 2024-01-01T00:00:00Z or 2024-01-01T00:00:00+01:00

Example (used in filters):

qb.Filter(fmt.Sprintf("CreatedAt ge %s", DateTimeOffsetValue(startDate)))

func DateTimeValue

func DateTimeValue(t time.Time) string

DateTimeValue produces an OData DateTime literal for use in filter expressions.

DateTimeValue generates the OData v2 DateTime format suitable for $filter expressions. It's a wrapper around DateTimeValueBytes for convenience when a string is needed.

Returns string in the format: datetime'2024-01-01T00:00:00'

Example (used in filters):

qb.Filter(fmt.Sprintf("CreatedAt ge %s", DateTimeValue(startDate)))

func DateTimeValueBytes

func DateTimeValueBytes(t time.Time) []byte

DateTimeValueBytes produces an OData DateTime literal for use in filter expressions as bytes.

DateTimeValueBytes generates the OData v2 DateTime format suitable for use in $filter expressions. It pre-allocates a buffer and appends the formatted datetime to minimize allocations compared to string concatenation.

Returns bytes in the format: datetime'2024-01-01T00:00:00'

Example:

filter := string(DateTimeValueBytes(time.Now()))
// filter = "datetime'2024-01-01T12:34:56'"

func DecimalValue

func DecimalValue(v float64) string

DecimalValue produces an OData Decimal literal for filters. Returns: 3.14M This is a wrapper around DecimalValueBytes for backward compatibility.

func DecimalValueBytes

func DecimalValueBytes(v float64) []byte

DecimalValueBytes produces an OData Decimal literal for filters as bytes. Returns: []byte("3.14M") This is the optimized version that avoids string allocations.

func ExecuteActionAs

func ExecuteActionAs[T any](a *ActionBuilder, ctx context.Context) (T, error)

ExecuteActionAs is an alias for ExecuteActionJsonAs for backward compatibility. Deprecated: Use ExecuteActionJsonAs or ExecuteActionXmlAs instead.

func ExecuteActionJsonAs added in v0.16.0

func ExecuteActionJsonAs[T any](a *ActionBuilder, ctx context.Context) (T, error)

ExecuteActionJsonAs is the JSON-format generic version of ActionBuilder.Execute.

ExecuteActionJsonAs calls the action and unmarshals the response result to type T using JSON. It uses [mapToJsonStruct] for type conversion, supporting all Go types with JSON marshaling.

Returns the action result as type T, or an error if the call fails or type conversion fails.

Example:

type ApprovalResult struct {
	Approved bool   `json:"approved"`
	Message  string `json:"message"`
}

result, err := ExecuteActionJsonAs[ApprovalResult](
	client.Action("ApproveOrder").WithBody(approvalData),
	ctx,
)

func ExecuteActionXmlAs added in v0.16.0

func ExecuteActionXmlAs[T any](a *ActionBuilder, ctx context.Context) (T, error)

ExecuteActionXmlAs is the XML-format generic version of ActionBuilder.Execute.

ExecuteActionXmlAs calls the action and unmarshals the response result to type T with XML struct tags. It uses [mapToXmlStruct] for conversion with struct tags in xml:"..." format.

Returns the action result as type T, or an error if the call fails or type conversion fails.

Example:

type ApprovalResult struct {
	Approved bool   `xml:"approved"`
	Message  string `xml:"message"`
}

result, err := ExecuteActionXmlAs[ApprovalResult](
	client.Action("ApproveOrder").WithBody(approvalData),
	ctx,
)

func ExecuteFunctionAs

func ExecuteFunctionAs[T any](f *FunctionBuilder, ctx context.Context) (T, error)

ExecuteFunctionAs is an alias for ExecuteFunctionJsonAs for backward compatibility. Deprecated: Use ExecuteFunctionJsonAs or ExecuteFunctionXmlAs instead.

func ExecuteFunctionImportAs

func ExecuteFunctionImportAs[T any](f *FunctionImportBuilder, ctx context.Context) (T, error)

ExecuteFunctionImportAs is an alias for ExecuteFunctionImportJsonAs for backward compatibility. Deprecated: Use ExecuteFunctionImportJsonAs or ExecuteFunctionImportXmlAs instead.

func ExecuteFunctionImportJsonAs added in v0.16.0

func ExecuteFunctionImportJsonAs[T any](f *FunctionImportBuilder, ctx context.Context) (T, error)

ExecuteFunctionImportJsonAs is the JSON-format generic version of FunctionImportBuilder.Execute.

func ExecuteFunctionImportXmlAs added in v0.16.0

func ExecuteFunctionImportXmlAs[T any](f *FunctionImportBuilder, ctx context.Context) (T, error)

ExecuteFunctionImportXmlAs is the XML-format generic version of FunctionImportBuilder.Execute.

func ExecuteFunctionJsonAs added in v0.16.0

func ExecuteFunctionJsonAs[T any](f *FunctionBuilder, ctx context.Context) (T, error)

ExecuteFunctionJsonAs is the JSON-format generic version of FunctionBuilder.Execute.

ExecuteFunctionJsonAs calls the function and unmarshals the response result to type T using JSON. It uses [mapToJsonStruct] for type conversion, supporting all Go types with JSON marshaling.

Returns the function result as type T, or an error if the call fails or type conversion fails.

Example:

type TopProducts struct {
	Products []Product `json:"products"`
}

result, err := ExecuteFunctionJsonAs[TopProducts](
	client.Function("GetTopProducts").Param("count", 10),
	ctx,
)

func ExecuteFunctionXmlAs added in v0.16.0

func ExecuteFunctionXmlAs[T any](f *FunctionBuilder, ctx context.Context) (T, error)

ExecuteFunctionXmlAs is the XML-format generic version of FunctionBuilder.Execute.

ExecuteFunctionXmlAs calls the function and unmarshals the response result to type T with XML struct tags. It uses [mapToXmlStruct] for conversion with struct tags in xml:"..." format.

Returns the function result as type T, or an error if the call fails or type conversion fails.

Example:

type TopProducts struct {
	Products []Product `xml:"products"`
}

result, err := ExecuteFunctionXmlAs[TopProducts](
	client.Function("GetTopProducts").Param("count", 10),
	ctx,
)

func FetchPropertyAs added in v0.4.0

func FetchPropertyAs[T any](qb *QueryBuilder, ctx context.Context, property string) (T, error)

FetchPropertyAs retrieves a single scalar or object property from an OData entity using the standard OData property path pattern:

GET /EntitySet(Key)/PropertyName

This is the idiomatic way to fetch one field from a known entity without downloading the full record. Useful for large entities where only a single field (e.g. a price, a flag, or a blob link) is needed.

The qb parameter must already point to the full entity including its key, e.g. built via:

qb := client.From("/sap/opu/odata/sap/UI_PRODUCTLIST/ProductList(Product='3001008',Plant='1010',ValuationType='')")
price, err := traverse.FetchPropertyAs[string](qb, ctx, "PriceUnitQty")

Returns the zero value of T and an error if the property is not found or cannot be decoded.

func FindByKeyAs

func FindByKeyAs[T any](qb *QueryBuilder, ctx context.Context, key interface{}) (T, error)

FindByKeyAs is an alias for FindByKeyJsonAs for backward compatibility. Deprecated: Use FindByKeyJsonAs or FindByKeyXmlAs instead.

func FindByKeyJsonAs added in v0.16.0

func FindByKeyJsonAs[T any](qb *QueryBuilder, ctx context.Context, key interface{}) (T, error)

FindByKeyJsonAs retrieves a single entity by its key and decodes it to type T using JSON.

FindByKeyJsonAs is the JSON-format variant of QueryBuilder.FindByKey. It constructs a single-entity query using the provided key and returns the entity as type T.

The key can be a single value (for single-part keys) or a composite key using a map (for entities with compound keys).

Returns the entity as type T, or an error if not found or query fails.

Example:

type Customer struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

customer, err := FindByKeyJsonAs[Customer](qb, ctx, 42)
// or with composite key:
customer, err := FindByKeyJsonAs[Customer](qb, ctx, map[string]interface{}{"CompanyID": 1, "CustomerID": 42})

func FindByKeyXmlAs added in v0.16.0

func FindByKeyXmlAs[T any](qb *QueryBuilder, ctx context.Context, key interface{}) (T, error)

FindByKeyXmlAs retrieves a single entity by its key and decodes it to type T with XML struct tags.

FindByKeyXmlAs is the XML-tag variant of QueryBuilder.FindByKey. It constructs a single-entity query using the provided key and returns the entity as type T.

The "XmlAs" suffix indicates that the target struct T has xml:"..." tags for field mapping, not that the server response is XML. The server response is always JSON, which is unmarshaled into structs with XML tags.

The key can be a single value (for single-part keys) or a composite key using a map (for entities with compound keys).

Returns the entity as type T, or an error if not found or query fails.

Example:

type Customer struct {
	ID   int    `xml:"id"`
	Name string `xml:"name"`
}

customer, err := FindByKeyXmlAs[Customer](qb, ctx, 42)
// or with composite key:
customer, err := FindByKeyXmlAs[Customer](qb, ctx, map[string]interface{}{"CompanyID": 1, "CustomerID": 42})

func FirstAs

func FirstAs[T any](qb *QueryBuilder, ctx context.Context) (T, error)

FirstAs is an alias for FirstJsonAs for backward compatibility. Deprecated: Use FirstJsonAs or FirstXmlAs instead.

func FirstJsonAs added in v0.16.0

func FirstJsonAs[T any](qb *QueryBuilder, ctx context.Context) (T, error)

FirstJsonAs retrieves the first result from the query and decodes it to type T using JSON.

FirstJsonAs is the JSON-format version of QueryBuilder.First. It executes the query with the $top=1 modifier to retrieve only the first matching entity, then unmarshals it to type T.

This is efficient for single-item lookups and is equivalent to:

result, _ := FirstJsonAs[T](qb, ctx)
// vs
item, _ := qb.Top(1).First(ctx)
item, _ := mapToJsonStruct[T](item)

Returns the first entity as type T, or an error if the query fails or no results match.

Example:

type Customer struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

customer, err := FirstJsonAs[Customer](qb.Filter("Name eq 'Alice'"), ctx)

func FirstXmlAs added in v0.16.0

func FirstXmlAs[T any](qb *QueryBuilder, ctx context.Context) (T, error)

FirstXmlAs retrieves the first result from the query and decodes it to type T with XML struct tags.

FirstXmlAs is the XML-tag version of QueryBuilder.First. It executes the query with the $top=1 modifier to retrieve only the first matching entity, then unmarshals it to type T.

The "XmlAs" suffix indicates that the target struct T has xml:"..." tags for field mapping, not that the server response is XML. The server response is always JSON.

Returns the first entity as type T, or an error if the query fails or no results match.

Example:

type Customer struct {
	ID   int    `xml:"id"`
	Name string `xml:"name"`
}

customer, err := FirstXmlAs[Customer](qb.Filter("Name eq 'Alice'"), ctx)

func GeoDistanceFilter added in v0.10.0

func GeoDistanceFilter(property string, point GeographyPoint, operator string, maxDistance float64) string

GeoDistanceFilter returns an OData geo.distance() filter expression string.

The generated expression can be used directly with Filter():

client.From("Shops").Filter(traverse.GeoDistanceFilter("Location", traverse.GeographyPoint{13.408, 52.518}, "le", 1000))
// $filter=geo.distance(Location,geography'SRID=4326;POINT(13.408 52.518)') le 1000

The operator must be one of: lt, le, gt, ge, eq, ne. The maxDistance is the threshold value (in the same unit as the CRS - typically metres).

func GeoDistanceFilterGeom added in v0.10.0

func GeoDistanceFilterGeom(property string, point GeometryPoint, operator string, maxDistance float64) string

GeoDistanceFilterGeom returns an OData geo.distance() filter expression for geometry coordinates.

func GeoIntersectsFilter added in v0.10.0

func GeoIntersectsFilter(property string, polygon GeographyPolygon) string

GeoIntersectsFilter returns an OData geo.intersects() filter expression string.

geo.intersects(property, polygon) returns true when the point property falls within the given polygon boundary.

client.From("POI").Filter(traverse.GeoIntersectsFilter("Coordinates", myPolygon))
// $filter=geo.intersects(Coordinates,geography'SRID=4326;POLYGON(...)')

func GeoIntersectsFilterGeom added in v0.10.0

func GeoIntersectsFilterGeom(property string, polygon GeometryPolygon) string

GeoIntersectsFilterGeom returns an OData geo.intersects() filter expression for geometry.

func GeoLengthFilter added in v0.10.0

func GeoLengthFilter(property string, operator string, threshold float64) string

GeoLengthFilter returns an OData geo.length() filter expression string.

geo.length(linestring_property) returns the length of the LineString in the same unit as the coordinate reference system.

client.From("Routes").Filter(traverse.GeoLengthFilter("Path", "le", 50000))
// $filter=geo.length(Path) le 50000

func GlobalCacheSize

func GlobalCacheSize() int

GlobalCacheSize returns the current size of the global string interning cache.

GlobalCacheSize returns the number of unique interned strings in the global cache. Use this to monitor cache growth and detect potential memory leaks.

func GuidValue

func GuidValue(id string) string

GuidValue produces an OData Guid literal for use in filter expressions.

GuidValue wraps the provided GUID string in the OData Guid format suitable for $filter expressions on Guid fields.

Returns string in the format: guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

Example (used in filters):

qb.Filter(fmt.Sprintf("ID eq %s", GuidValue("550e8400-e29b-41d4-a716-446655440000")))

func InternString

func InternString(s string) string

InternString interns a string using the global interning cache.

InternString should be used for frequently repeated strings like entity names and property names that appear across millions of records.

The global cache is automatically initialized and can be managed with ClearGlobalCache and GlobalCacheSize.

Example:

propName := traverse.InternString("CustomerName")  // first call allocates
propName = traverse.InternString("CustomerName")   // second call returns cached ref

func IsAtomContentType added in v0.8.0

func IsAtomContentType(contentType string) bool

IsAtomContentType returns true when the Content-Type header indicates an Atom/XML feed.

func IsConcurrencyConflict

func IsConcurrencyConflict(err error) bool

IsConcurrencyConflict returns true if err is or wraps ErrConcurrencyConflict.

IsConcurrencyConflict indicates whether the error is due to an ETag mismatch (concurrent modification by another client). This typically requires the caller to retry with a fresh entity version.

func IsEntityNotFound

func IsEntityNotFound(err error) bool

IsEntityNotFound returns true if err is or wraps ErrEntityNotFound.

IsEntityNotFound provides a convenient way to check if a specific error is a "not found" error without type assertion.

func IsOf added in v0.6.0

func IsOf(args ...string) string

IsOf returns an OData filter expression string that tests whether entities or a property value are of the given type (isof() function).

IsOf implements the OData v4.0 type system function defined in spec section 5.1.1.6.3. Two forms are supported:

  • isof(TypeName) - tests whether the entity itself is of the given type
  • isof(propertyPath, TypeName) - tests whether a property is of the given type

The result can be used directly in QueryBuilder.Filter.

Example:

// $filter=isof(Model.Manager)
client.From("Employees").Filter(traverse.IsOf("Model.Manager")).Collect(ctx)

// $filter=isof(Address, Model.CnAddress)
client.From("People").Filter(traverse.IsOf("Address", "Model.CnAddress")).Collect(ctx)

func ParseAtomFeed added in v0.8.0

func ParseAtomFeed(r io.Reader, page *Page) error

atomFeedParser implements token-by-token streaming of an OData v2 Atom/XML feed.

The Atom format for OData v2 uses the following structure:

<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
      xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <m:count>100</m:count>
  <link rel="next" href="..."/>
  <entry>
    <id>...</id>
    <content type="application/xml">
      <m:properties>
        <d:OrderID m:type="Edm.Int32">1</d:OrderID>
        <d:Status>Active</d:Status>
      </m:properties>
    </content>
  </entry>
</feed>

ParseAtomFeed reads directly from the io.Reader and populates the Page.

func ReleaseGeographyPoint added in v0.10.0

func ReleaseGeographyPoint(p *GeographyPoint)

ReleaseGeographyPoint returns the point to the pool.

func ReleaseGeometryPoint added in v0.10.0

func ReleaseGeometryPoint(p *GeometryPoint)

ReleaseGeometryPoint returns the point to the pool.

func SingletonAs added in v0.6.0

func SingletonAs[T any](c *Client, name string) (*T, error)

SingletonAs fetches a singleton resource and decodes it into the provided type T.

SingletonAs is a convenience wrapper around Client.Singleton that handles the common pattern of fetching a singleton and decoding it into a typed struct.

Example:

type Me struct {
    ID          string `json:"id"`
    DisplayName string `json:"displayName"`
    Mail        string `json:"mail"`
}

me, err := traverse.SingletonAs[Me](client, "me")

func SingletonAsCtx added in v0.6.0

func SingletonAsCtx[T any](c *Client, ctx context.Context, name string) (*T, error)

SingletonAsCtx fetches a singleton resource and decodes it into the provided type T, using the provided context for cancellation and deadline control.

Example:

me, err := traverse.SingletonAsCtx[Me](client, ctx, "me")

func StreamAs

func StreamAs[T any](qb *QueryBuilder, ctx context.Context, bufferSize ...int) <-chan Result[T]

StreamAs is an alias for StreamJsonAs for backward compatibility. Deprecated: Use StreamJsonAs or StreamXmlAs instead.

func StreamJsonAs added in v0.16.0

func StreamJsonAs[T any](qb *QueryBuilder, ctx context.Context, bufferSize ...int) <-chan Result[T]

StreamJsonAs is the JSON-format streaming method for type T.

StreamJsonAs returns a channel of Result items typed to T, enabling incremental processing of large result sets without materializing all data in memory. Each result can be of type T or contain an error.

The method is optimized to use [QueryBuilder.streamRaw] for direct JSON unmarshaling, avoiding the intermediate map[string]interface{} allocation required by CollectJsonAs. This makes it significantly faster for streaming large datasets.

The bufferSize parameter controls the capacity of the result channel (default 256). For large record sizes or high network latency, increase this value to reduce blocking. For small records, the default is usually optimal.

Results include pagination information (Page, Index) for tracking position within large result sets.

Returns a receive-only channel that yields Result items as they become available. The channel is closed when all results have been processed or an error occurs.

Example:

type Product struct {
	ID    int     `json:"id"`
	Price float64 `json:"price"`
}

// Stream 1 million products incrementally
results := StreamJsonAs[Product](qb.Filter("Price gt 50"), ctx, 512)
for result := range results {
	if result.Err != nil {
		log.Println("Error:", result.Err)
		continue
	}

	product := result.Value
	fmt.Printf("Product %d (page %d): $%.2f\n", product.ID, result.Page, product.Price)
}

func StreamXmlAs added in v0.16.0

func StreamXmlAs[T any](qb *QueryBuilder, ctx context.Context, bufferSize ...int) <-chan Result[T]

StreamXmlAs is the XML-tag streaming method for type T.

StreamXmlAs returns a channel of Result items typed to T, enabling incremental processing of large result sets without materializing all data in memory. Each result can be of type T or contain an error.

The "XmlAs" suffix indicates that the target struct T has xml:"..." tags for field mapping, not that the server returns XML responses. The server response is always JSON, which is unmarshaled into structs with XML tags.

The bufferSize parameter controls the capacity of the result channel (default 256). For large record sizes or high network latency, increase this value to reduce blocking. For small records, the default is usually optimal.

Results include pagination information (Page, Index) for tracking position within large result sets.

Returns a receive-only channel that yields Result items as they become available. The channel is closed when all results have been processed or an error occurs.

Example:

type Product struct {
	ID    int     `xml:"id"`
	Price float64 `xml:"price"`
}

// Stream 1 million products
results := StreamXmlAs[Product](qb.Filter("Price gt 50"), ctx, 512)
for result := range results {
	if result.Err != nil {
		log.Println("Error:", result.Err)
		continue
	}

	product := result.Value
	fmt.Printf("Product %d (page %d): $%.2f\n", product.ID, result.Page, product.Price)
}

func UpdateAs

func UpdateAs[T any](c *Client, ctx context.Context, entitySet string, key interface{}, data interface{}) error

UpdateAs is the generic version of Client.Update.

UpdateAs updates an existing entity identified by its key. The update data is marshaled to JSON and sent as a PATCH request.

Note: OData PATCH requests typically do not return an entity body in the response, so this is primarily a type-safe wrapper around Client.Update. The generic type parameter T is included for API consistency but does not affect the response.

Returns an error if the update fails (entity not found, invalid data, etc.).

Example:

err := UpdateAs[Order](client, ctx, "Orders", 123, map[string]interface{}{"total": 150.00})

func ValidateFilter

func ValidateFilter(expr string) error

ValidateFilter performs basic OData filter syntax validation.

ValidateFilter checks the filter expression for common syntax errors such as:

  • Unbalanced parentheses
  • Unbalanced quotes
  • Invalid operator usage
  • Unknown function names

ValidateFilter is a permissive validator that only catches obvious errors, not all possible invalid syntax. An empty filter expression is considered valid.

This is typically called before executing a query to provide early error detection, avoiding unnecessary server round-trips for invalid filters.

func XmlBytesToStruct added in v0.21.3

func XmlBytesToStruct[T any](xmlData []byte) (T, error)

XmlBytesToStruct converts raw XML bytes directly to a typed value T with XML struct tags.

XmlBytesToStruct performs true XML unmarshaling using xml.Unmarshal, which respects xml:"..." struct tags. This is the correct method for handling native XML responses from OData v2 services that return Atom+XML format (e.g., SAP systems).

This method is used internally by the XML decoding path when Content-Type is XML or the response body starts with '<'.

Example:

type Order struct {
	ID    int       `xml:"id"`
	Total float64   `xml:"total"`
}

rawXML := []byte(`<Order><id>123</id><total>99.99</total></Order>`)
order, err := XmlBytesToStruct[Order](rawXML)

Types

type ActionBuilder

type ActionBuilder struct {
	// contains filtered or unexported fields
}

ActionBuilder provides a fluent API for calling OData Actions (v4).

ActionBuilder enables execution of OData Action calls, which may perform side effects (create, update, delete). Actions use HTTP POST and can include both body data and parameters.

Typical usage:

result, err := client.Action("ApproveOrder").
	WithBody(orderData).
	Param("approverID", 42).
	Execute(ctx)

func (*ActionBuilder) Execute

func (a *ActionBuilder) Execute(ctx context.Context) (map[string]interface{}, error)

Execute calls the action and returns the result as a map.

Execute sends an HTTP POST request to the action with optional body and parameter data. The request body is marshaled to JSON automatically. The response is parsed into a map.

If ActionBuilder.WithBody was called, that data is sent as the primary body. Otherwise, if parameters were added via ActionBuilder.Param, they are sent as JSON. If neither, an empty POST is sent.

Returns a map containing the action response, or an error if the call fails.

Example:

result, err := client.Action("ApproveOrder").
	WithBody(approvalData).
	Execute(ctx)

func (*ActionBuilder) Invoke added in v0.2.11

func (a *ActionBuilder) Invoke(ctx context.Context, result any) error

Invoke calls the action and unmarshals the response into result.

Invoke is the result-receiver variant of ActionBuilder.Execute. It sends an HTTP POST request to the OData action URL and unmarshals the response body into the value pointed to by result. result may be nil when no response body is expected.

If ActionBuilder.WithBody was called, that data forms the request body. Otherwise parameters added via ActionBuilder.Param are sent as JSON.

Returns an error if the HTTP call fails, the response status is not 2xx, or unmarshaling fails.

Example:

type ApprovalResult struct {
	Approved bool   `json:"approved"`
	Message  string `json:"message"`
}

var res ApprovalResult
err := client.Action("ApproveOrder").Param("orderID", 42).Invoke(ctx, &res)

func (*ActionBuilder) Param

func (a *ActionBuilder) Param(key string, value interface{}) *ActionBuilder

Param adds a parameter to the action call.

Param appends a key-value parameter for the action. Parameters can be sent in the request body as JSON.

Returns the receiver for method chaining.

func (*ActionBuilder) WithBody

func (a *ActionBuilder) WithBody(data interface{}) *ActionBuilder

WithBody sets the request body for the action.

WithBody sets the main request body data for the action. The data is automatically marshaled to JSON.

Returns the receiver for method chaining.

Example:

action.WithBody(map[string]interface{}{
	"orderID": 12345,
	"approvedBy": "Manager",
})

type ActionInfo

type ActionInfo struct {
	Name       string
	Parameters []FunctionParameter
	ReturnType string
}

ActionInfo represents an OData action (v4).

type AnalyticsVocabulary added in v0.8.0

type AnalyticsVocabulary struct {
	// AggregationMethod is the aggregation function to apply to this property.
	// Common values: "sum", "min", "max", "average", "count", "countdistinct".
	// Corresponds to Org.OData.Aggregation.V1.default.
	AggregationMethod string
	// IsDimension is true when the property is an analytical dimension.
	// Corresponds to Org.OData.Aggregation.V1.Dimensionality="Dimension".
	IsDimension bool
	// IsMeasure is true when the property is an analytical measure.
	// Corresponds to Org.OData.Aggregation.V1.Dimensionality="Measure".
	IsMeasure bool
	// RollupLevels specifies the hierarchy levels for dimension rollup.
	// Corresponds to Org.OData.Aggregation.V1.RollupLevels.
	RollupLevels int
	// ReferencedProperties lists properties that this aggregation references.
	// Corresponds to Org.OData.Aggregation.V1.ReferencedProperties.
	ReferencedProperties []string
	// GroupableProperties lists the properties that can be used in $apply groupby().
	// Corresponds to Org.OData.Aggregation.V1.GroupableProperties.
	GroupableProperties []string
	// AggregatableProperties lists properties that can be aggregated via $apply aggregate().
	// Corresponds to Org.OData.Aggregation.V1.AggregatableProperties.
	AggregatableProperties []string
}

AnalyticsVocabulary defines Org.OData.Aggregation.V1 annotation terms for analytics.

The Aggregation (Analytics) vocabulary annotates entity types and properties with analytical semantics, indicating which properties are dimensions, measures, or aggregation methods. These annotations are defined in the OASIS OData Aggregation vocabulary (namespace: Org.OData.Aggregation.V1), and also appear in the SAP Analytics vocabulary (namespace: com.sap.vocabularies.Analytics.v1).

func ParseAnalyticsVocabulary added in v0.8.0

func ParseAnalyticsVocabulary(annotations map[string]string) AnalyticsVocabulary

ParseAnalyticsVocabulary extracts Org.OData.Aggregation.V1 (and SAP Analytics v1) annotation terms from a raw annotation map.

Both the OASIS aggregation namespace and the SAP-specific analytics namespace are recognised, so annotations from either source populate the same struct.

Example annotations map:

map[string]string{
    "Org.OData.Aggregation.V1.default":               "sum",
    "Org.OData.Aggregation.V1.RollupLevels":          "3",
    "com.sap.vocabularies.Analytics.v1.Dimension":    "true",
    "com.sap.vocabularies.Analytics.v1.Measure":      "false",
}

type AnnotatedEntity added in v0.2.27

type AnnotatedEntity[T any] struct {
	// Entity holds the decoded entity value.
	Entity T
	// Annotations holds all '@'-prefixed annotation properties as raw JSON.
	Annotations map[string]json.RawMessage
}

AnnotatedEntity wraps a decoded entity and exposes its OData instance annotations.

OData responses may include annotation properties alongside entity data. Annotations are any property whose key begins with '@', such as '@odata.etag', '@odata.count', or custom vocabulary terms like '@Custom.Score'.

Example usage:

result, err := DecodeAnnotated[Product](data)
if err != nil { ... }
fmt.Println(result.Entity.Name)
var etag string
_ = result.GetAnnotation("@odata.etag", &etag)

func DecodeAnnotated added in v0.2.27

func DecodeAnnotated[T any](data []byte) (AnnotatedEntity[T], error)

DecodeAnnotated decodes an OData JSON object, separating '@'-prefixed annotation properties from the regular entity fields.

The raw JSON object is first unmarshalled into a map. Keys beginning with '@' are stored as Annotations; the remaining keys are re-encoded and decoded into T.

func (*AnnotatedEntity[T]) GetAnnotation added in v0.2.27

func (a *AnnotatedEntity[T]) GetAnnotation(name string, target any) error

GetAnnotation retrieves a typed annotation value by name and unmarshals it into target.

Returns an error if the annotation is not present or cannot be decoded into target.

Example:

var etag string
if err := result.GetAnnotation("@odata.etag", &etag); err != nil { ... }

type Association

type Association struct {
	// Name is the name of the association.
	Name string
	// From is the source end of the association.
	From AssociationEnd
	// To is the target end of the association.
	To AssociationEnd
}

Association represents an OData association between entity types.

type AssociationEnd

type AssociationEnd struct {
	// EntityType is the fully qualified entity type name at this end.
	EntityType string
	// Multiplicity describes the multiplicity (0..1, 1, *).
	Multiplicity string
}

AssociationEnd represents one end of an OData association.

type AsyncOpPoller added in v0.2.6

type AsyncOpPoller struct {
	// contains filtered or unexported fields
}

AsyncOpPoller polls an OData async operation status URL until completion.

Typical OData async flow:

  1. POST/DELETE/PATCH with "Prefer: respond-async" header
  2. Server responds 202 Accepted + Location header (status URL)
  3. Poll Location URL until 200/204 (done) or 4xx/5xx (failed)

Usage:

resp, err := client.Execute(...)  // initial request returns 202
if resp.StatusCode == 202 {
    poller := client.NewAsyncPoller(resp.Headers.Get("Location"))
    result, err := poller.Wait(ctx)
}

func (*AsyncOpPoller) Wait added in v0.2.6

func (p *AsyncOpPoller) Wait(ctx context.Context) (*AsyncResult, error)

Wait polls the status URL until the operation completes or ctx is cancelled. Returns ErrAsyncOpFailed if the server signals failure, ErrAsyncOpTimeout if maxPolls is exhausted before completion.

func (*AsyncOpPoller) WithMaxPolls added in v0.2.6

func (p *AsyncOpPoller) WithMaxPolls(n int) *AsyncOpPoller

WithMaxPolls sets the maximum number of poll attempts before giving up. Use 0 for unlimited (rely on ctx cancellation).

func (*AsyncOpPoller) WithPollInterval added in v0.2.6

func (p *AsyncOpPoller) WithPollInterval(d time.Duration) *AsyncOpPoller

WithPollInterval sets the interval between poll attempts.

type AsyncOpStatus added in v0.2.6

type AsyncOpStatus int

AsyncOpStatus represents the state of a long-running OData async operation.

const (
	// AsyncOpRunning indicates the operation is still in progress.
	AsyncOpRunning AsyncOpStatus = iota
	// AsyncOpSucceeded indicates the operation completed successfully.
	AsyncOpSucceeded
	// AsyncOpFailed indicates the operation failed.
	AsyncOpFailed
	// AsyncOpCancelled indicates the operation was cancelled by the server.
	AsyncOpCancelled
)

func (AsyncOpStatus) String added in v0.2.6

func (s AsyncOpStatus) String() string

String returns a human-readable name for the status.

type AsyncResult added in v0.2.6

type AsyncResult struct {
	// Status is the final resolved status.
	Status AsyncOpStatus
	// StatusCode is the HTTP status code of the final poll response.
	StatusCode int
	// Body is the raw response body (may be nil for 204 No Content).
	Body []byte
}

AsyncResult holds the outcome of a completed async OData operation.

type AuthorizationVocabulary added in v0.8.0

type AuthorizationVocabulary struct {
	// Authorizations is the list of named authorization schemes that apply.
	// Corresponds to Org.OData.Authorization.V1.Authorizations.
	// Each entry is the Name of a SecurityScheme defined on the service.
	Authorizations []string
	// RequiredScopes is the list of OAuth2/OpenID scopes required.
	// Corresponds to Org.OData.Authorization.V1.RequiredScopes.
	RequiredScopes []string
	// SecuritySchemeType identifies the type of scheme: "ApiKey", "Http", "OAuth2", "OpenIDConnect".
	// Set on service-level annotations that define the security scheme itself.
	SecuritySchemeType string
	// KeyName is the name of the API key parameter (for ApiKey schemes).
	// Corresponds to Org.OData.Authorization.V1.KeyName.
	KeyName string
	// KeyLocation is where the API key is sent: "header", "query", "cookie" (for ApiKey schemes).
	// Corresponds to Org.OData.Authorization.V1.Location.
	KeyLocation string
	// Scheme is the HTTP authentication scheme (for Http type), e.g. "bearer", "basic".
	// Corresponds to Org.OData.Authorization.V1.Scheme.
	Scheme string
	// BearerFormat describes the bearer token format (for Http+bearer), e.g. "JWT".
	// Corresponds to Org.OData.Authorization.V1.BearerFormat.
	BearerFormat string
	// AuthorizationURL is the OAuth2 authorization endpoint URL.
	// Corresponds to Org.OData.Authorization.V1.AuthorizationURL.
	AuthorizationURL string
	// TokenURL is the OAuth2 token endpoint URL.
	// Corresponds to Org.OData.Authorization.V1.TokenURL.
	TokenURL string
	// OpenIDConnectURL is the OpenID Connect discovery document URL.
	// Corresponds to Org.OData.Authorization.V1.OpenIDConnectUrl.
	OpenIDConnectURL string
}

AuthorizationVocabulary defines Org.OData.Authorization.V1 annotation terms.

The Authorization vocabulary annotates services and operations with the security schemes required to access them. These annotations are defined in the OASIS OData Authorization vocabulary (namespace: Org.OData.Authorization.V1).

func ParseAuthorizationVocabulary added in v0.8.0

func ParseAuthorizationVocabulary(annotations map[string]string) AuthorizationVocabulary

ParseAuthorizationVocabulary extracts Org.OData.Authorization.V1 annotation terms from a raw annotation map. The map keys are fully-qualified term names (e.g. "Org.OData.Authorization.V1.Authorizations").

Example annotations map:

map[string]string{
    "Org.OData.Authorization.V1.Authorizations":  "OAuth2Implicit",
    "Org.OData.Authorization.V1.RequiredScopes":  "read:data,write:data",
    "Org.OData.Authorization.V1.Scheme":          "bearer",
    "Org.OData.Authorization.V1.BearerFormat":    "JWT",
}

type BatchOperation

type BatchOperation struct {
	// Method is the HTTP method (GET, POST, PATCH, DELETE)
	Method string
	// URL is the entity set or entity reference
	URL string
	// Headers are operation-specific HTTP headers
	Headers map[string]string
	// Body is the request body as raw JSON (for POST/PATCH)
	Body json.RawMessage
	// ChangesetID identifies the changeset this operation belongs to
	ChangesetID string
}

BatchOperation represents a single operation within a batch request.

BatchOperation encapsulates a single OData operation (GET, POST, PATCH, DELETE) with its method, URL, headers, and optional body data.

func (*BatchOperation) SetBody

func (op *BatchOperation) SetBody(data interface{}) error

SetBody sets the Body field, marshaling data if needed.

SetBody converts the provided data to JSON and stores it as a json.RawMessage in the Body field. Returns an error if JSON marshaling fails. Nil data results in a nil Body.

This helper maintains API compatibility while optimizing allocations by using raw JSON instead of unmarshaling on the server side.

type BatchRequest

type BatchRequest struct {
	// contains filtered or unexported fields
}

BatchRequest represents a collection of OData operations to execute as a batch.

BatchRequest builds a $batch request combining multiple operations (GET, POST, PATCH, DELETE) into a single HTTP request. Operations can be grouped into changesets for atomic transactions.

Typical usage:

resp, err := client.Batch().
	Get("Customers", 1).
	Create("Orders", orderData).
	Execute(ctx)

For transactional operations, use BeginChangeset/EndChangeset:

resp, err := client.Batch().
	BeginChangeset("tx1").
	Create("Orders", order).
	Update("Customers", 1, updateData).
	EndChangeset().
	Execute(ctx)

func (*BatchRequest) BeginChangeset

func (b *BatchRequest) BeginChangeset(id string) *BatchRequest

BeginChangeset starts a changeset (atomic transaction group).

BeginChangeset marks the beginning of a set of operations that should be executed atomically. All operations added after this call (until EndChangeset) belong to this changeset. Multiple changesets can exist in a single batch.

The id parameter is used to group related operations. If a changeset was already open, it is closed first.

Returns the receiver for method chaining.

Example:

batch.BeginChangeset("group-1").
	Create("Orders", order).
	Create("OrderItems", item1).
	EndChangeset()

func (*BatchRequest) Create

func (b *BatchRequest) Create(entitySet string, data interface{}) *BatchRequest

Create adds a POST (create) operation to the batch.

Create inserts a new entity into the specified entity set. The data is marshaled to JSON automatically. If Create is called within a changeset (between BeginChangeset and EndChangeset), the operation becomes part of that transaction.

Returns the receiver for method chaining.

Example:

batch.Create("Orders", map[string]interface{}{"OrderID": 123, "Total": 99.99})

func (*BatchRequest) Delete

func (b *BatchRequest) Delete(entitySet string, key interface{}) *BatchRequest

Delete adds a DELETE operation to the batch.

Delete removes an entity identified by its key. If called within a changeset, the deletion becomes part of that atomic transaction.

Returns the receiver for method chaining.

Example:

batch.Delete("Orders", 123)

func (*BatchRequest) EndChangeset

func (b *BatchRequest) EndChangeset() *BatchRequest

EndChangeset ends the current changeset.

EndChangeset closes the active changeset, storing it for later execution. All subsequent operations will be standalone batch operations (not in a changeset) until another BeginChangeset is called.

Returns the receiver for method chaining.

Example:

batch.BeginChangeset("tx1").
	Create("Orders", orderData).
	EndChangeset().
	Get("Customers", 1)

func (*BatchRequest) Execute

func (b *BatchRequest) Execute(ctx context.Context) (*BatchResponse, error)

Execute sends the batch request and returns all results at once.

Execute constructs a multipart/mixed HTTP request containing all batch operations and changesets, sends it to the OData service, and parses the response. The operation automatically closes any open changeset before execution.

All results are materialized into memory before returning. For large batches, consider using BatchRequest.ExecuteStream for memory-efficient incremental processing.

On error, returns a non-nil error. The returned BatchResponse contains individual results for each operation, even if some operations failed.

Example:

resp, err := batch.Execute(ctx)
if err != nil {
	log.Fatal(err)
}
for _, result := range resp.Results {
	fmt.Println(result.Status, result.Data)
}

func (*BatchRequest) ExecuteJSON added in v0.13.0

func (b *BatchRequest) ExecuteJSON(ctx context.Context) (*BatchResponse, error)

ExecuteJSON sends the batch request using the OData 4.01 JSON batch format.

ExecuteJSON is an alternative to BatchRequest.Execute that uses JSON encoding instead of multipart/mixed. The OData 4.01 JSON batch format is more compact, easier to debug, and supported by modern OData services.

OData 4.01 spec reference: section 18 (Batch Requests and Responses).

Operations within changesets are grouped using the "atomicityGroup" property. All operations in a changeset must succeed or all will be rolled back.

Example:

resp, err := client.Batch().
    Get("Products", 1).
    Get("Products", 2).
    ExecuteJSON(ctx)
if err != nil {
    log.Fatal(err)
}
for _, result := range resp.Results {
    fmt.Println(result.StatusCode, string(result.Body))
}

func (*BatchRequest) ExecuteJSONStream added in v0.13.0

func (b *BatchRequest) ExecuteJSONStream(ctx context.Context) <-chan BatchResult

ExecuteJSONStream sends the batch using JSON format and streams results via a channel.

ExecuteJSONStream is the streaming variant of BatchRequest.ExecuteJSON. It reads the entire JSON response but emits results to the channel one by one to allow concurrent processing.

Example:

for result := range batch.ExecuteJSONStream(ctx) {
    if result.Err != nil {
        log.Println("Operation failed:", result.Err)
        continue
    }
    fmt.Println(result.StatusCode, string(result.Body))
}

func (*BatchRequest) ExecuteStream

func (b *BatchRequest) ExecuteStream(ctx context.Context) <-chan BatchResult

ExecuteStream sends the batch request and streams results incrementally via a channel.

ExecuteStream is more memory-efficient than BatchRequest.Execute for large batches. Results are parsed and sent to the returned channel as they arrive, allowing processing to begin before the entire response is received. The operation automatically closes any open changeset before execution.

The returned channel is buffered with capacity 8 and will be closed when all results have been sent or an error occurs. Errors are sent as BatchResult items with non-nil Err field.

Example:

results := batch.ExecuteStream(ctx)
for result := range results {
	if result.Err != nil {
		log.Println("Operation failed:", result.Err)
		continue
	}
	fmt.Println(result.Status, string(result.Body))
}

func (*BatchRequest) Get

func (b *BatchRequest) Get(entitySet string, key interface{}) *BatchRequest

Get adds a GET operation to the batch.

Get retrieves a single entity by its key and adds the operation to the batch. The operation is read-only and cannot be part of a changeset.

Returns the receiver for method chaining.

Example:

batch.Get("Customers", 1)

func (*BatchRequest) Update

func (b *BatchRequest) Update(entitySet string, key interface{}, data interface{}) *BatchRequest

Update adds a PATCH (update) operation to the batch.

Update modifies an existing entity identified by its key. The data is marshaled to JSON and sent as PATCH. If called within a changeset, the operation becomes part of that atomic transaction.

Returns the receiver for method chaining.

Example:

batch.Update("Orders", 123, map[string]interface{}{"Total": 150.00})

type BatchResponse

type BatchResponse struct {
	// Results is the slice of results, one per operation
	Results []BatchResult
}

BatchResponse contains the results of all operations in a batch request, with one BatchResult entry per operation in the same order as submitted.

type BatchResult

type BatchResult struct {
	// StatusCode is the HTTP status code of the operation
	StatusCode int
	// Headers are the response headers
	Headers map[string]string
	// Body is the response body
	Body []byte
	// Err is an error if one occurred
	Err error
}

BatchResult represents the result of a single operation in the batch.

BatchResult contains the HTTP response status, headers, and body from one operation within a batch request.

type Binary

type Binary []byte

Binary represents an OData Edm.Binary value (base64 encoded binary data).

Binary is a byte slice with custom JSON marshaling/unmarshaling for base64 encoded binary data. This is used for BLOB/binary fields in OData services.

Example formats in JSON responses:

"aGVsbG8gd29ybGQ="  // base64 encoded: "hello world"

Example:

type Document struct {
	Content traverse.Binary `json:"content"`
}

json.Unmarshal([]byte(`{"content":"aGVsbG8="}`), &doc)
fmt.Println(string(doc.Content)) // "hello"

func (Binary) MarshalJSON

func (b Binary) MarshalJSON() ([]byte, error)

MarshalJSON encodes binary data as a base64 JSON string.

MarshalJSON converts the raw binary bytes to a standard base64-encoded string (RFC 4648 §4) and wraps it in JSON quotes, matching the OData v4 Edm.Binary wire format.

func (*Binary) UnmarshalJSON

func (b *Binary) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes base64 encoded binary data.

UnmarshalJSON accepts a JSON string containing standard or URL-safe base64 encoded binary data and decodes it to the internal byte slice. OData v4 uses standard base64 (RFC 4648 §4); some SAP services use URL-safe base64 (RFC 4648 §5). Both are tried in order.

Returns an error if the input is not a valid JSON string or cannot be decoded as base64.

type CacheStore

type CacheStore interface {
	// Get retrieves cached metadata by key.
	// Returns nil and false if key is not found.
	Get(key string) (*Metadata, bool)

	// Set stores metadata in the cache with the given key.
	Set(key string, metadata *Metadata)

	// Clear removes all entries from the cache.
	Clear()
}

CacheStore is an interface for caching OData metadata.

CacheStore provides a contract for caching Metadata retrieved from OData services. Different implementations can use in-memory storage, Redis, file-based caching, or other strategies.

This interface allows the client to optimize metadata fetches by caching the service schema across requests, avoiding repeated $metadata calls for the same service.

Example custom implementation:

type RedisCache struct {
	client *redis.Client
}
func (c *RedisCache) Get(key string) (*Metadata, bool) { ... }
func (c *RedisCache) Set(key string, metadata *Metadata) { ... }
func (c *RedisCache) Clear() { ... }

type CapabilitiesRegistry added in v0.2.21

type CapabilitiesRegistry struct {
	// contains filtered or unexported fields
}

CapabilitiesRegistry holds parsed capabilities for all entity sets.

func NewCapabilitiesRegistry added in v0.2.21

func NewCapabilitiesRegistry() *CapabilitiesRegistry

NewCapabilitiesRegistry creates a new empty registry.

func ParseCapabilities added in v0.2.21

func ParseCapabilities(edmxXML []byte) (*CapabilitiesRegistry, error)

ParseCapabilities parses an OData v4 EDMX metadata document.

func (*CapabilitiesRegistry) Get added in v0.2.21

Get returns the capabilities for the named entity set.

type CapabilityError added in v0.2.21

type CapabilityError struct {
	EntitySet string
	Operation string
	Property  string
	Message   string
}

CapabilityError is returned when a requested operation is not supported.

func (*CapabilityError) Error added in v0.2.21

func (e *CapabilityError) Error() string

Error implements the error interface.

type Client

type Client struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...Option) (*Client, error)

New creates a new Client with the provided options. At minimum, WithBaseURL must be provided unless a pre-configured relay client is passed via WithRelayClient, in which case the base URL is inherited from the relay client automatically. Returns an error if required options are missing or invalid.

Example:

client, err := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v2"),
    traverse.WithBasicAuth("user", "pass"),
    traverse.WithPageSize(5000),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

func NewGraphClient added in v0.2.15

func NewGraphClient(relayClient *relay.Client, cfg GraphConfig) *Client

func (*Client) Action

func (c *Client) Action(name string) *ActionBuilder

Action starts building a call to an OData Action (v4).

Action creates a new ActionBuilder for calling OData Actions. Actions may perform side effects and can include both request bodies and parameters.

Returns an ActionBuilder ready for body/parameter configuration and execution.

Example:

result, err := client.Action("ApproveOrder").
	WithBody(approvalData).
	Param("approverID", emp123).
	Execute(ctx)

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the base URL of the OData service that this Client is connected to.

BaseURL returns the configured service root URL, which serves as the base for all OData entity set operations. This is the URL passed to New() via WithBaseURL().

The returned URL typically has the form: https://odata.example.com/v4 or for SAP systems: https://sap.example.com/sap/opu/odata/sap/ZESX_PRODUCT/

Returns:

  • The base URL string (always non-empty for a valid client)

Example:

url := client.BaseURL()
// Returns: "https://odata.example.com/v4"

func (*Client) Batch

func (c *Client) Batch() *BatchRequest

Batch starts building a batch request.

Batch returns a new BatchRequest associated with the client, ready to add operations (Get, Create, Update, Delete) or changesets.

Example:

batch := client.Batch().
	Get("Customers", 1).
	Create("Orders", orderData)
resp, err := batch.Execute(ctx)

func (*Client) CircuitBreakerState added in v0.2.0

func (c *Client) CircuitBreakerState() relay.CircuitBreakerState

breaker: Closed (healthy), Open (failing, requests rejected), or Half-Open (probing for recovery).

Returns relay.StateClosed if the circuit breaker is not configured.

Example:

if client.CircuitBreakerState() == relay.StateOpen {
    log.Println("OData service unavailable - circuit is open")
}

func (*Client) Close

func (c *Client) Close() error

Close closes the client and releases all associated resources.

Close gracefully shuts down the underlying HTTP client (relay.Client), terminating any active connections and releasing network resources. This should always be called when the client is no longer needed, preferably via defer for cleanup guarantee.

After Close is called, the client must not be reused. Attempting to make requests after Close will result in errors.

Returns an error if the shutdown process fails, though in most cases this is safe to ignore. However, it's still good practice to check and log errors:

if err := client.Close(); err != nil {
	log.Printf("error closing client: %v", err)
}

Typical usage with defer:

client, _ := traverse.New(traverse.WithBaseURL("..."))
defer client.Close()  // Ensures cleanup even if panic occurs
// Use client...

func (*Client) Create

func (c *Client) Create(ctx context.Context, entitySet string, data interface{}) (map[string]interface{}, error)

Create creates a new entity in the specified entity set.

Create sends an HTTP POST request with the provided entity data to the service, creating a new record and returning the complete entity as created by the server. This includes any server-generated fields such as identity columns, default values, and computed fields.

Parameters:

  • ctx: Context for request cancellation and timeout
  • entitySet: Name of the entity set (e.g., "Products", "Orders")
  • data: Entity data as map[string]interface{} or struct

Response: The method returns the complete created entity including server-generated fields. The response format differs by OData version:

  • OData v2: Entity wrapped in {"d": {...}} response
  • OData v4: Entity returned directly in response body

Error Cases:

  • HTTP 400: Invalid data or constraint violations
  • HTTP 409: Duplicate key or concurrency conflict
  • Network errors: Connection failures

HTTP Status: 201 Created (success)

Example with Map:

newProduct := map[string]interface{}{
	"Name": "Premium Widget",
	"Price": 29.99,
	"Category": "Electronics",
}
created, err := client.Create(ctx, "Products", newProduct)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("Created product with ID: %v\n", created["ID"])
fmt.Printf("Server name: %v\n", created["Name"])  // May differ if server applies defaults

Example with Struct:

type Product struct {
	Name  string  `json:"Name"`
	Price float64 `json:"Price"`
}
product := Product{Name: "Widget", Price: 9.99}
created, err := client.Create(ctx, "Products", product)

func (*Client) CrossJoin added in v0.2.23

func (c *Client) CrossJoin(entitySets ...string) *CrossJoinBuilder

CrossJoin creates a CrossJoinBuilder for the given entity sets.

At least two entity set names are required; the method panics if fewer than two are provided to surface the configuration error at init time rather than silently producing an invalid URL.

Example:

client.CrossJoin("Products", "Categories").
    Filter("Products/CategoryID eq Categories/ID")

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, entitySet string, key interface{}) error

Delete deletes an entity from the specified entity set.

Delete sends an HTTP DELETE request to remove the entity identified by the provided key. Once deleted, the entity can no longer be accessed through normal queries; it is permanently removed from the data store (subject to any database retention policies).

Key Encoding:

  • String keys: Automatically quoted and escaped for OData URL encoding ('MAT001')
  • Numeric keys: Converted to string representation (123)
  • Invalid keys: Returns error during encoding

Concurrency Control:

  • Returns ErrConcurrencyConflict if entity was modified elsewhere (ETag mismatch)
  • The service may return HTTP 409 if concurrent modification is detected

Parameters:

  • ctx: Context for request cancellation and timeout
  • entitySet: Name of the entity set (e.g., "Products", "Orders")
  • key: Entity key for identification (int or string)

HTTP Method: DELETE Response: HTTP 204 No Content on success (no body returned)

Error Cases:

  • HTTP 404: Entity not found
  • HTTP 409: Concurrency conflict (entity modified elsewhere)
  • Network errors: Connection failures

Important Notes:

  • Deletion is permanent; deleted data cannot be recovered
  • Some services may use soft deletes (marking as deleted) instead of hard deletion
  • Deleting a parent may cascade to child entities (depending on foreign key constraints)

Example - Delete by Numeric ID:

err := client.Delete(ctx, "Products", 123)
if err != nil {
	if errors.Is(err, traverse.ErrConcurrencyConflict) {
		log.Println("Product was modified by another user")
	} else {
		log.Fatal(err)
	}
}
log.Println("Product deleted successfully")

Example - Delete by String ID:

err := client.Delete(ctx, "Materials", "MAT001")
if err != nil {
	log.Fatal(err)
}

func (*Client) DeleteWithETag added in v0.2.5

func (c *Client) DeleteWithETag(ctx context.Context, entitySet string, key interface{}, etag ETag) error

DeleteWithETag deletes an entity guarded by an ETag. If etag is non-empty, an If-Match header is sent. HTTP 412 is translated to ErrConcurrencyConflict.

func (*Client) DeleteWithOptions added in v0.5.0

func (c *Client) DeleteWithOptions(ctx context.Context, entitySet string, key interface{}, opts ...DeleteOption) error

DeleteWithOptions deletes an entity, applying the provided DeleteOption values.

DeleteWithOptions extends the base Delete method with support for cascade delete, ETag conditions, and return-representation semantics.

Parameters:

  • ctx: Request context for cancellation and timeout
  • entitySet: Name of the entity set (e.g. "Orders")
  • key: Entity key (int or string)
  • opts: Zero or more DeleteOption values

Example:

err := client.DeleteWithOptions(ctx, "Orders", 1,
    traverse.WithDeleteCascade(),
    traverse.WithDeleteIfMatch(`W/"abc"`),
)

func (*Client) FetchPageAt added in v0.2.5

func (c *Client) FetchPageAt(ctx context.Context, rawURL string) (*Page, error)

FetchPageAt fetches and parses an OData page from an arbitrary URL. It is primarily used by Paginator to follow @odata.nextLink (or __next) URLs returned by the server between pages.

The rawURL must be a fully-qualified URL (absolute) or a path relative to the client base URL that the server returned.

func (*Client) From

func (c *Client) From(entitySet string) *QueryBuilder

From creates a new QueryBuilder for querying the specified entity set.

From starts the construction of an OData query for the given entity set name. The returned QueryBuilder supports a fluent API for adding query parameters (filtering, selection, ordering, expansion, pagination) before execution.

The QueryBuilder provides:

  • Filter(): Add OData $filter expressions
  • Select(): Choose specific properties ($select)
  • OrderBy(): Sort results ($orderby)
  • Expand(): Include related entities ($expand)
  • Top(): Limit result count ($top)
  • Skip(): Skip records for pagination ($skip)
  • Collect(): Execute and collect all results
  • Stream(): Execute and stream results without buffering

The QueryBuilder uses method chaining for ergonomic query construction. All methods are optional-you can execute with just From() for a basic query.

Query Execution:

  • Call Collect() to fetch all results at once
  • Call Stream() for memory-efficient streaming of large datasets
  • Call Count() to get the record count with $count
  • Call Single() for a single-record query with error if 0 or >1 result

Thread Safety: Each QueryBuilder is independent and not thread-safe for concurrent modifications. Create separate QueryBuilders for concurrent queries.

Example:

qb := client.From("Products").
	Filter("Price gt 100").
	Select("ProductID", "Name", "Price").
	OrderBy("Name asc").
	Top(50)
results, _ := qb.Collect(context.Background())

Or for streaming:

stream := client.From("LargeDataSet").Stream(context.Background())
for result := range stream {
	processRecord(result.Value)
}

func (*Client) Function

func (c *Client) Function(name string) *FunctionBuilder

Function starts building a call to an OData Function (v4).

Function creates a new FunctionBuilder for calling OData Functions. Functions are read-only operations that may take parameters and return data.

Returns a FunctionBuilder ready for parameter addition and execution.

Example:

result, err := client.Function("GetProductsByCategory").
	Param("category", "Electronics").
	Execute(ctx)

func (*Client) FunctionImport

func (c *Client) FunctionImport(name string) *FunctionImportBuilder

FunctionImport starts building a call to an OData Function Import (v2).

FunctionImport creates a new FunctionImportBuilder for calling OData v2 Function Imports. Function Imports are similar to v4 Functions and use HTTP GET with URL-encoded parameters.

Returns a FunctionImportBuilder ready for parameter addition and execution.

Example:

result, err := client.FunctionImport("GetTop10Orders").Execute(ctx)

func (*Client) Metadata

func (c *Client) Metadata(ctx context.Context) (*Metadata, error)

Metadata returns the OData $metadata service document, which contains the complete Entity Data Model (EDM) describing all entity types, properties, relationships, and operations.

Metadata fetches and caches the $metadata document, which defines:

  • All entity types and their properties
  • Navigation properties (relationships between entities)
  • Associations and multiplicities
  • Complex types and enumerations
  • Function imports and action definitions
  • Sap annotations and extensions for SAP systems

Caching Behavior:

The result is cached using sync.Once to ensure metadata is fetched only once per client lifetime, even with concurrent calls. Subsequent calls return the cached metadata without network requests, providing significant performance benefits for services with large metadata documents.

The cache key includes the base URL to support scenarios with multiple clients to different services. Custom cache implementations can be provided via WithMetadataCache().

Use Cases:

  • Query validation: Check entity types before constructing queries
  • Dynamic query building: Inspect available properties and relationships
  • Type information: Determine property types for serialization/deserialization
  • Navigation: Discover related entities through association mappings

Returns:

  • *Metadata: Parsed EDM model describing the service schema
  • error: Returns ErrMetadataInvalid if metadata cannot be parsed
  • error: Returns network/HTTP errors if fetch fails

Example:

meta, err := client.Metadata(ctx)
if err != nil {
	log.Fatal(err)
}

// Find entity type definition
productType := meta.FindEntityType("Product")
for _, prop := range productType.Properties {
	fmt.Printf("Property: %s (Type: %s)\n", prop.Name, prop.Type)
}

// Use discovered metadata for dynamic query building
for _, navProp := range productType.NavigationProperties {
	fmt.Printf("Related: %s\n", navProp.Name)
}

Thread Safety: Metadata is thread-safe. Multiple goroutines can safely call Metadata() concurrently; only the first call fetches the metadata, while others wait for the result.

func (*Client) NewAsyncPoller added in v0.2.6

func (c *Client) NewAsyncPoller(statusURL string) *AsyncOpPoller

NewAsyncPoller creates an AsyncOpPoller for the given status URL. Default: 5s poll interval, 60 max polls (5 minutes total).

func (*Client) NewDeltaSync

func (c *Client) NewDeltaSync(entitySet string) *DeltaSync

NewDeltaSync creates a new delta sync handler for an entity set.

NewDeltaSync initializes a DeltaSync for incremental synchronization of the specified entity set. The delta token is initially empty; call DeltaSync.Full first to obtain an initial token.

Returns a DeltaSync instance ready for use.

Example:

ds := client.NewDeltaSync("Customers")

func (*Client) PageSize

func (c *Client) PageSize() int

PageSize returns the configured page size for pagination.

PageSize returns the number of records fetched per HTTP request when using $top-based pagination. This is the default page size for all queries unless overridden with QueryBuilder.Top().

Pagination Details:

  • Controls the default $top parameter for OData queries
  • Reduces per-request latency by controlling chunk size
  • Larger values: Fewer requests but higher memory per request
  • Smaller values: More requests but lower memory per request
  • Typical value: 1000-5000 records (default: 1000)

This is configured via WithPageSize() when creating the client.

Returns:

  • The page size in records (default: 1000)

Example:

pageSize := client.PageSize()
// Default returns 1000
// Can be overridden per query with QueryBuilder.Top()

func (*Client) ReadWithETag added in v0.2.5

func (c *Client) ReadWithETag(ctx context.Context, entitySet string, key interface{}) (*EntityWithETag, error)

ReadWithETag fetches a single entity by key and returns it together with the server ETag. The ETag can be passed to Client.UpdateWithETag, Client.ReplaceWithETag, or Client.DeleteWithETag to guard against concurrent modifications.

The ETag is extracted from the response ETag header. If the server does not return an ETag header, EntityWithETag.ETag.IsEmpty() returns true - the entity is still returned normally.

entity, err := client.ReadWithETag(ctx, "Products", 42)
if err != nil { ... }
// modify entity.Entity ...
err = client.UpdateWithETag(ctx, "Products", 42, changes, entity.ETag)

func (*Client) RelayClient added in v0.2.20

func (c *Client) RelayClient() *relay.Client

RelayClient returns the underlying relay.Client used for HTTP transport.

This provides access to relay's full request API for advanced use cases such as extension modules that need to issue raw HTTP calls (e.g. OData webhook subscription management) while reusing the same configured transport, auth, retry and circuit-breaker settings as the parent traverse.Client.

The returned client must not be closed independently - use Client.Close to shut down both the traverse and relay clients together.

func (*Client) Replace

func (c *Client) Replace(ctx context.Context, entitySet string, key interface{}, data interface{}) error

Replace replaces an entire entity with new data using HTTP PUT.

Replace sends an HTTP PUT request with the complete new entity state to replace the existing entity. Unlike Update() which performs a partial merge (PATCH), Replace completely replaces all properties of the entity. Properties not explicitly provided in the data parameter are typically set to their default values or NULL, which can result in data loss if not all properties are included.

Use Cases:

  • Complete entity replacement: When you have the full new state
  • Wholesale updates: Replacing entire records from batch operations
  • Consistency: Ensuring a known complete state after update

Compare with Update():

  • Update (PATCH): Partial update, only changed fields
  • Replace (PUT): Complete replacement, unspecified fields set to defaults

Parameters:

  • ctx: Context for request cancellation and timeout
  • entitySet: Name of the entity set (e.g., "Products")
  • key: Entity key for identification (ID or primary key)
  • data: Complete new entity state (all properties that should exist)

HTTP Method: PUT (HTTP 201-204 on success)

Response: Returns nil on success with HTTP 204 No Content. Returns ErrConcurrencyConflict if an ETag mismatch occurs (HTTP 409).

Warning: Replace can result in data loss if the data parameter doesn't include all required fields. Prefer Update() for partial updates unless you specifically need complete replacement.

Example - Complete Replacement:

newProduct := map[string]interface{}{
	"ID": 123,  // Must include key
	"Name": "Completely New Name",
	"Price": 49.99,
	"Stock": 50,
	"Active": true,
}
err := client.Replace(ctx, "Products", 123, newProduct)
if err != nil {
	log.Fatal(err)
}
log.Println("Product completely replaced")

func (*Client) ReplaceWithETag added in v0.2.5

func (c *Client) ReplaceWithETag(ctx context.Context, entitySet string, key interface{}, data interface{}, etag ETag) error

ReplaceWithETag performs a full replacement (PUT) guarded by an ETag. If etag is non-empty, an If-Match header is sent to guard against concurrent modifications (HTTP 412 -> ErrConcurrencyConflict).

func (*Client) ResetCircuitBreaker added in v0.2.0

func (c *Client) ResetCircuitBreaker()

ResetCircuitBreaker resets the circuit breaker to the Closed state, allowing requests to flow again immediately. This is useful after a manual intervention or during testing.

No-op if the circuit breaker is not configured.

func (*Client) Service

func (c *Client) Service(ctx context.Context) (*ServiceDocument, error)

Service fetches the OData service document, which lists all available entity sets and operations.

Service makes an HTTP GET request to the service root URL (typically /odata/v4 or /sap/opu/odata/...) and returns a ServiceDocument containing all entity sets exposed by the service.

The Service Document:

  • Lists all available entity sets that can be queried
  • Includes singleton entities and function imports
  • Is version-aware: Parsed differently for OData v2 vs v4
  • Is useful for dynamic service discovery without prior knowledge of entity set names

Response Format:

  • OData v2: {"d": {"EntitySets": [{...}, ...]}} with "d" wrapper for CSRF protection
  • OData v4: {"value": [{...}, ...]} at the root

Use Case:

This is typically called once during service initialization to discover available entity sets, or when the service schema is not known in advance. For known schemas, use From() directly.

Returns:

  • *ServiceDocument: Contains all entity sets available in the service
  • error: Network error, HTTP error, or parsing error

Example:

doc, err := client.Service(ctx)
if err != nil {
	log.Fatal(err)
}
for _, es := range doc.EntitySets {
	fmt.Printf("Entity Set: %s (URL: %s)\n", es.Name, es.URL)
}

// Now you can query known entity sets
results, _ := client.From(doc.EntitySets[0].Name).Collect(ctx)

func (*Client) Singleton added in v0.6.0

func (c *Client) Singleton(name string) *QueryBuilder

Singleton returns a QueryBuilder targeting a named singleton resource at the service root.

A singleton is a single-entity resource addressable directly by name, without a key predicate. It is defined in OData v4.0 spec section 11.2.4.

Microsoft Graph uses singletons extensively (e.g., "/me", "/organization"). SAP uses them for session-level resources.

The returned QueryBuilder supports the full query API including $select, $expand, $filter, and navigation to related collections via [From].

Example:

// Fetch the "me" singleton
me, err := client.Singleton("me").Page(ctx)

// Expand a navigation property from a singleton
result, err := client.Singleton("me").Expand("manager").Page(ctx)

// Navigate to a related collection on a singleton
for r := range client.Singleton("me").From("messages").Stream(ctx) {
    // process r.Value
}

func (*Client) Update

func (c *Client) Update(ctx context.Context, entitySet string, key interface{}, data interface{}) error

Update updates an existing entity using a partial update (PATCH/MERGE operation).

Update sends an HTTP PATCH request (or HTTP MERGE for OData v2 compatibility) with the entity data. This performs a partial update where only provided properties are modified; omitted properties remain unchanged.

This is the standard way to update entities-use Replace() for complete entity replacement instead.

Parameters:

  • ctx: Context for request cancellation and timeout
  • entitySet: Name of the entity set (e.g., "Products", "Orders")
  • key: Entity key for identification (typically an ID or primary key)
  • data: Properties to update as map[string]interface{} or struct (only these fields change)

Key Formats:

  • Single key: numeric ID (123) or string ID ('MAT001')
  • Composite key: Not directly supported; use URL encoding if needed

HTTP Method:

  • PATCH (standard) for OData v4
  • MERGE (legacy) for OData v2

Response: Returns nil on success with HTTP 200 OK (with updated entity) or HTTP 204 No Content (standard). The method doesn't parse the response body for 204 responses.

Concurrency Control: Returns ErrConcurrencyConflict if:

  • ETag mismatch (entity was modified elsewhere)
  • HTTP 409 Conflict response

Example - Partial Update:

err := client.Update(ctx, "Products", 123, map[string]interface{}{
	"Price": 19.99,
	"LastModified": time.Now(),
})
if err != nil {
	if err == traverse.ErrConcurrencyConflict {
		log.Println("Entity was modified by another user")
	} else {
		log.Fatal(err)
	}
}
log.Println("Product updated successfully")

Example - Update Multiple Fields:

updates := map[string]interface{}{
	"Status": "Active",
	"Priority": 1,
	"Description": "Updated description",
}
err := client.Update(ctx, "Tasks", "TASK-001", updates)

func (*Client) UpdateWithETag added in v0.2.5

func (c *Client) UpdateWithETag(ctx context.Context, entitySet string, key interface{}, data interface{}, etag ETag) error

UpdateWithETag performs a partial update (PATCH/MERGE) guarded by an ETag. If etag is non-empty, an If-Match header is sent. The server will reject the update with HTTP 412 Precondition Failed if the entity was modified since the ETag was fetched; this is translated to ErrConcurrencyConflict.

Pass a zero ETag (or ETag{}) to skip the concurrency check.

entity, err := client.ReadWithETag(ctx, "Products", 42)
// ...
err = client.UpdateWithETag(ctx, "Products", 42, map[string]any{"Price": 9.99}, entity.ETag)

func (*Client) Upsert added in v0.2.5

func (c *Client) Upsert(ctx context.Context, entitySet string, key interface{}, data interface{}) error

Upsert creates a new entity or replaces an existing one using OData upsert semantics (PUT with If-None-Match: * header for create-or-replace).

Upsert behaviour depends on whether the entity already exists:

  • If the entity does NOT exist: the server creates it (HTTP 201 Created).
  • If the entity already exists: the server replaces it (HTTP 200/204).

This uses the OData upsert pattern (If-None-Match: *) as described in OData 4.01 section 11.4.4. Not all servers support this pattern; SAP OData services typically do via the PUT endpoint.

err := client.Upsert(ctx, "Products", 42, map[string]any{
    "ID":    42,
    "Name":  "Widget",
    "Price": 9.99,
})

func (*Client) Version

func (c *Client) Version() ODataVersion

Version returns the OData protocol version (v2 or v4) configured for this Client.

Version returns the OData protocol version that the client is using for requests and response parsing. This is configured via WithODataVersion() when creating the client.

OData Version Differences:

  • ODataV2: Legacy SAP standard, uses {"d": {"results": [...]}} response structure
  • ODataV4: Current OASIS standard, uses {"value": [...]} response structure

The version affects:

  • JSON response parsing (d.results vs value array)
  • DateTime format handling
  • Query operator availability
  • Metadata structure interpretation

Returns:

  • ODataV2 (value 2) or ODataV4 (value 4)

Example:

version := client.Version()
if version == traverse.ODataV4 {
	// Use OData v4-specific features
}

type ComplexType added in v0.2.28

type ComplexType struct {
	// Name is the name of the complex type.
	Name string
	// Properties is the list of all properties defined for this complex type.
	Properties []Property
}

ComplexType represents an OData complex type definition. Complex types are structured types without a key (not independently addressable).

type CoreVocabulary added in v0.2.29

type CoreVocabulary struct {
	// Description corresponds to Org.OData.Core.V1.Description.
	Description string
	// LongDescription corresponds to Org.OData.Core.V1.LongDescription.
	LongDescription string
	// IsLanguageDependent corresponds to Org.OData.Core.V1.IsLanguageDependent.
	IsLanguageDependent bool
	// Immutable corresponds to Org.OData.Core.V1.Immutable.
	Immutable bool
	// Computed corresponds to Org.OData.Core.V1.Computed.
	Computed bool
	// Permissions corresponds to Org.OData.Core.V1.Permissions (e.g., Read, Write).
	Permissions []string
	// Example corresponds to Org.OData.Core.V1.Example (for documentation purposes).
	Example string
}

CoreVocabulary defines commonly used Org.OData.Core.V1 annotation terms. These match the official Org.OData.Core.V1 namespace.

func ParseCoreVocabulary added in v0.2.29

func ParseCoreVocabulary(annotations map[string]string) CoreVocabulary

ParseCoreVocabulary extracts Org.OData.Core.V1 annotation terms from a raw annotation map. The map keys are fully-qualified term names (e.g. "Org.OData.Core.V1.Description").

type CrossJoinBuilder added in v0.2.23

type CrossJoinBuilder struct {
	// contains filtered or unexported fields
}

CrossJoinBuilder constructs an OData $crossjoin query against two or more entity sets, returning tuples of related entities.

$crossjoin is an OData v4 feature (section 11.2.10) that produces the Cartesian product of the specified entity sets, optionally narrowed by a $filter expression. The response is an array of objects where each key is the entity set name and the value is a single entity instance.

Build a cross-join with Client.CrossJoin, then call CrossJoinBuilder.Filter, CrossJoinBuilder.Select, and CrossJoinBuilder.Expand to refine the query. Execute with CrossJoinBuilder.Collect or [CrossJoinBuilder.Stream].

Example:

pairs, err := client.CrossJoin("Customers", "Orders").
    Filter("Customers/ID eq Orders/CustomerID").
    Collect(ctx)

func (*CrossJoinBuilder) Collect added in v0.2.23

func (b *CrossJoinBuilder) Collect(ctx context.Context) ([]CrossJoinResult, error)

Collect executes the $crossjoin query and returns all result rows.

Collect follows @odata.nextLink pagination automatically, accumulating all pages before returning. For very large result sets, prefer iterating over pages manually with repeated Collect calls using CrossJoinBuilder.Param.

Example:

rows, err := client.CrossJoin("Products", "Categories").
    Filter("Products/CategoryID eq Categories/ID").
    Collect(ctx)
if err != nil {
    return err
}
for _, row := range rows {
    var p Product
    var c Category
    _ = row.Decode("Products", &p)
    _ = row.Decode("Categories", &c)
}

func (*CrossJoinBuilder) Expand added in v0.2.23

func (b *CrossJoinBuilder) Expand(navProps ...string) *CrossJoinBuilder

Expand requests inline expansion of navigation properties.

Each value must use the qualified form "EntitySetName/NavigationProperty".

Example:

.Expand("Products/Supplier")

func (*CrossJoinBuilder) Filter added in v0.2.23

func (b *CrossJoinBuilder) Filter(expr string) *CrossJoinBuilder

Filter adds an OData $filter expression to narrow the cross-join result.

The filter can reference properties from any of the joined entity sets using the qualified form "EntitySetName/PropertyName".

Example:

.Filter("Products/CategoryID eq Categories/ID and Products/Price gt 100.0")

func (*CrossJoinBuilder) Param added in v0.2.23

func (b *CrossJoinBuilder) Param(key, value string) *CrossJoinBuilder

Param adds a custom query parameter.

Use this for service-specific OData parameters not covered by the builder (e.g., SAP sap-client or service-defined preference hints).

func (*CrossJoinBuilder) Select added in v0.2.23

func (b *CrossJoinBuilder) Select(fields ...string) *CrossJoinBuilder

Select limits the properties returned for each entity set in the result.

Each selector must use the qualified form "EntitySetName/PropertyName".

Example:

.Select("Products/Name", "Products/Price", "Categories/Name")

type CrossJoinResult added in v0.2.23

type CrossJoinResult map[string]json.RawMessage

CrossJoinResult is a single row returned from a $crossjoin query. Each key is an entity set name; the value is the raw JSON of one entity.

Use CrossJoinResult.Decode to unmarshal a specific entity set's data into a typed struct.

func (CrossJoinResult) Decode added in v0.2.23

func (r CrossJoinResult) Decode(entitySet string, dest any) error

Decode unmarshals the entity from the specified entity set into dest.

Example:

var product Product
err := row.Decode("Products", &product)

type DateTime

type DateTime time.Time

DateTime represents an OData Edm.DateTime value (OData v2).

DateTime wraps time.Time to provide custom JSON marshaling/unmarshaling for the OData v2 DateTime format: /Date(milliseconds)/ or /Date(milliseconds+offset)/. This format is common in SAP NetWeaver Gateway and legacy OData v2 services.

Example formats in JSON responses:

"/Date(1704067200000)/"      // 2024-01-01 UTC
"/Date(1704067200000+0100)/" // With timezone offset

Internally stored as UTC time.Time for consistency. Use DateTime.Time to convert to time.Time for standard Go operations.

Example:

type Order struct {
	CreatedAt traverse.DateTime `json:"createdAt"`
}

json.Unmarshal([]byte(`{"createdAt":"/Date(1704067200000)/"}`), &order)
t := order.CreatedAt.Time() // Convert to time.Time

func (DateTime) MarshalJSON

func (d DateTime) MarshalJSON() ([]byte, error)

MarshalJSON encodes DateTime to OData v2 format: /Date(milliseconds)/.

MarshalJSON converts the internal time.Time to milliseconds since epoch and formats it as /Date(milliseconds)/.

func (DateTime) String

func (d DateTime) String() string

String returns the time.Time string representation.

String converts DateTime to time.Time and returns its standard string format.

func (DateTime) Time

func (d DateTime) Time() time.Time

Time converts DateTime to time.Time.

Time returns the underlying time.Time value for use with standard Go time operations.

func (*DateTime) UnmarshalJSON

func (d *DateTime) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes OData DateTime format: /Date(milliseconds)/ or /Date(milliseconds±offset)/.

UnmarshalJSON parses the OData v2 DateTime format and extracts milliseconds since epoch. The timezone offset (if present) is ignored; the result is always in UTC.

Returns an error if the format is invalid or milliseconds cannot be parsed.

type DateTimeOffset

type DateTimeOffset time.Time

DateTimeOffset represents an OData Edm.DateTimeOffset value (OData v4).

DateTimeOffset wraps time.Time to provide custom JSON marshaling/unmarshaling for the OData v4 DateTimeOffset format: ISO 8601 (2024-01-01T00:00:00Z or 2024-01-01T00:00:00+01:00). This format is standard in modern OData v4 services like SAP S/4HANA.

Example formats in JSON responses:

"2024-01-01T00:00:00Z"         // UTC (RFC 3339)
"2024-01-01T00:00:00+01:00"    // With timezone offset
"2024-01-01T00:00:00"          // Without timezone (treated as UTC)

Use DateTimeOffset.Time to convert to time.Time for standard Go operations.

Example:

type Order struct {
	CreatedAt traverse.DateTimeOffset `json:"createdAt"`
}

json.Unmarshal([]byte(`{"createdAt":"2024-01-01T00:00:00Z"}`), &order)
t := order.CreatedAt.Time() // Convert to time.Time

func (DateTimeOffset) MarshalJSON

func (d DateTimeOffset) MarshalJSON() ([]byte, error)

MarshalJSON encodes to ISO 8601 (RFC 3339).

MarshalJSON converts DateTimeOffset to RFC 3339 format for JSON output.

func (DateTimeOffset) Time

func (d DateTimeOffset) Time() time.Time

Time converts DateTimeOffset to time.Time.

Time returns the underlying time.Time value for use with standard Go time operations.

func (*DateTimeOffset) UnmarshalJSON

func (d *DateTimeOffset) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes ISO 8601 format (RFC 3339).

UnmarshalJSON parses ISO 8601 formatted datetime strings. Supports both full timestamps with timezone (RFC 3339) and timestamps without timezone (treated as UTC).

Returns an error if the format cannot be parsed.

type Decimal

type Decimal struct {
	// contains filtered or unexported fields
}

Decimal represents an OData Edm.Decimal value (arbitrary precision decimal).

Decimal uses math/big.Float internally to store arbitrary precision decimal numbers. This is essential for financial calculations and other high-precision scenarios where standard float64 cannot represent the exact value.

The underlying value is stored as a pointer to big.Float to enable nil representation for SQL NULL equivalents.

Example formats in JSON responses:

"123.456789012345678901234567890"
123.45 (numeric literal)

Example:

type Product struct {
	Price traverse.Decimal `json:"price"`
}

json.Unmarshal([]byte(`{"price":"19.99"}`), &product)
fmt.Println(product.Price.String()) // "19.99" with full precision

func (Decimal) MarshalJSON

func (d Decimal) MarshalJSON() ([]byte, error)

MarshalJSON encodes decimal to JSON string (preserves precision).

MarshalJSON returns the decimal as a JSON string to preserve arbitrary precision during round-tripping. Nil values are encoded as null.

func (Decimal) String

func (d Decimal) String() string

String returns the decimal string representation.

String returns the full precision string representation of the decimal. Returns "0" for nil values.

func (*Decimal) UnmarshalJSON

func (d *Decimal) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes decimal values from JSON (string or numeric).

UnmarshalJSON accepts both string and numeric representations of decimal values. Strings are preferred for preserving arbitrary precision. Numeric literals are converted to string first for parsing.

Returns an error if the value cannot be parsed as a valid decimal number.

type DeepInsertOptions added in v0.2.8

type DeepInsertOptions struct {
	// ReturnRepresentation instructs the server to return the created entity
	// (adds "return=representation" to the Prefer header).
	ReturnRepresentation bool
	// ContinueOnError instructs the server to continue on partial failures
	// (adds "odata.continue-on-error" to the Prefer header).
	ContinueOnError bool
}

DeepInsertOptions configures the Prefer header for a deep insert request.

DeepInsertOptions allows control over server behavior during a deep insert:

  • ReturnRepresentation: ask the server to return the created entity
  • ContinueOnError: ask the server to continue processing remaining operations on error

func (DeepInsertOptions) PreferHeader added in v0.2.8

func (o DeepInsertOptions) PreferHeader() string

PreferHeader generates the Prefer header value from the options. Returns an empty string if no options are set.

type DeepUpdateOptions added in v0.7.0

type DeepUpdateOptions struct {
	// ReturnRepresentation instructs the server to return the updated entity
	// (adds "return=representation" to the Prefer header).
	ReturnRepresentation bool
	// ContinueOnError instructs the server to continue on partial failures
	// (adds "odata.continue-on-error" to the Prefer header).
	ContinueOnError bool
}

DeepUpdateOptions configures the Prefer header for a deep update request.

DeepUpdateOptions allows control over server behavior during a deep update:

  • ReturnRepresentation: ask the server to return the updated entity
  • ContinueOnError: ask the server to continue processing remaining operations on error

func (DeepUpdateOptions) PreferHeader added in v0.7.0

func (o DeepUpdateOptions) PreferHeader() string

PreferHeader generates the Prefer header value from the options. Returns an empty string if no options are set.

type DeleteOption added in v0.5.0

type DeleteOption func(*DeleteOptions)

DeleteOption is a functional option for configuring delete behaviour.

func WithDeleteCascade added in v0.5.0

func WithDeleteCascade() DeleteOption

WithDeleteCascade enables cascade delete on related navigation properties. The Prefer: odata.cascade-delete header is sent with the DELETE request.

Example:

err := client.Delete(ctx, "Orders", 1, traverse.WithDeleteCascade())

func WithDeleteIfMatch added in v0.5.0

func WithDeleteIfMatch(etag string) DeleteOption

WithDeleteIfMatch adds an If-Match ETag condition to the delete request. The server will only delete the entity if its current ETag matches. Returns ErrConcurrencyConflict if the ETag does not match (HTTP 412).

Example:

err := client.Delete(ctx, "Products", 42, traverse.WithDeleteIfMatch(`W/"abc123"`))

func WithDeleteReturnRepresentation added in v0.5.0

func WithDeleteReturnRepresentation() DeleteOption

WithDeleteReturnRepresentation requests that the deleted entity be returned in the response. Sends header: Prefer: return=representation

Note: Most OData services return HTTP 204 No Content for deletes; this option is only useful with services that support return=representation on deletes.

Example:

err := client.Delete(ctx, "Orders", 1, traverse.WithDeleteReturnRepresentation())

type DeleteOptions added in v0.5.0

type DeleteOptions struct {
	// CascadeNavigationProperties triggers cascade delete on related entities.
	// Sends header: Prefer: odata.cascade-delete
	CascadeNavigationProperties bool

	// IfMatch sets the ETag condition for the delete.
	IfMatch string

	// ReturnRepresentation requests the deleted entity in the response.
	// Sends header: Prefer: return=representation
	ReturnRepresentation bool
}

DeleteOptions configures a delete operation.

type DeltaResult

type DeltaResult struct {
	// Value is the entity record (nil for deleted records)
	Value map[string]interface{}
	// Removed is true if the record was deleted
	Removed bool
	// Reason is the deletion reason if available
	Reason string
	// Err is an error if one occurred
	Err error
}

DeltaResult represents a record from a delta sync operation.

DeltaResult is used with delta queries to track both added/modified records (in Value) and deleted records (indicated by Removed=true).

Fields:

  • Value: the entity record (for non-deleted records)
  • Removed: true if the record was deleted
  • Reason: optional deletion reason (if provided by server)
  • Err: error if one occurred during parsing

See QueryBuilder.WithDeltaToken for incremental sync usage.

type DeltaResultAs

type DeltaResultAs[T any] struct {
	Value   T
	Removed bool
	Reason  string
	Err     error
}

DeltaResultAs is the typed version of delta result.

type DeltaSync

type DeltaSync struct {
	// contains filtered or unexported fields
}

DeltaSync manages incremental synchronization using OData delta links (OData v4).

DeltaSync enables efficient synchronization of large datasets by supporting incremental updates. On the first sync, all records are read. On subsequent syncs, only changes (modifications and deletions) are returned using a delta token.

Delta sync uses the $deltatoken query parameter to mark a point in time. The server returns only records that have changed since that token was issued. Deleted records are marked with the @removed annotation.

Typical workflow:

  1. Initialize: ds := client.NewDeltaSync("Customers")
  2. Full sync: records, token, err := ds.Full(ctx) // Read all records
  3. Save token for later
  4. Incremental: changes, newToken, err := ds.Incremental(ctx, token) // Only changes
  5. Process changes, save new token, repeat step 4

This approach significantly reduces bandwidth and latency for large datasets that change infrequently.

func (*DeltaSync) Full

func (d *DeltaSync) Full(ctx context.Context, bufferSize ...int) (<-chan Result[map[string]interface{}], string, error)

Full performs a complete (initial) synchronization, returning all records.

Full returns a channel streaming all records from the entity set and automatically extracts the delta token from the server response metadata (DeltaLink). This token can be used for subsequent incremental syncs via DeltaSync.Incremental.

The method also stores the token internally for convenience, so you can call DeltaSync.Incremental without providing the token explicitly.

The bufferSize parameter controls the result channel capacity (default 256). For large record sizes or high network latency, increase this value to reduce blocking.

Returns:

  • A receive-only channel of Result items containing all records
  • The delta token for use in subsequent incremental syncs
  • An error if the query fails

Example:

records, token, err := ds.Full(ctx)
if err != nil {
	log.Fatal(err)
}
for result := range records {
	if result.Err != nil {
		log.Println("Error:", result.Err)
		continue
	}
	processRecord(result.Value)
}
// Save token for next sync: ds.Incremental(ctx, token)

func (*DeltaSync) Incremental

func (d *DeltaSync) Incremental(ctx context.Context, token string, bufferSize ...int) (<-chan DeltaResult, string, error)

Incremental performs an incremental sync using a delta token.

Incremental returns only records that have changed or been deleted since the provided delta token was issued. Changes include both new records and modifications to existing records. Deleted records are marked with Removed=true and include a Reason (typically "changed" or "deleted").

The token parameter specifies which delta point to start from. If empty, the internally stored token (from previous DeltaSync.Full or DeltaSync.Incremental) is used. Returns an error if no token is available.

A new delta token is extracted from the response and can be used for the next incremental sync. It's automatically stored internally as well.

The bufferSize parameter controls the result channel capacity (default 256).

Returns:

  • A receive-only channel of DeltaResult items containing changed records
  • The new delta token for use in subsequent incremental syncs
  • An error if the query fails or no token is available

Example:

// First, get initial token from Full sync
_, token, _ := ds.Full(ctx)

// Later, sync only changes
for result := range ds.Incremental(ctx, token) {
	if result.Removed {
		deleteRecord(result.Value)
	} else {
		updateRecord(result.Value)
	}
}

func (*DeltaSync) SetToken

func (d *DeltaSync) SetToken(token string)

SetToken sets the current delta token.

func (*DeltaSync) Token

func (d *DeltaSync) Token() string

Token returns the current delta token.

type DeltaSyncAs

type DeltaSyncAs[T any] struct {
	// contains filtered or unexported fields
}

DeltaSyncAs is an alias for DeltaSyncJsonAs for backward compatibility. Deprecated: Use DeltaSyncJsonAs or DeltaSyncXmlAs instead.

func NewDeltaSyncAs

func NewDeltaSyncAs[T any](c *Client, entitySet string) *DeltaSyncAs[T]

NewDeltaSyncAs is an alias for NewDeltaSyncJsonAs for backward compatibility. Deprecated: Use NewDeltaSyncJsonAs or NewDeltaSyncXmlAs instead.

func (*DeltaSyncAs[T]) Full

func (d *DeltaSyncAs[T]) Full(ctx context.Context, bufferSize ...int) (<-chan Result[T], string, error)

Full is an alias for the JSON-format Full method. Deprecated: Use DeltaSyncJsonAs.Full or DeltaSyncXmlAs.Full instead.

func (*DeltaSyncAs[T]) Incremental

func (d *DeltaSyncAs[T]) Incremental(ctx context.Context, token string, bufferSize ...int) (<-chan DeltaResultAs[T], string, error)

Incremental is an alias for the JSON-format Incremental method. Deprecated: Use DeltaSyncJsonAs.Incremental or DeltaSyncXmlAs.Incremental instead.

type DeltaSyncJsonAs added in v0.16.0

type DeltaSyncJsonAs[T any] struct {
	// contains filtered or unexported fields
}

DeltaSyncJsonAs is the JSON-format generic version of delta sync.

func NewDeltaSyncJsonAs added in v0.16.0

func NewDeltaSyncJsonAs[T any](c *Client, entitySet string) *DeltaSyncJsonAs[T]

NewDeltaSyncJsonAs creates a typed delta sync handler for JSON format.

func (*DeltaSyncJsonAs[T]) Full added in v0.16.0

func (d *DeltaSyncJsonAs[T]) Full(ctx context.Context, bufferSize ...int) (<-chan Result[T], string, error)

Full performs a complete sync with type T using JSON format.

func (*DeltaSyncJsonAs[T]) Incremental added in v0.16.0

func (d *DeltaSyncJsonAs[T]) Incremental(ctx context.Context, token string, bufferSize ...int) (<-chan DeltaResultAs[T], string, error)

Incremental performs an incremental sync with type T using JSON format.

type DeltaSyncXmlAs added in v0.16.0

type DeltaSyncXmlAs[T any] struct {
	// contains filtered or unexported fields
}

DeltaSyncXmlAs is the XML-format generic version of delta sync.

func NewDeltaSyncXmlAs added in v0.16.0

func NewDeltaSyncXmlAs[T any](c *Client, entitySet string) *DeltaSyncXmlAs[T]

NewDeltaSyncXmlAs creates a typed delta sync handler for XML format.

func (*DeltaSyncXmlAs[T]) Full added in v0.16.0

func (d *DeltaSyncXmlAs[T]) Full(ctx context.Context, bufferSize ...int) (<-chan Result[T], string, error)

Full performs a complete sync with type T using XML format.

func (*DeltaSyncXmlAs[T]) Incremental added in v0.16.0

func (d *DeltaSyncXmlAs[T]) Incremental(ctx context.Context, token string, bufferSize ...int) (<-chan DeltaResultAs[T], string, error)

Incremental performs an incremental sync with type T using XML format.

type ETag added in v0.2.5

type ETag struct {
	// Value is the raw ETag string as returned by the server in the ETag header
	// (e.g. W/"123", "abc456").
	Value string
}

ETag wraps an entity value together with its HTTP ETag for concurrency control. Obtain an ETag value via Client.ReadWithETag and pass it back to Client.UpdateWithETag, Client.ReplaceWithETag, or Client.DeleteWithETag to detect concurrent modifications.

func (ETag) IsEmpty added in v0.2.5

func (e ETag) IsEmpty() bool

IsEmpty reports whether the ETag is unset.

func (ETag) IsWeak added in v0.2.5

func (e ETag) IsWeak() bool

IsWeak reports whether this is a weak ETag (prefixed with W/).

func (ETag) String added in v0.2.5

func (e ETag) String() string

String returns the raw ETag value.

type EntityCapabilities added in v0.2.21

type EntityCapabilities struct {
	Filterable                     bool
	NonFilterableProperties        []string
	Sortable                       bool
	NonSortableProperties          []string
	ExpandableNavigationProperties []string
	Insertable                     bool
	Updatable                      bool
	Deletable                      bool
}

EntityCapabilities describes what operations are supported for an entity set.

type EntitySchema added in v0.2.12

type EntitySchema struct {
	// Properties maps property name -> property type.
	// Valid types: "string", "int", "float", "bool", "datetime", "guid"
	Properties map[string]string
}

EntitySchema defines the known properties of an entity type for filter validation.

EntitySchema provides a declarative way to specify the properties of an OData entity type so that filter and orderby expressions can be validated before sending to the service. This catches typos in field names and type mismatches at build time rather than at runtime when the service rejects the request.

The Properties map associates property names with their types, allowing validation of filter expressions to ensure they reference only valid properties with appropriate types.

Example:

schema := EntitySchema{
    Properties: map[string]string{
        "ID":        "int",
        "Name":      "string",
        "Email":     "string",
        "Age":       "int",
        "Birthdate": "datetime",
        "IsActive":  "bool",
    },
}
query := client.From("Users").WithSchema(schema)

type EntitySetInfo

type EntitySetInfo struct {
	// Name is the name of the entity set.
	Name string
	// EntityTypeName is the fully qualified name of the entity type.
	EntityTypeName string
	// NavigationBindings contains navigation property binding targets for this entity set.
	NavigationBindings []NavigationBinding
	// SAP contains SAP-specific entity-set-level annotations (sap:creatable, sap:deletable, etc.).
	SAP SAPAnnotations
}

EntitySetInfo represents an OData entity set definition. An entity set is a collection of entities of a specific type.

type EntitySetReference

type EntitySetReference struct {
	// Name is the name of the entity set.
	Name string
	// URL is the URL path relative to the service root.
	URL string
}

EntitySetReference represents a reference to an entity set in the service document. It provides the name and URL path for accessing the entity set.

type EntityType

type EntityType struct {
	// Name is the name of the entity type.
	Name string
	// Abstract indicates whether this entity type is abstract (cannot be instantiated directly).
	Abstract bool
	// BaseType is the fully qualified name of the parent entity type (e.g. "Namespace.ParentType").
	BaseType string
	// Key is the list of properties that form the primary key.
	Key []PropertyRef
	// Properties is the list of all properties defined for this entity type.
	Properties []Property
	// NavigationProperties is the list of navigation properties (relationships).
	NavigationProperties []NavigationProperty
}

EntityType represents an OData entity type definition. It describes the structure of entities of this type, including their properties and relationships.

type EntityWithETag added in v0.2.5

type EntityWithETag struct {
	// Entity is the entity properties as returned by the server.
	Entity map[string]interface{}
	// ETag is the concurrency token for this entity revision.
	ETag ETag
}

EntityWithETag pairs a fetched entity map with its server ETag.

type EnumMember added in v0.2.28

type EnumMember struct {
	// Name is the name of the enum member.
	Name string
	// Value is the integer value of the enum member.
	Value int
}

EnumMember represents a single member of an OData enum type.

type EnumType added in v0.2.28

type EnumType struct {
	// Name is the name of the enum type.
	Name string
	// Members is the list of enum members.
	Members []EnumMember
	// IsFlags indicates whether multiple members can be combined.
	IsFlags bool
	// UnderlyingType is the underlying integer type (e.g., "Edm.Int32").
	UnderlyingType string
}

EnumType represents an OData enum type definition.

type ExpandBuilder added in v0.2.6

type ExpandBuilder struct {
	// contains filtered or unexported fields
}

ExpandBuilder configures a nested expand expression with optional sub-query options.

ExpandBuilder is obtained via QueryBuilder.ExpandNested and allows specifying per-navigation-property $select, $filter, $orderby, $top, and further nested $expand options.

func (*ExpandBuilder) Done added in v0.2.6

func (b *ExpandBuilder) Done() *QueryBuilder

Done finalizes the nested expand and returns the parent QueryBuilder.

func (*ExpandBuilder) Expand added in v0.2.6

func (b *ExpandBuilder) Expand(property string) *ExpandBuilder

Expand adds a further nested expand within this navigation property.

func (*ExpandBuilder) Filter added in v0.2.6

func (b *ExpandBuilder) Filter(expr string) *ExpandBuilder

Filter applies a filter expression to the expanded entities.

func (*ExpandBuilder) OrderBy added in v0.2.6

func (b *ExpandBuilder) OrderBy(expr string) *ExpandBuilder

OrderBy sets the sort order for expanded entities.

func (*ExpandBuilder) Select added in v0.2.6

func (b *ExpandBuilder) Select(fields ...string) *ExpandBuilder

Select limits the properties returned for the expanded entity.

func (*ExpandBuilder) Top added in v0.2.6

func (b *ExpandBuilder) Top(n int) *ExpandBuilder

Top limits the number of expanded entities returned.

type ExpandOption

type ExpandOption func(*expandConfig)

ExpandOption is a functional option for configuring entity expansion.

ExpandOption functions configure which fields, filters, ordering, and limits apply to related entities when using QueryBuilder.Expand. Multiple options can be passed to Expand to build complex nested queries.

See WithExpandSelect, WithExpandFilter, WithExpandOrderBy, etc. for available option constructors.

Example:

query.Expand("Orders",
	WithExpandSelect("OrderID", "Amount"),
	WithExpandFilter("Status eq 'Completed'"),
	WithExpandTop(5),
)

func WithExpandFilter

func WithExpandFilter(expr string) ExpandOption

WithExpandFilter specifies a filter for related entities.

WithExpandFilter adds a $filter clause to the expand, returning only related entities that match the filter expression.

Example:

Expand("Orders", WithExpandFilter("Status eq 'Completed'"))

func WithExpandLevels added in v0.6.0

func WithExpandLevels(n int) ExpandOption

WithExpandLevels sets the $levels query option on a nested expand, enabling recursive expansion of a navigation property.

Pass a positive integer for a fixed recursion depth, or LevelsMax to expand to the maximum depth the server supports.

This implements OData v4 spec section 11.2.5.2.1. Only OData v4 services support $levels; the option is silently ignored on v2 services.

Example:

// Expand recursively to maximum depth
Expand("Children", WithExpandLevels(traverse.LevelsMax))

// Expand exactly 3 levels deep
Expand("Children", WithExpandLevels(3))

func WithExpandOrderBy

func WithExpandOrderBy(field string) ExpandOption

WithExpandOrderBy specifies ascending order for related entities.

WithExpandOrderBy adds an $orderby clause (ascending) to the expand. Multiple calls accumulate order expressions.

Example:

Expand("Orders", WithExpandOrderBy("OrderDate"))

func WithExpandOrderByDesc

func WithExpandOrderByDesc(field string) ExpandOption

WithExpandOrderByDesc specifies descending order for related entities.

WithExpandOrderByDesc adds an $orderby clause (descending) to the expand. Multiple calls accumulate order expressions.

Example:

Expand("Orders", WithExpandOrderByDesc("OrderDate"))

func WithExpandSelect

func WithExpandSelect(fields ...string) ExpandOption

WithExpandSelect specifies which fields to include from related entities.

WithExpandSelect adds a $select clause to the expand, limiting related entity results to only the specified fields. This reduces response size and improves performance.

Example:

Expand("Orders", WithExpandSelect("OrderID", "Amount", "OrderDate"))

func WithExpandSkip

func WithExpandSkip(n int) ExpandOption

WithExpandSkip skips a number of related entities.

WithExpandSkip adds a $skip clause to the expand, skipping the specified number of related entities before returning results. Typically used with WithExpandTop.

Example:

Expand("Orders", WithExpandSkip(5).WithExpandTop(10))

func WithExpandTop

func WithExpandTop(n int) ExpandOption

WithExpandTop limits the number of related entities to return.

WithExpandTop adds a $top clause to the expand, limiting the number of related entities to the specified count.

Example:

Expand("Orders", WithExpandTop(10))

type FilterBuilder

type FilterBuilder struct {
	// contains filtered or unexported fields
}

FilterBuilder provides a type-safe, chainable interface for building OData filter expressions.

FilterBuilder is obtained from QueryBuilder.Where(fieldName) and allows constructing filter conditions without writing raw OData syntax. Each comparison method returns *QueryBuilder to enable method chaining directly back to query building.

Lifecycle and Pooling: FilterBuilder instances are managed by an internal sync.Pool to reduce allocations during filtering operations. Instances obtained from Where() should NOT be retained after the method returns-they may be reused for subsequent Where() calls.

Object Pooling Benefits: - -1 allocation per Where() call (amortized) - Efficient for queries with many filters - Transparent to users; no manual pool management required

Chaining Pattern: Where() methods return *QueryBuilder, enabling seamless method chaining:

query.Where("Field1").Eq(value1).
      Where("Field2").Gt(value2).
      Where("Field3").StartsWith("prefix")

Type Safety: Comparison methods accept interface{} and automatically handle type conversion:

  • int, int64: Converted to decimal notation
  • string: Quoted with escape handling ('value' with ” for internal quotes)
  • bool: Converted to "true" or "false"
  • time.Time: Formatted per OData version (v2: /Date(ms)/, v4: RFC3339)
  • nil: Converted to "null"

Available Comparisons: Numeric: Eq, Ne, Gt, Ge, Lt, Le String: Eq, Ne, StartsWith, EndsWith, Contains Boolean/Null: Eq, Ne Date/Time: All numeric comparisons (Gt, Lt, etc.)

String Functions vs Operators: String comparisons can use either:

  • Operators: Eq() for exact match
  • Functions: StartsWith(), Contains(), EndsWith() for partial matching

Examples:

Single filter:

query.Where("Status").Eq("Active")
// OData: $filter=Status eq 'Active'

Multiple filters (AND):

query.Where("Status").Eq("Active").
	  Where("Age").Gt(30)
// OData: $filter=Status eq 'Active' and Age gt 30

String operations:

query.Where("Name").StartsWith("John")
// OData: $filter=startswith(Name, 'John')

Mixed types:

query.Where("OrderDate").Gt(time.Now()).
	  Where("TotalAmount").Lt(1000).
	  Where("Status").Ne("Cancelled")
// OData: $filter=OrderDate gt <timestamp> and TotalAmount lt 1000 and Status ne 'Cancelled'

func (*FilterBuilder) Contains

func (f *FilterBuilder) Contains(substr string) *QueryBuilder

Contains creates a substring filter using the OData substringof function.

Contains filters for records where the field value contains the specified substring anywhere within the value (case-sensitive per OData spec).

Behavior: Matches partial strings at any position:

  • "John Smith" contains "Smith" ✓
  • "Smith, John" contains "Smith" ✓
  • "smith" contains "Smith" ✗ (case-sensitive)

OData Implementation: Contains uses the substringof() function (OData standard):

OData: substringof('substring', FieldName)

Performance Note: Substring searches typically require full table scans without indexes. For large datasets, ensure database indexes exist or use more specific filters.

Chaining: Contains returns *QueryBuilder to combine with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Basic substring match:

query.Where("Name").Contains("John")
// OData: $filter=substringof('John', Name)
// Matches: "John", "John Smith", "Johnny", "Johanna", "Adjourn"

Email domain search:

query.Where("Email").Contains("@example.com")
// OData: $filter=substringof('@example.com', Email)
// Matches: "user@example.com", "admin@example.com"

Combined with other filters:

query.Where("Company").Contains("Inc").
	  Where("Status").Eq("Active")
// OData: $filter=substringof('Inc', Company) and Status eq 'Active'
// Active companies with "Inc" in name

Search functionality:

query.Where("Description").Contains("urgent").
	  Where("Priority").Gt(2)
// OData: $filter=substringof('urgent', Description) and Priority gt 2
// High-priority items with "urgent" in description

func (*FilterBuilder) EndsWith

func (f *FilterBuilder) EndsWith(suffix string) *QueryBuilder

EndsWith creates a suffix match filter using the OData endswith function.

EndsWith filters for records where the field value ends with the specified suffix string (case-sensitive per OData spec).

Behavior: Matches only at the end of the field value:

  • "john.smith@example.com" endswith "@example.com" ✓
  • "example.com" endswith "example.com" ✓
  • "john.smith@example.com" endswith "@example.COM" ✗ (case-sensitive)

Efficiency: EndsWith may require full table scans unless database provides suffix indexes. More expensive than StartsWith for large datasets.

Use Cases:

  • Email domains: EndsWith("@example.com")
  • File extensions: EndsWith(".pdf"), EndsWith(".jpg")
  • Phone number suffixes: EndsWith("-1234")
  • URL patterns: EndsWith(".com"), EndsWith(".org")
  • TLDs: EndsWith(".co.uk")

OData Implementation: EndsWith generates: endswith(FieldName, 'suffix')

Chaining: EndsWith returns *QueryBuilder to combine with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Email domain filter:

query.Where("Email").EndsWith("@company.com")
// OData: $filter=endswith(Email, '@company.com')
// Matches: "user@company.com", "admin@company.com"

File type filtering:

query.Where("Filename").EndsWith(".pdf")
// OData: $filter=endswith(Filename, '.pdf')
// Matches: "document.pdf", "report_2024.pdf"

Phone number area code:

query.Where("Phone").EndsWith("-2000")
// OData: $filter=endswith(Phone, '-2000')
// Matches: "555-1234-2000", "555-2000"

Combined with other filters:

query.Where("Email").EndsWith("@partner.com").
	  Where("Status").Eq("Active")
// OData: $filter=endswith(Email, '@partner.com') and Status eq 'Active'
// Active partner email addresses

func (*FilterBuilder) Eq

func (f *FilterBuilder) Eq(value interface{}) *QueryBuilder

Eq creates an equality filter: field eq value.

Eq filters for records where the field exactly equals the specified value. This is the most common filter operation.

Type Handling: The value parameter is automatically converted to OData literal format:

  • Strings: Quoted and escaped ('value' format)
  • Numbers: Decimal notation (42, -3.14, etc.)
  • Booleans: "true" or "false"
  • DateTime: OData timestamp format
  • null/nil: Converts to "null" for null checks

Chaining: Eq returns *QueryBuilder to chain with other filters or query methods:

query.Where("Status").Eq("Active").Where("Age").Gt(30)

Returns:

  • *QueryBuilder: For method chaining

Examples:

Simple equality:

query.Where("Status").Eq("Active")
// OData: $filter=Status eq 'Active'

Numeric equality:

query.Where("ID").Eq(42)
// OData: $filter=ID eq 42

Boolean:

query.Where("IsDeleted").Eq(false)
// OData: $filter=IsDeleted eq false

Null check:

query.Where("DeletedAt").Eq(nil)
// OData: $filter=DeletedAt eq null

Chained with other filters:

query.Where("Status").Eq("Active").
	  Where("Priority").Gt(1).
	  Where("DueDate").Gt(today)

func (*FilterBuilder) Ge

func (f *FilterBuilder) Ge(value interface{}) *QueryBuilder

Ge creates a greater-than-or-equal filter: field ge value.

Ge filters for records where the field value is greater than or equal to the specified value. Includes the boundary value unlike Gt.

Typical Uses:

  • Minimum thresholds: Where("Score").Ge(80) for passing grades
  • Date ranges: Where("CreatedDate").Ge(startDate)
  • Cutoff values: Where("Priority").Ge(3)
  • Age verification: Where("Age").Ge(18)

Boundary Behavior: Unlike Gt, Ge INCLUDES records that exactly equal the specified value. Ge(100) returns 100, 101, 102, ... while Gt(100) returns 101, 102, ...

Chaining: Ge returns *QueryBuilder for combining with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Minimum score threshold:

query.Where("Score").Ge(80)
// OData: $filter=Score ge 80
// Includes score=80 and above

Date range (start inclusive):

query.Where("CreatedDate").Ge(startDate)
// Returns records from startDate onward (inclusive)

Combined with less-than for range:

query.Where("Price").Ge(50).Where("Price").Le(200)
// OData: $filter=Price ge 50 and Price le 200
// Returns $50-$200 range (both boundaries included)

Inventory threshold:

query.Where("StockLevel").Ge(10)
// OData: $filter=StockLevel ge 10
// Returns items with 10 or more in stock

func (*FilterBuilder) Gt

func (f *FilterBuilder) Gt(value interface{}) *QueryBuilder

Gt creates a greater-than filter: field gt value.

Gt filters for records where the field value is strictly greater than the specified value. Useful for numeric ranges, dates, and ordered comparisons.

Typical Uses:

  • Price ranges: Where("Price").Gt(100)
  • Date filtering: Where("CreatedDate").Gt(startDate)
  • Numeric thresholds: Where("Age").Gt(18)
  • Ordered sequences: Where("Sequence").Gt(currentSeq)

Type Support: Works with any OData type supporting numeric comparison:

  • Integers: 42
  • Decimals: 99.99
  • Dates: time.Time (converted to OData timestamp)
  • Enums: Numeric enum values
  • Durations: In milliseconds or appropriate units

Chaining: Gt returns *QueryBuilder to combine with other filters and query methods.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Simple numeric threshold:

query.Where("Age").Gt(30)
// OData: $filter=Age gt 30
// Returns people over 30 years old

Date filtering:

query.Where("OrderDate").Gt(startOfMonth)
// OData: $filter=OrderDate gt <timestamp>
// Returns orders from start of month onward

Price range (combined with Lt):

query.Where("Price").Gt(10).Where("Price").Lt(100)
// OData: $filter=Price gt 10 and Price lt 100
// Returns products between $10 and $100

Complex filtering:

query.Where("Salary").Gt(50000).
	  Where("Department").Eq("Sales").
	  Where("HireDate").Gt(minDate)

func (*FilterBuilder) In

func (f *FilterBuilder) In(values ...interface{}) *QueryBuilder

In creates a membership test filter for checking if a field value is in a set.

In generates an OData v4 "in" expression: field in (val1, val2, val3, ...) This is efficient for testing whether a value belongs to a known set.

OData Version: In is only available in OData v4. OData v2 requires using multiple Eq conditions joined with "or": (field eq val1) or (field eq val2) or (field eq val3)

Behavior: Returns all records where the field equals ANY of the provided values. Equivalent to: OR-ing multiple Eq conditions.

Empty Values: If In is called with no values, it returns the query unchanged (no filter added). This allows optional filtering: if len(categories) > 0 { q = q.Where("Cat").In(categories...) }

Performance: Much more efficient than multiple Where().Eq() calls:

  • Single clause: Where("Status").In("Active", "Pending", "Review")
  • vs multiple: Where("Status").Eq("Active").Where("Status").Eq("Pending")...
  • Network: 1 query parameter vs 3
  • Processing: Server can use IN index optimization

Type Handling: All value types are automatically converted to OData literals:

  • Strings: Single-quoted ('value')
  • Numbers: Decimal notation
  • Booleans: true/false
  • Dates: OData timestamp format

Chaining: In returns *QueryBuilder to combine with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Status filter (common use):

query.Where("Status").In("Active", "Pending", "Review")
// OData: $filter=Status in ('Active', 'Pending', 'Review')
// Matches any of three statuses

Category selection:

query.Where("CategoryID").In(1, 2, 3, 5)
// OData: $filter=CategoryID in (1, 2, 3, 5)
// Products in categories 1, 2, 3, or 5

Priority levels:

priorities := []int{1, 2, 3}  // High, Medium, Low
query.Where("Priority").In(append(priorities, 0)...)
// All high/medium/low plus unassigned

Optional filtering:

var allowedStatuses []string
q := client.From("Orders")
if len(allowedStatuses) > 0 {
	q = q.Where("Status").In(allowedStatuses...)
}
results, _ := q.Collect(ctx)

Combined with other filters:

query.Where("Region").In("North", "South", "West").
	  Where("SalesAmount").Gt(10000).
	  OrderByDesc("SalesAmount").
	  Top(100)
// High-value sales in three regions, sorted

func (*FilterBuilder) Le

func (f *FilterBuilder) Le(value interface{}) *QueryBuilder

Le creates a less-than-or-equal filter: field le value.

Le filters for records where the field value is less than or equal to the specified value. Includes the boundary value unlike Lt.

Typical Uses:

  • Maximum thresholds: Where("Duration").Le(60) for minute-long clips
  • On-or-before dates: Where("EndDate").Le(quarterEnd)
  • Upper limits: Where("PageSize").Le(1000)
  • Age/date cutoffs: Where("Age").Le(65)

Boundary Behavior: Unlike Lt, Le INCLUDES records that exactly equal the specified value. Le(100) returns 100, 99, 98, ... while Lt(100) returns 99, 98, ...

Common Pattern: Combine with Ge for inclusive ranges: Where("Age").Ge(18).Where("Age").Le(65)

Chaining: Le returns *QueryBuilder for combining with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Maximum duration (inclusive):

query.Where("DurationSeconds").Le(300)
// OData: $filter=DurationSeconds le 300
// Videos 300 seconds or shorter

On-or-before date:

query.Where("EndDate").Le(today)
// Includes today and all past dates

Inclusive range:

query.Where("Price").Ge(50).Where("Price").Le(200)
// OData: $filter=Price ge 50 and Price le 200
// Exactly $50-$200 including boundaries

Age demographic:

query.Where("Age").Ge(13).Where("Age").Le(19)
// OData: $filter=Age ge 13 and Age le 19
// Ages 13 through 19 inclusive (teenagers)

func (*FilterBuilder) Lt

func (f *FilterBuilder) Lt(value interface{}) *QueryBuilder

Lt creates a less-than filter: field lt value.

Lt filters for records where the field value is strictly less than the specified value. Useful for upper bounds and before/prior comparisons.

Typical Uses:

  • Upper price limits: Where("Price").Lt(100)
  • Before dates: Where("DeadlineDate").Lt(today)
  • Below thresholds: Where("DaysOverdue").Lt(7)
  • Version checks: Where("BuildNumber").Lt(2000)

Boundary Behavior: Lt does NOT include the specified value. Lt(100) matches 99, 98, 97... Use Le() to include the boundary.

Chaining: Lt returns *QueryBuilder for combining with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Upper price limit (exclusive):

query.Where("Price").Lt(100)
// OData: $filter=Price lt 100
// Returns prices < $100 (99.99, 50, etc., but not 100)

Past events:

query.Where("EventDate").Lt(today)
// Returns events before today (yesterday and earlier)

Range query (both exclusive):

query.Where("Age").Gt(18).Where("Age").Lt(65)
// OData: $filter=Age gt 18 and Age lt 65
// Returns ages 19-64

Early warning system:

query.Where("DaysUntilExpiry").Lt(30)
// OData: $filter=DaysUntilExpiry lt 30
// Items expiring within 30 days

func (*FilterBuilder) Ne

func (f *FilterBuilder) Ne(value interface{}) *QueryBuilder

Ne creates a not-equal filter: field ne value.

Ne filters for records where the field does NOT equal the specified value. Useful for excluding specific values or statuses.

Use Cases:

  • Exclude deleted records: Where("Status").Ne("Deleted")
  • Exclude null values: Where("Email").Ne(nil)
  • Exclude specific user: Where("UserID").Ne(adminID)

Type Handling: Same automatic conversion as Eq() for all value types.

Chaining: Ne returns *QueryBuilder to chain with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Exclude status:

query.Where("Status").Ne("Deleted")
// OData: $filter=Status ne 'Deleted'

Exclude null:

query.Where("Email").Ne(nil)
// OData: $filter=Email ne null

Exclude numeric value:

query.Where("Quantity").Ne(0)
// OData: $filter=Quantity ne 0

Combined filtering (active non-deleted):

query.Where("Status").Eq("Active").
	  Where("Deleted").Ne(true)
// OData: $filter=Status eq 'Active' and Deleted ne true

func (*FilterBuilder) StartsWith

func (f *FilterBuilder) StartsWith(prefix string) *QueryBuilder

StartsWith creates a prefix match filter using the OData startswith function.

StartsWith filters for records where the field value begins with the specified prefix string (case-sensitive per OData spec).

Behavior: Matches only at the beginning of the field value:

  • "Product A" startswith "Product" ✓
  • "Product A" startswith "product" ✗ (case-sensitive)
  • "A Product" startswith "Product" ✗ (not at start)

Efficiency: StartsWith is more efficient than Contains for prefix matching and can use database indexes on string columns (B-tree prefix matching).

Use Cases:

  • Category codes: StartsWith("A-") for category A codes
  • Postal codes: StartsWith("90210") for specific zip code area
  • Enum prefixes: StartsWith("STATUS_") for status fields
  • Name filtering: StartsWith("John") for first names
  • Auto-complete: StartsWith(userInput) for search suggestions

OData Implementation: StartsWith generates: startswith(FieldName, 'prefix')

Chaining: StartsWith returns *QueryBuilder to combine with other filters.

Returns:

  • *QueryBuilder: For method chaining

Examples:

Product code filtering:

query.Where("ProductCode").StartsWith("ELEC-")
// OData: $filter=startswith(ProductCode, 'ELEC-')
// Matches: "ELEC-001", "ELEC-TV-50"

Name search (auto-complete):

query.Where("LastName").StartsWith("Smith")
// OData: $filter=startswith(LastName, 'Smith')
// Matches: "Smith", "Smithson", "Smithers" (but not "Smithy" capitalization)

Regional filtering:

query.Where("ZipCode").StartsWith("90210")
// OData: $filter=startswith(ZipCode, '90210')
// Matches: "90210", "90210-1234" (plus 4 format)

Combined with other filters:

query.Where("Email").StartsWith("admin").
	  Where("Department").Eq("IT")
// OData: $filter=startswith(Email, 'admin') and Department eq 'IT'
// IT admin email addresses

type FilterExpr added in v0.2.14

type FilterExpr struct {
	// contains filtered or unexported fields
}

FilterExpr is a chainable OData filter expression builder.

FilterExpr provides a fluent API for constructing type-safe OData $filter expressions without string concatenation. Each method returns *FilterExpr to enable method chaining, and the final Build() or String() method generates the OData filter string.

Example:

expr := F("Name").Eq("Alice")
filter := expr.Build() // "Name eq 'Alice'"

func And added in v0.2.14

func And(exprs ...*FilterExpr) *FilterExpr

And combines multiple filter expressions with logical AND.

And takes multiple FilterExpr arguments and combines them with the 'and' operator, wrapping each in parentheses. If no expressions are provided, returns nil.

Example:

And(F("Age").Ge(18), F("Active").Eq(true)).Build()
// "(Age ge 18) and (Active eq true)"

func F added in v0.2.14

func F(field string) *FilterExpr

F starts a filter expression for a field.

F creates a new FilterExpr targeting the specified field. This is the entry point for building type-safe OData filter expressions.

Example:

F("Age").Gt(18).Build()

func Not added in v0.2.14

func Not(expr *FilterExpr) *FilterExpr

Not creates a logical NOT filter: not (expression).

Not negates the given filter expression.

Example:

Not(F("Deleted").Eq(true)).Build()
// "not (Deleted eq true)"

func Or added in v0.2.14

func Or(exprs ...*FilterExpr) *FilterExpr

Or combines multiple filter expressions with logical OR.

Or takes multiple FilterExpr arguments and combines them with the 'or' operator, wrapping each in parentheses. If no expressions are provided, returns nil.

Example:

Or(F("City").Eq("NY"), F("City").Eq("LA")).Build()
// "(City eq 'NY') or (City eq 'LA')"

func (*FilterExpr) Build added in v0.2.14

func (f *FilterExpr) Build() string

Build returns the OData filter string.

Build finalizes the filter expression and returns the OData $filter string. This method does not modify the FilterExpr and can be called multiple times.

func (*FilterExpr) Contains added in v0.2.14

func (f *FilterExpr) Contains(value string) *FilterExpr

Contains creates a string contains filter: contains(field, value).

Contains checks if the field (string) contains the specified substring.

func (*FilterExpr) EndsWith added in v0.2.14

func (f *FilterExpr) EndsWith(value string) *FilterExpr

EndsWith creates a string ends-with filter: endswith(field, value).

EndsWith checks if the field (string) ends with the specified suffix.

func (*FilterExpr) Eq added in v0.2.14

func (f *FilterExpr) Eq(value interface{}) *FilterExpr

Eq creates an equality filter: field eq value.

func (*FilterExpr) Ge added in v0.2.14

func (f *FilterExpr) Ge(value interface{}) *FilterExpr

Ge creates a greater-than-or-equal filter: field ge value.

func (*FilterExpr) Gt added in v0.2.14

func (f *FilterExpr) Gt(value interface{}) *FilterExpr

Gt creates a greater-than filter: field gt value.

func (*FilterExpr) Le added in v0.2.14

func (f *FilterExpr) Le(value interface{}) *FilterExpr

Le creates a less-than-or-equal filter: field le value.

func (*FilterExpr) Lt added in v0.2.14

func (f *FilterExpr) Lt(value interface{}) *FilterExpr

Lt creates a less-than filter: field lt value.

func (*FilterExpr) Ne added in v0.2.14

func (f *FilterExpr) Ne(value interface{}) *FilterExpr

Ne creates an inequality filter: field ne value.

func (*FilterExpr) Reset added in v0.22.0

func (f *FilterExpr) Reset() *FilterExpr

Reset clears the filter expression, allowing the FilterExpr to be reused for building a new expression without allocating a new object.

Example:

expr := F("Name").Eq("Alice")
filter1 := expr.Build()  // "Name eq 'Alice'"
expr.Reset()
expr.Eq("Bob")
filter2 := expr.Build()  // "Name eq 'Bob'"

func (*FilterExpr) StartsWith added in v0.2.14

func (f *FilterExpr) StartsWith(value string) *FilterExpr

StartsWith creates a string starts-with filter: startswith(field, value).

StartsWith checks if the field (string) starts with the specified prefix.

func (*FilterExpr) String added in v0.2.14

func (f *FilterExpr) String() string

String implements the Stringer interface, returning the OData filter string.

String is equivalent to Build() and allows FilterExpr to be used with fmt.Print and other functions expecting Stringer.

type FunctionBuilder

type FunctionBuilder struct {
	// contains filtered or unexported fields
}

FunctionBuilder provides a fluent API for calling OData Functions (v4).

FunctionBuilder enables execution of OData Function calls, which are read-only operations that may take parameters and return values. Functions use HTTP GET with parameters encoded in the URL path.

Typical usage:

result, err := client.Function("GetProducts").
	Param("category", "Electronics").
	Param("maxPrice", 1000).
	Execute(ctx)

func (*FunctionBuilder) Execute

func (f *FunctionBuilder) Execute(ctx context.Context) (map[string]interface{}, error)

Execute calls the function and returns the result as a map.

Execute constructs the OData function URL with parameters, sends an HTTP GET request, and parses the response into a map. The response format is normalized by [parseResponseValue].

Returns a map containing the function result, or an error if the call fails or the response status is not 2xx.

Example:

result, err := client.Function("GetTopProducts").
	Param("count", 10).
	Execute(ctx)
// result contains the function response data

func (*FunctionBuilder) Invoke added in v0.2.11

func (f *FunctionBuilder) Invoke(ctx context.Context, result any) error

Invoke calls the function and unmarshals the response into result.

Invoke is the result-receiver variant of FunctionBuilder.Execute. It sends an HTTP GET request to the OData function URL (with inline parameters) and unmarshals the response body into the value pointed to by result.

result must be a non-nil pointer. Invoke uses JSON round-trip via [mapToJsonStruct] for the unmarshaling, so standard json struct tags are respected.

Returns an error if the HTTP call fails, the response status is not 2xx, or unmarshaling fails.

Example:

type StatsResult struct {
	Count int    `json:"count"`
	Label string `json:"label"`
}

var stats StatsResult
err := client.Function("GetStats").Param("top", 10).Invoke(ctx, &stats)

func (*FunctionBuilder) Param

func (f *FunctionBuilder) Param(key string, value interface{}) *FunctionBuilder

Param adds a parameter to the function call.

Param appends a key-value parameter to the function. Parameters are encoded in the URL path (e.g., /GetProducts(category='Electronics',maxPrice=1000)).

Returns the receiver for method chaining.

Example:

f.Param("name", "John").Param("age", 30)

type FunctionImportBuilder

type FunctionImportBuilder struct {
	// contains filtered or unexported fields
}

FunctionImportBuilder provides a fluent API for calling OData Function Imports (v2).

FunctionImportBuilder enables execution of OData v2 Function Imports, which are similar to v4 Functions but use the v2 protocol. Function Imports use HTTP GET with parameters encoded in the URL path.

Typical usage:

result, err := client.FunctionImport("GetProductsByRating").
	Param("minRating", 4).
	Execute(ctx)

func (*FunctionImportBuilder) Execute

func (f *FunctionImportBuilder) Execute(ctx context.Context) (map[string]interface{}, error)

Example:

result, err := client.FunctionImport("GetTop10Orders").Execute(ctx)

func (*FunctionImportBuilder) Invoke added in v0.2.19

func (f *FunctionImportBuilder) Invoke(ctx context.Context, result any) error

Invoke calls the function import and decodes the response into result.

For GET requests, parameters are encoded in the URL as FuncName(k=v,...). For POST requests, parameters are sent as a JSON body.

The response is unwrapped from the OData {"d":{...}} envelope when present before decoding into result. Pass nil to discard the response body.

Returns an error if the HTTP call fails, the status is not 2xx, or JSON decoding fails.

Example:

var stats Stats
err := client.FunctionImport("GetStats").Invoke(ctx, &stats)

func (*FunctionImportBuilder) InvokeCollection added in v0.2.19

func (f *FunctionImportBuilder) InvokeCollection(ctx context.Context, results any) error

InvokeCollection calls the function import and decodes a collection response.

It handles the following OData collection envelope formats:

  • {"d":{"results":[...]}} - OData v2 collection
  • {"results":[...]} - flat results array
  • {"value":[...]} - OData v4 collection
  • [...] - bare JSON array

results must be a pointer to a slice, or nil to discard.

Returns an error if the HTTP call fails, the status is not 2xx, or JSON decoding fails.

Example:

var orders []Order
err := client.FunctionImport("GetOrders").InvokeCollection(ctx, &orders)

func (*FunctionImportBuilder) Method added in v0.2.19

Execute calls the function import and returns the result as a map.

Execute constructs the OData v2 function import URL with parameters, sends an HTTP GET request, and parses the response into a map. This is similar to FunctionBuilder.Execute but for OData v2 Function Imports.

Returns a map containing the function import result, or an error if the call fails. Method sets the HTTP method for the function import call (default is "GET").

Use Method("POST") for function imports that modify state or when parameters must be sent in the request body instead of the URL.

Returns the receiver for method chaining.

Example:

err := client.FunctionImport("ProcessQueue").Method("POST").Invoke(ctx, &result)

func (*FunctionImportBuilder) Param

func (f *FunctionImportBuilder) Param(key string, value interface{}) *FunctionImportBuilder

Param adds a parameter to the function import.

Param appends a key-value parameter to the function import. Parameters are encoded in the URL path, similar to FunctionBuilder.Param.

Returns the receiver for method chaining.

type FunctionImportInfo

type FunctionImportInfo struct {
	// Name is the name of the function import.
	Name string
	// Parameters is the list of parameters for this function.
	Parameters []FunctionParameter
	// ReturnType is the return type of the function.
	ReturnType string
	// IsComposable indicates if the function can be used in URL composition.
	IsComposable bool
}

FunctionImportInfo represents a function import definition (OData v2). Function imports are operations that can be called on the service.

type FunctionInfo

type FunctionInfo struct {
	Name         string
	Parameters   []FunctionParameter
	ReturnType   string
	IsComposable bool
}

FunctionInfo represents an OData function (v4).

type FunctionParameter

type FunctionParameter struct {
	Name     string
	Type     string
	Nullable bool
}

FunctionParameter represents a function parameter.

type GeographyLineString added in v0.10.0

type GeographyLineString struct {
	Points []GeographyPoint
}

GeographyLineString represents a geographic line string (Edm.GeographyLineString). A LineString requires at least 2 points.

func (GeographyLineString) MarshalJSON added in v0.10.0

func (ls GeographyLineString) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeographyLineString.

func (GeographyLineString) ODataLiteral added in v0.10.0

func (ls GeographyLineString) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeographyLineString. Format: geography'SRID=4326;LINESTRING(lon1 lat1,lon2 lat2,...)'

func (*GeographyLineString) UnmarshalJSON added in v0.10.0

func (ls *GeographyLineString) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeographyLineString.

type GeographyMultiPoint added in v0.10.0

type GeographyMultiPoint struct {
	Points []GeographyPoint
}

GeographyMultiPoint represents a collection of geographic points (Edm.GeographyMultiPoint).

func (GeographyMultiPoint) MarshalJSON added in v0.10.0

func (mp GeographyMultiPoint) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeographyMultiPoint.

func (*GeographyMultiPoint) UnmarshalJSON added in v0.10.0

func (mp *GeographyMultiPoint) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeographyMultiPoint.

type GeographyPoint added in v0.10.0

type GeographyPoint struct {
	// Longitude is the east-west coordinate (X axis), range -180 to 180.
	Longitude float64
	// Latitude is the north-south coordinate (Y axis), range -90 to 90.
	Latitude float64
}

GeographyPoint represents a WGS84 geographic coordinate (Edm.GeographyPoint).

Coordinates follow the OData/WKT convention: Longitude first, then Latitude. This matches GeoJSON (RFC 7946) order.

Example OData literal: geography'SRID=4326;POINT(13.408 52.518)' Example GeoJSON: {"type":"Point","coordinates":[13.408,52.518]}

func AcquireGeographyPoint added in v0.10.0

func AcquireGeographyPoint(lon, lat float64) *GeographyPoint

AcquireGeographyPoint returns a *GeographyPoint from the pool. The caller must call ReleaseGeographyPoint when done.

func ParseGeographyPoint added in v0.10.0

func ParseGeographyPoint(s string) (GeographyPoint, error)

ParseGeographyPoint parses an OData geography point literal. Accepts: geography'SRID=4326;POINT(lon lat)' or POINT(lon lat)

func (GeographyPoint) MarshalJSON added in v0.10.0

func (p GeographyPoint) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeographyPoint. Produces GeoJSON Point: {"type":"Point","coordinates":[lon,lat]}

func (GeographyPoint) ODataLiteral added in v0.10.0

func (p GeographyPoint) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeographyPoint. Format: geography'SRID=4326;POINT(lon lat)'

func (*GeographyPoint) UnmarshalJSON added in v0.10.0

func (p *GeographyPoint) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeographyPoint.

type GeographyPolygon added in v0.10.0

type GeographyPolygon struct {
	ExteriorRing  []GeographyPoint
	InteriorRings [][]GeographyPoint
}

GeographyPolygon represents a geographic polygon (Edm.GeographyPolygon). ExteriorRing is the outer boundary; InteriorRings are holes. All rings must be closed (first point == last point).

func (GeographyPolygon) MarshalJSON added in v0.10.0

func (poly GeographyPolygon) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeographyPolygon.

func (GeographyPolygon) ODataLiteral added in v0.10.0

func (poly GeographyPolygon) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeographyPolygon. Format: geography'SRID=4326;POLYGON((ring1),(ring2),...)'

func (*GeographyPolygon) UnmarshalJSON added in v0.10.0

func (poly *GeographyPolygon) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeographyPolygon.

type GeometryLineString added in v0.10.0

type GeometryLineString struct {
	Points []GeometryPoint
}

GeometryLineString represents a planar line string (Edm.GeometryLineString).

func (GeometryLineString) MarshalJSON added in v0.10.0

func (ls GeometryLineString) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeometryLineString.

func (GeometryLineString) ODataLiteral added in v0.10.0

func (ls GeometryLineString) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeometryLineString.

func (*GeometryLineString) UnmarshalJSON added in v0.10.0

func (ls *GeometryLineString) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeometryLineString.

type GeometryMultiPoint added in v0.10.0

type GeometryMultiPoint struct {
	Points []GeometryPoint
}

GeometryMultiPoint represents a collection of planar points (Edm.GeometryMultiPoint).

func (GeometryMultiPoint) MarshalJSON added in v0.10.0

func (mp GeometryMultiPoint) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeometryMultiPoint.

func (*GeometryMultiPoint) UnmarshalJSON added in v0.10.0

func (mp *GeometryMultiPoint) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeometryMultiPoint.

type GeometryPoint added in v0.10.0

type GeometryPoint struct {
	X float64
	Y float64
}

GeometryPoint represents a point in a flat (Euclidean) coordinate system (Edm.GeometryPoint).

Example OData literal: geometry'SRID=0;POINT(1.5 2.5)' Example GeoJSON: {"type":"Point","coordinates":[1.5,2.5]}

func AcquireGeometryPoint added in v0.10.0

func AcquireGeometryPoint(x, y float64) *GeometryPoint

AcquireGeometryPoint returns a *GeometryPoint from the pool.

func ParseGeometryPoint added in v0.10.0

func ParseGeometryPoint(s string) (GeometryPoint, error)

ParseGeometryPoint parses an OData geometry point literal. Accepts: geometry'SRID=0;POINT(x y)' or POINT(x y)

func (GeometryPoint) MarshalJSON added in v0.10.0

func (p GeometryPoint) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeometryPoint. Produces GeoJSON Point: {"type":"Point","coordinates":[x,y]}

func (GeometryPoint) ODataLiteral added in v0.10.0

func (p GeometryPoint) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeometryPoint. Format: geometry'SRID=0;POINT(x y)'

func (*GeometryPoint) UnmarshalJSON added in v0.10.0

func (p *GeometryPoint) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeometryPoint.

type GeometryPolygon added in v0.10.0

type GeometryPolygon struct {
	ExteriorRing  []GeometryPoint
	InteriorRings [][]GeometryPoint
}

GeometryPolygon represents a planar polygon (Edm.GeometryPolygon).

func (GeometryPolygon) MarshalJSON added in v0.10.0

func (poly GeometryPolygon) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for GeometryPolygon.

func (GeometryPolygon) ODataLiteral added in v0.10.0

func (poly GeometryPolygon) ODataLiteral() string

ODataLiteral returns the OData URL literal for a GeometryPolygon.

func (*GeometryPolygon) UnmarshalJSON added in v0.10.0

func (poly *GeometryPolygon) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for GeometryPolygon.

type GraphConfig added in v0.2.15

type GraphConfig struct {
	Version          string
	AccessToken      string
	ConsistencyLevel string
}

type GraphError added in v0.2.15

type GraphError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (*GraphError) Error added in v0.2.15

func (e *GraphError) Error() string

type Guid

type Guid [16]byte

Guid represents an OData Edm.Guid value (UUID).

Guid wraps [16]byte to represent OData GUID values in the standard UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12 hex digits).

The Guid type provides custom JSON marshaling/unmarshaling to convert between UUID string format and the internal 16-byte representation.

Example formats in JSON responses:

"550e8400-e29b-41d4-a716-446655440000"

Use Guid.String to convert to standard UUID string format.

Example:

type Entity struct {
	ID traverse.Guid `json:"id"`
}

json.Unmarshal([]byte(`{"id":"550e8400-e29b-41d4-a716-446655440000"}`), &entity)
uuidStr := entity.ID.String() // "550e8400-e29b-41d4-a716-446655440000"

func (Guid) String

func (g Guid) String() string

String returns the UUID string representation.

String converts the internal 16-byte representation to standard UUID format (8-4-4-4-12 hex digits).

func (*Guid) UnmarshalJSON

func (g *Guid) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

UnmarshalJSON parses a UUID string and converts it to the internal 16-byte representation. The UUID must be in standard format with 8-4-4-4-12 hex digit groups.

Returns an error if the format is invalid or contains non-hexadecimal characters.

type LambdaBuilder added in v0.2.8

type LambdaBuilder struct {
	// contains filtered or unexported fields
}

LambdaBuilder constructs the expression inside an OData lambda operator (any/all).

LambdaBuilder is obtained from QueryBuilder.LambdaAny or QueryBuilder.LambdaAll via a callback function. Use Field() to start building conditions within the lambda.

The variable name used in the lambda expression is automatically derived from the first letter of the collection field name (e.g. "tags" -> "t", "items" -> "i").

func (*LambdaBuilder) Field added in v0.2.8

func (b *LambdaBuilder) Field(name string) *LambdaCondition

Field begins a condition expression for the given field within the lambda. The field is referenced as varName/fieldName in the generated OData expression.

b.Field("name").Eq("admin")
// produces: t/name eq 'admin'  (when varName is "t")

type LambdaCondition added in v0.2.8

type LambdaCondition struct {
	// contains filtered or unexported fields
}

LambdaCondition builds a comparison or function expression within a lambda.

LambdaCondition is obtained from LambdaBuilder.Field and provides comparison and string function operators. Each operator appends an expression to the parent LambdaBuilder and returns the LambdaBuilder for further chaining.

func (*LambdaCondition) Contains added in v0.2.8

func (c *LambdaCondition) Contains(s string) *LambdaBuilder

Contains adds a contains() function condition.

func (*LambdaCondition) EndsWith added in v0.2.8

func (c *LambdaCondition) EndsWith(s string) *LambdaBuilder

EndsWith adds an endswith() function condition.

func (*LambdaCondition) Eq added in v0.2.8

func (c *LambdaCondition) Eq(v any) *LambdaBuilder

Eq adds an equality condition: path eq value.

func (*LambdaCondition) Ge added in v0.2.8

func (c *LambdaCondition) Ge(v any) *LambdaBuilder

Ge adds a greater-than-or-equal condition: path ge value.

func (*LambdaCondition) Gt added in v0.2.8

func (c *LambdaCondition) Gt(v any) *LambdaBuilder

Gt adds a greater-than condition: path gt value.

func (*LambdaCondition) Le added in v0.2.8

func (c *LambdaCondition) Le(v any) *LambdaBuilder

Le adds a less-than-or-equal condition: path le value.

func (*LambdaCondition) Lt added in v0.2.8

func (c *LambdaCondition) Lt(v any) *LambdaBuilder

Lt adds a less-than condition: path lt value.

func (*LambdaCondition) Ne added in v0.2.8

func (c *LambdaCondition) Ne(v any) *LambdaBuilder

Ne adds a not-equal condition: path ne value.

func (*LambdaCondition) StartsWith added in v0.2.8

func (c *LambdaCondition) StartsWith(s string) *LambdaBuilder

StartsWith adds a startswith() function condition.

type MeasuresVocabulary added in v0.8.0

type MeasuresVocabulary struct {
	// ISOCurrency is the ISO 4217 currency code for a monetary amount property.
	// Corresponds to Org.OData.Measures.V1.ISOCurrency.
	// The value may be a literal currency code (e.g. "USD") or a path to a property
	// that holds the currency code.
	ISOCurrency string
	// Scale is the number of significant decimal places for a currency or measure value.
	// Corresponds to Org.OData.Measures.V1.Scale.
	Scale *int
	// Unit is the unit of measure for the property value.
	// Corresponds to Org.OData.Measures.V1.Unit.
	// The value may be a literal unit symbol or a path to a property that holds it.
	Unit string
	// SIPrefix is the SI prefix that multiplies the unit (e.g. "Kilo", "Mega", "Milli").
	// Corresponds to Org.OData.Measures.V1.SIPrefix.
	SIPrefix string
	// DurationGranularity is the minimum granularity of duration values (e.g. "days", "hours").
	// Corresponds to Org.OData.Measures.V1.DurationGranularity.
	DurationGranularity string
}

MeasuresVocabulary defines Org.OData.Measures.V1 annotation terms.

The Measures vocabulary annotates properties with unit-of-measure semantics. These annotations are defined in the OASIS OData Measures vocabulary (namespace: Org.OData.Measures.V1).

func ParseMeasuresVocabulary added in v0.8.0

func ParseMeasuresVocabulary(annotations map[string]string) MeasuresVocabulary

ParseMeasuresVocabulary extracts Org.OData.Measures.V1 annotation terms from a raw annotation map. The map keys are fully-qualified term names (e.g. "Org.OData.Measures.V1.ISOCurrency").

Example annotations map:

map[string]string{
    "Org.OData.Measures.V1.ISOCurrency": "USD",
    "Org.OData.Measures.V1.Scale":       "2",
    "Org.OData.Measures.V1.Unit":        "kg",
}

type MemoryCache

type MemoryCache struct {
	// contains filtered or unexported fields
}

MemoryCache is a simple in-memory cache for OData metadata.

MemoryCache is safe for concurrent use using lock-free sync.Map. It stores Metadata objects keyed by service endpoint or custom identifier.

Metadata is cached for the lifetime of the MemoryCache instance; it is not automatically expired. For long-running processes, consider periodic cache invalidation or custom expiration logic in a wrapper.

Example:

cache := traverse.NewMemoryCache()
client, _ := traverse.New(
	traverse.WithBaseURL("https://odata.service/"),
	traverse.WithCache(cache),
)
// First call fetches and caches metadata
metadata, _ := client.Metadata(ctx)
// Subsequent calls hit the cache
metadata, _ := client.Metadata(ctx)

func NewMemoryCache

func NewMemoryCache() *MemoryCache

NewMemoryCache creates a new in-memory metadata cache.

NewMemoryCache returns a ready-to-use *MemoryCache that is safe for concurrent access from multiple goroutines.

func (*MemoryCache) Clear

func (c *MemoryCache) Clear()

Clear removes all entries from the cache.

Clear iterates over all cached entries and deletes them, leaving the cache empty. Useful for cache invalidation or cleanup.

func (*MemoryCache) Get

func (c *MemoryCache) Get(key string) (*Metadata, bool)

Get retrieves cached metadata by key.

Get returns the Metadata stored under the given key, or (nil, false) if not cached.

func (*MemoryCache) Set

func (c *MemoryCache) Set(key string, metadata *Metadata)

Set stores metadata in the cache with the given key.

Set overwrites any existing entry for the key. The operation is atomic and safe for concurrent use.

type Metadata

type Metadata struct {
	// Version is the OData protocol version (e.g., "2.0" or "4.0").
	Version string
	// Namespace is the target namespace of the metadata document.
	Namespace string
	// EntityTypes is the list of all entity types defined in the service.
	EntityTypes []EntityType
	// EntitySets is the list of all entity set definitions.
	EntitySets []EntitySetInfo
	// Associations describes relationships between entity types.
	Associations []Association
	// FunctionImports contains function imports available in the service.
	FunctionImports []FunctionImportInfo
	// Actions contains action definitions (OData v4).
	Actions []ActionInfo
	// Functions contains function definitions (OData v4).
	Functions []FunctionInfo
	// ComplexTypes contains complex type definitions (OData v4).
	ComplexTypes []ComplexType
	// EnumTypes contains enum type definitions (OData v4).
	EnumTypes []EnumType
}

Metadata represents the parsed OData $metadata document. It contains the Entity Data Model (EDM) describing all entity types, relationships, and operations available in the service.

func ParseCSDLJSON added in v0.2.28

func ParseCSDLJSON(data []byte) (*Metadata, error)

ParseCSDLJSON parses an OData CSDL JSON v4.01 document and returns the service metadata. The input may be a full service document or just the $metadata endpoint response in JSON format.

CSDL JSON is the JSON alternative to the XML EDMX format used by services such as Microsoft Graph. The top-level object contains $Version and $EntityContainer annotations alongside schema namespace objects whose entries describe entity types, complex types, enum types, actions, functions, and entity containers.

Returns ErrMetadataInvalid wrapped in the error if the document cannot be decoded as valid JSON.

func ParseCSDLJSONReader added in v0.2.28

func ParseCSDLJSONReader(r io.Reader) (*Metadata, error)

ParseCSDLJSONReader is like ParseCSDLJSON but reads from an io.Reader.

ParseCSDLJSONReader is convenient when the CSDL JSON document is available as an HTTP response body or other streaming source.

func ParseEDMX

func ParseEDMX(reader io.Reader) (*Metadata, error)

ParseEDMX parses an OData $metadata XML response in EDMX format.

ParseEDMX reads an OData service's metadata document (typically from $metadata endpoint) and extracts schema information including entity types, entity sets, properties, navigation properties, and associations.

The EDMX (Entity Data Model XML) format is the standard way OData services describe their data model. This parser supports both OData v2 and v4 metadata formats with special handling for SAP annotations (sap:label, sap:sortable, sap:filterable, sap:searchable, sap:required-in-filter, sap:text, sap:unit, sap:value-list, sap:display-format, sap:field-control, sap:semantics, sap:key, sap:updatable-path, and entity-set-level sap:creatable, sap:updatable, sap:deletable, sap:pageable, sap:addressable, sap:requires-filter, sap:change-tracking).

The parser extracts:

  • Entity types with their properties (name, type, nullable, length constraints)
  • Entity sets (the exposed data sources)
  • Navigation properties (relationships between entities)
  • Associations (cardinality and relationship definitions)
  • SAP-specific annotations for UI rendering hints

Returns a Metadata struct containing the parsed schema information, or an error if XML parsing fails.

Example:

resp, _ := http.Get("https://odata.service/$metadata")
metadata, err := ParseEDMX(resp.Body)
for _, et := range metadata.EntityTypes {
	fmt.Println("Entity:", et.Name, "Key:", et.Key)
}
type NavigationBinding struct {
	// Path is the navigation property name or path.
	Path string
	// Target is the name of the target entity set.
	Target string
}

NavigationBinding maps a navigation property path to its target entity set.

type NavigationProperty struct {
	// Name is the name of the navigation property.
	Name string
	// FromEntityType is the entity type this property originates from.
	FromEntityType string
	// ToEntityType is the target entity type.
	ToEntityType string
	// FromMultiplicity describes the multiplicity from the source (0..1, 1, *).
	FromMultiplicity string
	// ToMultiplicity describes the multiplicity to the target (0..1, 1, *).
	ToMultiplicity string
	// Partner is the name of the inverse navigation property.
	Partner string
}

NavigationProperty represents a navigation property in an OData entity type. Navigation properties define relationships to other entity types.

type NoOpCache

type NoOpCache struct{}

NoOpCache is a no-op cache implementation that doesn't cache anything.

NoOpCache is useful for development, testing, or when caching is not desired. It always returns cache misses and ignores Set/Clear operations.

func (*NoOpCache) Clear

func (c *NoOpCache) Clear()

Clear is a no-op.

func (*NoOpCache) Get

func (c *NoOpCache) Get(key string) (*Metadata, bool)

Get returns nil, false (cache miss).

func (*NoOpCache) Set

func (c *NoOpCache) Set(key string, metadata *Metadata)

Set is a no-op.

type ODataError

type ODataError struct {
	Code       string                 `json:"code"`
	Message    string                 `json:"message"`
	Target     string                 `json:"target,omitempty"`
	Details    []ODataErrorDetail     `json:"details,omitempty"`
	InnerError map[string]interface{} `json:"innererror,omitempty"` // SAP extensions
}

ODataError represents an error returned by an OData service.

ODataError encapsulates structured error information from OData responses, including error codes, messages, affected targets, and optional details. It also includes support for SAP-specific inner error extensions.

OData services return errors in a standardized JSON format, which traverse parses and wraps in this type for easier error handling and inspection.

Example:

err := client.From("InvalidSet").First(ctx)
if odErr, ok := IsODataError(err); ok {
	fmt.Println("OData Error:", odErr.Code, odErr.Message)
	for _, detail := range odErr.Details {
		fmt.Println("Detail:", detail.Code, detail.Message)
	}
}

func IsODataError

func IsODataError(err error) (*ODataError, bool)

IsODataError extracts an *ODataError from an error value.

IsODataError uses errors.As to unwrap and retrieve the *ODataError from a potentially wrapped error. Returns the *ODataError and true if found, otherwise returns nil and false.

Example:

if odErr, ok := IsODataError(err); ok {
	fmt.Println("Code:", odErr.Code)
}

func (*ODataError) Error

func (e *ODataError) Error() string

Error implements the error interface.

Error formats the ODataError into a human-readable string for logging and display. It includes the code (if available), message, and traverse prefix.

type ODataErrorDetail

type ODataErrorDetail struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Target  string `json:"target,omitempty"`
}

ODataErrorDetail represents a detail in an OData error response.

ODataErrorDetail provides granular error information for specific fields or sub-operations that failed within a larger request.

type ODataVersion

type ODataVersion int

ODataVersion represents the OData protocol version.

ODataVersion is used to distinguish between different OData protocol versions supported by different SAP systems. Version detection can happen automatically during query execution via HTTP headers.

const (
	ODataV2 ODataVersion = 2 // SAP NetWeaver Gateway, OData v2 (legacy)
	ODataV4 ODataVersion = 4 // SAP S/4HANA, OData v4 (standard)
)

func AutoDetectODataVersion

func AutoDetectODataVersion(headers map[string][]string) ODataVersion

AutoDetectODataVersion attempts to detect the OData version from the response. Checks the "OData-Version" header first, then falls back to structural detection. AutoDetectODataVersion detects the OData version from HTTP response headers.

AutoDetectODataVersion examines the "Odata-Version" and "Dataserviceversion" headers to determine whether the response follows OData v2 or v4 conventions.

Returns ODataV4 as default if the version cannot be determined from headers.

Supported versions:

  • OData v4: "Odata-Version: 4.0"
  • OData v2: "Odata-Version: 2.0" or "Dataserviceversion: 2.0"

type Option

type Option func(*clientConfig) error

Option is a functional option for configuring a Client.

Option implements the functional options pattern for flexible client construction, allowing callers to customize client behavior without modifying the New signature.

Example:

client, _ := traverse.New(
	"https://odata.service/",
	traverse.WithODataVersion(traverse.ODataV4),
	traverse.WithPageSize(1000),
	traverse.WithBasicAuth("user", "pass"),
)

func WithAPIKey

func WithAPIKey(header, value string) Option

WithAPIKey sets API key authentication using a custom header.

WithAPIKey adds a custom header with the provided key/value to all requests. This is useful for services that require API keys in custom headers (e.g., X-API-Key, X-Custom-Auth-Token).

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("https://api.service/odata/"),
	traverse.WithAPIKey("X-API-Key", "sk-abcd1234..."),
)

func WithAfterExecute

func WithAfterExecute(hook func(*QueryBuilder) error) Option

WithAfterExecute adds a hook function that is called after executing each query.

WithAfterExecute registers a callback that fires immediately after each OData query completes. Multiple hooks can be added and are called in registration order. The hook receives the QueryBuilder and can access query results, state, or perform actions after execution.

Useful for:

  • Collecting metrics or statistics
  • Logging or tracing
  • Implementing response validation
  • Cleanup operations

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithAfterExecute(func(qb *traverse.QueryBuilder) error {
		metrics.RecordQuery(qb.EntitySet())
		return nil
	}),
)

func WithBaseURL

func WithBaseURL(url string) Option

WithBaseURL sets the base URL of the OData service.

WithBaseURL is required; New() will return an error if no base URL is provided. The URL should be the root endpoint of the OData service (e.g., "https://sap.example.com/sap/opu/odata/sap/MY_SRV").

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("https://sap.example.com/sap/opu/odata/sap/Products"),
)

func WithBasicAuth

func WithBasicAuth(user, pass string) Option

WithBasicAuth sets HTTP Basic Authentication credentials.

WithBasicAuth adds a Basic Authorization header to all requests using the provided username and password. Credentials are base64-encoded.

Note: Basic auth should only be used over HTTPS to avoid credential exposure.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("https://sap.example.com/..."),
	traverse.WithBasicAuth("user@example.com", "password123"),
)

func WithBearerToken

func WithBearerToken(token string) Option

WithBearerToken sets a Bearer token for OAuth2/token-based authentication.

WithBearerToken adds a Bearer Authorization header to all requests using the provided token. Tokens are typically obtained from an OAuth2 token endpoint.

Example:

token := getOAuth2Token() // From OAuth2 provider
client, _ := traverse.New(
	traverse.WithBaseURL("https://odata.service/"),
	traverse.WithBearerToken(token),
)

func WithBeforeQuery

func WithBeforeQuery(hook func(*QueryBuilder) error) Option

WithBeforeQuery adds a hook function that is called before executing each query.

WithBeforeQuery registers a callback that fires immediately before each OData query is executed. Multiple hooks can be added and are called in registration order. The hook receives the QueryBuilder and can modify it, perform validation, or apply common filters/options.

Return an error from the hook to abort query execution.

Useful for:

  • Adding common filters or parameters
  • Logging or tracing
  • Implementing access control
  • Modifying query behavior dynamically

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithBeforeQuery(func(qb *traverse.QueryBuilder) error {
		qb.Logger(log.Printf) // Log all queries
		return nil
	}),
)

func WithCapabilitiesValidation added in v0.2.21

func WithCapabilitiesValidation(registry *CapabilitiesRegistry) Option

WithCapabilitiesValidation enables capability checking on the client.

func WithCircuitBreaker added in v0.2.0

func WithCircuitBreaker(cfg *relay.CircuitBreakerConfig) Option

WithCircuitBreaker enables the circuit-breaker pattern for all requests.

The circuit breaker opens after relay.CircuitBreakerConfig.MaxFailures consecutive failures, preventing further requests until the reset timeout elapses. This protects downstream OData services from cascading failure.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithCircuitBreaker(&relay.CircuitBreakerConfig{
        MaxFailures:      5,
        ResetTimeout:     30 * time.Second,
        HalfOpenRequests: 2,
        OnStateChange: func(from, to relay.CircuitBreakerState) {
            log.Printf("circuit breaker: %s → %s", from, to)
        },
    }),
)

func WithConnectTimeout added in v0.2.0

func WithConnectTimeout(d time.Duration) Option

WithConnectTimeout sets the TCP/TLS connection timeout independently of the overall transfer timeout set by WithTimeout.

Use WithTimeout to limit the total time for a request (including reading the response body). Use WithConnectTimeout to limit only the time spent establishing the connection and reading response headers.

The default is 30 seconds. Setting 0 disables the connection timeout.

Example:

// 5-second connection deadline, no overall transfer limit
traverse.WithConnectTimeout(5 * time.Second)

func WithCookieJar added in v0.2.0

func WithCookieJar(jar http.CookieJar) Option

WithCookieJar sets a custom net/http.CookieJar for session-based authentication.

Many OData services (particularly SAP) rely on session cookies after an initial CSRF token handshake. Providing a cookie jar allows traverse to maintain and reuse those session cookies across requests automatically.

Example - use the standard library cookie jar:

jar, _ := cookiejar.New(nil)
traverse.WithCookieJar(jar)

func WithFormat

func WithFormat(f ResponseFormat) Option

WithFormat sets the OData response format (FormatJSON or FormatAtom).

WithFormat controls the $format parameter sent to the OData service. Defaults to FormatJSON if not specified. Most modern OData services prefer JSON format.

- FormatJSON: Returns responses as JSON (recommended for performance) - FormatAtom: Returns responses as Atom+XML (legacy format)

func WithHTTPOption added in v0.2.26

func WithHTTPOption(opt relay.Option) Option

WithHTTPOption passes a raw relay.Option through to the underlying relay client.

Use this to inject low-level relay transport options that do not have a dedicated traverse wrapper - for example, audit trail or custom transport middleware provided by a third-party extension.

Example:

import "github.com/jhonsferg/traverse/ext/audit"

logger := audit.AuditLoggerFunc(func(ctx context.Context, e audit.AuditEntry) {
    log.Printf("[AUDIT] %s %s %d", e.Operation, e.EntitySet, e.StatusCode)
})
client, _ := traverse.New(url,
    traverse.WithHTTPOption(audit.WithAuditTrail(logger)),
)

func WithHeader

func WithHeader(key, value string) Option

WithHeader adds a custom HTTP header to all requests made by this Client.

WithHeader can be called multiple times to add multiple custom headers. Useful for setting headers like User-Agent, Accept-Language, or custom tracking IDs.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithHeader("User-Agent", "MyApp/1.0"),
	traverse.WithHeader("X-Tracking-ID", "12345"),
)

func WithLogger

func WithLogger(l relay.Logger) Option

WithLogger sets a relay.Logger for diagnostic output.

WithLogger enables logging of HTTP requests/responses and other diagnostic information. If not provided, logging is disabled. Useful for debugging authentication issues or tracing OData query execution.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithLogger(customLogger),
)

func WithMaxPages added in v0.13.26

func WithMaxPages(n int) Option

WithMaxPages sets the maximum number of pages to follow when paginating via nextLink. This guards against servers that always return a nextLink (whether by misconfiguration or malice), which would otherwise cause an infinite loop.

The default is [defaultMaxPages] (10,000). Omit or pass a positive value. A value of 1 fetches only the first page regardless of nextLink.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithMaxPages(100), // cap at 100 pages (e.g. 500k records at pageSize=5000)
)

func WithMaxRedirects added in v0.2.0

func WithMaxRedirects(n int) Option

WithMaxRedirects sets the maximum number of HTTP redirects to follow.

The default is 10. Set to 0 to disable redirect following entirely.

Example:

traverse.WithMaxRedirects(0) // never follow redirects

func WithMetadataCache

func WithMetadataCache(cache CacheStore) Option

WithMetadataCache sets a custom metadata cache implementation.

WithMetadataCache allows you to provide a CacheStore implementation for caching OData service metadata. By default, metadata is cached after the first fetch using NewMemoryCache. This option allows distributed caching or custom expiration policies.

Example implementations: Redis, memcached, file-based, or SQL database backends.

Example with memory cache:

cache := traverse.NewMemoryCache()
client, err := traverse.New(
    traverse.WithBaseURL("https://odata.service/"),
    traverse.WithMetadataCache(cache),
)

func WithODataErrors added in v0.2.0

func WithODataErrors() Option

WithODataErrors configures the client to decode OData error responses into structured ODataError values.

When enabled, any 4xx or 5xx response whose body follows the OData v2 or v4 error format is decoded into an ODataError instead of a generic HTTP error. Use IsODataError to inspect the result.

This option is recommended for all production clients as it surfaces the OData error code and message directly from the service.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithODataErrors(),
)

_, err := client.From("InvalidEntity").First(ctx)
if odErr, ok := traverse.IsODataError(err); ok {
    fmt.Println("OData error code:", odErr.Code)
}

func WithODataVersion

func WithODataVersion(v ODataVersion) Option

WithODataVersion sets the OData protocol version (ODataV2 or ODataV4).

WithODataVersion controls how requests are constructed and responses are parsed. Defaults to ODataV4 if not specified.

- ODataV2: Used by classic SAP ABAP Gateway; responses wrapped in "d" object - ODataV4: Modern protocol used by S/4HANA and most new OData services

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithODataVersion(traverse.ODataV2), // For legacy SAP systems
)

func WithPageSize

func WithPageSize(n int) Option

WithPageSize sets the number of records fetched per HTTP request during pagination.

WithPageSize controls the $top parameter sent to the server. Defaults to 1000 if not specified. Larger values reduce network round-trips but consume more memory per request. Smaller values reduce per-request memory but increase latency due to more requests.

The server may reject the value or return fewer records; OData clients should handle the actual page size in responses.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithPageSize(5000), // Fetch 5000 records per request
)

func WithProxy added in v0.2.0

func WithProxy(proxyURL string) Option

WithProxy sets an HTTP or HTTPS proxy for all outgoing requests.

proxyURL must be a fully qualified URL, e.g. "http://proxy.corp.com:3128" or "https://user:pass@proxy.corp.com:8080".

Example:

traverse.WithProxy("http://proxy.internal.corp:3128")

func WithRateLimit added in v0.2.0

func WithRateLimit(rps float64, burst int) Option

WithRateLimit constrains the number of outgoing HTTP requests per second.

rps is the sustained request rate (e.g. 50.0 for 50 req/s). burst is the maximum number of requests allowed in a single instant above rps.

This is particularly useful when querying SAP OData services that enforce throttling via HTTP 429 responses.

Example:

// Allow 100 req/s with a burst of up to 110
traverse.WithRateLimit(100, 110)

func WithRelayClient

func WithRelayClient(rc *relay.Client) Option

WithRelayClient injects a pre-configured relay.Client for custom HTTP configuration.

WithRelayClient allows advanced users to configure HTTP behavior (connection pooling, custom transport, proxies, etc.) before passing the client to Traverse. If not provided, Traverse creates a default relay.Client.

When a relay client is provided, WithBaseURL on traverse is optional: the base URL is automatically inherited from the relay client's own relay.WithBaseURL option. This means you do NOT need to repeat the base URL in both clients.

Any traverse-level options that map to relay behaviour (e.g. WithHeader, WithTimeout, [WithBeforeRequest]) are applied on top of the provided relay client via relay.Client.With, so they are not silently dropped.

This is useful for:

  • Configuring connection pooling for large-scale operations
  • Setting up proxy or TLS configuration
  • Using a relay client shared across multiple services

Example - base URL inherited automatically:

httpClient := relay.New(
	relay.WithTimeout(60 * time.Second),
	relay.WithBaseURL("https://sap.example.com/sap/opu/odata/sap"),
)
// No traverse.WithBaseURL needed - inherited from httpClient
client, _ := traverse.New(
	traverse.WithRelayClient(httpClient),
	traverse.WithODataVersion(traverse.ODataV2),
)

func WithRequestHook added in v0.2.0

func WithRequestHook(fn func(context.Context, *relay.Request) error) Option

WithRequestHook registers a low-level HTTP hook called before every request.

The hook receives the underlying relay.Request and may modify headers, inject tracing metadata, or cancel the context. It is called after OData-level hooks (WithBeforeQuery) but before the request is sent over the wire.

To register OData-level hooks (called before query building), use WithBeforeQuery instead.

Example - add a request-ID header to every request:

traverse.WithRequestHook(func(ctx context.Context, r *relay.Request) error {
    r.WithHeader("X-Request-ID", uuid.New().String())
    return nil
})

func WithResponseCache added in v0.2.25

func WithResponseCache(cache ResponseCache) Option

WithResponseCache sets a custom HTTP-level response cache for entity set queries.

WithResponseCache attaches a ResponseCache to the client. When a QueryBuilder uses QueryBuilder.WithCache, query responses are stored in this cache and served on subsequent identical requests until the TTL expires.

The default is no response cache. Provide NewInMemoryResponseCache for a simple in-process cache, or implement ResponseCache for a distributed backend.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithResponseCache(traverse.NewInMemoryResponseCache()),
)

// Cache Products queries for 5 minutes
products, _ := client.From("Products").
    WithCache(5 * time.Minute).
    Collect(ctx)

func WithResponseHook added in v0.2.0

func WithResponseHook(fn func(context.Context, *relay.Response) error) Option

WithResponseHook registers a low-level HTTP hook called after every response.

The hook receives the underlying relay.Response and may inspect headers, record metrics, or return an error to abort further processing. It is called before OData-level hooks (WithAfterExecute).

Example - record response latency to a metrics system:

traverse.WithResponseHook(func(ctx context.Context, r *relay.Response) error {
    metrics.Record("odata.latency", r.Timing.Total)
    return nil
})

func WithRetry added in v0.2.0

func WithRetry(rc *relay.RetryConfig) Option

WithRetry configures automatic retry behaviour for transient failures.

Relay will retry requests that fail with a network error or a 429/5xx status code. The retry strategy is exponential back-off with jitter.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithRetry(&relay.RetryConfig{
        MaxAttempts:     4,
        InitialInterval: 200 * time.Millisecond,
        MaxInterval:     10 * time.Second,
        Multiplier:      2.0,
        RandomFactor:    0.3,
    }),
)

func WithSchemaVersion added in v0.6.0

func WithSchemaVersion(version string) Option

WithSchemaVersion sets the OData-SchemaVersion request header on all requests made by this Client.

OData 4.01 (spec section 8.2.10) introduced this header to allow clients to request a specific version of the service's metadata schema. Services that evolve their schema over time can use this to serve different schema versions to different clients simultaneously.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://api.example.com/odata/"),
    traverse.WithSchemaVersion("2.0"),
)

func WithSigner added in v0.2.0

func WithSigner(s relay.RequestSigner) Option

WithSigner sets a relay.RequestSigner that signs every outgoing HTTP request.

This is useful for OData services that require request signing such as AWS API Gateway (SigV4) or custom HMAC schemes.

Example - sign requests with a custom HMAC scheme:

traverse.WithSigner(relay.RequestSignerFunc(func(r *http.Request) error {
    mac := hmac.New(sha256.New, secretKey)
    mac.Write([]byte(r.Method + r.URL.Path))
    r.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
    return nil
}))

func WithTLSConfig added in v0.2.0

func WithTLSConfig(cfg *tls.Config) Option

WithTLSConfig sets a custom crypto/tls.Config for the HTTP transport.

Use this to configure mutual TLS, disable certificate verification for on-premise services with self-signed certificates, or pin specific CAs.

Example - disable TLS verification for a local development OData service:

traverse.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) // #nosec G402

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the request timeout for all HTTP requests.

WithTimeout controls the timeout for individual OData requests. Defaults to 30 seconds if not specified. Applies to query execution, metadata fetching, and all CRUD operations.

For operations that process millions of records with streaming, consider a longer timeout.

Example:

client, _ := traverse.New(
	traverse.WithBaseURL("..."),
	traverse.WithTimeout(60 * time.Second),
)

type Page

type Page struct {
	// Value is the slice of entities in this page
	Value []map[string]interface{}
	// RawValue stores raw JSON for direct unmarshaling (optimization)
	RawValue []json.RawMessage
	// NextLink is the URL to fetch the next page (@odata.nextLink in v4, __next in v2)
	NextLink string
	// DeltaLink is the URL for delta queries for incremental sync (@odata.deltaLink)
	DeltaLink string
	// Count is the total count of matching records if $count=true was used
	Count *int64
	// Context is the @odata.context value (OData v4 only)
	Context string
}

Page represents a single page of OData results with pagination metadata.

Page is returned by QueryBuilder.Page and contains the results for one page of a query, along with metadata for fetching subsequent pages or performing incremental updates.

Fields:

  • Value: slice of entities for this page
  • RawValue: slice of raw JSON for each entity (for optimization purposes)
  • NextLink: URL to fetch the next page (if more records exist)
  • DeltaLink: URL for delta queries to fetch only changed records
  • Count: total count of all matching records (if WithCount was used)
  • Context: OData context URI (v4 only)

func QueryParallel

func QueryParallel(ctx context.Context, queries ...*QueryBuilder) ([]Page, error)

QueryParallel executes multiple queries concurrently and returns results in order.

QueryParallel takes multiple QueryBuilder queries and executes them in parallel using goroutines. Results are returned in the same order as the input queries, making it easy to correlate results.

Each query is executed independently. If any query fails, QueryParallel returns the first error encountered; partial results are discarded. For independent error handling per query, execute queries separately with goroutines and collect errors manually.

Example:

q1 := client.For("Customers").Filter("Country eq 'USA'")
q2 := client.For("Orders").Filter("Status eq 'Completed'")
results, err := QueryParallel(ctx, q1, q2)
// results[0] is from q1, results[1] is from q2

type Paginator added in v0.2.5

type Paginator[T any] struct {
	// contains filtered or unexported fields
}

Paginator provides a typed, iterator-style interface for paging through an OData entity set one page at a time. It automatically follows @odata.nextLink (OData v4) or __next (OData v2) links between pages.

Obtain a Paginator via NewPaginator or [NewPaginatorJSON].

p := traverse.NewPaginator[Product](client.From("Products").Top(50))
for p.HasMorePages() {
    items, err := p.NextPage(ctx)
    if err != nil { break }
    for _, item := range items {
        process(item)
    }
}

A Paginator is NOT safe for concurrent use; use separate Paginators in separate goroutines.

func NewPaginator added in v0.2.5

func NewPaginator[T any](query *QueryBuilder) *Paginator[T]

NewPaginator creates a typed Paginator that deserialises each entity via json.Unmarshal into T. The query is executed lazily - no network call is made until the first call to Paginator.NextPage.

type Product struct {
    ID   int    `json:"ProductID"`
    Name string `json:"Name"`
}
p := traverse.NewPaginator[Product](client.From("Products").Top(100))

func NewPaginatorWithDecoder added in v0.2.5

func NewPaginatorWithDecoder[T any](query *QueryBuilder, decode func(json.RawMessage) (T, error)) *Paginator[T]

NewPaginatorWithDecoder creates a Paginator that uses a custom decode function to convert each raw JSON entity into T. Use this when you need non-standard unmarshalling (e.g. case-insensitive keys, custom types).

p := traverse.NewPaginatorWithDecoder[Product](
    client.From("Products"),
    func(raw json.RawMessage) (Product, error) {
        var p Product
        return p, json.Unmarshal(raw, &p)
    },
)

func (*Paginator[T]) HasMorePages added in v0.2.5

func (p *Paginator[T]) HasMorePages() bool

HasMorePages reports whether there are more pages to fetch. It returns true before the first call to Paginator.NextPage and after any page that included a nextLink. It returns false once the final page has been consumed.

func (*Paginator[T]) NextPage added in v0.2.5

func (p *Paginator[T]) NextPage(ctx context.Context) ([]T, error)

NextPage fetches the next page of results and returns a typed slice. On the first call the original query is executed; subsequent calls follow the nextLink returned by the server until no more pages are available.

After the last page is returned, Paginator.HasMorePages returns false and subsequent calls to NextPage return an empty slice with no error.

func (*Paginator[T]) Reset added in v0.2.5

func (p *Paginator[T]) Reset()

Reset resets the Paginator to the beginning of the result set, discarding any accumulated nextLink. The next call to Paginator.NextPage will re-issue the original query from the start.

func (*Paginator[T]) TotalCount added in v0.2.5

func (p *Paginator[T]) TotalCount(ctx context.Context) (int64, error)

TotalCount returns the total count of matching records as reported by the server (requires $count=true on the original query). Returns 0 if the server did not return a count. Note that this value is only available after the first page has been fetched.

type Property

type Property struct {
	// Name is the name of the property.
	Name string
	// Type is the OData type (e.g., "Edm.String", "Edm.Int32", "Edm.DateTime").
	Type string
	// Nullable indicates whether the property can have a null value.
	Nullable bool
	// MaxLength is the maximum string length (for string types).
	MaxLength *int
	// Precision is the number of significant digits (for numeric types).
	Precision *int
	// Scale is the number of decimal places (for decimal types).
	Scale *int
	// SAP contains SAP-specific annotations for this property.
	SAP SAPAnnotations
	// Core contains OData Core vocabulary annotations when present in metadata.
	Core *CoreVocabulary
	// Validation contains OData Validation vocabulary annotations when present in metadata.
	Validation *ValidationVocabulary
	// Measures contains OData Measures vocabulary annotations (unit-of-measure, currency, scale).
	Measures *MeasuresVocabulary
	// Analytics contains OData Aggregation/Analytics vocabulary annotations (dimension, measure, aggregation).
	Analytics *AnalyticsVocabulary
}

Property represents an OData property definition. Properties describe the data that can be stored in entities.

type PropertyRef

type PropertyRef struct {
	// Name is the name of the property.
	Name string
}

PropertyRef represents a reference to a property that is part of an entity's key.

type QueryBuilder

type QueryBuilder struct {
	// contains filtered or unexported fields
}

QueryBuilder builds and executes OData queries with a fluent, chainable API.

QueryBuilder provides an ergonomic interface for constructing complex OData queries with support for filtering, selection, ordering, pagination, expansion, and advanced features. All methods return the receiver (*QueryBuilder) to enable method chaining for readable, fluent query construction.

Core Query Methods (chainable):

  • Select(): Choose specific properties to return ($select)
  • Filter() / Where(): Add row filtering ($filter)
  • OrderBy() / OrderByDesc(): Sort results ($orderby)
  • Top(): Limit result count ($top for pagination)
  • Skip(): Skip records before returning ($skip for pagination)
  • Expand(): Include related entities ($expand)
  • WithCount(): Include total record count

Execution Methods (terminal operations):

  • Collect(ctx): Fetch all results at once into memory
  • Stream(ctx): Stream results one at a time (O(1) memory)
  • Page(ctx): Fetch one page of results
  • Count(ctx): Get total matching record count
  • Single(ctx): Get exactly one result (error if 0 or >1)
  • First(ctx): Get first result or nil

QueryBuilder is NOT thread-safe. Create separate QueryBuilders for concurrent queries. Modifications to a QueryBuilder during execution produce undefined behavior.

Performance Notes:

  • Lazy URL construction: URLs are rebuilt only when needed (marked by urlDirty flag)
  • Object pooling: Uses buffer and FilterBuilder pools to reduce allocations
  • Streaming support: Stream() provides O(1) memory for large datasets
  • Query hooks: Client-level hooks can intercept before/after query execution

Fluent API Examples:

Simple query with filtering and sorting:

results, err := client.From("Products").
	Filter("Price gt 100 and Category eq 'Electronics'").
	OrderBy("Price desc").
	Top(50).
	Select("ProductID", "Name", "Price").
	Collect(ctx)

Query with type-safe filtering:

results, err := client.From("Orders").
	Where("Status").Eq("Completed").
	Where("Amount").Gt(1000).
	OrderByDesc("CreatedDate").
	Expand("Customer").
	Collect(ctx)

Streaming large dataset (constant memory):

stream := client.From("LargeDataSet").
	Filter("Active eq true").
	Stream(ctx)
for result := range stream {
	if result.Error != nil {
		log.Printf("Error: %v", result.Error)
		break
	}
	processRecord(result.Value)
}

Pagination with streaming:

stream := client.From("Products").
	Top(1000).
	Skip(offset).
	Stream(ctx)

func (*QueryBuilder) Apply

func (q *QueryBuilder) Apply(expr string) *QueryBuilder

Apply applies an aggregation or data transformation using the $apply system query option.

Apply is only available in OData v4 and is used for complex data aggregation. It follows the OData Data Aggregation Extension specification. Apply is chainable and returns q for method chaining.

Example:

query.Apply("groupby((Region),aggregate(Sales with sum as Total))")  // OData v4 only

func (*QueryBuilder) AsType added in v0.6.0

func (q *QueryBuilder) AsType(typeName string) *QueryBuilder

AsType appends a type cast segment to the entity set path, restricting results to entities of the specified derived type.

AsType implements OData v4.0 spec section 4.3 (type cast segments) and is used to query polymorphic entity sets returning only instances of a derived type.

The typeName should be the fully-qualified OData type name, e.g. "Model.Manager". The resulting URL path becomes /EntitySet/Namespace.DerivedType.

Example:

// GET /Employees/Model.Manager?$select=Name,Budget
managers, err := client.From("Employees").AsType("Model.Manager").
    Select("Name", "Budget").Collect(ctx)

func (*QueryBuilder) BoundAction added in v0.2.11

func (q *QueryBuilder) BoundAction(name string) *ActionBuilder

BoundAction creates a builder for an OData action bound to this entity set.

The action URL is constructed as: /<entitySet>/<name> Parameters are sent as a JSON body. This is typically used for namespace-qualified actions bound to a collection or a specific entity, for example:

// Bound to collection: POST /Products/Namespace.BulkDiscount {"percent": 10}
var result ActionResult
err := client.From("Products").
	BoundAction("Namespace.BulkDiscount").
	Param("percent", 10).
	Invoke(ctx, &result)

func (*QueryBuilder) BoundFunction added in v0.2.11

func (q *QueryBuilder) BoundFunction(name string) *FunctionBuilder

BoundFunction creates a builder for an OData function bound to this entity set.

The function URL is constructed as: /<entitySet>/<name>(params...) This is typically used for namespace-qualified functions bound to a collection or a specific entity, for example:

// Bound to collection: /Products/Namespace.GetDiscounted(threshold=10)
var result MyResult
err := client.From("Products").
	BoundFunction("Namespace.GetDiscounted").
	Param("threshold", 10).
	Invoke(ctx, &result)

func (*QueryBuilder) BulkDelete added in v0.2.27

func (q *QueryBuilder) BulkDelete(ctx context.Context) error

BulkDelete deletes all entities in the entity set that match the current filter.

BulkDelete sends a DELETE request against the collection URL with any $filter already set on the QueryBuilder (OData v4.01 collection DELETE pattern).

This is useful for removing many records at once, such as purging old log entries, without fetching them first.

Example:

err := client.From("TempLogs").Filter("CreatedAt lt 2024-01-01").BulkDelete(ctx)

func (*QueryBuilder) BulkUpdate added in v0.6.0

func (q *QueryBuilder) BulkUpdate(ctx context.Context, data interface{}) error

BulkUpdate updates all entities in the entity set that match the current filter by applying the provided partial update data.

BulkUpdate sends a PATCH request against the collection URL with any $filter already set on the QueryBuilder (OData v4.01 section 11.4.13 - collection PATCH).

The data parameter can be any JSON-serializable value: a map, a typed struct, or a pointer to a struct. Only the fields present in data are updated on each matching entity; other fields are left unchanged.

BulkUpdate returns ErrEntityNotFound when no entities match the filter, and ErrConcurrencyConflict on a 409 or 412 response.

Example:

// Archive all discontinued products
err := client.From("Products").
    Filter("Category eq 'Discontinued'").
    BulkUpdate(ctx, map[string]any{"Status": "Archived"})

// Typed struct patch
err := client.From("Orders").
    Filter("Status eq 'Draft'").
    BulkUpdate(ctx, OrderPatch{Status: "Confirmed"})

func (*QueryBuilder) Collect

func (q *QueryBuilder) Collect(ctx context.Context) ([]map[string]interface{}, error)

Collect materializes all matching records into a single slice.

Collect streams all pages and accumulates all matching records into a single in-memory slice. It uses the Stream method internally for multi-page fetching and pre-allocates memory based on query parameters to reduce allocations.

Memory Usage: Collect loads the ENTIRE result set into memory. For large result sets, memory usage can be problematic:

  • 1M records × 1 KB each = ~1 GB
  • 10M records × 1 KB each = ~10 GB

Use Stream() for large datasets to avoid memory exhaustion.

When to Use Collect:

  • Dataset is known to be small (<10K records)
  • Top() limits the result set
  • Where() filters results to manageable size
  • API endpoint or demo code

When NOT to Use Collect:

  • Unbounded queries on large tables
  • Production code handling user input
  • Processing 100K+ records

Capacity Estimation: If Top() is set, Collect pre-allocates that capacity plus any Skip() offset. Otherwise defaults to 1000 record estimate. This reduces allocation overhead.

Error Handling: If any page fails to fetch, the partial results accumulated so far are lost and the error is returned.

Returns:

  • []map[string]interface{}: All matching records
  • error: If any page fetch fails

Examples:

OK: Limited result set:

// Collect with explicit limit
products, _ := client.From("Products").
	Where("Category").Eq("Electronics").
	Top(500).
	Collect(ctx)

DANGER: Unbounded query:

// WARNING: Could exhaust memory on large table!
all, _ := client.From("Products").Collect(ctx)

Safe filtered query:

// Filter reduces result set to manageable size
orders, _ := client.From("Orders").
	Where("OrderDate").Gt(startDate).
	Where("OrderDate").Lt(endDate).
	Collect(ctx)

Convert streaming to slice:

results := make([]map[string]interface{}, 0, 100)
for result := range client.From("Orders").Stream(ctx) {
	if result.Err != nil {
		return result.Err
	}
	results = append(results, result.Value)
}

func (*QueryBuilder) Compute added in v0.2.27

func (q *QueryBuilder) Compute(expressions ...string) *QueryBuilder

Compute adds a $compute expression to the OData query.

The $compute system query option (OData v4.01) allows defining computed properties that can be referenced in $select, $filter, and $orderby. Multiple calls append expressions separated by commas.

Compute is chainable and returns q for method chaining.

Example:

query.Compute("Price mul Quantity as Total")
query.Compute("Price mul Quantity as Total", "Tax div 100 as TaxRate")

func (*QueryBuilder) Count

func (q *QueryBuilder) Count(ctx context.Context) (int64, error)

Count returns the total count of matching records without transferring entity data.

Count executes a /$count query which returns only the total number of matching entities without downloading the actual entity data. This is the most efficient way to determine result set size, as it minimizes network traffic and processing.

Filters Applied: Count applies any filters set via Filter() or Where() methods. Select, OrderBy, Top, and Skip are ignored (meaningless for counts).

Use Cases:

  • Check if results exist: Count() > 0
  • Determine pagination requirements
  • Validate constraints before operations
  • Performance optimization: 364x faster than Collect for large datasets

Returns:

  • int64: Total number of matching records
  • error: If the query fails or server returns error

Performance: Ultra-fast operation returning just an integer. Typical time: 1-2ms even for millions of records. Ideal for existence checks and result set sizing.

Examples:

Check if any records match:

count, _ := client.From("Orders").
	Where("Status").Eq("Pending").
	Count(ctx)
if count > 0 {
	// Process pending orders
}

Pagination decision:

total, _ := client.From("Products").Count(ctx)
pageCount := (total + 99) / 100  // Ceiling division for 100-item pages

Filtered count:

activeCount, _ := client.From("Employees").
	Where("Department").Eq("Sales").
	Where("Active").Eq(true).
	Count(ctx)

func (*QueryBuilder) CreateDeep added in v0.2.8

func (q *QueryBuilder) CreateDeep(ctx context.Context, entity any) (*relay.Response, error)

CreateDeep performs an OData 4.01 deep insert of an entity with related entities.

CreateDeep POSTs the entity to the entity set, setting the required headers for deep insert: Content-Type is set to "application/json;odata.metadata=minimal" and Prefer is set to "return=representation". The caller constructs nested structs or maps with related entities embedded.

Returns the raw relay.Response so the caller can decode nested result structures.

Example:

type OrderLine struct {
    ProductID int    `json:"ProductID"`
    Quantity  int    `json:"Quantity"`
}
type Order struct {
    CustomerID string      `json:"CustomerID"`
    Lines      []OrderLine `json:"Lines"`
}
resp, err := client.From("Orders").CreateDeep(ctx, Order{
    CustomerID: "CUST1",
    Lines: []OrderLine{{ProductID: 1, Quantity: 5}},
})

func (*QueryBuilder) CreateDeepWithPrefer added in v0.2.8

func (q *QueryBuilder) CreateDeepWithPrefer(ctx context.Context, entity any, prefer string) (*relay.Response, error)

CreateDeepWithPrefer performs a deep insert with a custom Prefer header value.

Use this when you need full control over the Prefer header, for example to combine "return=representation" with "odata.continue-on-error", or to use a custom preference. Pass an empty string to omit the Prefer header.

opts := traverse.DeepInsertOptions{
    ReturnRepresentation: true,
    ContinueOnError:      true,
}
resp, err := client.From("Orders").
    CreateDeepWithPrefer(ctx, order, opts.PreferHeader())
func (q *QueryBuilder) DeleteLink(ctx context.Context, navProperty string, relatedKey any) error

DeleteLink removes a specific navigation link between two entities.

DeleteLink sends a DELETE request to:

/{EntitySet}({key})/{navProperty}/$ref?$id={relatedKey}

This removes the relationship between the source entity and the target entity without deleting either entity from the data store.

Parameters:

  • ctx: Request context
  • navProperty: Name of the navigation property on the source entity
  • relatedKey: Primary key of the target entity to unlink

Example:

// Remove item 5 from order 1's Items collection
err := client.From("Orders").Key(1).DeleteLink(ctx, "Items", 5)
func (q *QueryBuilder) DeleteLinks(ctx context.Context, navProperty string) error

DeleteLinks removes all navigation links for a given navigation property.

DeleteLinks sends a DELETE request to:

/{EntitySet}({key})/{navProperty}/$ref

This removes all relationships for the navigation property without deleting any of the related entities.

Parameters:

  • ctx: Request context
  • navProperty: Name of the navigation property whose links should be removed

Example:

// Remove all items from order 1's Items collection
err := client.From("Orders").Key(1).DeleteLinks(ctx, "Items")

func (*QueryBuilder) Expand

func (q *QueryBuilder) Expand(navProp string, opts ...ExpandOption) *QueryBuilder

Expand adds a $expand clause for including related entities in the response.

Expand specifies navigation properties (relationships to other entities) to include in the OData response using the $expand system query option. Related entities are returned nested within the parent entity rather than requiring separate requests.

Navigation Properties: A navigation property represents a relationship to another entity type. For example, a Customer entity might have navigation properties to Orders, Addresses, Payments, etc.

Multiple Expand Calls: Calling Expand() multiple times accumulates properties. Expand("Orders") then Expand("Addresses") includes both Orders and Addresses relationships.

Nested Expansion Options: Expand supports optional ExpandOption parameters for filtering and selecting specific properties from related entities:

  • WithExpandSelect(): Choose specific properties from related entities
  • WithExpandFilter(): Filter related entities
  • WithExpandOrderBy(): Sort related entities
  • WithExpandTop(): Limit related entity count

Performance Considerations:

  • Related entities increase response size; use Select() to limit properties
  • Nested Expand can exponentially increase response size; be careful
  • Use carefully with large entity collections (100k+ records)

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes

Examples:

Simple expand (include all related entities):

query.Expand("Orders")
// OData: $expand=Orders

Multiple navigation properties:

query.Expand("Orders").Expand("Addresses")
// OData: $expand=Orders,Addresses

Expand with nested options:

query.Expand("Orders",
	WithExpandSelect("OrderID", "Amount"),
	WithExpandFilter("Status eq 'Completed'"))
// OData: $expand=Orders($select=OrderID,Amount;$filter=Status eq 'Completed')

Complex query with expand and filtering:

results, _ := client.From("Customers").
	Where("Country").Eq("USA").
	Select("CustomerID", "Name").
	Expand("Orders", WithExpandSelect("OrderID", "Amount")).
	Collect(ctx)

func (*QueryBuilder) ExpandNested added in v0.2.6

func (q *QueryBuilder) ExpandNested(property string) *ExpandBuilder

ExpandNested begins a nested expand configuration for the given navigation property. Call the builder methods to configure sub-query options, then call Done() to return to the parent QueryBuilder.

query := client.From("Orders").
    ExpandNested("Items").
        Select("ID", "Qty").
        Filter("Qty gt 0").
        Top(10).
    Done()

func (*QueryBuilder) Filter

func (q *QueryBuilder) Filter(expr string) *QueryBuilder

Filter adds a raw OData filter expression using the $filter system query option.

Filter specifies a raw OData filter expression for row filtering. The filter expression should be a valid OData v2 or v4 filter expression following OData specification syntax.

Filter vs Where:

  • Filter(): For raw OData expressions; caller responsible for syntax
  • Where(): Type-safe builder interface; prevents syntax errors

Multiple Filter Calls: If Filter is called multiple times, the last expression replaces previous ones. Use the Where() method for type-safe, chainable filtering instead.

Valid Operators:

  • Comparison: eq (equals), ne (not equals), gt, ge, lt, le
  • Logical: and, or, not
  • String: startswith, endswith, contains, substring, length, toupper, tolower
  • Math: add, sub, mul, div, mod, round, floor, ceiling
  • Date: year, month, day, hour, minute, second, now
  • Membership: in, has (enum flags)

Escaping: String literals in expressions must be single-quoted with internal quotes doubled:

'John''s Company' for the value John's Company

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes (but replaces previous filter)

Examples:

Simple comparison:

query.Filter("Age gt 30")
// OData: $filter=Age gt 30

Complex expression with AND/OR:

query.Filter("Region eq 'North' and Status ne 'Inactive'")
// OData: $filter=Region eq 'North' and Status ne 'Inactive'

String functions:

query.Filter("startswith(CompanyName, 'Alpine')")
// OData: $filter=startswith(CompanyName, 'Alpine')

Date/time filtering:

query.Filter("year(OrderDate) eq 2024")
// OData: $filter=year(OrderDate) eq 2024

For safer filtering without syntax errors, use Where() instead:

query.Where("Age").Gt(30).Where("Status").Ne("Inactive")

func (*QueryBuilder) FilterBy added in v0.2.14

func (q *QueryBuilder) FilterBy(expr *FilterExpr) *QueryBuilder

FilterBy sets the $filter using a type-safe FilterExpr.

FilterBy accepts a *FilterExpr (built via F() and logical combinators And/Or/Not) and sets it as the query filter. This method provides a fluent alternative to Filter() with full type safety and composability.

Type Safety: FilterBy prevents string concatenation errors by using the FilterExpr builder API. Filters are built programmatically using F(), And(), Or(), and Not() functions.

Validation: If the QueryBuilder has a schema set, the filter expression will be validated.

Example:

expr := And(F("Age").Ge(18), F("Active").Eq(true))
query.FilterBy(expr)
// OData: $filter=(Age ge 18) and (Active eq true)

func (*QueryBuilder) FilterLambda added in v0.2.8

func (q *QueryBuilder) FilterLambda(expression string) *QueryBuilder

FilterLambda appends a raw lambda expression string to the query filter. Use this for complex lambda expressions that cannot be expressed through the typed LambdaAny/LambdaAll builders.

The expression is combined with any existing filter using "and".

query.FilterLambda("tags/any(t: t/name eq 'admin')")

func (*QueryBuilder) FindByCompositeKey

func (q *QueryBuilder) FindByCompositeKey(ctx context.Context, keys map[string]interface{}) (map[string]interface{}, error)

FindByCompositeKey retrieves a single entity by a composite primary key.

FindByCompositeKey fetches the entity using a composite key where multiple properties form the primary key. The keys parameter should be a map of property names to their values.

For single-key entities, use QueryBuilder.FindByKey instead. FindByCompositeKey returns an error if keys is empty or if the server cannot find an entity matching the composite key.

Example:

entity, err := query.FindByCompositeKey(ctx, map[string]interface{}{
	"Company": "ABC",
	"Material": "XYZ",
})

func (*QueryBuilder) FindByKey

func (q *QueryBuilder) FindByKey(ctx context.Context, key interface{}) (map[string]interface{}, error)

FindByKey retrieves a single entity by its primary key.

FindByKey fetches the entity directly using the OData GET endpoint with the key in the URL path: GET /<EntitySet>(<key>)

The key parameter should be the primary key value. For composite keys, use QueryBuilder.FindByCompositeKey instead. FindByKey returns ErrEntityNotFound if no entity matches the key.

Example:

entity, err := query.FindByKey(ctx, 42)  // Single key

func (*QueryBuilder) First

func (q *QueryBuilder) First(ctx context.Context) (map[string]interface{}, error)

First executes the query and returns only the first matching entity.

First automatically adds $top=1 to the query and returns the first matching entity without fetching additional results. It returns ErrEntityNotFound if no matching entities exist.

First is a convenience method for queries where only the first result matters. It's equivalent to calling Page().Value[0] but with built-in error handling.

Efficiency: First is more efficient than Collect() when only one record is needed, since it limits the OData query to return just one result.

Returns:

  • map[string]interface{}: The first matching entity
  • error: ErrEntityNotFound if no matches, or other error on query failure

Common Patterns:

  • Get first item sorted by a key: OrderBy().First()
  • Get first matching filtered item: Where().First()
  • Check if any item exists: if _, err := query.First(ctx); err == nil { /* exists */ }

Examples:

Find first user by ID:

user, err := client.From("Users").
	Where("ID").Eq(42).
	First(ctx)
if errors.Is(err, traverse.ErrEntityNotFound) {
	log.Println("User not found")
}

Get most recently created record:

latest, _ := client.From("Orders").
	OrderByDesc("CreatedDate").
	First(ctx)
log.Printf("Latest order: %v", latest)

Check if user exists (ignore result):

_, err := client.From("Users").
	Where("Email").Eq("test@example.com").
	First(ctx)
if err == nil {
	log.Println("User already exists")
}

Safe pattern with filter verification:

result, err := client.From("Products").
	Where("SKU").Eq("ABC123").
	First(ctx)
if err != nil {
	return fmt.Errorf("product lookup failed: %w", err)
}
productID := result["ID"]

func (*QueryBuilder) GeoDistance added in v0.10.0

func (q *QueryBuilder) GeoDistance(property string, point GeographyPoint, operator string, distance float64) *QueryBuilder

GeoDistance adds a geo.distance() filter to the query.

GeoDistance filters entities whose spatial property is within (or beyond) the specified distance from the given geographic point.

Example:

client.From("Restaurants").
    GeoDistance("Location", traverse.GeographyPoint{Longitude: 13.408, Latitude: 52.518}, "le", 500)
// $filter=geo.distance(Location,geography'SRID=4326;POINT(13.408 52.518)') le 500

func (*QueryBuilder) GeoDistanceGeom added in v0.10.0

func (q *QueryBuilder) GeoDistanceGeom(property string, point GeometryPoint, operator string, distance float64) *QueryBuilder

GeoDistanceGeom adds a geo.distance() filter for geometry coordinates.

func (*QueryBuilder) GeoIntersects added in v0.10.0

func (q *QueryBuilder) GeoIntersects(property string, polygon GeographyPolygon) *QueryBuilder

GeoIntersects adds a geo.intersects() filter to the query.

GeoIntersects filters entities where the spatial point property lies within the given polygon.

Example:

poly := traverse.GeographyPolygon{ExteriorRing: []traverse.GeographyPoint{...}}
client.From("POI").GeoIntersects("Coordinates", poly)
// $filter=geo.intersects(Coordinates,geography'SRID=4326;POLYGON(...)')

func (*QueryBuilder) GeoIntersectsGeom added in v0.10.0

func (q *QueryBuilder) GeoIntersectsGeom(property string, polygon GeometryPolygon) *QueryBuilder

GeoIntersectsGeom adds a geo.intersects() filter for geometry coordinates.

func (*QueryBuilder) GeoLength added in v0.10.0

func (q *QueryBuilder) GeoLength(property string, operator string, threshold float64) *QueryBuilder

GeoLength adds a geo.length() filter to the query.

GeoLength filters entities where the length of the spatial LineString property satisfies the comparison.

Example:

client.From("Routes").GeoLength("Path", "le", 50000)
// $filter=geo.length(Path) le 50000

func (*QueryBuilder) IfMatch added in v0.2.8

func (q *QueryBuilder) IfMatch(etag string) *QueryBuilder

IfMatch sets the If-Match conditional request header on the query. The server will only process the request if the entity's current ETag matches. Returns HTTP 412 Precondition Failed if the ETags do not match.

Commonly used to guard updates and deletes against concurrent modifications:

page, err := client.From("Orders").
    IfMatch(`W/"abc123"`).
    Page(ctx)

func (*QueryBuilder) IfModifiedSince added in v0.2.8

func (q *QueryBuilder) IfModifiedSince(t time.Time) *QueryBuilder

IfModifiedSince sets the If-Modified-Since conditional request header. The server will only return data if the resource has been modified after t. Returns HTTP 304 Not Modified if unchanged.

since := time.Now().Add(-24 * time.Hour)
page, err := client.From("Orders").
    IfModifiedSince(since).
    Page(ctx)

func (*QueryBuilder) IfNoneMatch added in v0.2.8

func (q *QueryBuilder) IfNoneMatch(etag string) *QueryBuilder

IfNoneMatch sets the If-None-Match conditional request header on the query. The server will only return data if none of the given ETags match the current entity. Used for cache validation: the server returns 304 Not Modified if the ETag matches.

page, err := client.From("Products").
    IfNoneMatch(`"xyz789"`).
    Page(ctx)

func (*QueryBuilder) IfUnmodifiedSince added in v0.2.8

func (q *QueryBuilder) IfUnmodifiedSince(t time.Time) *QueryBuilder

IfUnmodifiedSince sets the If-Unmodified-Since conditional request header. The server will only process the request if the resource has not been modified since t. Returns HTTP 412 Precondition Failed if it was modified.

checkpoint := time.Now().Add(-1 * time.Hour)
page, err := client.From("Products").
    IfUnmodifiedSince(checkpoint).
    Page(ctx)

func (*QueryBuilder) Key added in v0.2.27

func (q *QueryBuilder) Key(key any) *QueryBuilder

Key sets the entity key for operations that target a single entity, such as DeleteLink and DeleteLinks.

Key is chainable and returns q for method chaining.

Example:

err := client.From("Orders").Key(1).DeleteLink(ctx, "Items", 5)

func (*QueryBuilder) LambdaAll added in v0.2.8

func (q *QueryBuilder) LambdaAll(collectionField string, fn func(*LambdaBuilder)) *QueryBuilder

LambdaAll appends an OData "all" lambda filter to the query. The fn callback receives a LambdaBuilder; use Field() to build conditions.

Generated OData: collectionField/all(v: <expression>)

The result is combined with any existing filter using "and".

query.LambdaAll("Items", func(b *LambdaBuilder) {
    b.Field("Price").Gt(100)
})
// $filter=Items/all(i: i/Price gt 100)

func (*QueryBuilder) LambdaAny added in v0.2.8

func (q *QueryBuilder) LambdaAny(collectionField string, fn func(*LambdaBuilder)) *QueryBuilder

LambdaAny appends an OData "any" lambda filter to the query. The fn callback receives a LambdaBuilder; use Field() to build conditions.

Generated OData: collectionField/any(v: <expression>)

The result is combined with any existing filter using "and".

query.LambdaAny("Tags", func(b *LambdaBuilder) {
    b.Field("Name").Eq("admin")
})
// $filter=Tags/any(t: t/Name eq 'admin')

func (*QueryBuilder) LinkTo added in v0.2.10

func (q *QueryBuilder) LinkTo(ctx context.Context, key any, navProperty string, targetEntitySet string, targetKey any) error

LinkTo creates a reference link from the current entity to a target entity.

LinkTo sends a PUT request to EntitySet(key)/NavProperty/$ref with a body containing the OData ID of the target entity. This establishes a navigation property relationship between two existing entities.

OData: PUT /EntitySet(key)/NavProperty/$ref Body: {"@odata.id": "serviceRoot/TargetEntitySet(targetKey)"}

Parameters:

  • ctx: Request context
  • key: Primary key of the source entity
  • navProperty: Navigation property name on the source entity
  • targetEntitySet: Entity set name of the target entity
  • targetKey: Primary key of the target entity

Example:

err := client.From("Orders").LinkTo(ctx, 1, "Customer", "Customers", "ALFKI")

func (*QueryBuilder) NoCache added in v0.2.25

func (q *QueryBuilder) NoCache() *QueryBuilder

NoCache bypasses the cache for this request.

NoCache adds a Cache-Control: no-cache header to the outgoing request, forcing revalidation with the server even if a fresh cache entry exists in the relay-level HTTP cache. It does not affect the traverse-level ResponseCache; use QueryBuilder.WithCache with a zero TTL to skip that.

Use NoCache when you need guaranteed fresh data for a single query without disabling caching globally.

Example:

// Always fetch fresh data for this call
page, _ := client.From("Orders").
    NoCache().
    Page(ctx)

func (*QueryBuilder) OrderBy

func (q *QueryBuilder) OrderBy(field string) *QueryBuilder

OrderBy adds a $orderby clause to sort results in ascending order.

OrderBy specifies one or more properties to sort the result set in ascending order using the OData $orderby system query option. Multiple calls to OrderBy accumulate sort expressions; results are sorted first by the first OrderBy, then by the second, etc.

Sort Order: Results are sorted by properties in the order OrderBy methods are called:

  • First OrderBy: Primary sort key
  • Second OrderBy: Secondary sort key (within primary groups)
  • And so on...

For descending order, use OrderByDesc() instead.

Performance: OData services may create database indexes on frequently used sort keys. Sorting on non-indexed properties can be slow for large datasets.

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes (accumulates with subsequent OrderBy calls)

Examples:

Single sort:

query.OrderBy("LastName")
// OData: $orderby=LastName asc

Multiple sort keys (hierarchical):

query.OrderBy("LastName").OrderBy("FirstName")
// OData: $orderby=LastName asc,FirstName asc
// Sorts by last name first, then first name within each last name group

Descending sort:

query.OrderByDesc("CreatedDate")
// OData: $orderby=CreatedDate desc

Mixed ascending/descending:

query.OrderBy("Department").OrderByDesc("Salary")
// Sorts by department ascending, then salary descending within each department

Combined with filtering:

query.Where("Status").Eq("Active").
	OrderBy("Priority").
	OrderByDesc("CreatedDate").
	Top(100)

func (*QueryBuilder) OrderByDesc

func (q *QueryBuilder) OrderByDesc(field string) *QueryBuilder

OrderByDesc adds a $orderby clause to sort results in descending order.

OrderByDesc specifies one or more properties to sort the result set in descending order using the OData $orderby system query option. Multiple calls to OrderByDesc accumulate sort expressions; results are sorted first by the first OrderBy/OrderByDesc, then by the second, etc.

Sort Order: Results are sorted by properties in the order OrderBy/OrderByDesc methods are called:

  • First call: Primary sort key (ascending or descending)
  • Second call: Secondary sort key (ascending or descending)
  • And so on...

Mixing Ascending and Descending: You can mix OrderBy() (ascending) and OrderByDesc() (descending) in the same query. Each call maintains its own sort direction.

Performance: OData services may create database indexes on frequently used sort keys. Sorting on non-indexed properties can be slow for large datasets.

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes (accumulates with subsequent OrderBy/OrderByDesc calls)

Examples:

Single descending sort:

query.OrderByDesc("Price")
// OData: $orderby=Price desc

Multiple descending sorts:

query.OrderByDesc("DateCreated").OrderByDesc("Priority")
// OData: $orderby=DateCreated desc,Priority desc
// Most recent first, then highest priority within each date

Mixed ascending and descending:

query.OrderBy("Category").OrderByDesc("Revenue")
// OData: $orderby=Category asc,Revenue desc
// Categories A-Z, highest revenue first within each category

Complex query with multiple sort criteria:

results, _ := client.From("Orders").
	Where("Status").Eq("Shipped").
	OrderByDesc("OrderDate").
	OrderBy("CustomerName").
	Top(50).
	Collect(ctx)

func (*QueryBuilder) Page

func (q *QueryBuilder) Page(ctx context.Context) (*Page, error)

Page returns a single page of results with pagination metadata.

Page executes the query and returns one page of results along with pagination metadata. The returned Page struct contains:

  • Value: List of entities for this page (typically 1-1000 records)
  • NextLink: URL to fetch the next page (empty if this is the last page)
  • Count: Total count of matching records (only if WithCount() was called)
  • DeltaLink: URL for delta queries (OData v4 with delta queries only)

Pagination Workflow: Page is useful when implementing paginated interfaces or processing results in controlled chunks. Manually fetch pages using NextLink or use Stream() for automatic multi-page streaming.

Typical Pagination Loop:

  1. query.Top(pageSize).WithCount().Page(ctx) - Get first page with total count
  2. Check page.NextLink to see if more results exist
  3. Call next query with Skip((page-1)*pageSize) for subsequent pages
  4. Repeat until page.NextLink is empty

Alternative to Manual Pagination: For simpler code, use Collect() to fetch all results at once, or Stream() for memory-efficient processing of large datasets.

Returns:

  • *Page: Pagination metadata and results
  • error: If query fails or server returns error

Thread Safety: The returned Page and its Value slice are not thread-safe if modified. Create copies before sharing across goroutines.

Examples:

Get first page with total count:

page, _ := client.From("Customers").
	OrderBy("CustomerID").
	Top(25).
	WithCount().
	Page(ctx)
log.Printf("Page 1 of %d (25 records per page)", page.Count/25)

Manual pagination loop:

pageSize := 50
for page := 1; ; page++ {
	results, _ := client.From("Products").
		OrderBy("ProductID").
		Skip((page - 1) * pageSize).
		Top(pageSize).
		Page(ctx)
	if len(results.Value) == 0 {
		break
	}
	processPage(results.Value, page)
}

Using NextLink for pagination:

for url := initialURL; url != ""; {
	page, _ := client.From("Orders").Page(ctx)
	processBatch(page.Value)
	url = page.NextLink  // May require converting to query
}

func (*QueryBuilder) Param

func (q *QueryBuilder) Param(key, value string) *QueryBuilder

Param adds a custom query parameter to the request.

Param allows adding arbitrary query parameters beyond the standard OData options. If called multiple times with the same key, the last value replaces the previous one. Param is chainable and returns q for method chaining.

Note: Param uses lazy initialization to avoid allocating a params map until needed.

Example:

query.Param("api-version", "1.0")

func (*QueryBuilder) Search

func (q *QueryBuilder) Search(expr SearchExpression) *QueryBuilder

Search performs a full-text search using the $search system query option.

Search is only available in OData v4. Pass a SearchExpression built with SearchWord, SearchPhrase, SearchAnd, SearchOr, or SearchNot to construct complex search expressions. Use the package-level Search convenience function to create a simple word-or-phrase term from a plain string. Search is chainable and returns q for method chaining.

Examples:

// Simple word search
query.Search(traverse.SearchWord("mountain"))

// Combined expression
query.Search(traverse.SearchAnd(traverse.SearchWord("mountain"), traverse.SearchNot(traverse.SearchWord("bike"))))

func (*QueryBuilder) Select

func (q *QueryBuilder) Select(fields ...string) *QueryBuilder

Select limits the returned properties to only the specified fields.

Select specifies which properties should be included in the query response using the OData $select system query option. When Select is used, the OData service returns only the specified properties, reducing bandwidth and improving query performance.

If Select is called multiple times, fields are accumulated (not replaced). Calling Select("A", "B") then Select("C") results in properties A, B, and C being selected.

Useful for:

  • Reducing response size: Only fetch needed properties
  • Improving performance: Fewer properties to transmit and parse
  • Security: Exclude sensitive properties from result sets
  • Bandwidth optimization: Critical for mobile or limited bandwidth scenarios

If Select is never called, all properties are returned (default behavior).

Parameters:

  • fields: Property names to include (comma-separated is also valid: "ID,Name,Price")

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes

Examples:

Select specific fields:

query.Select("CustomerID", "Name", "Country")
// OData: $select=CustomerID,Name,Country

Multiple Select calls accumulate:

query.Select("ID", "Name").Select("Email")
// OData: $select=ID,Name,Email

Exclude internal/large properties:

query.Select("ProductID", "Name", "Price", "Category").
	Filter("Price gt 100")
// Gets only essential info, improving performance

func (*QueryBuilder) Skip

func (q *QueryBuilder) Skip(n int) *QueryBuilder

Skip skips a specified number of records using the $skip system query option.

Skip specifies the number of entities to skip before returning results. This is typically used with Top for cursor-based pagination through large datasets.

Behavior: If Skip is called multiple times, the last value replaces previous ones (not cumulative). Skip(10).Skip(20) results in $skip=20, not $skip=30.

Pagination Pattern: Combine Skip with Top to implement page-based pagination:

  • Page 1: Top(10) [Skip defaults to 0]
  • Page 2: Skip(10).Top(10) [Results 11-20]
  • Page 3: Skip(20).Top(10) [Results 21-30]
  • Page N: Skip((N-1)*PageSize).Top(PageSize)

Zero-Based Indexing: Skip uses zero-based indexing: Skip(0) means "don't skip any", Skip(10) means "skip the first 10 and start at the 11th record".

Performance: Skipping many records can be slower than using previous result bookmarks or date-based filters. For large datasets, consider filtering by date or using the Page() method which may be more efficient.

Relation to Page(): For simpler pagination with metadata, use Page(pageNumber, pageSize) instead of manually calculating Skip/Top offsets.

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes

Examples:

Skip first 10, get next 25:

query.Skip(10).Top(25)
// OData: $skip=10&$top=25
// Returns records 11-35

Pagination loop (50 records per page):

pageSize := 50
for page := 1; ; page++ {
	results, _ := client.From("Customers").
		OrderBy("CustomerID").
		Skip((page - 1) * pageSize).
		Top(pageSize).
		Collect(ctx)
	if len(results) == 0 {
		break
	}
	processPage(results)
}

Combined with filtering and sorting:

query.Where("Status").Eq("Active").
	OrderByDesc("CreatedDate").
	Skip(100).
	Top(50)
// Skip first 100 active records, get next 50 (most recent first)

func (*QueryBuilder) Stream

func (q *QueryBuilder) Stream(ctx context.Context, bufferSize ...int) <-chan Result[map[string]interface{}]

Stream streams all matching records from all pages using adaptive buffering. The returned channel will receive Result values containing either a data record (in Result.Value) or an error (in Result.Err). The channel is closed when streaming completes or an error occurs.

The bufferSize parameter controls the channel buffer capacity:

  • If bufferSize is omitted or ≤0, adaptive buffering is used (recommended for large datasets)
  • If bufferSize > 0, that exact buffer size is used

Adaptive buffering estimates the buffer size as: 10MB / avgRecordSize, clamped to [32, 1024]. This reduces memory usage and GC pressure for large datasets with small records.

Stream uses a worker pool for efficient goroutine management and implements backpressure through channel buffering to prevent unbounded memory growth.

Example:

for result := range query.Stream(ctx) {
	if result.Err != nil {
		return result.Err
	}
	process(result.Value)  // map[string]interface{}
}

func (*QueryBuilder) StreamAs

func (q *QueryBuilder) StreamAs(ctx context.Context, bufferSize ...int) <-chan Result[map[string]interface{}]

StreamAs is the generic typed version of Stream.

StreamAs is identical to Stream but allows specifying a result type through generic type parameters. Currently, this returns the same untyped map[string]interface{} as Stream, but the method signature allows for future type-safe streaming implementations.

See QueryBuilder.Stream for usage details and examples.

func (*QueryBuilder) StreamProperty added in v0.2.8

func (q *QueryBuilder) StreamProperty(ctx context.Context, propertyName string) (io.ReadCloser, error)

StreamProperty fetches a named binary stream property and returns its content as an io.ReadCloser. The caller is responsible for closing the returned reader.

StreamProperty issues a GET request to EntitySet/PropertyName with the Accept: application/octet-stream header. The response body is returned directly without buffering, making this suitable for large binary properties.

The entitySet on the QueryBuilder should include the entity key, e.g.:

reader, err := client.From("Products(42)").StreamProperty(ctx, "Photo")
if err != nil { ... }
defer reader.Close()
io.Copy(file, reader)

func (*QueryBuilder) StreamPropertySize added in v0.2.8

func (q *QueryBuilder) StreamPropertySize(ctx context.Context, propertyName string) (int64, error)

StreamPropertySize issues a HEAD request to determine the byte size of a named stream property without downloading its content.

Returns the value of the Content-Length response header. Returns -1 if the server does not include a Content-Length header.

size, err := client.From("Products(42)").StreamPropertySize(ctx, "Photo")

func (*QueryBuilder) Top

func (q *QueryBuilder) Top(n int) *QueryBuilder

Top limits the number of records returned using the $top system query option.

Top specifies the maximum number of entities to return in a single response. Combined with Skip, Top enables cursor-based pagination through large datasets.

Behavior: If Top is called multiple times, the last value replaces previous ones (not cumulative). Top(10).Top(20) results in $top=20, not $top=30.

Pagination Pattern: Combine Top with Skip to implement page-based pagination:

  • Page 1: Skip(0).Top(10) or just Top(10)
  • Page 2: Skip(10).Top(10)
  • Page 3: Skip(20).Top(10)
  • And so on...

Server-Side Limits: Many OData services enforce maximum Top values. If you request $top=10000 but the service limit is 1000, the service may return only 1000 records. Check service documentation for limits.

Relation to Page(): For simpler pagination with metadata, use Page(pageNumber, pageSize) instead of manually calculating Skip/Top offsets.

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes

Examples:

Get first 10 records:

query.Top(10)
// OData: $top=10

Pagination (page 3, 25 per page):

query.Skip(50).Top(25)
// OData: $skip=50&$top=25
// Returns records 51-75

Combined with filtering and sorting:

query.Where("Status").Eq("Active").
	OrderByDesc("CreatedDate").
	Top(50)
// Get 50 most recent active records

Implementation of paging loop:

for page := 1; page <= maxPages; page++ {
	results, _ := client.From("Products").
		OrderBy("ProductID").
		Skip((page - 1) * 100).
		Top(100).
		Collect(ctx)
}

func (*QueryBuilder) UnlinkFrom added in v0.2.10

func (q *QueryBuilder) UnlinkFrom(ctx context.Context, key any, navProperty string, targetKey ...any) error

UnlinkFrom removes a reference link from the current entity.

For a single-valued navigation property (no targetKey), it sends:

DELETE /EntitySet(key)/NavProperty/$ref

For a collection-valued navigation property (with targetKey), it sends:

DELETE /EntitySet(key)/NavProperty(targetKey)/$ref

Parameters:

  • ctx: Request context
  • key: Primary key of the source entity
  • navProperty: Navigation property name on the source entity
  • targetKey: (optional) Primary key of the target entity for collection nav properties

Example (single-valued):

err := client.From("Orders").UnlinkFrom(ctx, 1, "Customer")

Example (collection-valued):

err := client.From("Orders").UnlinkFrom(ctx, 1, "Items", 42)

func (*QueryBuilder) UpdateDeep added in v0.7.0

func (q *QueryBuilder) UpdateDeep(ctx context.Context, entity any) (*relay.Response, error)

UpdateDeep performs an OData 4.01 deep update of an entity with related entities.

UpdateDeep PATCHes the entity at the keyed URL, embedding related entities in the request body for atomic nested updates. The request sets Content-Type to "application/json;odata.metadata=minimal" and Prefer to "return=representation".

Call QueryBuilder.Key before UpdateDeep to set the entity key:

resp, err := client.From("Orders").Key(1).UpdateDeep(ctx, OrderWithItems{
    Status: "Confirmed",
    Items:  []Item{{ID: 10, Qty: 5}},
})

Spec ref: OData 4.01 Part 1 - Section 11.4.3 (Update Related Entities When Updating an Entity).

func (*QueryBuilder) UpdateDeepWithPrefer added in v0.7.0

func (q *QueryBuilder) UpdateDeepWithPrefer(ctx context.Context, entity any, prefer string) (*relay.Response, error)

UpdateDeepWithPrefer performs a deep update with a custom Prefer header value.

Use this when full control over the Prefer header is required, for example to combine "return=representation" with "odata.continue-on-error", or to use a custom preference. Pass an empty string to omit the Prefer header entirely.

opts := traverse.DeepUpdateOptions{
    ReturnRepresentation: true,
    ContinueOnError:      true,
}
resp, err := client.From("Orders").Key(1).
    UpdateDeepWithPrefer(ctx, patch, opts.PreferHeader())

func (*QueryBuilder) Where

func (q *QueryBuilder) Where(field string) *FilterBuilder

Where starts building a type-safe filter expression for the given field.

Where returns a FilterBuilder that provides type-safe filter building with methods like Eq(), Gt(), StartsWith(), Contains(), etc. The FilterBuilder implements the builder pattern and returns *QueryBuilder from each method to enable chaining.

Type Safety: Unlike Filter() which uses raw strings, Where() prevents many syntax errors through a typed builder interface. Property names and operators are checked before sending to the service.

Object Pooling: FilterBuilder instances are obtained from a sync.Pool to reduce allocations. Returned FilterBuilder instances should NOT be retained after the QueryBuilder is returned; they are reused for subsequent calls.

Comparison Methods Available on FilterBuilder:

  • Eq(): Equal (eq)
  • Ne(): Not equal (ne)
  • Gt(): Greater than (gt)
  • Ge(): Greater or equal (ge)
  • Lt(): Less than (lt)
  • Le(): Less or equal (le)
  • StartsWith(): String startswith
  • EndsWith(): String endswith
  • Contains(): String contains

Returns:

  • *FilterBuilder: For chainable filter building

Chainable: Yes (chains to QueryBuilder through FilterBuilder methods)

Examples:

Single condition:

query.Where("Age").Gt(30)
// OData: $filter=Age gt 30

Multiple conditions (chained):

query.Where("Status").Eq("Active").Where("Priority").Gt(1)
// OData: $filter=Status eq 'Active' and Priority gt 1

String operations:

query.Where("Name").StartsWith("John")
// OData: $filter=startswith(Name, 'John')

Nested filtering with expand:

query.Where("Status").Eq("Active").
	Expand("Orders").
	Collect(ctx)

func (*QueryBuilder) WithCache added in v0.2.25

func (q *QueryBuilder) WithCache(ttl time.Duration) *QueryBuilder

WithCache marks the query as cacheable with the given TTL.

When WithCache is set and the client has a ResponseCache configured via WithResponseCache, the response body is stored in the cache under the full request URL key. Subsequent identical requests served within the TTL window are returned from cache without making an HTTP request.

If the cached response carries an ETag or Last-Modified header, and the entry has expired, traverse sends a conditional request (If-None-Match or If-Modified-Since). A 304 Not Modified response renews the entry without re-downloading the body.

Cache invalidation happens automatically after Client.Create, Client.Update, Client.Replace, and Client.Delete operations on the same entity set.

WithCache applies to QueryBuilder.Page, QueryBuilder.Collect, and QueryBuilder.First calls. It has no effect if no ResponseCache is configured.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithResponseCache(traverse.NewInMemoryResponseCache()),
)
products, _ := client.From("Products").
    WithCache(5 * time.Minute).
    Collect(ctx)

func (*QueryBuilder) WithCount

func (q *QueryBuilder) WithCount() *QueryBuilder

WithCount includes the total count of matching records in results.

When called, the response will include a $count field with the total number of records matching the query filters, even if fewer records are returned due to Top/Skip settings. This is useful for pagination scenarios where the client needs to know the total result set size. WithCount is chainable and returns q for method chaining.

Example:

page, err := query.Top(10).WithCount().Page(ctx)
if err == nil {
	log.Printf("Total records: %d", page.Count)
}

func (*QueryBuilder) WithDeltaToken

func (q *QueryBuilder) WithDeltaToken(token string) *QueryBuilder

WithDeltaToken enables incremental updates using a delta token.

WithDeltaToken is only available in OData v4 and is used with delta queries to retrieve only entities that have changed since the previous query. The delta token is provided by a previous successful delta query in the Page.DeltaLink field. WithDeltaToken is chainable and returns q for method chaining.

Example:

page, err := query.WithDeltaToken(previousToken).Page(ctx)  // OData v4 only

func (*QueryBuilder) WithNoPrefetch added in v0.2.27

func (q *QueryBuilder) WithNoPrefetch() *QueryBuilder

WithNoPrefetch disables background page prefetching.

WithNoPrefetch is useful when strict sequential page fetching is required, or when you want to undo an earlier WithPrefetch call on the same builder. WithNoPrefetch is chainable and returns q for method chaining.

func (*QueryBuilder) WithPrefer added in v0.6.0

func (q *QueryBuilder) WithPrefer(pref string) *QueryBuilder

WithPrefer adds an OData Prefer header value to this query.

WithPrefer can be called multiple times; values are comma-joined into a single Prefer header, as specified in RFC 7240. Use the Prefer* constants for the standard OData preference values.

This implements OData v4.0 spec section 8.2.8.

Examples:

// Strict query option handling
client.From("Products").WithPrefer(traverse.PreferHandlingStrict).Collect(ctx)

// Request delta tracking
client.From("Orders").WithPrefer(traverse.PreferTrackChanges).Page(ctx)

// Combine multiple preferences
client.From("Products").
    WithPrefer(traverse.PreferHandlingLenient).
    WithPrefer(traverse.PreferReturnRepresentation).
    Collect(ctx)

func (*QueryBuilder) WithPrefetch added in v0.2.27

func (q *QueryBuilder) WithPrefetch(bufferPages int) *QueryBuilder

WithPrefetch enables background prefetching of the next page while the current page is being processed by the caller.

bufferPages controls how many pages to prefetch ahead. Values below 1 default to 1; values above 3 are clamped to 3. When Stream() is called on a query with prefetching enabled, an internal goroutine fetches page N+1 (and up to bufferPages-1 more) while the caller iterates over page N, reducing per-page latency for paginated reads.

WithPrefetch is chainable and returns q for method chaining.

Example:

for result := range client.From("Orders").WithPrefetch(1).Stream(ctx) {
	// page 2 is already being fetched while you process page 1
}

func (*QueryBuilder) WithSchema added in v0.2.12

func (q *QueryBuilder) WithSchema(schema *EntitySchema) *QueryBuilder

WithSchema attaches an EntitySchema to the QueryBuilder for filter and orderby validation.

WithSchema enables schema-based validation of filter and orderby expressions before sending the request to the OData service. This catches typos in field names and type mismatches at build time rather than at runtime when the service rejects the request.

When a schema is set, subsequent calls to Filter() and OrderBy() validate field names against the schema properties. If a referenced field is not found, a SchemaValidationError is returned immediately.

Backward Compatibility: If schema is nil (not set), validation is skipped and the QueryBuilder behaves as before. This ensures existing code continues to work without modification.

Returns:

  • *QueryBuilder: For method chaining

Chainable: Yes

Examples:

Define a schema and attach it:

schema := &EntitySchema{
    Properties: map[string]string{
        "ID":    "int",
        "Name":  "string",
        "Email": "string",
    },
}
query := client.From("Users").WithSchema(schema)

Now Filter() validates field names:

query.Filter("UnknownField eq 'value'")  // Returns error

Valid filters are accepted:

query.Filter("Name eq 'John'")  // OK, Name exists in schema

func (*QueryBuilder) WithSchemaVersion added in v0.6.0

func (q *QueryBuilder) WithSchemaVersion(version string) *QueryBuilder

WithSchemaVersion sets the OData-SchemaVersion request header for this query only.

This overrides any schema version set at the client level for this specific query. See traverse.WithSchemaVersion for the client-level option.

Example:

client.From("Products").WithSchemaVersion("1.5").Collect(ctx)

type RawResult

type RawResult struct {
	// Raw is the raw JSON bytes for the record
	Raw json.RawMessage
	// Page is the 1-based page number this result came from
	Page int
	// Index is the 0-based index within the page
	Index int
	// Err is an error if one occurred, nil otherwise
	Err error
}

RawResult represents a raw JSON record from an optimized streaming path.

RawResult is used internally for zero-allocation streaming scenarios where direct unmarshaling to custom types is needed. The Raw field contains the JSON bytes for the record, bypassing intermediate map allocations.

type ResponseCache added in v0.2.25

type ResponseCache interface {
	// Get returns the cached entry for key.
	// Expired entries are returned (callers must check isExpired) so that
	// their ETag or Last-Modified can be used for conditional revalidation.
	// Returns nil and false if the key is absent entirely.
	Get(key string) (*ResponseCacheEntry, bool)

	// Set stores entry for key. When ttl > 0, ExpiresAt is set to now+ttl.
	// A zero or negative ttl stores the entry without automatic expiry.
	Set(key string, entry *ResponseCacheEntry, ttl time.Duration)

	// Delete removes the entry for key. No-op if key is absent.
	Delete(key string)

	// Invalidate removes all entries whose key begins with prefix.
	// Used to purge all cached pages for an entity set after mutations.
	Invalidate(prefix string)

	// Clear removes all entries from the cache.
	Clear()
}

ResponseCache is the storage backend for HTTP-level response caching in traverse.

Implement this interface to plug in a custom cache backend such as Redis, Memcached, or a specialised in-memory store. All methods must be safe for concurrent use.

The default implementation is returned by NewInMemoryResponseCache.

Example:

client, _ := traverse.New(
    traverse.WithBaseURL("https://odata.example.com/v4"),
    traverse.WithResponseCache(traverse.NewInMemoryResponseCache()),
)

func NewInMemoryResponseCache added in v0.2.25

func NewInMemoryResponseCache() ResponseCache

NewInMemoryResponseCache returns a new in-memory ResponseCache backed by sync.Map. It is safe for concurrent use and requires no external dependencies.

Entries are stored until they are explicitly removed via Delete, Invalidate, or Clear, or until they are lazily cleaned up on access after expiry.

type ResponseCacheEntry added in v0.2.25

type ResponseCacheEntry struct {
	// Body is the raw JSON response body bytes.
	Body []byte
	// ETag is the value of the ETag response header, used for If-None-Match.
	ETag string
	// LastModified is the value of the Last-Modified response header,
	// used for If-Modified-Since when ETag is absent.
	LastModified string
	// ExpiresAt is the absolute time after which this entry must be revalidated.
	// A zero value means the entry never expires automatically.
	ExpiresAt time.Time
}

ResponseCacheEntry holds a cached HTTP response body alongside metadata needed for conditional revalidation (ETag, Last-Modified) and TTL tracking.

type ResponseFormat

type ResponseFormat int

Response format options

const (
	FormatJSON ResponseFormat = iota // JSON (default)
	FormatAtom                       // XML/ATOM (legacy OData v2)
)

type Result

type Result[T any] struct {
	// Value is the actual data record
	Value T
	// Err is an error if one occurred, nil otherwise
	Err error
	// Page is the 1-based page number this result came from
	Page int
	// Index is the 0-based index within the page
	Index int
}

Result represents a single record result from a stream, along with any error. Result represents a single streamed result from a query.

Result is a generic type that contains either a data value or an error. When streaming results via QueryBuilder.Stream, each item on the returned channel is a Result. Check Err before using Value.

Fields:

  • Value: the actual data (map[string]interface{} for most queries)
  • Err: error encountered during streaming (or nil if successful)
  • Page: 1-based page number of this result
  • Index: 0-based index within the page

type SAPAnnotations

type SAPAnnotations struct {
	// Label is a human-readable label for the property (sap:label).
	Label string
	// Filterable indicates if the property can be used in $filter clauses (sap:filterable).
	Filterable bool
	// Sortable indicates if the property can be used in $orderby clauses (sap:sortable).
	Sortable bool
	// Searchable indicates if the property can be used in search operations (sap:searchable).
	Searchable bool
	// Required indicates if the property is required in filter expressions (sap:required-in-filter).
	Required bool
	// Text refers to a property containing a descriptive text for this property (sap:text).
	Text string
	// Unit refers to a property holding the unit of measure for this property (sap:unit).
	Unit string
	// ValueList describes how value help is provided: "standard" or "fixed-values" (sap:value-list).
	ValueList string
	// DisplayFormat specifies the display format, e.g. "UpperCase", "NonNegative", "Date" (sap:display-format).
	DisplayFormat string
	// FieldControl specifies the field-control property name or mode: Mandatory, Optional, ReadOnly (sap:field-control).
	FieldControl string
	// Semantics is the semantic type of the value, e.g. "email", "phone", "url" (sap:semantics).
	Semantics string
	// IsKey is true when sap:key="true" is present on the property.
	IsKey bool
	// UpdatablePath refers to a boolean property that controls whether this property is updatable (sap:updatable-path).
	UpdatablePath string
	// Creatable indicates if the entity set supports create operations (sap:creatable). Used on EntitySet level.
	Creatable bool
	// Updatable indicates if the entity set supports update operations (sap:updatable). Used on EntitySet level.
	Updatable bool
	// Deletable indicates if the entity set supports delete operations (sap:deletable). Used on EntitySet level.
	Deletable bool
	// Pageable indicates if the entity set supports server-side paging (sap:pageable). Used on EntitySet level.
	Pageable bool
	// Addressable indicates if individual entities are directly addressable by key (sap:addressable). Used on EntitySet level.
	Addressable bool
	// RequiresFilter indicates if $filter must be specified in queries (sap:requires-filter). Used on EntitySet level.
	RequiresFilter bool
	// ChangeTracking indicates if the entity set supports delta/change-tracking (sap:change-tracking). Used on EntitySet level.
	ChangeTracking bool
}

SAPAnnotations contains SAP-specific annotations for properties and entity types. These annotations provide hints about how SAP systems use the properties.

type SAPError added in v0.21.3

type SAPError struct {
	Code       string       // SAP error code (e.g., "/IWFND/MED/170")
	Message    string       // Human-readable error message
	InnerError string       // SAP inner error details if present
	ErrorType  SAPErrorType // Categorized error type
	RawBody    string       // Original response body for debugging
	IsXML      bool         // Whether original response was XML format
}

SAPError represents a structured error from a SAP OData service.

SAPError encapsulates both JSON and XML error formats that SAP systems return, parsing and normalizing them into a unified structure. This allows consumers to branch on error type (configuration, authorization, CSRF, transient) rather than inspecting raw response bodies.

SAP returns errors in multiple formats:

  • JSON format (OData v4): {"error":{"code":"...","message":{"value":"..."},"innererror":{...}}}
  • XML Atom format (OData v2): <error><code>...</code><message>...</message><innererror>...</innererror></error>

Example:

if sapErr := ParseSAPError(resp); sapErr != nil {
	switch sapErr.ErrorType {
	case SAPErrorTypeNotFound:
		// Handle entity not found
	case SAPErrorTypeCSRF:
		// Handle CSRF token expiration
	case SAPErrorTypeUnauthorized:
		// Handle auth failure
	}
}

func IsSAPError added in v0.21.3

func IsSAPError(err error) (*SAPError, bool)

IsSAPError extracts a *SAPError from an error value.

IsSAPError uses errors.As to unwrap and retrieve the *SAPError from a potentially wrapped error. Returns the *SAPError and true if found, otherwise returns nil and false.

func ParseSAPError added in v0.21.3

func ParseSAPError(body string) *SAPError

ParseSAPError extracts and categorizes a structured error from SAP response body.

ParseSAPError attempts to parse the response body as either JSON (OData v4 format) or XML (OData v2 Atom format), categorizing the error type for programmatic handling. Returns nil if the body is not a recognized SAP error format.

Supported categorizations:

  • CSRF errors: Contains "csrf", "token invalid", "token expired", "x-csrf-token"
  • Authorization errors: Contains "forbidden", "unauthorized", "permission"
  • Configuration errors: Contains "/IWFND/MED", "not found", "unknown service"
  • Transient errors: Contains "timeout", "gateway", "temporarily"

func (*SAPError) Error added in v0.21.3

func (e *SAPError) Error() string

Error implements the error interface for SAPError.

type SAPErrorType added in v0.21.3

type SAPErrorType int

SAPErrorType categorizes SAP errors for programmatic handling.

const (
	SAPErrorTypeUnknown       SAPErrorType = iota // Unknown or uncategorized error
	SAPErrorTypeNotFound                          // Service or entity not found
	SAPErrorTypeCSRF                              // CSRF token invalid or missing
	SAPErrorTypeUnauthorized                      // Authentication or authorization failure
	SAPErrorTypeServiceConfig                     // SAP configuration or setup issue
	SAPErrorTypeTransient                         // Transient error (timeout, gateway issue)
)

type SchemaValidationError added in v0.2.12

type SchemaValidationError struct {
	Field   string
	Message string
}

SchemaValidationError is returned when a filter or orderby references an unknown field or has a type mismatch.

SchemaValidationError provides detailed information about validation failures so that developers can quickly identify and fix issues in their filter expressions.

Example:

if err != nil {
    if schemaErr, ok := err.(*SchemaValidationError); ok {
        fmt.Printf("Unknown field: %s\n", schemaErr.Field)
        fmt.Printf("Details: %s\n", schemaErr.Message)
    }
}

func (*SchemaValidationError) Error added in v0.2.12

func (e *SchemaValidationError) Error() string

Error implements the error interface.

type SearchExpression added in v0.2.23

type SearchExpression interface {
	// contains filtered or unexported methods
}

SearchExpression represents an OData $search expression. OData $search is distinct from $filter - it performs full-text search across all searchable properties of an entity.

Example:

results, err := client.From("Products").
    Search(SearchAnd(SearchWord("mountain"), SearchNot(SearchWord("bike")))).
    Collect(ctx)
func Search(s string) SearchExpression

Search is a convenience constructor that auto-detects whether the input should be a word (no spaces) or a phrase (contains spaces).

Example:

Search("mountain")       // same as SearchWord("mountain")
Search("mountain bike")  // same as SearchPhrase("mountain bike")

func SearchAnd added in v0.2.23

func SearchAnd(exprs ...SearchExpression) SearchExpression

SearchAnd combines two or more search expressions with logical AND.

Example:

SearchAnd(SearchWord("mountain"), SearchWord("view"))
// "(mountain AND view)"

func SearchNot added in v0.2.23

func SearchNot(expr SearchExpression) SearchExpression

SearchNot negates a search expression.

Example:

SearchNot(SearchWord("bike")).searchString() // "NOT bike"

func SearchOr added in v0.2.23

func SearchOr(exprs ...SearchExpression) SearchExpression

SearchOr combines two or more search expressions with logical OR.

Example:

SearchOr(SearchWord("mountain"), SearchWord("hill"))
// "(mountain OR hill)"

func SearchPhrase added in v0.2.23

func SearchPhrase(phrase string) SearchExpression

SearchPhrase creates a quoted-phrase OData $search term.

The phrase is wrapped in double quotes so the OData service treats it as a literal phrase rather than individual words.

Example:

SearchPhrase("mountain bike").searchString() // `"mountain bike"`

func SearchWord added in v0.2.23

func SearchWord(word string) SearchExpression

SearchWord creates a single-word OData $search term.

The word is emitted unquoted. Use SearchPhrase for multi-word expressions.

Example:

SearchWord("mountain").searchString() // "mountain"

type ServiceDocument

type ServiceDocument struct {
	// EntitySets is the list of available entity sets.
	EntitySets []EntitySetReference
}

ServiceDocument represents the root OData service document. It contains a list of all available entity sets in the OData service.

type StringInterning

type StringInterning struct {
	// contains filtered or unexported fields
}

StringInterning provides optimized string deduplication to reduce memory allocations when processing entity and property names that are repeated across many records.

When processing millions of records from an OData service, entity names and property names are repeated in every record's JSON response. StringInterning deduplicates these strings, ensuring that identical strings reference the same memory location.

For example, if every record has a "Name" property, StringInterning returns the same string reference for all 1 million records instead of allocating 1 million string copies.

Example:

si := traverse.NewStringInterning()
propName1 := si.Intern("Name")      // allocates
propName2 := si.Intern("Name")      // returns cached reference
propName1 == propName2              // true, same memory location

func NewStringInterning

func NewStringInterning() *StringInterning

NewStringInterning creates a new string interning cache.

NewStringInterning returns a ready-to-use *StringInterning with an initial capacity of 256 entries. It is safe for concurrent use.

func (*StringInterning) CacheSize

func (si *StringInterning) CacheSize() int

CacheSize returns the current number of strings in the cache.

CacheSize is useful for monitoring cache growth during bulk operations.

func (*StringInterning) Clear

func (si *StringInterning) Clear()

Clear removes all strings from the cache.

Clear is useful for testing or resetting memory state. It reinitializes the cache with the default capacity of 256 entries.

func (*StringInterning) Intern

func (si *StringInterning) Intern(s string) string

Intern returns a deduplicated reference to the string.

If the string already exists in the cache, Intern returns the cached reference. Otherwise, it adds the string to the cache and returns it.

Intern uses a fast-path read lock for cache hits and a slow-path write lock for misses with double-check pattern to avoid race conditions.

Empty strings are returned as-is without caching.

func (*StringInterning) InternBatch

func (si *StringInterning) InternBatch(strings ...string) []string

InternBatch interns multiple strings efficiently with a single lock acquisition.

InternBatch is more efficient than calling [Intern] multiple times when processing many strings at once. It acquires one write lock to check/insert all provided strings, avoiding repeated lock acquisitions per string.

Returns a new slice with deduplicated string references.

type TrackedEntity added in v0.2.6

type TrackedEntity struct {
	// contains filtered or unexported fields
}

TrackedEntity wraps an entity snapshot and tracks which fields have been modified. Use it to generate minimal PATCH payloads with only changed fields.

Usage:

// Fetch an entity
entity := map[string]interface{}{"ID": 42, "Name": "Widget", "Price": 9.99}

// Start tracking
t := traverse.TrackEntity(entity)
t.Set("Name", "Updated Widget")
t.Set("Price", 19.99)

// Save only changed fields
err = t.SaveChanges(ctx, client, "Products", 42)

func TrackEntity added in v0.2.6

func TrackEntity(entity map[string]interface{}) *TrackedEntity

TrackEntity creates a TrackedEntity from an entity snapshot. The snapshot is copied so the original is preserved for comparison.

func (*TrackedEntity) Changes added in v0.2.6

func (t *TrackedEntity) Changes() map[string]interface{}

Changes returns a map containing only the fields that have been modified. This is the payload to send in a PATCH request.

func (*TrackedEntity) DirtyFields added in v0.2.6

func (t *TrackedEntity) DirtyFields() []string

DirtyFields returns the names of all modified fields.

func (*TrackedEntity) Discard added in v0.2.6

func (t *TrackedEntity) Discard()

Discard reverts all changes back to the original snapshot.

func (*TrackedEntity) Get added in v0.2.6

func (t *TrackedEntity) Get(field string) (interface{}, bool)

Get returns the current value of a field.

func (*TrackedEntity) IsDirty added in v0.2.6

func (t *TrackedEntity) IsDirty() bool

IsDirty reports whether any field has been modified.

func (*TrackedEntity) MarshalJSON added in v0.2.6

func (t *TrackedEntity) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler and encodes only the changed fields.

func (*TrackedEntity) Original added in v0.2.6

func (t *TrackedEntity) Original() map[string]interface{}

Original returns the unmodified snapshot of the entity as it was when TrackEntity was called.

func (*TrackedEntity) Reset added in v0.2.6

func (t *TrackedEntity) Reset()

Reset clears all dirty flags, making the current state the new baseline. Use this after a successful save to reset the tracker.

func (*TrackedEntity) SaveChanges added in v0.2.6

func (t *TrackedEntity) SaveChanges(ctx context.Context, client *Client, entitySet string, key interface{}) error

SaveChanges saves the dirty fields to the OData service using a PATCH request. If no fields are dirty, it is a no-op and returns nil.

SaveChanges is safe to call concurrently: it atomically snapshots the dirty fields and clears only those fields on success. Any Set calls that race with the in-flight PATCH remain dirty and will be included in the next SaveChanges.

t := traverse.TrackEntity(entity)
t.Set("Name", "New Name")
err := t.SaveChanges(ctx, client, "Products", 42)

func (*TrackedEntity) Set added in v0.2.6

func (t *TrackedEntity) Set(field string, value interface{})

Set updates a field value and marks it dirty.

type ValidationVocabulary added in v0.2.29

type ValidationVocabulary struct {
	// Minimum corresponds to Org.OData.Validation.V1.Minimum.
	Minimum *float64
	// Maximum corresponds to Org.OData.Validation.V1.Maximum.
	Maximum *float64
	// Pattern corresponds to Org.OData.Validation.V1.Pattern.
	Pattern string
	// AllowedValues corresponds to Org.OData.Validation.V1.AllowedValues.
	AllowedValues []string
	// Required corresponds to Org.OData.Validation.V1.Required.
	Required bool
}

ValidationVocabulary defines Org.OData.Validation.V1 annotation terms.

func ParseValidationVocabulary added in v0.2.29

func ParseValidationVocabulary(annotations map[string]string) ValidationVocabulary

ParseValidationVocabulary extracts Org.OData.Validation.V1 annotation terms from a raw annotation map. The map keys are fully-qualified term names (e.g. "Org.OData.Validation.V1.Minimum").

Directories

Path Synopsis
benchmarks
cmd
demo command
Package main implements a comprehensive OData v4 test server supporting both XML and JSON formats.
Package main implements a comprehensive OData v4 test server supporting both XML and JSON formats.
traverse command
traverse-gen command
traverse-tui command
Command traverse-tui provides an interactive terminal interface for building and executing OData queries.
Command traverse-tui provides an interactive terminal interface for building and executing OData queries.
ext
audit module
azure module
cache/memory module
cache/redis module
cache/stale module
dataverse module
graphql module
oauth2 module
offline module
openapi module
prometheus module
sap module
tracing module
webhooks module
internal
encoder
Package encoder provides OData URL construction and encoding utilities.
Package encoder provides OData URL construction and encoding utilities.
parser
Package parser provides OData filter expression parsing and serialization.
Package parser provides OData filter expression parsing and serialization.
tokenizer
Package tokenizer provides streaming JSON parsing utilities for OData responses.
Package tokenizer provides streaming JSON parsing utilities for OData responses.
Package testutil provides testing utilities for traverse users.
Package testutil provides testing utilities for traverse users.
tools
bench-compare command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL