Documentation
ΒΆ
Overview ΒΆ
Package bolt provides authentication adapters for integrating with NornicDB's auth package.
Package bolt implements the Neo4j Bolt protocol server for NornicDB.
Package bolt implements the Neo4j Bolt protocol server for NornicDB.
This package provides a Bolt protocol server that allows existing Neo4j drivers and tools to connect to NornicDB without modification. The server implements Bolt 4.x protocol specifications for maximum compatibility.
Neo4j Bolt Protocol Compatibility:
- Bolt 4.0, 4.1, 4.2, 4.3, 4.4 support
- PackStream serialization format
- Transaction management (BEGIN, COMMIT, ROLLBACK)
- Streaming result sets (RUN, PULL, DISCARD)
- Authentication handshake
- Connection pooling support
Supported Neo4j Drivers:
- Neo4j Java Driver
- Neo4j Python Driver (neo4j-driver)
- Neo4j JavaScript Driver
- Neo4j .NET Driver
- Neo4j Go Driver
- Community drivers (Rust, Ruby, etc.)
Example Usage:
// Create Bolt server with Cypher executor
config := bolt.DefaultConfig()
config.Port = 7687
config.MaxConnections = 100
// Implement query executor
executor := &MyQueryExecutor{db: nornicDB}
server := bolt.New(config, executor)
// Start server
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
// Server is now accepting Bolt connections on port 7687
Client Usage (any Neo4j driver):
// Python example
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687")
with driver.session() as session:
result = session.run("MATCH (n) RETURN count(n)")
print(result.single()[0])
// Go example
driver, _ := neo4j.NewDriver("bolt://localhost:7687", neo4j.NoAuth())
session := driver.NewSession(neo4j.SessionConfig{})
result, _ := session.Run("MATCH (n) RETURN count(n)", nil)
Protocol Flow:
1. **Handshake**:
- Client sends magic number (0x6060B017)
- Client sends supported versions
- Server responds with selected version
2. **Authentication**:
- Client sends HELLO message with credentials
- Server responds with SUCCESS or FAILURE
3. **Query Execution**:
- Client sends RUN message with Cypher query
- Server responds with SUCCESS (field names)
- Client sends PULL to stream results
- Server sends RECORD messages + final SUCCESS
4. **Transaction Management**:
- BEGIN: Start explicit transaction
- COMMIT: Commit transaction
- ROLLBACK: Rollback transaction
Message Types:
- HELLO: Authentication
- RUN: Execute Cypher query
- PULL: Stream result records
- DISCARD: Discard remaining results
- BEGIN/COMMIT/ROLLBACK: Transaction control
- RESET: Reset session state
- GOODBYE: Close connection
PackStream Encoding:
The Bolt protocol uses PackStream for efficient binary serialization: - Compact representation of common types - Support for nested structures - Streaming-friendly format
Performance:
- Binary protocol (faster than HTTP/JSON)
- Connection pooling and reuse
- Streaming results (low memory usage)
- Pipelining support
ELI12 (Explain Like I'm 12):
Think of the Bolt server like a translator at the United Nations:
**Different languages**: Neo4j drivers speak "Bolt language" but NornicDB speaks "NornicDB language". The Bolt server translates between them.
**Same conversation**: The drivers can have the same conversation they always had (asking questions in Cypher), they just don't know they're talking to a different database!
**Binary messages**: Instead of sending text messages (like HTTP), Bolt sends compact binary messages - like sending a compressed file instead of a text document. Much faster!
**Streaming**: Instead of waiting for ALL results before sending anything, Bolt can send results one-by-one as they're found, like a live news feed.
This lets existing Neo4j tools work with NornicDB without any changes!
Index ΒΆ
- Constants
- type AuthenticatorAdapter
- type BoltAuthResult
- type BoltAuthenticator
- type Config
- type DatabaseManagerInterface
- type DeferrableExecutor
- type FlushableExecutor
- type QueryExecutor
- type QueryResult
- type Server
- func (s *Server) Close() error
- func (s *Server) IsClosed() bool
- func (s *Server) ListenAndServe() error
- func (s *Server) SetDatabaseAccessMode(mode auth.DatabaseAccessMode)
- func (s *Server) SetDatabaseAccessModeResolver(resolver func(roles []string) auth.DatabaseAccessMode)
- func (s *Server) SetResolvedAccessResolver(resolver func(roles []string, dbName string) auth.ResolvedAccess)
- type Session
- type SessionExecutorFactory
- type TransactionalExecutor
Constants ΒΆ
const ( BoltV4_4 = 0x0404 // Bolt 4.4 BoltV4_3 = 0x0403 // Bolt 4.3 BoltV4_2 = 0x0402 // Bolt 4.2 BoltV4_1 = 0x0401 // Bolt 4.1 BoltV4_0 = 0x0400 // Bolt 4.0 )
Protocol versions supported
const ( MsgHello byte = 0x01 MsgGoodbye byte = 0x02 MsgReset byte = 0x0F MsgRun byte = 0x10 MsgDiscard byte = 0x2F MsgPull byte = 0x3F MsgBegin byte = 0x11 MsgCommit byte = 0x12 MsgRollback byte = 0x13 MsgRoute byte = 0x66 // Response messages MsgSuccess byte = 0x70 MsgRecord byte = 0x71 MsgIgnored byte = 0x7E MsgFailure byte = 0x7F )
Message types
Variables ΒΆ
This section is empty.
Functions ΒΆ
This section is empty.
Types ΒΆ
type AuthenticatorAdapter ΒΆ
type AuthenticatorAdapter struct {
// contains filtered or unexported fields
}
AuthenticatorAdapter wraps auth.Authenticator to implement BoltAuthenticator. This allows the Bolt server to use the same authentication system as the HTTP server, service accounts, and the UI.
The adapter translates Neo4j-style Bolt authentication (scheme, principal, credentials) to NornicDB's auth.Authenticator (username, password or JWT token).
Supported authentication schemes:
- "basic": Username/password authentication (same as HTTP basic auth)
- "bearer": JWT token authentication (for cluster inter-node auth)
- "none": Anonymous access (if enabled, grants viewer role)
Cluster Authentication with Shared JWT ΒΆ
For cluster deployments where all nodes need to authenticate with each other, use bearer token authentication with a shared JWT secret:
Configure all nodes with the same JWT secret: NORNICDB_JWT_SECRET=your-shared-secret-min-32-bytes
Generate a cluster token on any node: POST /api/v1/auth/cluster-token {"node_id": "node-2", "role": "admin"}
Connect from other nodes using the bearer scheme: driver = GraphDatabase.driver("bolt://node1:7687", auth=("", token)) # Empty username triggers bearer auth
Example:
// Create the shared authenticator
authConfig := auth.DefaultAuthConfig()
authConfig.JWTSecret = []byte("your-secret-key-shared-across-cluster")
authenticator, _ := auth.NewAuthenticator(authConfig)
// Create service accounts for server-to-server communication
authenticator.CreateUser("cluster-node-1", "secure-password", []auth.Role{auth.RoleAdmin})
authenticator.CreateUser("backup-service", "backup-password", []auth.Role{auth.RoleViewer})
// Create Bolt server with shared auth
boltConfig := bolt.DefaultConfig()
boltConfig.Authenticator = bolt.NewAuthenticatorAdapter(authenticator)
boltConfig.RequireAuth = true
boltServer := bolt.New(boltConfig, executor)
func NewAuthenticatorAdapter ΒΆ
func NewAuthenticatorAdapter(authenticator *auth.Authenticator) *AuthenticatorAdapter
NewAuthenticatorAdapter creates a new BoltAuthenticator that wraps auth.Authenticator. This enables the Bolt server to use the same user database and authentication as the HTTP server, ensuring consistent auth across all protocols.
Parameters:
- authenticator: The shared auth.Authenticator instance
Example:
authenticator, _ := auth.NewAuthenticator(auth.DefaultAuthConfig()) boltAuth := bolt.NewAuthenticatorAdapter(authenticator) config := bolt.DefaultConfig() config.Authenticator = boltAuth config.RequireAuth = true
func NewAuthenticatorAdapterWithAnonymous ΒΆ
func NewAuthenticatorAdapterWithAnonymous(authenticator *auth.Authenticator) *AuthenticatorAdapter
NewAuthenticatorAdapterWithAnonymous creates an adapter that allows anonymous connections. Anonymous users receive "viewer" role (read-only access).
Use with caution - this allows unauthenticated connections.
func (*AuthenticatorAdapter) Authenticate ΒΆ
func (a *AuthenticatorAdapter) Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error)
Authenticate validates credentials from the Bolt HELLO message. This method implements the BoltAuthenticator interface.
Supported schemes:
- "basic": Username/password authentication (same as HTTP basic auth)
- "bearer": JWT token authentication (credentials contains JWT, principal is ignored)
- "none": Anonymous access (if enabled, grants viewer role)
Cluster Authentication ΒΆ
For server-to-server clustering, you have two options:
Option 1: Service accounts with "basic" scheme
authenticator.CreateUser("cluster-node-west", "secure-password-123",
[]auth.Role{auth.RoleAdmin})
driver = GraphDatabase.driver("bolt://node-east:7687",
basic_auth("cluster-node-west", "secure-password-123"))
Option 2: JWT tokens with "bearer" scheme (recommended for clusters)
# Generate token via API:
curl -X POST http://node:7474/api/v1/auth/cluster-token \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"node_id": "node-2", "role": "admin"}'
# Connect with bearer token:
driver = GraphDatabase.driver("bolt://node:7687",
basic_auth("", token)) # Empty username = bearer auth
func (*AuthenticatorAdapter) SetAllowAnonymous ΒΆ
func (a *AuthenticatorAdapter) SetAllowAnonymous(allow bool)
SetAllowAnonymous enables or disables anonymous authentication.
func (*AuthenticatorAdapter) SetGetEffectivePermissions ΒΆ
func (a *AuthenticatorAdapter) SetGetEffectivePermissions(fn func(roles []string) []string)
SetGetEffectivePermissions sets the callback used to resolve roles to effective permission IDs. When set, BoltAuthResult.Permissions is filled so HasPermission uses stored role entitlements.
type BoltAuthResult ΒΆ
type BoltAuthResult struct {
Authenticated bool // Whether authentication succeeded
Username string // Authenticated username
Roles []string // User roles (admin, editor, viewer, etc.)
Permissions []string // Effective entitlement IDs (when set, used by HasPermission; else fallback to rolePerms)
}
BoltAuthResult contains the result of Bolt authentication.
func (*BoltAuthResult) HasPermission ΒΆ
func (r *BoltAuthResult) HasPermission(perm string) bool
HasPermission checks if the auth result has a specific permission. When Permissions is set (from role entitlements store), uses that list; else falls back to auth.RolePermissions.
func (*BoltAuthResult) HasRole ΒΆ
func (r *BoltAuthResult) HasRole(role string) bool
HasRole checks if the auth result has a specific role.
type BoltAuthenticator ΒΆ
type BoltAuthenticator interface {
// Authenticate validates credentials from the Bolt HELLO message.
// Returns auth result on success, error on failure.
// scheme: "basic", "bearer", or "none"
// principal: username (basic), empty (bearer/none)
// credentials: password (basic), JWT token (bearer), empty (none)
Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error)
}
BoltAuthenticator is the interface for authenticating Bolt protocol connections. This supports Neo4j-compatible authentication schemes:
- "basic": Username/password authentication
- "bearer": JWT token authentication (for cluster inter-node auth)
- "none": Anonymous access (if allowed)
The Bolt protocol HELLO message contains authentication credentials:
- scheme: "basic", "bearer", or "none"
- principal: username (basic) or empty (bearer/none)
- credentials: password (basic) or JWT token (bearer)
For cluster deployments, use "bearer" scheme with a shared JWT secret:
# Generate cluster token on any node:
curl -X POST http://node1:7474/api/v1/auth/cluster-token \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"node_id": "node-2", "role": "admin"}'
# Use token to connect from other nodes:
driver = GraphDatabase.driver("bolt://node1:7687",
auth=("", token)) # scheme=bearer when principal is empty
Example Implementation:
type MyAuthenticator struct {
auth *auth.Authenticator
}
func (a *MyAuthenticator) Authenticate(scheme, principal, credentials string) (*BoltAuthResult, error) {
switch scheme {
case "none":
if a.allowAnonymous {
return &BoltAuthResult{Authenticated: true, Roles: []string{"viewer"}}, nil
}
return nil, fmt.Errorf("anonymous auth not allowed")
case "bearer":
claims, err := a.auth.ValidateToken(credentials)
if err != nil {
return nil, err
}
return &BoltAuthResult{Authenticated: true, Username: claims.Username, Roles: claims.Roles}, nil
case "basic":
// ... username/password validation
}
}
type Config ΒΆ
type Config struct {
Host string
Port int
MaxConnections int
ReadBufferSize int
WriteBufferSize int
LogQueries bool // Log all queries to stdout (for debugging)
// ServerAnnouncement overrides the Bolt HELLO SUCCESS "server" metadata.
// Leave empty to advertise NornicDB natively.
ServerAnnouncement string
// Authentication
Authenticator BoltAuthenticator // Authentication handler (nil = no auth)
RequireAuth bool // Require authentication for all connections
AllowAnonymous bool // Allow "none" auth scheme (grants viewer role)
}
Config holds Bolt protocol server configuration.
All settings have sensible defaults via DefaultConfig(). The configuration follows Neo4j Bolt server conventions where applicable.
Authentication:
- Set Authenticator to enable auth (nil = no auth, accepts all)
- RequireAuth: if true, connections without valid credentials are rejected
- AllowAnonymous: if true, "none" auth scheme is accepted (viewer role)
Example:
// Production configuration with auth
config := &bolt.Config{
Port: 7687, // Standard Bolt port
MaxConnections: 1000, // High concurrency
ReadBufferSize: 32768, // 32KB read buffer
WriteBufferSize: 32768, // 32KB write buffer
Authenticator: myAuth,
RequireAuth: true,
}
// Development configuration (no auth)
config = bolt.DefaultConfig()
config.Port = 7688 // Use different port
func DefaultConfig ΒΆ
func DefaultConfig() *Config
DefaultConfig returns Neo4j-compatible default Bolt server configuration.
Defaults match Neo4j Bolt server settings:
- Port 7687 (standard Bolt port)
- 100 max concurrent connections
- 8KB read/write buffers
Example:
config := bolt.DefaultConfig() server := bolt.New(config, executor)
type DatabaseManagerInterface ΒΆ
type DatabaseManagerInterface interface {
GetStorage(name string) (storage.Engine, error)
Exists(name string) bool
DefaultDatabaseName() string
}
DatabaseManagerInterface provides database management without importing multidb.
type DeferrableExecutor ΒΆ
type DeferrableExecutor interface {
FlushableExecutor
// SetDeferFlush enables/disables deferred flush mode.
SetDeferFlush(enabled bool)
}
DeferrableExecutor extends FlushableExecutor with deferred flush mode control.
type FlushableExecutor ΒΆ
type FlushableExecutor interface {
QueryExecutor
// Flush persists all pending writes to storage.
Flush() error
}
FlushableExecutor extends QueryExecutor with deferred commit support. This enables Neo4j-style optimization where writes are buffered until PULL.
type QueryExecutor ΒΆ
type QueryExecutor interface {
Execute(ctx context.Context, query string, params map[string]any) (*QueryResult, error)
}
QueryExecutor executes Cypher queries for the Bolt server.
This interface allows the Bolt server to be decoupled from the specific database implementation. The executor receives Cypher queries and parameters from Bolt clients and returns results in a standard format.
Example Implementation:
type MyExecutor struct {
db *nornicdb.DB
}
func (e *MyExecutor) Execute(ctx context.Context, query string, params map[string]any) (*QueryResult, error) {
// Execute query against NornicDB
result, err := e.db.ExecuteCypher(ctx, query, params)
if err != nil {
return nil, err
}
// Convert to Bolt format
return &QueryResult{
Columns: result.Columns,
Rows: result.Rows,
}, nil
}
The executor should handle:
- Cypher query parsing and execution
- Parameter substitution
- Result formatting
- Error handling and reporting
type QueryResult ΒΆ
QueryResult holds the result of a query.
type Server ΒΆ
type Server struct {
// contains filtered or unexported fields
}
Server implements a Neo4j Bolt protocol server for NornicDB.
The server handles multiple concurrent client connections, each running in its own goroutine. It manages the Bolt protocol handshake, authentication, and message routing to the configured query executor.
Example:
config := bolt.DefaultConfig()
executor := &MyExecutor{} // Implements QueryExecutor
server := bolt.New(config, executor)
go func() {
if err := server.ListenAndServe(); err != nil {
log.Printf("Bolt server error: %v", err)
}
}()
// Server is now accepting connections
fmt.Printf("Bolt server listening on bolt://localhost:%d\n", config.Port)
Thread Safety:
The server is thread-safe and handles concurrent connections safely.
func New ΒΆ
func New(config *Config, executor QueryExecutor) *Server
New creates a new Bolt protocol server with the given configuration and executor.
Parameters:
- config: Server configuration (uses DefaultConfig() if nil)
- executor: Query executor for handling Cypher queries (required if dbManager is nil)
- dbManager: Database manager for multi-database support (optional, if nil uses executor)
Returns:
- Server instance ready to start
Note: If dbManager is provided, it takes precedence and executor is ignored. The dbManager enables multi-database support where each connection can specify a database in the HELLO message.
Example:
config := bolt.DefaultConfig()
executor := &MyQueryExecutor{db: nornicDB}
server := bolt.New(config, executor)
// Start server
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
Example 1 - Basic Setup with Cypher Executor:
// Create storage engine
storage := storage.NewBadgerEngine("./data/nornicdb")
defer storage.Close()
// Create Cypher executor
cypherExec := cypher.NewStorageExecutor(storage)
// Create Bolt server
config := bolt.DefaultConfig()
config.Port = 7687
server := bolt.New(config, cypherExec)
// Start server (blocks until shutdown)
log.Fatal(server.ListenAndServe())
Example 2 - Production with Connection Limits:
config := bolt.DefaultConfig()
config.Port = 7687
config.MaxConnections = 500 // Handle 500 concurrent clients
config.ReadBufferSize = 8192 // 8KB buffer
config.WriteBufferSize = 8192
config.IdleTimeout = 10 * time.Minute
executor := cypher.NewStorageExecutor(storage)
server := bolt.New(config, executor)
// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("Shutting down Bolt server...")
server.Close()
}()
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
Example 3 - Custom Query Executor with Middleware:
// Create custom executor with auth and logging
type AuthExecutor struct {
inner cypher.Executor
auth *auth.Authenticator
audit *audit.Logger
}
func (e *AuthExecutor) Execute(ctx context.Context, query string, params map[string]any) (*bolt.QueryResult, error) {
// Extract user from context
user := ctx.Value("user").(string)
// Audit log
e.audit.LogDataAccess(user, user, "query", query, "EXECUTE", true, "")
// Execute query
result, err := e.inner.Execute(ctx, query, params)
// Convert to Bolt result format
return &bolt.QueryResult{
Columns: result.Fields,
Rows: result.Records,
}, err
}
executor := &AuthExecutor{
inner: cypher.NewStorageExecutor(storage),
auth: authenticator,
audit: auditLogger,
}
server := bolt.New(bolt.DefaultConfig(), executor)
server.ListenAndServe()
Example 4 - Testing with In-Memory Storage:
func TestMyBoltIntegration(t *testing.T) {
// In-memory storage for tests
storage := storage.NewMemoryEngine()
executor := cypher.NewStorageExecutor(storage)
// Bolt server on random port
config := bolt.DefaultConfig()
config.Port = 0 // OS assigns random available port
server := bolt.New(config, executor)
// Start server in background
go server.ListenAndServe()
defer server.Close()
// Connect with Neo4j driver
driver, _ := neo4j.NewDriver(
fmt.Sprintf("bolt://localhost:%d", server.Port()),
neo4j.NoAuth(),
)
defer driver.Close()
// Run test queries
session := driver.NewSession(neo4j.SessionConfig{})
result, _ := session.Run("CREATE (n:Test {value: 42}) RETURN n", nil)
// ... assertions ...
}
ELI12:
Think of the Bolt server like a translator at the UN:
- Neo4j drivers speak "Bolt language" (binary protocol)
- NornicDB speaks "Cypher language" (graph queries)
- The Bolt server translates between them!
Why do we need this translator?
- Neo4j drivers already exist (Python, Java, JavaScript, Go, etc.)
- Tools like Neo4j Browser, Bloom, and Cypher Shell work out of the box
- No need to write new drivers for every programming language
How it works:
- Driver connects: "Hi, I speak Bolt 4.3"
- Server responds: "Cool, I understand Bolt 4.3"
- Driver sends: "RUN: MATCH (n) RETURN n LIMIT 10"
- Server executes Cypher and sends back results
- Driver receives results in Bolt format
Real-world analogy:
- HTTP is like writing letters (text-based, verbose)
- Bolt is like speaking on the phone (binary, efficient)
- Bolt is ~3-5x faster than HTTP for graph queries!
Compatible Tools:
- Neo4j Browser (web UI)
- Neo4j Desktop
- Cypher Shell (CLI)
- Neo4j Bloom (graph visualization)
- Any app using Neo4j drivers
Protocol Advantages:
- Binary format (smaller, faster)
- Connection pooling (reuse connections)
- Streaming results (low memory)
- Transaction support (BEGIN/COMMIT/ROLLBACK)
- Pipelining (send multiple queries without waiting)
Performance:
- Handles 100-500 concurrent connections easily
- ~1ms overhead per query
- Streaming results use O(1) memory per connection
- Binary PackStream is ~40% smaller than JSON
Thread Safety:
Server handles concurrent connections safely.
func NewWithDatabaseManager ΒΆ
func NewWithDatabaseManager(config *Config, executor QueryExecutor, dbManager DatabaseManagerInterface) *Server
NewWithDatabaseManager creates a new Bolt protocol server with multi-database support.
Parameters:
- config: Server configuration (uses DefaultConfig() if nil)
- executor: Query executor (ignored if dbManager is provided, kept for backward compatibility)
- dbManager: Database manager for multi-database support (optional)
Returns:
- Server instance ready to start
If dbManager is provided, queries are routed to the correct database based on the "db" or "database" parameter in the HELLO message. If not provided, the server uses the single executor for all queries (backward compatible).
func (*Server) ListenAndServe ΒΆ
ListenAndServe starts the Bolt server and begins accepting connections.
The server listens on the configured port and handles incoming Bolt connections. Each connection is handled in a separate goroutine.
Returns:
- nil if server shuts down cleanly
- Error if failed to bind to port or other startup error
Example:
server := bolt.New(config, executor)
// Start server (blocks until shutdown)
if err := server.ListenAndServe(); err != nil {
log.Fatalf("Bolt server failed: %v", err)
}
The server will print its listening address when started successfully.
func (*Server) SetDatabaseAccessMode ΒΆ
func (s *Server) SetDatabaseAccessMode(mode auth.DatabaseAccessMode)
SetDatabaseAccessMode sets the per-database access mode (e.g. from HTTP server). When set, CanAccessDatabase(dbName) is checked before running each query.
func (*Server) SetDatabaseAccessModeResolver ΒΆ
func (s *Server) SetDatabaseAccessModeResolver(resolver func(roles []string) auth.DatabaseAccessMode)
SetDatabaseAccessModeResolver sets a per-principal resolver (e.g. from HTTP server for Phase 3 allowlist). When set, the resolver is called with the session's roles to get the mode for each query.
func (*Server) SetResolvedAccessResolver ΒΆ
func (s *Server) SetResolvedAccessResolver(resolver func(roles []string, dbName string) auth.ResolvedAccess)
SetResolvedAccessResolver sets a per-(principal, db) resolver for Phase 4 write checks.
type Session ΒΆ
type Session struct {
// contains filtered or unexported fields
}
Session represents a client session.
type SessionExecutorFactory ΒΆ
type SessionExecutorFactory interface {
NewSessionExecutor() QueryExecutor
}
SessionExecutorFactory creates per-connection executors. Use this when executor implementations keep session-local state (e.g., explicit tx IDs).
type TransactionalExecutor ΒΆ
type TransactionalExecutor interface {
QueryExecutor
BeginTransaction(ctx context.Context, metadata map[string]any) error
CommitTransaction(ctx context.Context) error
RollbackTransaction(ctx context.Context) error
}
TransactionalExecutor extends QueryExecutor with transaction support.
If the executor implements this interface, the Bolt server will use real transactions for BEGIN/COMMIT/ROLLBACK messages. Otherwise, transaction messages are acknowledged but operations are auto-committed.
Example Implementation:
type TxExecutor struct {
db *nornicdb.DB
tx *storage.Transaction // Active transaction (nil if none)
}
func (e *TxExecutor) BeginTransaction(ctx context.Context) error {
e.tx = storage.NewTransaction(e.db.Engine())
return nil
}
func (e *TxExecutor) CommitTransaction(ctx context.Context) error {
if e.tx == nil {
return nil
}
err := e.tx.Commit()
e.tx = nil
return err
}
func (e *TxExecutor) RollbackTransaction(ctx context.Context) error {
if e.tx == nil {
return nil
}
err := e.tx.Rollback()
e.tx = nil
return err
}