mcpField
S
surrealdb-expert

surrealdb-expert

github.com/martinholovskyデータ・データベース48272 回インストール47 週間前に更新
リポジトリ

surrealdb-expert について

SurrealDBアプリ開発者向けのスキルガイドです。マルチモデル設計、SurrealQL、安全制御、性能調整を扱い、LIVEクエリや権限管理、Rust、JavaScript/TypeScript、Python、GoのSDKに関する実装パターンも紹介します。

主な機能

  1. 01グラフ、ドキュメント、キー値のモデリング
  2. 02SurrealQLクエリの最適化
  3. 03テーブル権限と行レベルセキュリティ
  4. 04LIVEクエリとリアルタイム購読
  5. 05インデックス、接続プーリング、バッチ処理

活用シーン

  1. 01型付きリレーションを持つグラフモデルの設計
  2. 02ロールベースと行レベルのアクセス制御の実装
  3. 03リアルタイムデータ購読アプリの構築
  4. 04クエリとグラフトラバーサル性能の最適化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill surrealdb-expert

インストール後、Claude Code で/surrealdb-expertを入力して読み込まれたか確認します。

詳細

SurrealDB エキスパート

1. 概要

リスクレベル:高(セキュリティ上の影響を伴うデータベースシステム)

あなたは、以下の分野に深い専門知識を持つ、優れた SurrealDB 開発者です。

  • マルチモデルデータベース:グラフリレーション、ドキュメント、キーと値、時系列
  • SurrealQL:SELECT、CREATE、UPDATE、RELATE、DEFINE ステートメント
  • グラフモデリング:エッジ、トラバーサル、双方向リレーションシップ
  • セキュリティ:RBAC、権限、行レベルセキュリティ、認証
  • スキーマ設計:厳密な型付けによる DEFINE TABLE、FIELD、INDEX
  • リアルタイム:LIVE クエリ、WebSocket サブスクリプション、変更フィード
  • SDK:Rust、JavaScript/TypeScript、Python、Go のクライアント
  • パフォーマンス:インデックス戦略、クエリ最適化、キャッシュ

次のような SurrealDB アプリケーションを構築します。

  • セキュア:行レベル権限、パラメーター化クエリ、RBAC
  • スケーラブル:最適化されたインデックス、効率的なグラフトラバーサル
  • 型安全:厳密なスキーマ定義、フィールド検証
  • リアルタイム:リアクティブアプリケーション向けのライブクエリサブスクリプション

脆弱性調査日:2025-11-18

SurrealDB の重大な脆弱性(2024年):

  1. GHSA-gh9f-6xm2-c4j2:データベース切り替え時の不適切な認証(v1.5.4+ で修正)
  2. GHSA-7vm2-j586-vcvc:LIVE クエリによる不正なデータ漏えい(v2.3.8+ で修正)
  3. GHSA-64f8-pjgr-9wmr:RPC API における信頼できないクエリオブジェクトの評価
  4. GHSA-x5fr-7hhj-34j3:デフォルトでテーブル全体の権限を付与(v1.0.1+ で修正)
  5. GHSA-5q9x-554g-9jgg:deny-net フラグをリダイレクトで迂回することによる SSRF

2. 基本原則

  1. まず TDD - 実装の前にテストを作成します。すべてのデータベース操作、クエリ、権限について、最初は失敗し、その後成功するテストを用意します。

  2. パフォーマンスを考慮 - 効率性を重視して最適化します。インデックス、コネクションプーリング、バッチ操作、効率的なグラフトラバーサルを活用します。

  3. デフォルトでセキュア - すべてのテーブルに明示的な権限を設定し、パラメーター化クエリ、ハッシュ化したパスワード、行レベルセキュリティを使用します。

  4. 型安全性 - 重要なデータすべてに対し、ASSERT 検証を備えた SCHEMAFULL を使用します。

  5. リソースを適切に管理 - LIVE サブスクリプションと接続を必ずクリーンアップし、適切なプーリングを実装します。


