import gc
import os
import resource
import sys
import subprocess
import tempfile
import shutil
import pytest
import pyarrow as pa
import duckdb
def get_rss_mb():
usage = resource.getrusage(resource.RUSAGE_SELF)
import sys
if sys.platform == 'darwin':
return usage.ru_maxrss / (1024 * 1024)
else:
return usage.ru_maxrss / 1024
def get_current_rss_mb():
import sys
pid = os.getpid()
if sys.platform == 'darwin':
import subprocess
try:
out = subprocess.check_output(['ps', '-o', 'rss=', '-p', str(pid)], text=True)
return int(out.strip()) / 1024 except Exception:
return get_rss_mb()
else:
try:
with open(f'/proc/{pid}/status') as f:
for line in f:
if line.startswith('VmRSS:'):
return int(line.split()[1]) / 1024 except Exception:
return get_rss_mb()
NUM_ROWS = 10_000
def generate_test_data(n=NUM_ROWS):
return {
'id': list(range(n)),
'value_a': [i * 17 % 9973 for i in range(n)],
'value_b': [i * 31 % 7919 for i in range(n)],
'value_c': [float(i % 1000) + 0.5 for i in range(n)],
'category': [f'cat_{i % 50}' for i in range(n)],
'label': [f'label_{i % 200}' for i in range(n)],
}
def _data_to_arrow(data):
return pa.table(data)
class TestMemoryComparison:
def _measure_apexbase_memory(self, data, tmp_dir):
from apexbase import ApexClient
gc.collect()
rss_before = get_current_rss_mb()
client = ApexClient(dirpath=os.path.join(tmp_dir, 'apex'))
client.create_table('bench')
client.store(data)
client.flush()
r1 = client.execute("SELECT COUNT(*) FROM bench")
r2 = client.execute("SELECT category, SUM(value_a) FROM bench GROUP BY category")
r3 = client.execute("SELECT * FROM bench WHERE value_a > 5000 LIMIT 100")
gc.collect()
rss_after = get_current_rss_mb()
client.close()
del client, r1, r2, r3
gc.collect()
return rss_after - rss_before
def _measure_duckdb_memory(self, data, tmp_dir):
gc.collect()
rss_before = get_current_rss_mb()
con = duckdb.connect(':memory:')
arrow_table = _data_to_arrow(data)
con.execute("CREATE TABLE bench AS SELECT * FROM arrow_table")
r1 = con.execute("SELECT COUNT(*) FROM bench").fetchall()
r2 = con.execute("SELECT category, SUM(value_a) FROM bench GROUP BY category").fetchall()
r3 = con.execute("SELECT * FROM bench WHERE value_a > 5000 LIMIT 100").fetchall()
gc.collect()
rss_after = get_current_rss_mb()
con.close()
del con, arrow_table, r1, r2, r3
gc.collect()
return rss_after - rss_before
def test_memory_usage_within_duckdb_10_percent(self):
data = generate_test_data()
with tempfile.TemporaryDirectory() as tmp_dir:
duckdb_mem = self._measure_duckdb_memory(data, tmp_dir)
gc.collect()
with tempfile.TemporaryDirectory() as tmp_dir:
apex_mem = self._measure_apexbase_memory(data, tmp_dir)
gc.collect()
duckdb_baseline = max(duckdb_mem, 5.0)
threshold = duckdb_baseline * 1.10
print(f"\n=== Memory Comparison ({NUM_ROWS} rows, 6 columns) ===")
print(f" DuckDB RSS delta: {duckdb_mem:.2f} MB")
print(f" ApexBase RSS delta: {apex_mem:.2f} MB")
print(f" Threshold (110%): {threshold:.2f} MB")
print(f" Ratio: {apex_mem / duckdb_baseline:.2f}x")
assert apex_mem <= threshold, (
f"ApexBase memory ({apex_mem:.1f} MB) exceeds DuckDB ({duckdb_mem:.1f} MB) "
f"by more than 10% (threshold: {threshold:.1f} MB)"
)
def test_query_memory_on_demand(self):
from apexbase import ApexClient
data = generate_test_data()
with tempfile.TemporaryDirectory() as tmp_dir:
apex_dir = os.path.join(tmp_dir, 'apex')
client = ApexClient(dirpath=apex_dir)
client.create_table('bench')
client.store(data)
client.flush()
client.close()
del client
gc.collect()
gc.collect()
rss_before = get_current_rss_mb()
client2 = ApexClient(dirpath=apex_dir)
client2.use_table('bench')
result = client2.execute("SELECT COUNT(*) FROM bench")
count = result.scalar()
gc.collect()
rss_after = get_current_rss_mb()
mem_delta = rss_after - rss_before
client2.close()
print(f"\n=== On-Demand Memory (COUNT(*) on {NUM_ROWS} rows) ===")
print(f" RSS delta for COUNT(*): {mem_delta:.2f} MB")
print(f" Row count: {count}")
assert count == NUM_ROWS
assert mem_delta < 50, (
f"COUNT(*) caused {mem_delta:.1f} MB memory increase — "
f"suggests entire dataset may have been loaded"
)