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_candidate_keeps_residual_predicate(self, client):
rows = [
{
"city": "NYC" if i < 400 else "LA",
"age": 10 if i < 200 else 30,
"name": f"user_{i}",
}
for i in range(1000)
]
client.store(rows)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
result = client.execute(
"SELECT name FROM idx_test "
"WHERE city = 'NYC' AND age > 20 ORDER BY name"
)
names = result.to_pandas()["name"].tolist()
assert len(names) == 200
assert names[0] == "user_200"
assert names[-1] == "user_399"
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
def test_composite_btree_prefix_equality_and_range(self, client):
rows = [
{"city": city, "age": age, "name": f"{city}_{age}"}
for city in ("NYC", "SF")
for age in (20, 30, 40)
]
client.store(rows)
client.execute(
"CREATE INDEX idx_city_age ON idx_test (city, age) USING BTREE"
)
prefix = client.execute(
"SELECT name FROM idx_test WHERE city = 'NYC' ORDER BY name"
).to_pandas()
assert prefix["name"].tolist() == ["NYC_20", "NYC_30", "NYC_40"]
ranged = client.execute(
"SELECT name FROM idx_test "
"WHERE city = 'NYC' AND age BETWEEN 20 AND 40 ORDER BY age"
).to_pandas()
assert ranged["name"].tolist() == ["NYC_20", "NYC_30", "NYC_40"]
def test_and_intersection_and_or_union_are_exact(self, client):
rows = [
{
"city": "NYC" if i % 2 == 0 else "SF",
"age": 20 + i % 3,
"name": f"user_{i}",
}
for i in range(60)
]
client.store(rows)
client.execute("CREATE INDEX idx_city ON idx_test (city)")
client.execute("CREATE INDEX idx_age ON idx_test (age)")
intersection = client.execute(
"SELECT name FROM idx_test "
"WHERE city = 'NYC' AND age = 21 ORDER BY name"
).to_pandas()
assert intersection["name"].tolist() == sorted(
f"user_{i}" for i in range(60) if i % 2 == 0 and 20 + i % 3 == 21
)
union = client.execute(
"SELECT name FROM idx_test "
"WHERE city = 'NYC' OR age = 21 ORDER BY name"
).to_pandas()
assert union["name"].tolist() == sorted(
f"user_{i}" for i in range(60) if i % 2 == 0 or 20 + i % 3 == 21
)
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"