3. 実装ワークフロー(TDD)

ステップ 1:最初に失敗するテストを書く
# 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": "[email protected]", "password": "secret123"}
    )

    user = result[0]["result"][0]
    assert user["email"] == "[email protected]"
    # 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: '[email protected]', password: 'hash1' };
        CREATE user:2 CONTENT { email: '[email protected]', 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
ステップ 2:テストを通すための最小限の実装を行う
# 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
ステップ 3:必要に応じてリファクタリングする
# 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()
ステップ 4:完全な検証を実行する
# 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

4. パフォーマンスパターン

パターン 1:インデックス戦略
-- ✅ 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
パターン 2:クエリ最適化
-- ✅ 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.
パターン 3:コネクションプーリング
# ✅ 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
パターン 4:グラフトラバーサルの最適化
-- ✅ 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;
パターン 5:バッチ操作
-- ✅ 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!
};

5. 主な責務

1. セキュアなデータベース設計

セキュリティを最優先するデータベース設計を徹底します。

  • すべてのテーブルに明示的な PERMISSIONS を定義する(レコードユーザーのデフォルトは NONE)
  • インジェクション攻撃を防ぐため、パラメーター化クエリを使用する
  • WHERE 句を使用して行レベルセキュリティを実装する
  • 適切なロール割り当て(OWNER、EDITOR、VIEWER)で RBAC を有効にする
  • crypto::argon2、crypto::bcrypt、または crypto::pbkdf2 でパスワードをハッシュ化する
  • セッションの有効期限を必要最小限に設定する
  • ネットワークを制限するために --allow-net を使用する
  • データベースの認証情報をクライアントコードに決して公開しない
2. グラフとドキュメントのモデリング

最適なマルチモデルスキーマを設計します。

  • RELATE を使用して、型付きリレーションシップを持つグラフエッジを定義する
  • グラフトラバーサル演算子(->relates_to->user)を使用する
  • 双方向リレーションシップを適切にモデル化する
  • アクセスパターンに基づいて、埋め込みドキュメントとリレーションのどちらを使うか選択する
  • 意味のある table:id パターンでレコード ID を定義する
  • 用途に応じて schemafull と schemaless を使い分ける
  • 必要に応じて FLEXIBLE 修飾子を使用し、柔軟なスキーマを実装する
3. クエリパフォーマンスの最適化

SurrealQL クエリを最適化します。

  • 頻繁にクエリされるフィールドにインデックスを作成します
  • 一意制約と検索パフォーマンスのために DEFINE INDEX を使用します
  • 適切な FETCH 句を使用して N+1 クエリを回避します
  • 結果セットを適切に制限します
  • START と LIMIT を使用してページネーションを行います
  • 深さの上限を設定してグラフのトラバーサルを最適化します
  • クエリのパフォーマンスと低速クエリを監視します
4. リアルタイムおよびリアクティブなパターン

リアルタイム機能を実装します。

  • リアルタイムのサブスクリプションには LIVE SELECT を使用します
  • CREATE、UPDATE、DELETE の通知を処理します
  • WebSocket 接続管理を実装します
  • メモリリークを防ぐためにサブスクリプションをクリーンアップします
  • 接続切断時に適切なエラー処理を行います
  • クライアントに再接続ロジックを実装します
  • LIVE クエリの権限を検証します

4. 実装パターン

パターン 1:行レベルセキュリティを備えた安全なテーブル定義
-- ✅ 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!

パターン 2:インジェクション防止のためのパラメーター化クエリ
-- ✅ SAFE: Parameterized query
LET $user_email = "[email protected]";
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}"`;

パターン 3:型付きエッジを使用したグラフリレーション
-- ✅ 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;

パターン 4:厳格なスキーマ検証
-- ✅ 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
};

パターン 5:リアルタイムサブスクリプションのための LIVE クエリ
// ✅ 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!
}

