import pytest
import tempfile
import shutil
from pathlib import Path
import sys
import os
import subprocess
import threading
import time
import numpy as np
from datetime import datetime, date
from decimal import Decimal
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'apexbase', 'python'))
try:
from apexbase import ApexClient, ARROW_AVAILABLE, POLARS_AVAILABLE
except ImportError as e:
pytest.skip(f"ApexBase not available: {e}", allow_module_level=True)
try:
import pandas as pd
PANDAS_AVAILABLE = True
except ImportError:
PANDAS_AVAILABLE = False
try:
import polars as pl
POLARS_DF_AVAILABLE = True
except ImportError:
POLARS_DF_AVAILABLE = False
try:
import pyarrow as pa
PYARROW_AVAILABLE = True
except ImportError:
PYARROW_AVAILABLE = False
class TestSingleRecordStorage:
PERSON_SCHEMA = {"name": "str", "age": "int", "score": "float", "city": "str"}
STREAM_SCHEMA = {"name": "str", "seq": "int", "bucket": "str", "score": "float"}
def test_store_single_dict_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {"name": "Alice", "age": 25, "city": "NYC"}
client.store(data)
count = client.count_rows()
assert count == 1
result = client.retrieve(1)
assert result["name"] == "Alice"
assert result["age"] == 25
assert result["city"] == "NYC"
client.close()
def test_buffered_single_dict_flush_visibility(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.store({"name": "seed", "age": 1})
client.begin_buffered_writes()
client.store({"name": "buffered", "age": 2})
assert client.buffered_write_count() == 1
assert client.count_rows() == 1
assert client.flush_buffered_writes() == 1
assert client.buffered_write_count() == 0
assert client.count_rows() == 2
assert client.retrieve(2)["name"] == "buffered"
client.end_buffered_writes(flush=False)
client.close()
def test_buffered_single_dict_close_flushes(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.begin_buffered_writes()
client.store({"name": "close_flush", "age": 3})
assert client.buffered_write_count() == 1
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 1
assert reopened.retrieve(1)["name"] == "close_flush"
reopened.close()
def test_experimental_delta_single_dict_read_after_write(self, monkeypatch):
monkeypatch.setenv("APEXBASE_EXPERIMENTAL_DELTA_SINGLE_WRITE", "1")
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
client.store({"name": "delta", "age": 2, "score": 2.0, "city": "SH"})
assert client.count_rows() == 2
assert client.retrieve(2)["name"] == "delta"
result = client.execute(
"SELECT name, age, score FROM default WHERE _id = 2"
).to_dict()
assert result == [{"name": "delta", "age": 2, "score": 2.0}]
client.store({"name": "after_read", "age": 3, "score": 3.0, "city": "GZ"})
assert client.count_rows() == 3
assert client.retrieve(3)["name"] == "after_read"
client.close()
def test_experimental_memtable_single_dict_flush_reopen(self, monkeypatch):
monkeypatch.setenv("APEXBASE_EXPERIMENTAL_MEMTABLE_SINGLE_WRITE", "1")
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
client.store({"name": "memtable", "age": 2, "score": 2.0, "city": "SH"})
assert client.retrieve(2)["name"] == "memtable"
result = client.execute(
"SELECT name, age, score FROM default WHERE _id = 2"
).to_dict()
assert result == [{"name": "memtable", "age": 2, "score": 2.0}]
client.flush()
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 2
assert reopened.retrieve(2)["name"] == "memtable"
reopened.close()
def test_memtable_single_dict_close_persists_without_prior_read(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default")
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
client.store({"name": "close_memtable", "age": 2, "score": 2.0, "city": "SH"})
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 2
assert reopened.retrieve(2)["name"] == "close_memtable"
reopened.close()
def test_memtable_visibility_same_process_shared_client(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
writer = ApexClient(dirpath=temp_dir, durability="fast")
writer.create_table("default", schema=self.PERSON_SCHEMA)
writer.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
writer.store({"name": "shared_memtable", "age": 2, "score": 2.0, "city": "SH"})
reader = ApexClient(dirpath=temp_dir, durability="fast")
reader.use_table("default")
assert reader.retrieve(2)["name"] == "shared_memtable"
assert reader.count_rows() == 2
reader.close()
writer.close()
_CROSS_PROCESS_SNAPSHOT_SCRIPT = """
import sys
from apexbase import ApexClient
db_dir = sys.argv[1]
client = ApexClient(db_dir)
client.use_table("default")
count = client.count_rows()
row = client.retrieve(2)
client.close()
print(count, flush=True)
print(row, flush=True)
"""
def _run_cross_process_snapshot(self, db_dir: str, env: dict, timeout: float = 30):
result = subprocess.run(
[sys.executable, "-c", self._CROSS_PROCESS_SNAPSHOT_SCRIPT, db_dir],
capture_output=True,
text=True,
timeout=timeout,
env=env,
)
assert result.returncode == 0, result.stderr or result.stdout
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
assert len(lines) == 2, result.stdout
return lines[0], lines[1]
def test_memtable_visibility_cross_process_requires_flush(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
writer = ApexClient(dirpath=temp_dir, durability="fast")
writer.create_table("default", schema=self.PERSON_SCHEMA)
writer.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
writer.store({"name": "pending_memtable", "age": 2, "score": 2.0, "city": "SH"})
env = os.environ.copy()
python_path = os.path.join(os.path.dirname(__file__), '..', 'apexbase', 'python')
env["PYTHONPATH"] = python_path + os.pathsep + env.get("PYTHONPATH", "")
before_count, before_row = self._run_cross_process_snapshot(temp_dir, env)
assert before_count == "1"
assert before_row == "None"
writer.flush()
after_count, after_row = self._run_cross_process_snapshot(temp_dir, env)
assert after_count == "2"
assert "pending_memtable" in after_row
writer.close()
def test_fast_single_dict_memtable_overlays_broad_reads(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.PERSON_SCHEMA)
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
client.store({"name": "m2", "age": 2, "score": 2.0, "city": "SH"})
assert client.retrieve(2)["name"] == "m2"
assert client._storage.has_pending_memtable_rows() is True
assert client.count_rows() == 2
assert client._storage.has_pending_memtable_rows() is True
result = client.execute(
"SELECT * FROM default WHERE name = 'm2'",
show_internal_id=True,
).to_dict()
assert result == [{
"_id": 2,
"age": 2,
"score": 2.0,
"city": "SH",
"name": "m2",
}]
assert client._storage.has_pending_memtable_rows() is True
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 2
assert reopened.retrieve(2)["name"] == "m2"
reopened.close()
def test_fast_delete_pending_memtable_row_overlay(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.PERSON_SCHEMA)
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
client.store({"name": "pending_delete", "age": 2, "score": 2.0, "city": "SH"})
assert client._storage.has_pending_memtable_rows() is True
assert client.retrieve(2)["name"] == "pending_delete"
assert client.delete(2) is True
assert client.count_rows() == 1
assert client.retrieve(2) is None
assert client.execute("SELECT name FROM default WHERE _id = 2").to_dict() == []
assert client._storage.has_pending_memtable_rows() is True
client.flush()
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 1
assert reopened.retrieve(1)["name"] == "seed"
assert reopened.retrieve(2) is None
reopened.close()
def test_fast_delete_then_sql_update_same_id_stays_deleted(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default")
client.store({"name": ["seed"], "age": [1], "score": [1.0], "city": ["BJ"]})
assert client.retrieve(1)["name"] == "seed"
assert client.delete(1) is True
assert client.count_rows() == 0
assert client.retrieve(1) is None
updated = client.execute(
"UPDATE default SET score = 9.0 WHERE _id = 1"
).scalar()
assert updated == 0
assert client.count_rows() == 0
assert client.retrieve(1) is None
client.flush()
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 0
assert reopened.retrieve(1) is None
reopened.close()
def test_fast_replace_exact_schema_uses_delta_overlay(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default")
client.store({
"name": ["alice", "bob"],
"age": [25, 30],
"score": [10.0, 20.0],
"city": ["BJ", "SH"],
})
assert client.replace(1, {
"name": "alice2",
"age": 26,
"score": 11.5,
"city": "GZ",
}) is True
assert client.count_rows() == 2
assert client.retrieve(1)["name"] == "alice2"
rows = client.execute(
"SELECT name, age, score, city FROM default WHERE _id = 1"
).to_dict()
assert rows == [{"name": "alice2", "age": 26, "score": 11.5, "city": "GZ"}]
client.flush()
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 2
assert reopened.retrieve(1)["name"] == "alice2"
assert reopened.retrieve(2)["name"] == "bob"
reopened.close()
def test_fast_overlay_survives_memtable_append(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.PERSON_SCHEMA)
client.store({
"name": ["alice", "bob", "charlie"],
"age": [25, 30, 35],
"score": [10.0, 20.0, 30.0],
"city": ["BJ", "SH", "GZ"],
})
assert client.delete(2) is True
assert client.replace(1, {
"name": "alice2",
"age": 26,
"score": 11.5,
"city": "SZ",
}) is True
client.store({"name": "diana", "age": 28, "score": 40.0, "city": "HZ"})
assert client._storage.has_pending_memtable_rows() is True
assert client.count_rows() == 3
assert client.retrieve(1)["name"] == "alice2"
assert client.retrieve(2) is None
assert client.retrieve(4)["name"] == "diana"
rows = client.retrieve_all()
assert {row["name"] for row in rows} == {"alice2", "charlie", "diana"}
client.flush()
client.close()
reopened = ApexClient(dirpath=temp_dir)
reopened.use_table("default")
assert reopened.count_rows() == 3
assert reopened.retrieve(1)["name"] == "alice2"
assert reopened.retrieve(2) is None
assert reopened.retrieve(4)["name"] == "diana"
reopened.close()
def test_memtable_continuous_write_read_interleaving(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.STREAM_SCHEMA)
client.store({
"name": ["seed_0", "seed_1"],
"seq": [0, 1],
"bucket": ["base", "base"],
"score": [0.0, 1.0],
})
for seq in range(2, 22):
bucket = "even" if seq % 2 == 0 else "odd"
client.store({
"name": f"live_{seq}",
"seq": seq,
"bucket": bucket,
"score": float(seq),
})
assert client._storage.has_pending_memtable_rows() is True
assert client.count_rows() == seq + 1
assert client.execute("SELECT COUNT(*) AS cnt FROM default").to_dict()[0]["cnt"] == seq + 1
point = client.execute(
f"SELECT name, seq, bucket FROM default WHERE seq = {seq}"
).to_dict()
assert point == [{"name": f"live_{seq}", "seq": seq, "bucket": bucket}]
latest = client.retrieve(seq + 1)
assert latest["name"] == f"live_{seq}"
assert latest["seq"] == seq
grouped = client.execute(
"SELECT bucket, COUNT(*) AS cnt FROM default GROUP BY bucket ORDER BY bucket"
).to_dict()
assert {row["bucket"]: row["cnt"] for row in grouped} == {
"base": 2,
"even": 10,
"odd": 10,
}
top = client.execute(
"SELECT name, seq FROM default ORDER BY seq DESC LIMIT 3"
).to_dict()
assert [row["seq"] for row in top] == [21, 20, 19]
assert client._storage.has_pending_memtable_rows() is True
client.close()
def test_memtable_intermittent_reads_during_write_burst(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.STREAM_SCHEMA)
client.store({
"name": ["base_0", "base_1", "base_2"],
"seq": [0, 1, 2],
"bucket": ["cold", "hot", "cold"],
"score": [0.0, 1.0, 2.0],
})
written = [
{"name": "base_0", "seq": 0, "bucket": "cold", "score": 0.0},
{"name": "base_1", "seq": 1, "bucket": "hot", "score": 1.0},
{"name": "base_2", "seq": 2, "bucket": "cold", "score": 2.0},
]
checkpoints = {3, 4, 10, 20, 32}
for seq in range(3, 33):
row = {
"name": f"live_{seq}",
"seq": seq,
"bucket": "hot" if seq % 3 == 0 else "cold",
"score": float(seq),
}
client.store(row)
written.append(row)
if seq in checkpoints:
assert client.count_rows() == len(written)
hot_expected = sum(1 for item in written if item["bucket"] == "hot")
hot_count = client.execute(
"SELECT COUNT(*) AS cnt FROM default WHERE bucket = 'hot'"
).to_dict()[0]["cnt"]
assert hot_count == hot_expected
ranged = client.execute(
"SELECT seq FROM default WHERE seq BETWEEN 8 AND 12 ORDER BY seq"
).to_dict()
expected_range = [
item["seq"] for item in written if 8 <= item["seq"] <= 12
]
assert [item["seq"] for item in ranged] == expected_range
assert client._storage.has_pending_memtable_rows() is True
like_rows = client.execute(
"SELECT seq FROM default WHERE name LIKE 'live_1%' ORDER BY seq"
).to_dict()
assert [row["seq"] for row in like_rows] == list(range(10, 20))
assert client._storage.has_pending_memtable_rows() is True
client.close()
def test_memtable_later_same_process_sql_reader_before_flush(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
writer = ApexClient(dirpath=temp_dir, durability="fast")
writer.create_table("default", schema=self.STREAM_SCHEMA)
writer.store({
"name": ["base_0", "base_1"],
"seq": [0, 1],
"bucket": ["base", "base"],
"score": [0.0, 1.0],
})
for seq in range(2, 7):
writer.store({
"name": f"pending_{seq}",
"seq": seq,
"bucket": "pending",
"score": float(seq),
})
assert writer._storage.has_pending_memtable_rows() is True
reader = ApexClient(dirpath=temp_dir, durability="fast")
reader.use_table("default")
assert reader.count_rows() == 7
assert reader.execute(
"SELECT COUNT(*) AS cnt FROM default WHERE bucket = 'pending'"
).to_dict()[0]["cnt"] == 5
assert reader.execute(
"SELECT name FROM default WHERE seq = 6"
).to_dict() == [{"name": "pending_6"}]
assert reader.execute(
"SELECT seq FROM default ORDER BY seq DESC LIMIT 2"
).to_dict() == [{"seq": 6}, {"seq": 5}]
assert writer._storage.has_pending_memtable_rows() is True
reader.close()
writer.close()
def test_memtable_concurrent_write_stream_and_reader_loop(self, monkeypatch):
monkeypatch.delenv("APEXBASE_DISABLE_MEMTABLE_SINGLE_WRITE", raising=False)
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir, durability="fast")
client.create_table("default", schema=self.STREAM_SCHEMA)
client.store({
"name": ["seed"],
"seq": [0],
"bucket": ["base"],
"score": [0.0],
})
total_writes = 40
first_write = threading.Event()
done = threading.Event()
errors = []
observed_counts = []
def writer():
try:
for seq in range(1, total_writes + 1):
client.store({
"name": f"live_{seq}",
"seq": seq,
"bucket": f"b{seq % 4}",
"score": float(seq),
})
first_write.set()
if seq % 5 == 0:
time.sleep(0.001)
except Exception as exc:
errors.append(f"writer: {exc!r}")
finally:
done.set()
def reader():
if not first_write.wait(timeout=2):
errors.append("reader: writer did not start")
return
last_count = 0
deadline = time.time() + 5
try:
while not done.is_set() and time.time() < deadline:
count = client.execute(
"SELECT COUNT(*) AS cnt FROM default"
).to_dict()[0]["cnt"]
if count < last_count:
errors.append(f"reader: count moved backward {last_count}->{count}")
return
last_count = count
observed_counts.append(count)
time.sleep(0.0005)
if not done.is_set():
errors.append("reader: timed out waiting for writer")
except Exception as exc:
errors.append(f"reader: {exc!r}")
reader_thread = threading.Thread(target=reader, daemon=True)
writer_thread = threading.Thread(target=writer, daemon=True)
reader_thread.start()
writer_thread.start()
writer_thread.join(timeout=5)
reader_thread.join(timeout=5)
if writer_thread.is_alive() or reader_thread.is_alive():
errors.append("threads did not finish within timeout")
assert errors == []
assert observed_counts
final_count = client.execute(
"SELECT COUNT(*) AS cnt FROM default"
).to_dict()[0]["cnt"]
assert final_count == total_writes + 1
grouped = client.execute(
"SELECT bucket, COUNT(*) AS cnt FROM default GROUP BY bucket"
).to_dict()
assert sum(row["cnt"] for row in grouped) == final_count
assert client.execute(
f"SELECT name FROM default WHERE seq = {total_writes}"
).to_dict() == [{"name": f"live_{total_writes}"}]
assert client._storage.has_pending_memtable_rows() is True
client.close()
def test_store_single_dict_all_types(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"string_field": "test_string",
"int_field": 42,
"float_field": 3.14159,
"bool_field": True,
"none_field": None,
"bytes_field": b"binary_data",
"negative_int": -100,
"zero_float": 0.0,
"empty_string": "",
"false_bool": False,
}
client.store(data)
result = client.retrieve(1)
assert result["string_field"] == "test_string"
assert result["int_field"] == 42
assert result["float_field"] == 3.14159
assert result["bool_field"] is True
assert result["none_field"] is None
assert result["bytes_field"] == b"binary_data"
assert result["negative_int"] == -100
assert result["zero_float"] == 0.0
assert result["empty_string"] == ""
assert result["false_bool"] is False
client.close()
def test_store_single_dict_special_values(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"large_int": 2**63 - 1, "small_float": 1e-10,
"large_float": 1e10,
"infinity": float('inf'),
"neg_infinity": float('-inf'),
}
client.store(data)
result = client.retrieve(1)
assert result["large_int"] == 2**63 - 1
assert abs(result["small_float"] - 1e-10) < 1e-15
assert abs(result["large_float"] - 1e10) < 1e-5
client.close()
def test_store_empty_dict(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.store({})
count = client.count_rows()
assert count == 1
result = client.retrieve(1)
assert result == {} or result == {"_id": 1}
client.close()
def test_store_dict_with_unicode(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"chinese": "你好世界",
"emoji": "🌍🚀",
"arabic": "مرحبا بالعالم",
"russian": "Привет мир",
"french": "Bonjour le monde",
}
client.store(data)
result = client.retrieve(1)
for key, value in data.items():
assert result[key] == value
client.close()
class TestBatchStorage:
def test_store_list_of_dicts_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
{"name": "Charlie", "age": 35},
]
client.store(data)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
names = [r["name"] for r in results]
assert "Alice" in names
assert "Bob" in names
assert "Charlie" in names
client.close()
def test_store_empty_list(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.store([])
count = client.count_rows()
assert count == 0
client.close()
def test_store_list_with_various_types(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = [
{"type": "string", "value": "test"},
{"type": "int", "value": 42},
{"type": "float", "value": 3.14},
{"type": "bool", "value": True},
{"type": "none", "value": None},
{"type": "bytes", "value": b"binary"},
]
client.store(data)
count = client.count_rows()
assert count == 6
results = client.retrieve_all()
types = [r["type"] for r in results]
assert len(types) == 6
assert "string" in types
assert "int" in types
client.close()
def test_store_list_with_missing_fields(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = [
{"name": "Alice", "age": 25, "city": "NYC"},
{"name": "Bob", "age": 30}, {"name": "Charlie", "city": "LA"}, {"age": 40}, ]
client.store(data)
count = client.count_rows()
assert count == 4
results = client.retrieve_all()
assert len(results) == 4
client.close()
def test_store_large_list(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = [{"id": i, "value": f"record_{i}"} for i in range(1000)]
client.store(data)
count = client.count_rows()
assert count == 1000
result_0 = client.retrieve(1)
assert result_0["id"] == 0
assert result_0["value"] == "record_0"
result_999 = client.retrieve(1000)
assert result_999["id"] == 999
assert result_999["value"] == "record_999"
client.close()
class TestColumnarStorage:
def test_store_columnar_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"names": ["Alice", "Bob", "Charlie"],
"ages": [25, 30, 35],
"cities": ["NYC", "LA", "Chicago"],
}
client.store(data)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
names = [r["names"] for r in results]
assert "Alice" in names
assert "Bob" in names
assert "Charlie" in names
client.close()
def test_store_columnar_mixed_types(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"strings": ["a", "b", "c"],
"integers": [1, 2, 3],
"floats": [1.1, 2.2, 3.3],
"booleans": [True, False, True],
"bytes_data": [b"x", b"y", b"z"],
}
client.store(data)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_store_columnar_empty_columns(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"empty_list": [],
"non_empty": [1, 2, 3],
}
with pytest.raises(ValueError, match="same length"):
client.store(data)
client.close()
def test_store_columnar_unequal_lengths(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"short": [1, 2],
"long": [1, 2, 3, 4, 5],
}
with pytest.raises(ValueError, match="same length"):
client.store(data)
client.close()
def test_store_columnar_single_value(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"single": ["only_value"],
}
client.store(data)
count = client.count_rows()
assert count == 1
result = client.retrieve(1)
assert result["single"] == "only_value"
client.close()
@pytest.mark.skipif(not hasattr(np, 'array'), reason="NumPy not available")
class TestNumPyStorage:
def test_store_numpy_numeric(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"int_array": np.array([1, 2, 3, 4, 5], dtype=np.int64),
"float_array": np.array([1.1, 2.2, 3.3, 4.4, 5.5], dtype=np.float64),
"bool_array": np.array([True, False, True, False, True], dtype=np.bool_),
}
client.store(data)
count = client.count_rows()
assert count == 5
results = client.retrieve_all()
assert len(results) == 5
client.close()
def test_store_numpy_mixed_dtypes(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"int32": np.array([1, 2, 3], dtype=np.int32),
"int64": np.array([100, 200, 300], dtype=np.int64),
"float32": np.array([1.1, 2.2, 3.3], dtype=np.float32),
"float64": np.array([10.1, 20.2, 30.3], dtype=np.float64),
}
client.store(data)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_store_numpy_large_arrays(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
size = 10000
data = {
"large_int": np.arange(size, dtype=np.int64),
"large_float": np.random.random(size).astype(np.float64),
}
client.store(data)
count = client.count_rows()
assert count == size
result_0 = client.retrieve(1)
assert result_0["large_int"] == 0
result_last = client.retrieve(size)
assert result_last["large_int"] == size - 1
client.close()
def test_store_numpy_string_arrays(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"string_array": np.array(["a", "b", "c", "d"], dtype=str),
"unicode_array": np.array(["测试", "🚀", "café", "日本語"], dtype=str),
}
client.store(data)
count = client.count_rows()
assert count == 4
results = client.retrieve_all()
assert len(results) == 4
client.close()
@pytest.mark.skipif(not PANDAS_AVAILABLE, reason="Pandas not available")
class TestPandasStorage:
def test_store_pandas_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
})
client.store(df)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_store_pandas_mixed_dtypes(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pd.DataFrame({
"strings": ["a", "b", "c"],
"integers": pd.Series([1, 2, 3], dtype="int64"),
"floats": pd.Series([1.1, 2.2, 3.3], dtype="float64"),
"booleans": pd.Series([True, False, True], dtype="bool"),
"datetime": pd.date_range("2023-01-01", periods=3),
})
client.store(df)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_store_pandas_empty(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pd.DataFrame()
client.store(df)
count = client.count_rows()
assert count == 0
client.close()
def test_store_pandas_with_nan(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pd.DataFrame({
"col1": [1, 2, 3, 4],
"col2": ["a", "b", "c", "d"],
})
try:
client.store(df)
count = client.count_rows()
assert count == 4
except TypeError as e:
print(f"Pandas NaN: {e}")
client.close()
def test_from_pandas_method(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pd.DataFrame({
"product": ["A", "B", "C"],
"price": [10.99, 20.50, 30.75],
})
returned_client = client.from_pandas(df)
assert returned_client is client
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
@pytest.mark.skipif(not POLARS_DF_AVAILABLE, reason="Polars not available")
class TestPolarsStorage:
def test_store_polars_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
})
try:
client.store(df)
count = client.count_rows()
assert count == 3
except (AttributeError, TypeError) as e:
print(f"Polars storage issue: {e}")
client.close()
def test_store_polars_mixed_dtypes(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pl.DataFrame({
"strings": ["a", "b", "c"],
"integers": pl.Series("integers", [1, 2, 3], dtype=pl.Int64),
"floats": pl.Series("floats", [1.1, 2.2, 3.3], dtype=pl.Float64),
"booleans": pl.Series("booleans", [True, False, True], dtype=pl.Boolean),
})
try:
client.store(df)
count = client.count_rows()
assert count == 3
except (AttributeError, TypeError) as e:
print(f"Polars mixed dtypes issue: {e}")
client.close()
def test_from_polars_method(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
df = pl.DataFrame({
"id": [1, 2, 3],
"value": ["x", "y", "z"],
})
returned_client = client.from_polars(df)
assert returned_client is client
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
@pytest.mark.skipif(not PYARROW_AVAILABLE, reason="PyArrow not available")
class TestPyArrowStorage:
def test_store_arrow_basic(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
table = pa.Table.from_pydict({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"city": ["NYC", "LA", "Chicago"],
})
client.store(table)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_store_arrow_mixed_dtypes(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
table = pa.Table.from_pydict({
"strings": ["a", "b", "c"],
"integers": pa.array([1, 2, 3], type=pa.int64()),
"floats": pa.array([1.1, 2.2, 3.3], type=pa.float64()),
"booleans": pa.array([True, False, True], type=pa.bool_()),
})
client.store(table)
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
def test_from_pyarrow_method(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
table = pa.Table.from_pydict({
"key": ["a", "b", "c"],
"value": [1, 2, 3],
})
returned_client = client.from_pyarrow(table)
assert returned_client is client
count = client.count_rows()
assert count == 3
results = client.retrieve_all()
assert len(results) == 3
client.close()
class TestStorageEdgeCases:
def test_store_unsupported_format(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
with pytest.raises(ValueError, match="Data must be dict, list of dicts"):
client.store("unsupported_string")
with pytest.raises(ValueError, match="Data must be dict, list of dicts"):
client.store(123)
with pytest.raises(ValueError, match="Data must be dict, list of dicts"):
client.store(set([1, 2, 3]))
client.close()
def test_store_on_closed_client(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.close()
with pytest.raises(RuntimeError, match="connection has been closed"):
client.store({"test": "data"})
with pytest.raises(RuntimeError, match="connection has been closed"):
client.store([{"test": "data"}])
with pytest.raises(RuntimeError, match="connection has been closed"):
client.store({"col": [1, 2, 3]})
def test_store_very_large_values(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
long_string = "x" * 1000000
data = {
"long_string": long_string,
"normal": "test",
}
client.store(data)
result = client.retrieve(1)
assert len(result["long_string"]) == 1000000
assert result["normal"] == "test"
client.close()
def test_store_nested_structures(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"nested_dict": {"key": "value"},
"nested_list": [1, 2, 3],
"normal": "test",
}
try:
client.store(data)
result = client.retrieve(1)
assert "normal" in result
except Exception as e:
print(f"Nested structures handled as: {e}")
client.close()
def test_store_special_characters(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
data = {
"quotes": 'Single "double" quotes',
"newlines": "Line 1\nLine 2\rLine 3",
"tabs": "Tab\tseparated",
"backslashes": "Backslash\\test",
"unicode": "Unicode: ñáéíóú",
"emoji": "Emoji: 🎉🚀🌟",
"null_bytes": "Null\x00byte",
}
client.store(data)
result = client.retrieve(1)
for key, expected in data.items():
assert result[key] == expected
client.close()
def test_store_with_fts_enabled(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
client.init_fts(index_fields=["content", "title"])
data = {
"title": "Test Document",
"content": "This is searchable content",
"metadata": "not_indexed",
}
client.store(data)
results = client.search_text("searchable")
assert len(results) > 0
client.close()
def test_store_performance_considerations(self):
with tempfile.TemporaryDirectory() as temp_dir:
client = ApexClient(dirpath=temp_dir)
client.create_table("default")
base_data = [{"id": i, "value": f"item_{i}"} for i in range(100)]
client.store(base_data.copy())
list_count = client.count_rows()
client.create_table("columnar_test")
columnar_data = {
"id": list(range(100)),
"value": [f"item_{i}" for i in range(100)],
}
client.store(columnar_data)
columnar_count = client.count_rows()
assert list_count == 100
assert columnar_count == 100
client.close()
if __name__ == "__main__":
pytest.main([__file__, "-v"])