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()
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
- Clustering Guide - Set up multi-node clusters
- DB-API 2.0 - Use standard Python database interface
- SQLAlchemy - ORM integration
- API Reference - Complete API documentation