import os
import shutil
import tempfile
import time
import pytest
from apexbase import ApexClient
@pytest.fixture
def client():
tmpdir = tempfile.mkdtemp(prefix="apexbase_idx_")
c = ApexClient(dirpath=tmpdir)
c.create_table("idx_test")
yield c
c.close()
shutil.rmtree(tmpdir, ignore_errors=True)
class TestCreateDropIndex:
def test_create_hash_index(self, client):
rows = [{"city": "Beijing", "age": 30}, {"city": "Shanghai", "age": 25}]
client.store(rows)
result = client.execute("CREATE INDEX idx_city ON idx_test (city)")
assert result is not None
def test_create_btree_index(self, client):
rows = [{"score": 90}, {"score": 80}]
client.store(rows)
result = client.execute("CREATE INDEX idx_score ON idx_test (score) USING BTREE")
assert result is not None
def test_create_unique_index(self, client):
rows = [{"email": "a@b.com"}, {"email": "c@d.com"}]
client.store(rows)
result = client.execute("CREATE UNIQUE INDEX idx_email ON idx_test (email) USING HASH")
assert result is not None
def test_create_index_if_not_exists(self, client):
rows = [{"city": "Beijing"}]
client.store(rows)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("CREATE INDEX IF NOT EXISTS idx_city ON idx_test (city)")
assert result is not None
def test_create_index_duplicate_error(self, client):
rows = [{"city": "Beijing"}]
client.store(rows)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
with pytest.raises(Exception):
client.execute("CREATE INDEX idx_city ON idx_test (city)")
def test_drop_index(self, client):
rows = [{"city": "Beijing"}]
client.store(rows)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("DROP INDEX idx_city ON idx_test")
assert result is not None
def test_drop_index_if_exists(self, client):
result = client.execute("DROP INDEX IF EXISTS idx_nonexist ON idx_test")
assert result is not None
def test_drop_index_not_found_error(self, client):
with pytest.raises(Exception):
client.execute("DROP INDEX idx_nonexist ON idx_test")
def test_create_index_on_empty_table(self, client):
client.store([{"city": "test"}])
client.execute("TRUNCATE TABLE idx_test")
result = client.execute("CREATE INDEX idx_city ON idx_test (city)")
assert result is not None
def test_create_index_nonexistent_column(self, client):
rows = [{"city": "Beijing"}]
client.store(rows)
with pytest.raises(Exception):
client.execute("CREATE INDEX idx_foo ON idx_test (nonexistent_col)")
def test_create_index_streams_multiple_batches(self, client):
rows = [
{
"source": f"source_{i}",
"kind": f"kind_{i}",
"payload": f"payload_{i}",
}
for i in range(70_000)
]
client.store(rows)
client.execute("CREATE INDEX idx_source ON idx_test (source)")
result = client.execute("SELECT * FROM idx_test WHERE source = 'source_69999'")
df = result.to_pandas()
assert len(df) == 1
assert df.iloc[0]["kind"] == "kind_69999"
class TestIndexAcceleratedSelect:
def _setup_data(self, client, n=100):
cities = ["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Hangzhou"]
rows = []
for i in range(n):
rows.append({"city": cities[i % len(cities)], "age": 20 + (i % 50), "name": f"user_{i}"})
client.store(rows)
return rows
def test_index_equality_filter(self, client):
self._setup_data(client, 100)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
df = result.to_pandas()
assert len(df) == 20 assert all(df["city"] == "Beijing")
def test_index_equality_filter_no_match(self, client):
self._setup_data(client, 50)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("SELECT * FROM idx_test WHERE city = 'NonExistent'")
df = result.to_pandas()
assert len(df) == 0
def test_index_in_filter(self, client):
self._setup_data(client, 100)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("SELECT * FROM idx_test WHERE city IN ('Beijing', 'Shanghai')")
df = result.to_pandas()
assert len(df) == 40
def test_index_with_order_by(self, client):
self._setup_data(client, 100)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing' ORDER BY age DESC")
df = result.to_pandas()
assert len(df) == 20
ages = df["age"].tolist()
assert ages == sorted(ages, reverse=True)
def test_index_with_limit(self, client):
self._setup_data(client, 100)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing' LIMIT 5")
df = result.to_pandas()
assert len(df) == 5
def test_index_correctness_vs_scan(self, client):
self._setup_data(client, 200)
result_scan = client.execute("SELECT name FROM idx_test WHERE city = 'Guangzhou' ORDER BY name")
df_scan = result_scan.to_pandas()
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result_idx = client.execute("SELECT name FROM idx_test WHERE city = 'Guangzhou' ORDER BY name")
df_idx = result_idx.to_pandas()
assert len(df_scan) == len(df_idx)
assert df_scan["name"].tolist() == df_idx["name"].tolist()
def test_index_drop_fallback_to_scan(self, client):
self._setup_data(client, 50)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.execute("DROP INDEX idx_city ON idx_test")
result = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
df = result.to_pandas()
assert len(df) == 10
class TestIndexWithDML:
def test_sql_insert_updates_index(self, client):
client.store([{"city": "Beijing", "age": 30}])
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.execute("INSERT INTO idx_test (city, age) VALUES ('Shanghai', 40)")
result = client.execute("SELECT * FROM idx_test WHERE city = 'Shanghai'")
df = result.to_pandas()
assert len(df) == 1
assert df.iloc[0]["age"] == 40
def test_sql_delete_updates_index(self, client):
cities = ["Beijing", "Shanghai", "Guangzhou"]
client.store([{"city": c, "age": 20 + i} for i, c in enumerate(cities)])
client.execute("CREATE INDEX idx_city ON idx_test (city)")
r1 = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
assert len(r1.to_pandas()) == 1
client.execute("DELETE FROM idx_test WHERE city = 'Beijing'")
r2 = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
df2 = r2.to_pandas()
assert len(df2) == 0
r3 = client.execute("SELECT * FROM idx_test WHERE city = 'Shanghai'")
assert len(r3.to_pandas()) == 1
def test_sql_update_updates_index(self, client):
client.store([{"city": "Beijing", "age": 30}, {"city": "Shanghai", "age": 25}])
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.execute("UPDATE idx_test SET city = 'Chengdu' WHERE city = 'Beijing'")
r1 = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
assert len(r1.to_pandas()) == 0
r2 = client.execute("SELECT * FROM idx_test WHERE city = 'Chengdu'")
df2 = r2.to_pandas()
assert len(df2) == 1
assert df2.iloc[0]["age"] == 30
def test_delete_all_then_insert(self, client):
client.store([{"city": "Beijing"}, {"city": "Shanghai"}])
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.execute("DELETE FROM idx_test")
r1 = client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
assert len(r1.to_pandas()) == 0
client.execute("INSERT INTO idx_test (city) VALUES ('Hangzhou')")
r2 = client.execute("SELECT * FROM idx_test WHERE city = 'Hangzhou'")
assert len(r2.to_pandas()) == 1
def test_store_api_after_index(self, client):
client.store([{"city": "Beijing"}])
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.store([{"city": "Shanghai"}])
result = client.execute("SELECT city FROM idx_test WHERE city = 'Shanghai'")
assert result.to_dict() == [{"city": "Shanghai"}]
class TestIndexPerformance:
def test_no_regression_without_index(self, client):
cities = ["Beijing", "Shanghai", "Guangzhou"]
rows = [{"city": cities[i % 3], "age": i} for i in range(1000)]
client.store(rows)
start = time.time()
for _ in range(10):
client.execute("SELECT * FROM idx_test WHERE city = 'Beijing'")
elapsed_no_idx = time.time() - start
assert elapsed_no_idx < 5.0, f"No-index queries took {elapsed_no_idx:.2f}s"