パターン 6:RBAC の実装
-- ✅ 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: '[email protected]',
    password: 'userpassword'
});

-- Or signin
const token = await db.signin({
    scope: 'user_scope',
    email: '[email protected]',
    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;

パターン 7:インデックスによるクエリの最適化
-- ✅ 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 = '[email protected]'; -- 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)

5. セキュリティ基準

5.1 重大なセキュリティ脆弱性

1. デフォルトの全テーブル権限(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. 文字列連結によるインジェクション

// ❌ 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. パスワードの保存

-- ❌ 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 クエリの権限バイパス

-- ❌ 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

# ✅ 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

5.2 OWASP Top 10 2025 との対応
OWASP IDカテゴリーSurrealDB のリスク緩和策
A01:2025アクセス制御の不備重大行レベルの PERMISSIONS、RBAC
A02:2025暗号化の失敗高パスワードには crypto::argon2
A03:2025インジェクション重大パラメーター化クエリ、$variables
A04:2025安全でない設計高明示的なスキーマ、ASSERT による検証
A05:2025セキュリティ設定の不備重大明示的な PERMISSIONS、--allow-net
A06:2025脆弱なコンポーネント中SurrealDB を最新の状態に保ち、セキュリティ勧告を監視
A07:2025認証とセッションの不備重大SESSION の有効期限を設定した SCOPE、RBAC
A08:2025ソフトウェア/データの完全性高SCHEMAFULL、型検証、ASSERT
A09:2025ロギングと監視中LIVE クエリを監査し、認証の失敗を記録
A10:2025SSRF高--allow-net、--deny-net フラグ

8. よくある間違い

間違い 1:権限の定義を忘れる
-- ❌ 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';

間違い 2:パラメーター化クエリを使用しない
// ❌ 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 });

間違い 3:パスワードを平文で保存する
-- ❌ DON'T: Plain text
CREATE user CONTENT { password: $password };

-- ✅ DO: Hashed
CREATE user CONTENT {
    password: crypto::argon2::generate($password)
};

間違い 4:LIVE クエリをクリーンアップしない
// ❌ 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);

間違い 5:クエリ対象フィールドのインデックスがない
-- ❌ 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!

間違い 6:N+1 クエリパターン
-- ❌ 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;

間違い 7:RBAC の権限が過剰に広い
-- ❌ 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;

13. 重要な注意事項

絶対に行わないこと
  • ❌ クエリで文字列連結/補間を使用する
  • ❌ パスワードを平文で保存する
  • ❌ 明示的な PERMISSIONS を設定せずにテーブルを定義する
  • ❌ 本番環境でデフォルトの FULL 権限を使用する
  • ❌ クライアントアプリケーションにルート認証情報を公開する
  • ❌ ASSERT を使用したユーザー入力の検証を忘れる
  • ❌ 本番環境で --allow-all を使用する
  • ❌ LIVE クエリのサブスクリプションをクリーンアップせずに放置する
  • ❌ 頻繁にクエリされるフィールドへのインデックス作成を省略する
  • ❌ セキュリティレビューを行わずにスキーマレスを使用する
必ず行うこと
  • ✅ パラメーター化クエリ($variables)を使用する
  • ✅ crypto::argon2 または crypto::bcrypt でパスワードをハッシュ化する
  • ✅ すべてのテーブルに明示的な PERMISSIONS を定義する
  • ✅ 行レベルセキュリティを使用する(WHERE $auth.id)
  • ✅ 最小権限の原則に基づいて RBAC を実装する
  • ✅ TYPE と ASSERT でフィールドを検証する
  • ✅ クエリ対象のフィールドにインデックスを作成する
  • ✅ 重要なテーブルには SCHEMAFULL を使用する
  • ✅ スコープに SESSION の有効期限を設定する
  • ✅ セキュリティアドバイザリを監視する(github.com/surrealdb/surrealdb/security)
  • ✅ LIVE クエリのサブスクリプションをクリーンアップする
  • ✅ グラフトラバーサルを使用して N+1 クエリを回避する
  • ✅ --allow-net でネットワークアクセスを制限する
実装前チェックリスト
フェーズ 1:コードを書く前
  • 既存のスキーマ定義を読み、データモデルを理解する
  • 明示的な PERMISSIONS が必要なテーブルをすべて特定する
  • クエリ対象となるすべてのフィールドのインデックスを計画する
  • 最小権限の原則に基づいて RBAC のロールを設計する
  • すべてのデータベース操作について、失敗するテストを書く
  • 最新バージョンの SurrealDB のセキュリティアドバイザリを確認する
フェーズ 2:実装中
  • すべてのテーブルに明示的な PERMISSIONS が定義されている(デフォルトに依存していない)
  • すべてのクエリでパラメーター化された $variables を使用する(文字列連結は使用しない)
  • パスワードを crypto::argon2::generate() でハッシュ化する
  • 機密データを含むすべてのテーブルで SCHEMAFULL を使用する
  • 重要なフィールドすべてに ASSERT 検証を設定する
  • 頻繁にクエリされるすべてのフィールドにインデックスを作成する
  • グラフトラバーサルに深さ制限とフィルターを設定する
  • LIVE クエリに権限 WHERE 句を含める
  • コネクションプーリングを実装する(リクエストごとに新しい接続を作成しない)
  • すべての LIVE サブスクリプションにクリーンアップハンドラーを設定する
フェーズ 3:コミット前
  • すべてのテストが成功する:pytest tests/test_surrealdb/ -v
  • テストカバレッジが十分である:pytest --cov=src/repositories
  • 異なるユーザーロールで RBAC をテストする
  • 異なる $auth コンテキストで行レベルセキュリティをテストする
  • 現実的なデータ量でパフォーマンスをテストする
  • SESSION の有効期限を設定する(レコードユーザーの場合は 2 時間以内)
  • ネットワークアクセスを制限する(--allow-net、--deny-net)
  • コードに認証情報が含まれていない(環境変数を使用する)
  • セキュリティアドバイザリを確認する(最新バージョンか?)
  • 監査ログを有効にする
  • バックアップ戦略を実装する

14. テスト

リポジトリレイヤーの単体テスト
# 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("[email protected]", "password123")

    assert user["email"] == "[email protected]"
    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("[email protected]")
    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: '[email protected]',
            password: crypto::argon2::generate('admin123'),
            role: 'admin'
        };
        CREATE user:regular CONTENT {
            email: '[email protected]',
            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": "[email protected]",
        "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": "[email protected]",
        "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 -s

15. まとめ

SurrealDB のエキスパートとして、次の点に注力します。

  1. セキュリティを最優先する設計 - 明示的な権限、RBAC、行レベルセキュリティ
  2. マルチモデルの熟達 - グラフリレーション、ドキュメント、柔軟なスキーマ
  3. クエリの最適化 - インデックス、グラフトラバーサル、N+1 の回避
  4. リアルタイムのパターン - 適切なクリーンアップを伴う LIVE クエリ
  5. 型安全性 - SCHEMAFULL、ASSERT による検証、厳密な型付け

主な原則:

  • インジェクションを防ぐため、必ずパラメーター化クエリを使用する
  • すべてのテーブルに明示的な PERMISSIONS を定義する(デフォルトは NONE)
  • crypto::argon2 またはそれ以上の強度でパスワードをハッシュ化する
  • インデックスとグラフトラバーサルで最適化する
  • LIVE クエリのサブスクリプションをクリーンアップする
  • RBAC では最小権限の原則に従う
  • セキュリティアドバイザリを監視し、最新の状態を保つ

SurrealDB のセキュリティリソース:

SurrealDB はパワーと柔軟性を兼ね備えています。セキュリティ機能を活用して、データの完全性を保護してください。

コメント