Research-Stack/5-Applications/nodupe/tests/database/test_sharding.py
2026-05-05 21:15:26 -05:00

588 lines
21 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 NoDupeLabs
"""Comprehensive tests for database sharding functionality.
Tests cover:
- Shard creation and management
- Data distribution across shards
- Shard selection logic
- Identifier validation
- Initialize/shutdown lifecycle
"""
import os
import sqlite3
from unittest.mock import MagicMock
import pytest
from nodupe.tools.database.sharding import DatabaseShardingTool
class TestDatabaseShardingToolInit:
"""Tests for DatabaseShardingTool initialization."""
def test_init_with_no_config(self):
"""Test initialization without configuration."""
tool = DatabaseShardingTool()
assert tool.config == {}
assert tool._shards == {}
def test_init_with_config(self):
"""Test initialization with configuration."""
config = {"db_path": "/custom/path", "shard_count": 4}
tool = DatabaseShardingTool(config=config)
assert tool.config == config
assert tool._shards == {}
def test_init_with_empty_config_dict(self):
"""Test initialization with empty config dict."""
tool = DatabaseShardingTool(config={})
assert tool.config == {}
def test_init_creates_empty_shards_dict(self):
"""Test that _shards is initialized as empty dict."""
tool = DatabaseShardingTool()
assert isinstance(tool._shards, dict)
assert len(tool._shards) == 0
class TestDatabaseShardingToolProperties:
"""Tests for DatabaseShardingTool properties."""
def test_name_property(self):
"""Test name property returns correct value."""
tool = DatabaseShardingTool()
assert tool.name == "DatabaseSharding"
def test_version_property(self):
"""Test version property returns correct value."""
tool = DatabaseShardingTool()
assert tool.version == "1.0.0"
def test_dependencies_property(self):
"""Test dependencies property returns empty list."""
tool = DatabaseShardingTool()
assert tool.dependencies == []
assert isinstance(tool.dependencies, list)
def test_get_capabilities(self):
"""Test get_capabilities returns correct capabilities."""
tool = DatabaseShardingTool()
capabilities = tool.get_capabilities()
assert capabilities == {
"sharding": True,
"horizontal_partitioning": True,
"create_shard": True,
}
def test_get_capabilities_all_true(self):
"""Test all capabilities are True."""
tool = DatabaseShardingTool()
capabilities = tool.get_capabilities()
assert all(capabilities.values())
def test_metadata_property(self):
"""Test metadata property returns ToolMetadata."""
tool = DatabaseShardingTool()
metadata = tool.metadata
assert metadata.name == "DatabaseSharding"
assert metadata.version == "1.0.0"
assert "database" in metadata.tags
assert "sharding" in metadata.tags
assert "partitioning" in metadata.tags
def test_metadata_author(self):
"""Test metadata author is NoDupeLabs."""
tool = DatabaseShardingTool()
assert tool.metadata.author == "NoDupeLabs"
def test_metadata_license(self):
"""Test metadata license is Apache-2.0."""
tool = DatabaseShardingTool()
assert tool.metadata.license == "Apache-2.0"
def test_metadata_description(self):
"""Test metadata description contains sharding info."""
tool = DatabaseShardingTool()
assert "sharding" in tool.metadata.description.lower()
assert "horizontal" in tool.metadata.description.lower()
class TestIsValidIdentifier:
"""Tests for _is_valid_identifier method."""
def test_valid_simple_name(self):
"""Test valid simple alphanumeric name."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard1") is True
def test_valid_name_with_underscore(self):
"""Test valid name with underscores."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard_1") is True
assert tool._is_valid_identifier("my_shard") is True
def test_valid_name_with_hyphen(self):
"""Test valid name with hyphens."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard-1") is True
assert tool._is_valid_identifier("my-shard") is True
def test_valid_name_mixed(self):
"""Test valid name with mixed characters."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard_1-test") is True
def test_invalid_empty_string(self):
"""Test empty string is invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("") is False
def test_invalid_none(self):
"""Test None is invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier(None) is False
def test_invalid_starts_with_underscore(self):
"""Test name starting with underscore is invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("_shard") is False
assert tool._is_valid_identifier("__shard") is False
def test_invalid_too_long(self):
"""Test name longer than 64 chars is invalid."""
tool = DatabaseShardingTool()
long_name = "a" * 65
assert tool._is_valid_identifier(long_name) is False
def test_valid_max_length(self):
"""Test name exactly 64 chars is valid."""
tool = DatabaseShardingTool()
max_name = "a" * 64
assert tool._is_valid_identifier(max_name) is True
def test_invalid_special_characters(self):
"""Test names with special characters are invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard@1") is False
assert tool._is_valid_identifier("shard#1") is False
assert tool._is_valid_identifier("shard$1") is False
assert tool._is_valid_identifier("shard!1") is False
def test_invalid_spaces(self):
"""Test names with spaces are invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard 1") is False
def test_valid_numeric_string(self):
"""Test numeric string is valid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("123") is True
class TestCreateShard:
"""Tests for create_shard method."""
def test_create_shard_with_temp_db(self, tmp_path):
"""Test creating a shard with temporary database."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("test_shard")
assert os.path.exists(shard_path)
assert shard_path.endswith("test_shard.db")
assert "test_shard" in tool._shards
def test_create_shard_creates_table(self, tmp_path):
"""Test that create_shard creates shard_data table."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("test_shard")
conn = sqlite3.connect(shard_path)
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='shard_data'"
)
result = cursor.fetchone()
conn.close()
assert result is not None
def test_create_shard_table_schema(self, tmp_path):
"""Test shard_data table has correct schema."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("test_shard")
conn = sqlite3.connect(shard_path)
cursor = conn.execute("PRAGMA table_info(shard_data)")
columns = {row[1]: row[2] for row in cursor.fetchall()}
conn.close()
assert "id" in columns
assert "key" in columns
assert "value" in columns
assert "created_at" in columns
def test_create_shard_with_custom_path(self, tmp_path):
"""Test creating shard with custom database path."""
tool = DatabaseShardingTool()
custom_path = str(tmp_path / "custom_shard.db")
shard_path = tool.create_shard("custom", db_path=custom_path)
assert shard_path == custom_path
assert os.path.exists(shard_path)
def test_create_shard_registers_in_dict(self, tmp_path):
"""Test that created shard is registered in _shards dict."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
tool.create_shard("shard_a")
tool.create_shard("shard_b")
assert len(tool._shards) == 2
assert "shard_a" in tool._shards
assert "shard_b" in tool._shards
def test_create_shard_invalid_name(self):
"""Test that invalid shard name raises ValueError."""
tool = DatabaseShardingTool()
with pytest.raises(ValueError, match="Invalid shard name"):
tool.create_shard("_invalid_shard")
def test_create_shard_invalid_name_special_chars(self):
"""Test that special characters in name raises ValueError."""
tool = DatabaseShardingTool()
with pytest.raises(ValueError, match="Invalid shard name"):
tool.create_shard("shard@invalid")
def test_create_shard_creates_parent_dirs(self, tmp_path):
"""Test that create_shard creates parent directories if needed."""
tool = DatabaseShardingTool()
nested_dir = tmp_path / "nested" / "path"
nested_dir.mkdir(parents=True, exist_ok=True)
nested_path = str(nested_dir / "shard.db")
shard_path = tool.create_shard("nested", db_path=nested_path)
assert os.path.exists(shard_path)
def test_create_multiple_shards(self, tmp_path):
"""Test creating multiple shards."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
paths = []
for i in range(5):
paths.append(tool.create_shard(f"shard_{i}"))
assert len(tool._shards) == 5
for path in paths:
assert os.path.exists(path)
def test_create_shard_idempotent(self, tmp_path):
"""Test that creating same shard twice works (table exists)."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
path1 = tool.create_shard("same_shard")
path2 = tool.create_shard("same_shard")
assert path1 == path2
class TestListShards:
"""Tests for list_shards method."""
def test_list_shards_empty(self):
"""Test list_shards returns empty list when no shards."""
tool = DatabaseShardingTool()
assert tool.list_shards() == []
def test_list_shards_single(self, tmp_path):
"""Test list_shards with single shard."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
tool.create_shard("single")
assert tool.list_shards() == ["single"]
def test_list_shards_multiple(self, tmp_path):
"""Test list_shards with multiple shards."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
tool.create_shard("shard_a")
tool.create_shard("shard_b")
tool.create_shard("shard_c")
shards = tool.list_shards()
assert len(shards) == 3
assert set(shards) == {"shard_a", "shard_b", "shard_c"}
def test_list_shards_returns_list(self):
"""Test list_shards returns a list type."""
tool = DatabaseShardingTool()
assert isinstance(tool.list_shards(), list)
class TestInitializeAndShutdown:
"""Tests for initialize and shutdown methods."""
def test_initialize(self):
"""Test initialize method."""
tool = DatabaseShardingTool()
container = MagicMock()
# Should not raise
tool.initialize(container)
def test_shutdown(self):
"""Test shutdown method."""
tool = DatabaseShardingTool()
# Should not raise
tool.shutdown()
def test_initialize_with_shards(self, tmp_path):
"""Test initialize after creating shards."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
tool.create_shard("test")
container = MagicMock()
tool.initialize(container)
assert "test" in tool._shards
def test_lifecycle_initialize_then_shutdown(self):
"""Test full lifecycle: initialize then shutdown."""
tool = DatabaseShardingTool()
container = MagicMock()
# Should not raise
tool.initialize(container)
tool.shutdown()
class TestShardDataOperations:
"""Tests for shard data operations (integration tests)."""
def test_insert_data_into_shard(self, tmp_path):
"""Test inserting data into created shard."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("data_test")
conn = sqlite3.connect(shard_path)
conn.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("test_key", b"test_value")
)
conn.commit()
cursor = conn.execute("SELECT value FROM shard_data WHERE key = ?", ("test_key",))
result = cursor.fetchone()
conn.close()
assert result is not None
assert result[0] == b"test_value"
def test_shard_data_with_timestamp(self, tmp_path):
"""Test that shard_data has created_at timestamp."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("timestamp_test")
conn = sqlite3.connect(shard_path)
conn.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("key1", b"value1")
)
conn.commit()
cursor = conn.execute("SELECT created_at FROM shard_data WHERE key = ?", ("key1",))
result = cursor.fetchone()
conn.close()
assert result is not None
assert result[0] is not None
def test_shard_unique_constraint(self, tmp_path):
"""Test that key column has UNIQUE constraint."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shard_path = tool.create_shard("unique_test")
conn = sqlite3.connect(shard_path)
conn.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("unique_key", b"value1")
)
conn.commit()
with pytest.raises(sqlite3.IntegrityError):
conn.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("unique_key", b"value2")
)
conn.close()
class TestShardingEdgeCases:
"""Edge case tests for sharding functionality."""
def test_unicode_shard_name(self, tmp_path):
"""Test shard creation with unicode in name (should fail validation)."""
tool = DatabaseShardingTool()
# Unicode characters should fail identifier validation
assert tool._is_valid_identifier("shard_\u00e9") is False
def test_run_standalone_no_args(self, capsys):
"""Test run_standalone with no arguments."""
tool = DatabaseShardingTool()
result = tool.run_standalone([])
assert result == 1
captured = capsys.readouterr()
assert "Usage:" in captured.out
def test_run_standalone_create_command(self, tmp_path):
"""Test run_standalone with create command."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
result = tool.run_standalone(["create", "test_shard"])
assert result == 0
assert "test_shard" in tool.list_shards()
def test_run_standalone_list_command(self, tmp_path, capsys):
"""Test run_standalone with list command."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
tool.create_shard("shard_a")
tool.create_shard("shard_b")
result = tool.run_standalone(["list"])
assert result == 0
captured = capsys.readouterr()
assert "shard_a" in captured.out
assert "shard_b" in captured.out
def test_run_standalone_unknown_command(self, capsys):
"""Test run_standalone with unknown command."""
tool = DatabaseShardingTool()
result = tool.run_standalone(["unknown"])
assert result == 1
captured = capsys.readouterr()
assert "Unknown command" in captured.out
def test_describe_usage(self):
"""Test describe_usage method."""
tool = DatabaseShardingTool()
usage = tool.describe_usage()
assert "Database Sharding Tool" in usage
assert "sharding" in usage.lower()
def test_api_methods(self):
"""Test api_methods property."""
tool = DatabaseShardingTool()
methods = tool.api_methods
assert "create_shard" in methods
assert "list_shards" in methods
assert callable(methods["create_shard"])
assert callable(methods["list_shards"])
def test_whitespace_in_name(self, tmp_path):
"""Test that whitespace in name is invalid."""
tool = DatabaseShardingTool()
assert tool._is_valid_identifier("shard 1") is False
assert tool._is_valid_identifier(" shard") is False
assert tool._is_valid_identifier("shard ") is False
def test_create_shard_path_traversal(self, tmp_path):
"""Test that path traversal in shard name is prevented."""
tool = DatabaseShardingTool()
# Path traversal should fail identifier validation
assert tool._is_valid_identifier("../etc/shard") is False
def test_concurrent_shard_creation(self, tmp_path):
"""Test creating shards in sequence (simulating concurrent access)."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
shards = []
for i in range(10):
shards.append(tool.create_shard(f"concurrent_{i}"))
assert len(tool.list_shards()) == 10
for path in shards:
assert os.path.exists(path)
def test_shard_with_numbers_only_name(self, tmp_path):
"""Test shard creation with numeric-only name."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
path = tool.create_shard("12345")
assert os.path.exists(path)
assert "12345" in tool.list_shards()
def test_shard_with_mixed_case(self, tmp_path):
"""Test shard creation with mixed case name."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
path = tool.create_shard("MyShardName")
assert os.path.exists(path)
assert "MyShardName" in tool.list_shards()
class TestShardingIntegration:
"""Integration tests for sharding with database operations."""
def test_full_shard_lifecycle(self, tmp_path):
"""Test complete shard lifecycle: create, use, list."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
# Create shard
path = tool.create_shard("lifecycle_test")
assert os.path.exists(path)
# Verify in list
assert "lifecycle_test" in tool.list_shards()
# Use shard
conn = sqlite3.connect(path)
conn.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("lifecycle_key", b"lifecycle_value")
)
conn.commit()
conn.close()
# Verify data persisted
conn = sqlite3.connect(path)
cursor = conn.execute("SELECT value FROM shard_data WHERE key = ?", ("lifecycle_key",))
result = cursor.fetchone()
conn.close()
assert result[0] == b"lifecycle_value"
def test_multiple_shards_data_isolation(self, tmp_path):
"""Test that data in different shards is isolated."""
config = {"db_path": str(tmp_path)}
tool = DatabaseShardingTool(config=config)
path_a = tool.create_shard("shard_a")
path_b = tool.create_shard("shard_b")
# Insert different data in each shard
conn_a = sqlite3.connect(path_a)
conn_a.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("key_a", b"value_a")
)
conn_a.commit()
conn_a.close()
conn_b = sqlite3.connect(path_b)
conn_b.execute(
"INSERT INTO shard_data (key, value) VALUES (?, ?)",
("key_b", b"value_b")
)
conn_b.commit()
conn_b.close()
# Verify isolation
conn_a = sqlite3.connect(path_a)
cursor_a = conn_a.execute("SELECT COUNT(*) FROM shard_data")
count_a = cursor_a.fetchone()[0]
conn_a.close()
conn_b = sqlite3.connect(path_b)
cursor_b = conn_b.execute("SELECT COUNT(*) FROM shard_data")
count_b = cursor_b.fetchone()[0]
conn_b.close()
assert count_a == 1
assert count_b == 1