Skip to main content

Usage Guide

This guide covers common usage patterns for dqlitepy.

Basic Node Operations

Creating and Starting a Node

from dqlitepy import Node
from pathlib import Path

# Create a node
node = Node(
address="127.0.0.1:9001", # IP:port for cluster communication
data_dir=Path("/tmp/dqlite-data"), # Directory for Raft logs and snapshots
node_id=None, # Auto-generated if not provided
)

# Start the node
node.start()

# Check if running
print(f"Node {node.id} is running: {node.is_running}")

Opening a Database

# Open a database (creates if doesn't exist)
node.open_db("myapp.db")

# You can open multiple databases
node.open_db("analytics.db")
node.open_db("cache.db")

Executing SQL

DML Operations (exec)

Use exec() for INSERT, UPDATE, DELETE, CREATE TABLE, etc:

# Create table
node.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")

# Insert data
last_id, rows_affected = node.exec("INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')")
print(f"Inserted user with ID: {last_id}")

# Update data
_, rows_affected = node.exec("UPDATE users SET email = 'alice@newdomain.com' WHERE name = 'Alice'")
print(f"Updated {rows_affected} rows")

# Delete data
_, rows_affected = node.exec("DELETE FROM users WHERE id = 1")

Query Operations (query)

Use query() for SELECT statements:

# Query all rows
results = node.query("SELECT * FROM users")
for row in results:
print(f"ID: {row['id']}, Name: {row['name']}, Email: {row['email']}")

# Query with WHERE clause
results = node.query("SELECT name, email FROM users WHERE id > 10")

# Aggregate queries
results = node.query("SELECT COUNT(*) as total FROM users")
print(f"Total users: {results[0]['total']}")

Transactions

# Begin a transaction
node.begin()

try:
node.exec("INSERT INTO users (name) VALUES ('Bob')")
node.exec("INSERT INTO users (name) VALUES ('Charlie')")

# Commit the transaction
node.commit()
except Exception as e:
# Rollback on error
node.rollback()
print(f"Transaction rolled back: {e}")

Stopping a Node

# Graceful shutdown
#
node.stop()
Known Issue

There is a known upstream issue in dqlite that can cause segfaults when stopping nodes in certain conditions. See canonical/dqlite#spectacular-confusion for details.

Workaround: Set the DQLITEPY_BYPASS_STOP environment variable to skip the stop call and rely on process cleanup:

import os
os.environ["DQLITEPY_BYPASS_STOP"] = "1"

from dqlitepy import Node

node = Node("127.0.0.1:9001", "/tmp/dqlite")
node.start()
# ... use node ...
node.stop() # Will be bypassed, cleanup happens at process exit

Using the Client API

The Client API allows you to connect to a cluster without running a node:

from dqlitepy import Client

# Connect to cluster
client = Client(["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"])

# Find the leader
leader = client.leader()
print(f"Current leader: {leader.address}")

# Get cluster information
nodes = client.cluster()
for node_info in nodes:
print(f"Node {node_info.id}: {node_info.address} (role: {node_info.role})")

# Add a node to the cluster
client.add(node_id=12345, address="127.0.0.1:9004")

# Remove a node
client.remove(node_id=12345)

# Close the client
client.close()

Error Handling

dqlitepy provides specific exception classes for different error types:

from dqlitepy import (
Node,
NodeStartError,
NodeError,
DqliteError,
ClusterError,
NoLeaderError
)

try:
node = Node("127.0.0.1:9001", "/tmp/dqlite")
node.start()
node.open_db("myapp.db")

# This might fail if not leader
node.exec("INSERT INTO users (name) VALUES ('Test')")

except NodeStartError as e:
print(f"Failed to start node: {e}")
except NoLeaderError:
print("No leader elected yet, retry later")
except ClusterError as e:
print(f"Cluster error: {e}")
except DqliteError as e:
print(f"dqlite error: {e}")

Context Managers

Use context managers for automatic cleanup:

from dqlitepy import Node
from pathlib import Path

# Node automatically stopped when exiting context
with Node("127.0.0.1:9001", Path("/tmp/dqlite")) as node:
node.start()
node.open_db("myapp.db")

results = node.query("SELECT * FROM users")
print(results)
# Node.stop() called automatically

Environment Variables

Configure behavior with environment variables:

import os

# Bypass node stop to avoid segfault (workaround for known issue)
os.environ["DQLITEPY_BYPASS_STOP"] = "1"

from dqlitepy import Node

node = Node("127.0.0.1:9001", "/tmp/dqlite")
node.start()
# ... use node ...
# stop() will be bypassed, cleanup happens at process exit

Best Practices

1. Use Specific Addresses

Always use specific IP addresses, not 0.0.0.0:

# ✅ Good
node = Node("192.168.1.10:9001", "/data")

# ❌ Avoid
node = Node("0.0.0.0:9001", "/data")

2. Handle Leader Election

Writes must go to the leader. Handle NoLeaderError and retry:

import time
from dqlitepy import NoLeaderError

max_retries = 5
for attempt in range(max_retries):
try:
node.exec("INSERT INTO users (name) VALUES ('Alice')")
break
except NoLeaderError:
if attempt < max_retries - 1:
time.sleep(1) # Wait for leader election
continue
raise

3. Use Transactions for Multiple Operations

# ✅ Good - atomic
node.begin()
try:
node.exec("INSERT INTO accounts (name, balance) VALUES ('Alice', 1000)")
node.exec("INSERT INTO transactions (account, amount) VALUES ('Alice', -100)")
node.commit()
except:
node.rollback()
raise

# ❌ Avoid - not atomic
node.exec("INSERT INTO accounts (name, balance) VALUES ('Alice', 1000)")
node.exec("INSERT INTO transactions (account, amount) VALUES ('Alice', -100)")

4. Close Resources

Always close clients and stop nodes when done:

try:
client = Client(["127.0.0.1:9001"])
# ... use client ...
finally:
client.close()

Next Steps