feat: add SearchRepository for hash-based search

This commit is contained in:
waclaw-claw 2026-04-09 12:30:15 -04:00
parent c49574b6cb
commit 33f3356f2b
3 changed files with 199 additions and 0 deletions

52
src/db/search.py Normal file
View File

@ -0,0 +1,52 @@
"""Repository for search operations across blocks and transactions."""
from typing import Optional, Tuple
from sqlmodel import select
from db.clients import DbClient
from models.block import Block
from models.transactions.transaction import Transaction
class SearchRepository:
"""Repository for search operations across blocks and transactions."""
def __init__(self, client: DbClient):
self.client = client
async def search_by_hash(self, hash_value: str) -> Optional[Tuple[str, int]]:
"""
Search for a block or transaction by hash.
Args:
hash_value: Hex string hash (with or without 0x prefix)
Returns:
Tuple of (type, id) where type is "block" or "transaction",
or None if not found.
"""
# Normalize hash (handle 0x prefix)
if hash_value.startswith("0x"):
hash_bytes = bytes.fromhex(hash_value[2:])
else:
hash_bytes = bytes.fromhex(hash_value)
with self.client.session() as session:
# Try to find as block first
block_result = session.exec(
select(Block).where(Block.hash == hash_bytes)
).first()
if block_result:
return ("block", block_result.id)
# Try to find as transaction
tx_result = session.exec(
select(Transaction).where(Transaction.hash == hash_bytes)
).first()
if tx_result:
return ("transaction", tx_result.id)
return None

19
tests/conftest.py Normal file
View File

@ -0,0 +1,19 @@
"""Pytest configuration and fixtures."""
import sys
from pathlib import Path
import pytest
# Add src directory to path for imports
src_path = Path(__file__).parent.parent / "src"
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))
@pytest.fixture(scope="session")
def event_loop_policy():
"""Use default event loop policy."""
import asyncio
return asyncio.DefaultEventLoopPolicy()

128
tests/test_search.py Normal file
View File

@ -0,0 +1,128 @@
"""Tests for SearchRepository."""
import tempfile
from pathlib import Path
import pytest
from db.clients.sqlite import SqliteClient
from db.search import SearchRepository
from models.block import Block
@pytest.mark.asyncio
async def test_search_by_block_hash():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = f"sqlite:///{Path(tmpdir) / 'test.db'}"
client = SqliteClient(db_path)
repo = SearchRepository(client)
# Search for non-existent block should return None
result = await repo.search_by_hash("0x" + "00" * 32)
assert result is None
@pytest.mark.asyncio
async def test_search_by_transaction_hash():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = f"sqlite:///{Path(tmpdir) / 'test.db'}"
client = SqliteClient(db_path)
repo = SearchRepository(client)
# Search for non-existent transaction should return None
result = await repo.search_by_hash("0x" + "11" * 32)
assert result is None
@pytest.mark.asyncio
async def test_search_finds_block():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = f"sqlite:///{Path(tmpdir) / 'test.db'}"
client = SqliteClient(db_path)
repo = SearchRepository(client)
# Create a block
test_hash = bytes.fromhex("aa" * 32)
block = Block(
hash=test_hash,
parent_block=b"\x00" * 32,
slot=1,
height=0,
block_root=b"\x00" * 32,
proof_of_leadership=None,
)
with client.session() as session:
session.add(block)
session.flush()
block_id = block.id
session.commit()
# Search for the block
result = await repo.search_by_hash("0x" + "aa" * 32)
assert result is not None
assert result[0] == "block"
assert result[1] == block_id
@pytest.mark.asyncio
async def test_search_without_0x_prefix():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = f"sqlite:///{Path(tmpdir) / 'test.db'}"
client = SqliteClient(db_path)
repo = SearchRepository(client)
# Create a block
test_hash = bytes.fromhex("dd" * 32)
block = Block(
hash=test_hash,
parent_block=b"\x00" * 32,
slot=1,
height=0,
block_root=b"\x00" * 32,
proof_of_leadership=None,
)
with client.session() as session:
session.add(block)
session.flush()
session.commit()
# Search without 0x prefix
result = await repo.search_by_hash("dd" * 32)
assert result is not None
assert result[0] == "block"
@pytest.mark.asyncio
async def test_search_returns_tuple_with_id():
with tempfile.TemporaryDirectory() as tmpdir:
db_path = f"sqlite:///{Path(tmpdir) / 'test.db'}"
client = SqliteClient(db_path)
repo = SearchRepository(client)
# Create a block
test_hash = bytes.fromhex("ee" * 32)
block = Block(
hash=test_hash,
parent_block=b"\x00" * 32,
slot=1,
height=0,
block_root=b"\x00" * 32,
proof_of_leadership=None,
)
with client.session() as session:
session.add(block)
session.flush()
block_id = block.id
session.commit()
# Search should return tuple (type, id)
result = await repo.search_by_hash("0x" + "ee" * 32)
assert result is not None
assert isinstance(result, tuple)
assert len(result) == 2
assert result[0] == "block"
assert isinstance(result[1], int)
assert result[1] > 0