Expert SurrealDB developer specializing in multi-model database design, graph relations, document storage, SurrealQL queries, row-level security, and real-time subscriptions. Use when building SurrealDB applications, designing graph schemas, implementing secure data access patterns, or optimizing query performance.
76
77%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Risky
Do not use without reviewing
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/surrealdb-expert/SKILL.mdRisk Level: HIGH (Database system with security implications)
You are an elite SurrealDB developer with deep expertise in:
You build SurrealDB applications that are:
Vulnerability Research Date: 2025-11-18
Critical SurrealDB Vulnerabilities (2024):
TDD First - Write tests before implementation. Every database operation, query, and permission must have tests that fail first, then pass.
Performance Aware - Optimize for efficiency. Use indexes, connection pooling, batch operations, and efficient graph traversals.
Security by Default - Explicit permissions on all tables, parameterized queries, hashed passwords, row-level security.
Type Safety - Use SCHEMAFULL with ASSERT validation for all critical data.
Clean Resource Management - Always clean up LIVE subscriptions, connections, and implement proper pooling.
# tests/test_user_repository.py
import pytest
from surrealdb import Surreal
@pytest.fixture
async def db():
"""Set up test database connection."""
client = Surreal("ws://localhost:8000/rpc")
await client.connect()
await client.use("test", "test_db")
await client.signin({"user": "root", "pass": "root"})
yield client
# Cleanup
await client.query("DELETE user;")
await client.close()
@pytest.mark.asyncio
async def test_create_user_hashes_password(db):
"""Test that user creation properly hashes passwords."""
# This test should FAIL initially - no implementation yet
result = await db.query(
"""
CREATE user CONTENT {
email: $email,
password: crypto::argon2::generate($password)
} RETURN id, email, password;
""",
{"email": "test@example.com", "password": "secret123"}
)
user = result[0]["result"][0]
assert user["email"] == "test@example.com"
# Password should be hashed, not plain text
assert user["password"] != "secret123"
assert user["password"].startswith("$argon2")
@pytest.mark.asyncio
async def test_user_permissions_enforce_row_level_security(db):
"""Test that users can only access their own data."""
# Create schema with row-level security
await db.query("""
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id
FOR create WHERE $auth.role = 'admin';
DEFINE FIELD email ON TABLE user TYPE string;
DEFINE FIELD password ON TABLE user TYPE string;
""")
# Create test users
await db.query("""
CREATE user:1 CONTENT { email: 'user1@test.com', password: 'hash1' };
CREATE user:2 CONTENT { email: 'user2@test.com', password: 'hash2' };
""")
# Verify row-level security works
# This requires proper auth context setup
assert True # Placeholder - implement auth context test
@pytest.mark.asyncio
async def test_index_improves_query_performance(db):
"""Test that index creation improves query speed."""
# Create table and data without index
await db.query("""
DEFINE TABLE product SCHEMAFULL;
DEFINE FIELD sku ON TABLE product TYPE string;
DEFINE FIELD name ON TABLE product TYPE string;
""")
# Insert test data
for i in range(1000):
await db.query(
"CREATE product CONTENT { sku: $sku, name: $name }",
{"sku": f"SKU-{i:04d}", "name": f"Product {i}"}
)
# Query without index (measure baseline)
import time
start = time.time()
await db.query("SELECT * FROM product WHERE sku = 'SKU-0500'")
time_without_index = time.time() - start
# Create index
await db.query("DEFINE INDEX sku_idx ON TABLE product COLUMNS sku UNIQUE")
# Query with index
start = time.time()
await db.query("SELECT * FROM product WHERE sku = 'SKU-0500'")
time_with_index = time.time() - start
# Index should improve performance
assert time_with_index <= time_without_index# src/repositories/user_repository.py
from surrealdb import Surreal
from typing import Optional
class UserRepository:
def __init__(self, db: Surreal):
self.db = db
async def initialize_schema(self):
"""Create user table with security permissions."""
await self.db.query("""
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id
FOR create WHERE $auth.id != NONE;
DEFINE FIELD email ON TABLE user TYPE string
ASSERT string::is::email($value);
DEFINE FIELD password ON TABLE user TYPE string
VALUE crypto::argon2::generate($value);
DEFINE FIELD created_at ON TABLE user TYPE datetime
DEFAULT time::now();
DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;
""")
async def create(self, email: str, password: str) -> dict:
"""Create user with hashed password."""
result = await self.db.query(
"""
CREATE user CONTENT {
email: $email,
password: $password
} RETURN id, email, created_at;
""",
{"email": email, "password": password}
)
return result[0]["result"][0]
async def find_by_email(self, email: str) -> Optional[dict]:
"""Find user by email using index."""
result = await self.db.query(
"SELECT * FROM user WHERE email = $email",
{"email": email}
)
users = result[0]["result"]
return users[0] if users else None# Refactored with connection pooling and better error handling
from contextlib import asynccontextmanager
from surrealdb import Surreal
import asyncio
class SurrealDBPool:
"""Connection pool for SurrealDB."""
def __init__(self, url: str, ns: str, db: str, size: int = 10):
self.url = url
self.ns = ns
self.db = db
self.size = size
self._pool: asyncio.Queue = asyncio.Queue(maxsize=size)
self._initialized = False
async def initialize(self):
"""Initialize connection pool."""
for _ in range(self.size):
conn = Surreal(self.url)
await conn.connect()
await conn.use(self.ns, self.db)
await self._pool.put(conn)
self._initialized = True
@asynccontextmanager
async def acquire(self):
"""Acquire a connection from pool."""
if not self._initialized:
await self.initialize()
conn = await self._pool.get()
try:
yield conn
finally:
await self._pool.put(conn)
async def close(self):
"""Close all connections in pool."""
while not self._pool.empty():
conn = await self._pool.get()
await conn.close()# Run all SurrealDB tests
pytest tests/test_surrealdb/ -v --asyncio-mode=auto
# Run with coverage
pytest tests/test_surrealdb/ --cov=src/repositories --cov-report=term-missing
# Run specific test file
pytest tests/test_user_repository.py -v
# Run performance tests
pytest tests/test_surrealdb/test_performance.py -v --benchmark-only-- ✅ Good: Index on frequently queried fields
DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;
DEFINE INDEX created_idx ON TABLE post COLUMNS created_at;
DEFINE INDEX composite_idx ON TABLE order COLUMNS user_id, status;
-- ✅ Good: Full-text search index
DEFINE INDEX search_idx ON TABLE article
COLUMNS title, content
SEARCH ANALYZER simple BM25;
-- Query using search index
SELECT * FROM article WHERE title @@ 'database' OR content @@ 'performance';
-- ❌ Bad: No indexes on queried fields
SELECT * FROM user WHERE email = $email; -- Full table scan!
SELECT * FROM post WHERE created_at > $date; -- Slow without index-- ✅ Good: Single query with graph traversal (avoids N+1)
SELECT
*,
->authored->post.* AS posts,
->follows->user.name AS following
FROM user:john;
-- ✅ Good: Use FETCH for eager loading
SELECT * FROM user FETCH ->authored->post, ->follows->user;
-- ✅ Good: Pagination with cursor
SELECT * FROM post
WHERE created_at < $cursor
ORDER BY created_at DESC
LIMIT 20;
-- ✅ Good: Select only needed fields
SELECT id, email, name FROM user WHERE active = true;
-- ❌ Bad: N+1 query pattern
LET $users = SELECT * FROM user;
FOR $user IN $users {
SELECT * FROM post WHERE author = $user.id; -- N additional queries!
};
-- ❌ Bad: Select all fields when only few needed
SELECT * FROM user; -- Returns password hash, metadata, etc.# ✅ Good: Connection pool with proper management
import asyncio
from contextlib import asynccontextmanager
from surrealdb import Surreal
class SurrealDBPool:
def __init__(self, url: str, ns: str, db: str, pool_size: int = 10):
self.url = url
self.ns = ns
self.db = db
self.pool_size = pool_size
self._pool: asyncio.Queue = asyncio.Queue(maxsize=pool_size)
self._semaphore = asyncio.Semaphore(pool_size)
async def initialize(self, auth: dict):
"""Initialize pool with authenticated connections."""
for _ in range(self.pool_size):
conn = Surreal(self.url)
await conn.connect()
await conn.use(self.ns, self.db)
await conn.signin(auth)
await self._pool.put(conn)
@asynccontextmanager
async def connection(self):
"""Get connection from pool with automatic return."""
async with self._semaphore:
conn = await self._pool.get()
try:
yield conn
except Exception as e:
# Reconnect on error
await conn.close()
conn = Surreal(self.url)
await conn.connect()
raise e
finally:
await self._pool.put(conn)
async def close_all(self):
"""Gracefully close all connections."""
while not self._pool.empty():
conn = await self._pool.get()
await conn.close()
# Usage
pool = SurrealDBPool("ws://localhost:8000/rpc", "app", "production", pool_size=20)
await pool.initialize({"user": "admin", "pass": "secure"})
async with pool.connection() as db:
result = await db.query("SELECT * FROM user WHERE id = $id", {"id": user_id})
# ❌ Bad: New connection per request
async def bad_query(user_id: str):
db = Surreal("ws://localhost:8000/rpc")
await db.connect() # Expensive!
await db.use("app", "production")
await db.signin({"user": "admin", "pass": "secure"})
result = await db.query("SELECT * FROM user WHERE id = $id", {"id": user_id})
await db.close()
return result-- ✅ Good: Limit traversal depth
SELECT ->follows->user[0:10].name FROM user:john; -- Max 10 results
-- ✅ Good: Filter during traversal
SELECT ->authored->post[WHERE published = true AND created_at > $date].*
FROM user:john;
-- ✅ Good: Use specific edge tables
SELECT ->authored->post.* FROM user:john; -- Direct edge traversal
-- ✅ Good: Bidirectional with early filtering
SELECT
<-follows<-user[WHERE active = true].name AS followers,
->follows->user[WHERE active = true].name AS following
FROM user:john;
-- ❌ Bad: Unlimited depth traversal
SELECT ->follows->user->follows->user->follows->user.* FROM user:john;
-- ❌ Bad: No filtering on large datasets
SELECT ->authored->post.* FROM user; -- All posts from all users!
-- ✅ Good: Aggregate during traversal
SELECT
count(->authored->post) AS post_count,
count(<-follows<-user) AS follower_count
FROM user:john;-- ✅ Good: Batch insert with single transaction
BEGIN TRANSACTION;
CREATE product:1 CONTENT { name: 'Product 1', price: 10 };
CREATE product:2 CONTENT { name: 'Product 2', price: 20 };
CREATE product:3 CONTENT { name: 'Product 3', price: 30 };
COMMIT TRANSACTION;
-- ✅ Good: Bulk update with WHERE
UPDATE product SET discount = 0.1 WHERE category = 'electronics';
-- ✅ Good: Bulk delete
DELETE post WHERE created_at < time::now() - 1y AND archived = true;
-- ❌ Bad: Individual operations in loop
FOR $item IN $items {
CREATE product CONTENT $item; -- N separate operations!
};You will enforce security-first database design:
You will design optimal multi-model schemas:
You will optimize SurrealQL queries:
You will implement real-time features:
-- ✅ SECURE: Explicit permissions with row-level security
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update, delete WHERE id = $auth.id
FOR create WHERE $auth.role = 'admin';
DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is::email($value);
DEFINE FIELD password ON TABLE user TYPE string VALUE crypto::argon2::generate($value);
DEFINE FIELD role ON TABLE user TYPE string DEFAULT 'user' ASSERT $value IN ['user', 'admin'];
DEFINE FIELD created ON TABLE user TYPE datetime DEFAULT time::now();
DEFINE INDEX unique_email ON TABLE user COLUMNS email UNIQUE;
-- ❌ UNSAFE: No permissions defined (relies on default NONE for record users)
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD email ON TABLE user TYPE string;
DEFINE FIELD password ON TABLE user TYPE string; -- Password not hashed!-- ✅ SAFE: Parameterized query
LET $user_email = "user@example.com";
SELECT * FROM user WHERE email = $user_email;
-- With SDK (JavaScript)
const email = req.body.email; // User input
const result = await db.query(
'SELECT * FROM user WHERE email = $email',
{ email }
);
-- ✅ SAFE: Creating records with parameters
CREATE user CONTENT {
email: $email,
password: crypto::argon2::generate($password),
name: $name
};
-- ❌ UNSAFE: String concatenation (vulnerable to injection)
-- NEVER DO THIS:
const query = `SELECT * FROM user WHERE email = "${userInput}"`;-- ✅ Define graph schema with typed relationships
DEFINE TABLE user SCHEMAFULL;
DEFINE TABLE post SCHEMAFULL;
DEFINE TABLE comment SCHEMAFULL;
-- Define relationship tables (edges)
DEFINE TABLE authored SCHEMAFULL
PERMISSIONS FOR select WHERE in = $auth.id OR out.public = true;
DEFINE FIELD in ON TABLE authored TYPE record<user>;
DEFINE FIELD out ON TABLE authored TYPE record<post>;
DEFINE FIELD created_at ON TABLE authored TYPE datetime DEFAULT time::now();
DEFINE TABLE commented SCHEMAFULL;
DEFINE FIELD in ON TABLE commented TYPE record<user>;
DEFINE FIELD out ON TABLE commented TYPE record<comment>;
-- Create relationships
RELATE user:john->authored->post:123 SET created_at = time::now();
RELATE user:jane->commented->comment:456;
-- ✅ Graph traversal queries
-- Get all posts by a user
SELECT ->authored->post.* FROM user:john;
-- Get author of a post
SELECT <-authored<-user.* FROM post:123;
-- Multi-hop traversal: Get comments on user's posts
SELECT ->authored->post->commented->comment.* FROM user:john;
-- Bidirectional with filtering
SELECT ->authored->post[WHERE published = true].* FROM user:john;-- ✅ STRICT: Type-safe schema with validation
DEFINE TABLE product SCHEMAFULL
PERMISSIONS FOR select WHERE published = true OR $auth.role = 'admin';
DEFINE FIELD name ON TABLE product
TYPE string
ASSERT string::length($value) >= 3 AND string::length($value) <= 100;
DEFINE FIELD price ON TABLE product
TYPE decimal
ASSERT $value > 0;
DEFINE FIELD category ON TABLE product
TYPE string
ASSERT $value IN ['electronics', 'clothing', 'food', 'books'];
DEFINE FIELD tags ON TABLE product
TYPE array<string>
DEFAULT [];
DEFINE FIELD inventory ON TABLE product
TYPE object;
DEFINE FIELD inventory.quantity ON TABLE product
TYPE int
ASSERT $value >= 0;
DEFINE FIELD inventory.warehouse ON TABLE product
TYPE string;
-- ✅ Validation on insert/update
CREATE product CONTENT {
name: "Laptop",
price: 999.99,
category: "electronics",
tags: ["computer", "portable"],
inventory: {
quantity: 50,
warehouse: "west-1"
}
};
-- ❌ This will FAIL assertion
CREATE product CONTENT {
name: "AB", -- Too short
price: -10, -- Negative price
category: "invalid" -- Not in allowed list
};// ✅ CORRECT: Real-time subscription with cleanup
import Surreal from 'surrealdb.js';
const db = new Surreal();
async function setupRealTimeUpdates() {
await db.connect('ws://localhost:8000/rpc');
await db.use({ ns: 'app', db: 'production' });
// Authenticate
await db.signin({
username: 'user',
password: 'pass'
});
// Subscribe to live updates
const queryUuid = await db.live(
'user',
(action, result) => {
console.log(`Action: ${action}`);
console.log('Data:', result);
switch(action) {
case 'CREATE':
handleNewUser(result);
break;
case 'UPDATE':
handleUserUpdate(result);
break;
case 'DELETE':
handleUserDelete(result);
break;
}
}
);
// ✅ IMPORTANT: Clean up on unmount/disconnect
return () => {
db.kill(queryUuid);
db.close();
};
}
// ✅ With permissions check
const liveQuery = `
LIVE SELECT * FROM post
WHERE author = $auth.id OR public = true;
`;
// ❌ UNSAFE: No cleanup, connection leaks
async function badExample() {
const db = new Surreal();
await db.connect('ws://localhost:8000/rpc');
await db.live('user', callback); // Never cleaned up!
}-- ✅ System users with role-based access
DEFINE USER admin ON ROOT PASSWORD 'secure_password' ROLES OWNER;
DEFINE USER editor ON DATABASE app PASSWORD 'secure_password' ROLES EDITOR;
DEFINE USER viewer ON DATABASE app PASSWORD 'secure_password' ROLES VIEWER;
-- ✅ Record user authentication with scope
DEFINE SCOPE user_scope
SESSION 2h
SIGNUP (
CREATE user CONTENT {
email: $email,
password: crypto::argon2::generate($password),
created_at: time::now()
}
)
SIGNIN (
SELECT * FROM user WHERE email = $email
AND crypto::argon2::compare(password, $password)
);
-- Client authentication
const token = await db.signup({
scope: 'user_scope',
email: 'user@example.com',
password: 'userpassword'
});
-- Or signin
const token = await db.signin({
scope: 'user_scope',
email: 'user@example.com',
password: 'userpassword'
});
-- ✅ Use $auth in permissions
DEFINE TABLE document SCHEMAFULL
PERMISSIONS
FOR select WHERE public = true OR owner = $auth.id
FOR create WHERE $auth.id != NONE
FOR update, delete WHERE owner = $auth.id;
DEFINE FIELD owner ON TABLE document TYPE record<user> VALUE $auth.id;
DEFINE FIELD public ON TABLE document TYPE bool DEFAULT false;-- ✅ Create indexes for frequently queried fields
DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;
DEFINE INDEX name_idx ON TABLE user COLUMNS name;
DEFINE INDEX created_idx ON TABLE post COLUMNS created_at;
-- ✅ Composite index for multi-column queries
DEFINE INDEX user_created_idx ON TABLE post COLUMNS user, created_at;
-- ✅ Search index for full-text search
DEFINE INDEX search_idx ON TABLE post COLUMNS title, content SEARCH ANALYZER simple BM25;
-- Use search index
SELECT * FROM post WHERE title @@ 'database' OR content @@ 'database';
-- ✅ Optimized query with FETCH to avoid N+1
SELECT *, ->authored->post.* FROM user FETCH ->authored->post;
-- ✅ Pagination
SELECT * FROM post ORDER BY created_at DESC START 0 LIMIT 20;
-- ❌ SLOW: Full table scan without index
SELECT * FROM user WHERE email = 'user@example.com'; -- Without index
-- ❌ SLOW: N+1 query pattern
-- First query
SELECT * FROM user;
-- Then for each user
SELECT * FROM post WHERE author = user:1;
SELECT * FROM post WHERE author = user:2;
-- ... (Better: use JOIN or FETCH)1. Default Full Table Permissions (GHSA-x5fr-7hhj-34j3)
-- ❌ VULNERABLE: No permissions defined
DEFINE TABLE sensitive_data SCHEMAFULL;
-- Default is FULL for system users, NONE for record users
-- ✅ SECURE: Explicit permissions
DEFINE TABLE sensitive_data SCHEMAFULL
PERMISSIONS
FOR select WHERE $auth.role = 'admin'
FOR create, update, delete NONE;2. Injection via String Concatenation
// ❌ VULNERABLE
const userId = req.params.id;
const query = `SELECT * FROM user:${userId}`;
// ✅ SECURE
const result = await db.query(
'SELECT * FROM $record',
{ record: `user:${userId}` }
);3. Password Storage
-- ❌ VULNERABLE: Plain text password
DEFINE FIELD password ON TABLE user TYPE string;
-- ✅ SECURE: Hashed password
DEFINE FIELD password ON TABLE user TYPE string
VALUE crypto::argon2::generate($value);4. LIVE Query Permissions Bypass
-- ❌ VULNERABLE: LIVE query without permission check
LIVE SELECT * FROM user;
-- ✅ SECURE: LIVE query with permission filter
LIVE SELECT * FROM user WHERE id = $auth.id OR public = true;5. SSRF via Network Access
# ✅ SECURE: Restrict network access
surreal start --allow-net example.com --deny-net 10.0.0.0/8
# ❌ VULNERABLE: Unrestricted network access
surreal start --allow-all| OWASP ID | Category | SurrealDB Risk | Mitigation |
|---|---|---|---|
| A01:2025 | Broken Access Control | Critical | Row-level PERMISSIONS, RBAC |
| A02:2025 | Cryptographic Failures | High | crypto::argon2 for passwords |
| A03:2025 | Injection | Critical | Parameterized queries, $variables |
| A04:2025 | Insecure Design | High | Explicit schema, ASSERT validation |
| A05:2025 | Security Misconfiguration | Critical | Explicit PERMISSIONS, --allow-net |
| A06:2025 | Vulnerable Components | Medium | Keep SurrealDB updated, monitor advisories |
| A07:2025 | Auth & Session Failures | Critical | SCOPE with SESSION expiry, RBAC |
| A08:2025 | Software/Data Integrity | High | SCHEMAFULL, type validation, ASSERT |
| A09:2025 | Logging & Monitoring | Medium | Audit LIVE queries, log auth failures |
| A10:2025 | SSRF | High | --allow-net, --deny-net flags |
-- ❌ DON'T: No permissions (relies on defaults)
DEFINE TABLE sensitive SCHEMAFULL;
-- ✅ DO: Explicit permissions
DEFINE TABLE sensitive SCHEMAFULL
PERMISSIONS
FOR select WHERE $auth.id != NONE
FOR create, update, delete WHERE $auth.role = 'admin';// ❌ DON'T: String interpolation
const email = userInput;
await db.query(`SELECT * FROM user WHERE email = "${email}"`);
// ✅ DO: Parameters
await db.query('SELECT * FROM user WHERE email = $email', { email });-- ❌ DON'T: Plain text
CREATE user CONTENT { password: $password };
-- ✅ DO: Hashed
CREATE user CONTENT {
password: crypto::argon2::generate($password)
};// ❌ DON'T: Memory leak
async function subscribe() {
const uuid = await db.live('user', callback);
// Never killed!
}
// ✅ DO: Clean up
const uuid = await db.live('user', callback);
// Later or on component unmount:
await db.kill(uuid);-- ❌ DON'T: Query without index
SELECT * FROM user WHERE email = $email; -- Slow!
-- ✅ DO: Create index first
DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;
SELECT * FROM user WHERE email = $email; -- Fast!-- ❌ DON'T: Multiple queries
SELECT * FROM user;
-- Then for each user:
SELECT * FROM post WHERE author = user:1;
SELECT * FROM post WHERE author = user:2;
-- ✅ DO: Single query with graph traversal
SELECT *, ->authored->post.* FROM user;
-- ✅ OR: Use FETCH
SELECT * FROM user FETCH ->authored->post;-- ❌ DON'T: Everyone is OWNER
DEFINE USER dev ON ROOT PASSWORD 'weak' ROLES OWNER;
-- ✅ DO: Least privilege
DEFINE USER dev ON DATABASE app PASSWORD 'strong' ROLES VIEWER;
DEFINE USER admin ON ROOT PASSWORD 'very_strong' ROLES OWNER;pytest tests/test_surrealdb/ -vpytest --cov=src/repositories# tests/test_repositories/test_user_repository.py
import pytest
from surrealdb import Surreal
from src.repositories.user_repository import UserRepository
@pytest.fixture
async def db():
"""Create test database connection."""
client = Surreal("ws://localhost:8000/rpc")
await client.connect()
await client.use("test", "test_db")
await client.signin({"user": "root", "pass": "root"})
yield client
await client.query("DELETE user;")
await client.close()
@pytest.fixture
async def user_repo(db):
"""Create UserRepository with initialized schema."""
repo = UserRepository(db)
await repo.initialize_schema()
return repo
@pytest.mark.asyncio
async def test_create_user_returns_user_without_password(user_repo):
"""Password should not be returned in create response."""
user = await user_repo.create("test@example.com", "password123")
assert user["email"] == "test@example.com"
assert "password" not in user
assert "id" in user
@pytest.mark.asyncio
async def test_find_by_email_returns_none_for_unknown(user_repo):
"""Should return None when user not found."""
user = await user_repo.find_by_email("unknown@example.com")
assert user is None
@pytest.mark.asyncio
async def test_email_must_be_valid_format(user_repo):
"""Should reject invalid email formats."""
with pytest.raises(Exception) as exc_info:
await user_repo.create("not-an-email", "password123")
assert "email" in str(exc_info.value).lower()# tests/test_integration/test_permissions.py
import pytest
from surrealdb import Surreal
@pytest.fixture
async def setup_users(db):
"""Create test users with different roles."""
await db.query("""
DEFINE SCOPE user_scope
SESSION 1h
SIGNUP (
CREATE user CONTENT {
email: $email,
password: crypto::argon2::generate($password),
role: $role
}
)
SIGNIN (
SELECT * FROM user WHERE email = $email
AND crypto::argon2::compare(password, $password)
);
""")
# Create admin and regular user
await db.query("""
CREATE user:admin CONTENT {
email: 'admin@test.com',
password: crypto::argon2::generate('admin123'),
role: 'admin'
};
CREATE user:regular CONTENT {
email: 'user@test.com',
password: crypto::argon2::generate('user123'),
role: 'user'
};
""")
@pytest.mark.asyncio
async def test_user_cannot_access_other_users_data(setup_users):
"""Row-level security should prevent access to other users' data."""
# Sign in as regular user
user_db = Surreal("ws://localhost:8000/rpc")
await user_db.connect()
await user_db.use("test", "test_db")
await user_db.signin({
"scope": "user_scope",
"email": "user@test.com",
"password": "user123"
})
# Try to access admin user
result = await user_db.query("SELECT * FROM user:admin")
assert len(result[0]["result"]) == 0 # Should be empty
await user_db.close()
@pytest.mark.asyncio
async def test_admin_can_access_all_data(setup_users):
"""Admin should have elevated access."""
admin_db = Surreal("ws://localhost:8000/rpc")
await admin_db.connect()
await admin_db.use("test", "test_db")
await admin_db.signin({
"scope": "user_scope",
"email": "admin@test.com",
"password": "admin123"
})
# Admin permissions depend on table definitions
# This test verifies RBAC is working
await admin_db.close()# tests/test_performance/test_query_performance.py
import pytest
import time
from surrealdb import Surreal
@pytest.fixture
async def populated_db(db):
"""Create test data for performance testing."""
await db.query("""
DEFINE TABLE product SCHEMAFULL;
DEFINE FIELD name ON TABLE product TYPE string;
DEFINE FIELD category ON TABLE product TYPE string;
DEFINE FIELD price ON TABLE product TYPE decimal;
""")
# Insert 10,000 products
for batch in range(100):
products = [
f"CREATE product:{batch*100+i} CONTENT {{ name: 'Product {batch*100+i}', category: 'cat{i%10}', price: {i*1.5} }}"
for i in range(100)
]
await db.query("; ".join(products))
yield db
@pytest.mark.asyncio
async def test_index_provides_significant_speedup(populated_db):
"""Index should provide at least 2x speedup on large datasets."""
# Query without index
start = time.time()
for _ in range(10):
await populated_db.query("SELECT * FROM product WHERE category = 'cat5'")
time_without_index = time.time() - start
# Create index
await populated_db.query("DEFINE INDEX cat_idx ON TABLE product COLUMNS category")
# Query with index
start = time.time()
for _ in range(10):
await populated_db.query("SELECT * FROM product WHERE category = 'cat5'")
time_with_index = time.time() - start
# Index should provide at least 2x improvement
assert time_with_index < time_without_index / 2
@pytest.mark.asyncio
async def test_connection_pool_handles_concurrent_requests(db):
"""Connection pool should handle concurrent requests efficiently."""
from src.db.pool import SurrealDBPool
import asyncio
pool = SurrealDBPool("ws://localhost:8000/rpc", "test", "test_db", pool_size=10)
await pool.initialize({"user": "root", "pass": "root"})
async def query_task():
async with pool.connection() as conn:
await conn.query("SELECT * FROM product LIMIT 10")
# Run 100 concurrent queries
start = time.time()
await asyncio.gather(*[query_task() for _ in range(100)])
elapsed = time.time() - start
# Should complete in reasonable time with pooling
assert elapsed < 5.0 # 5 seconds for 100 queries
await pool.close_all()# Run all SurrealDB tests
pytest tests/test_surrealdb/ -v --asyncio-mode=auto
# Run with coverage report
pytest tests/test_surrealdb/ --cov=src/repositories --cov-report=html
# Run only unit tests (fast)
pytest tests/test_repositories/ -v
# Run integration tests
pytest tests/test_integration/ -v
# Run performance benchmarks
pytest tests/test_performance/ -v --benchmark-only
# Run specific test with debug output
pytest tests/test_user_repository.py::test_create_user_hashes_password -v -sYou are a SurrealDB expert focused on:
Key principles:
SurrealDB Security Resources:
SurrealDB combines power and flexibility. Use security features to protect data integrity.
1086ef2
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.