#!/usr/bin/env -S uv run # /// script # requires-python = ">=3.11" # /// """SQLite bulk-insert benchmark: elapsed time and process peak RSS. Run: uv run sqlite-bulk-benchmark-BUILDER-openai-gpt-5.6-sol.py Every timed trial executes in a fresh child process against a fresh database. The deliberately slow autocommit case uses a prefix sample and reports a clearly marked linear projection to one million rows. """ from __future__ import annotations import argparse import gc import itertools import json import os import platform import random import sqlite3 import subprocess import sys import tempfile import time from dataclasses import dataclass from pathlib import Path from typing import Iterable, Iterator, Sequence IDENTITY = "BUILDER-openai-gpt-5.6-sol" DEFAULT_ROWS = 1_000_000 DEFAULT_BASELINE_ROWS = 10_000 DEFAULT_REPEATS = 2 EXECUTEMANY_BATCH_ROWS = 10_000 MULTI_VALUES_TARGET_ROWS = 500 COLUMN_COUNT = 6 INSERT_ONE = """ INSERT INTO events (id, account_id, created_at, amount_cents, status, payload) VALUES (?, ?, ?, ?, ?, ?) """ SCHEMA = """ CREATE TABLE events ( id INTEGER PRIMARY KEY, account_id INTEGER NOT NULL, created_at INTEGER NOT NULL, amount_cents INTEGER NOT NULL, status TEXT NOT NULL CHECK (status IN ('new', 'paid', 'sent', 'void')), payload TEXT NOT NULL ) """ SET_BASED_INSERT = """ WITH RECURSIVE seq(i) AS ( VALUES(0) UNION ALL SELECT i + 1 FROM seq WHERE i + 1 < ? ), generated(i, mix) AS ( SELECT i, (i * 1103515245 + 12345) % 2147483648 FROM seq ) INSERT INTO events (id, account_id, created_at, amount_cents, status, payload) SELECT i, mix % 100003, 1700000000 + i, (mix % 2000001) - 1000000, CASE i % 4 WHEN 0 THEN 'new' WHEN 1 THEN 'paid' WHEN 2 THEN 'sent' ELSE 'void' END, printf( 'event-%010d-%010x-abcdefghijklmnopqrstuvwxyz0123456789', i, mix ) FROM generated """ DIGEST_SQL = """ SELECT count(*), sum(id), sum(account_id), sum(created_at), sum(amount_cents), sum(length(status)), sum(length(payload)), min(id), max(id), min(payload), max(payload) FROM events """ @dataclass(frozen=True) class Strategy: key: str label: str journal_mode: str synchronous: str implementation: str durability: str sampled: bool = False STRATEGIES: tuple[Strategy, ...] = ( Strategy( "naive_autocommit", "naive autocommit", "DELETE", "FULL", "naive", "durable", sampled=True, ), Strategy( "one_big_transaction", "one transaction + execute", "DELETE", "FULL", "one_transaction", "durable", ), Strategy( "executemany_streamed", "streamed executemany batches", "DELETE", "FULL", "executemany", "durable", ), Strategy( "wal_multi_values", "WAL/NORMAL + multi-VALUES", "WAL", "NORMAL", "multi_values", "consistent; latest commit power-loss risk", ), Strategy( "off_multi_values", "OFF/OFF + multi-VALUES", "OFF", "OFF", "multi_values", "rebuildable staging only", ), Strategy( "wal_set_based", "WAL/NORMAL + INSERT-SELECT", "WAL", "NORMAL", "set_based", "consistent; latest commit power-loss risk", ), Strategy( "off_set_based", "OFF/OFF + INSERT-SELECT", "OFF", "OFF", "set_based", "rebuildable staging only", ), ) STRATEGY_BY_KEY = {strategy.key: strategy for strategy in STRATEGIES} Row = tuple[int, int, int, int, str, str] Digest = tuple[int | str, ...] def make_row(i: int) -> Row: """The sole Python definition of benchmark data.""" mix = (i * 1103515245 + 12345) % 2147483648 status = ("new", "paid", "sent", "void")[i % 4] return ( i, mix % 100003, 1700000000 + i, (mix % 2000001) - 1000000, status, f"event-{i:010d}-{mix:010x}-abcdefghijklmnopqrstuvwxyz0123456789", ) def generated_rows(start: int, stop: int) -> Iterator[Row]: for i in range(start, stop): yield make_row(i) def expected_digest(row_count: int) -> Digest: count = 0 id_sum = account_sum = created_sum = amount_sum = 0 status_length_sum = payload_length_sum = 0 min_payload: str | None = None max_payload: str | None = None for row in generated_rows(0, row_count): row_id, account_id, created_at, amount, status, payload = row count += 1 id_sum += row_id account_sum += account_id created_sum += created_at amount_sum += amount status_length_sum += len(status) payload_length_sum += len(payload) if min_payload is None or payload < min_payload: min_payload = payload if max_payload is None or payload > max_payload: max_payload = payload return ( count, id_sum, account_sum, created_sum, amount_sum, status_length_sum, payload_length_sum, 0 if row_count else None, row_count - 1 if row_count else None, min_payload, max_payload, ) def peak_rss_bytes() -> int: if os.name == "nt": # ctypes is standard library. PeakWorkingSetSize is the Windows # equivalent of process peak RSS. import ctypes from ctypes import wintypes class ProcessMemoryCounters(ctypes.Structure): _fields_ = ( ("cb", wintypes.DWORD), ("PageFaultCount", wintypes.DWORD), ("PeakWorkingSetSize", ctypes.c_size_t), ("WorkingSetSize", ctypes.c_size_t), ("QuotaPeakPagedPoolUsage", ctypes.c_size_t), ("QuotaPagedPoolUsage", ctypes.c_size_t), ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t), ("QuotaNonPagedPoolUsage", ctypes.c_size_t), ("PagefileUsage", ctypes.c_size_t), ("PeakPagefileUsage", ctypes.c_size_t), ) counters = ProcessMemoryCounters() counters.cb = ctypes.sizeof(counters) get_current_process = ctypes.windll.kernel32.GetCurrentProcess get_current_process.restype = wintypes.HANDLE ok = ctypes.windll.psapi.GetProcessMemoryInfo( get_current_process(), ctypes.byref(counters), counters.cb ) if not ok: raise ctypes.WinError() return int(counters.PeakWorkingSetSize) import resource raw = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss # macOS reports bytes; Linux and the BSDs report KiB. if sys.platform == "darwin": return int(raw) return int(raw) * 1024 def setup_connection(db_path: Path, strategy: Strategy) -> sqlite3.Connection: connection = sqlite3.connect(db_path, isolation_level=None, timeout=60.0) connection.execute("PRAGMA page_size=4096") actual_journal = connection.execute( f"PRAGMA journal_mode={strategy.journal_mode}" ).fetchone()[0] if str(actual_journal).upper() != strategy.journal_mode: raise RuntimeError( f"requested journal_mode={strategy.journal_mode}, got {actual_journal}" ) connection.execute(f"PRAGMA synchronous={strategy.synchronous}") # Common controls: bounded cache, no mmap RSS ambiguity, disk-backed temp # structures, and spill enabled. These are identical for every strategy. connection.execute("PRAGMA cache_size=-2048") connection.execute("PRAGMA cache_spill=ON") connection.execute("PRAGMA temp_store=FILE") connection.execute("PRAGMA mmap_size=0") connection.execute("PRAGMA foreign_keys=ON") connection.execute(SCHEMA) return connection def finish_commit(connection: sqlite3.Connection, strategy: Strategy) -> None: connection.commit() # Include WAL's eventual write-back cost, rather than declaring victory # while the complete database still resides in a large sidecar file. if strategy.journal_mode == "WAL": result = connection.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone() if result[0] != 0: raise RuntimeError(f"WAL checkpoint was busy: {result!r}") def insert_naive(connection: sqlite3.Connection, row_count: int, strategy: Strategy) -> None: # isolation_level=None makes every statement its own transaction. for row in generated_rows(0, row_count): connection.execute(INSERT_ONE, row) def insert_one_transaction( connection: sqlite3.Connection, row_count: int, strategy: Strategy ) -> None: connection.execute("BEGIN IMMEDIATE") for row in generated_rows(0, row_count): connection.execute(INSERT_ONE, row) finish_commit(connection, strategy) def insert_executemany( connection: sqlite3.Connection, row_count: int, strategy: Strategy ) -> None: connection.execute("BEGIN IMMEDIATE") for start in range(0, row_count, EXECUTEMANY_BATCH_ROWS): stop = min(start + EXECUTEMANY_BATCH_ROWS, row_count) # An iterator, not a list: batching does not materialize input rows. connection.executemany(INSERT_ONE, generated_rows(start, stop)) finish_commit(connection, strategy) def insert_multi_values( connection: sqlite3.Connection, row_count: int, strategy: Strategy ) -> None: try: variable_limit = connection.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER) except AttributeError: variable_limit = 999 batch_rows = max( 1, min(MULTI_VALUES_TARGET_ROWS, variable_limit // COLUMN_COUNT) ) placeholder = "(" + ",".join("?" for _ in range(COLUMN_COUNT)) + ")" connection.execute("BEGIN IMMEDIATE") for start in range(0, row_count, batch_rows): stop = min(start + batch_rows, row_count) batch = list(generated_rows(start, stop)) statement = ( "INSERT INTO events " "(id, account_id, created_at, amount_cents, status, payload) VALUES " + ",".join(itertools.repeat(placeholder, len(batch))) ) bindings = [value for row in batch for value in row] connection.execute(statement, bindings) finish_commit(connection, strategy) def insert_set_based( connection: sqlite3.Connection, row_count: int, strategy: Strategy ) -> None: connection.execute("BEGIN IMMEDIATE") connection.execute(SET_BASED_INSERT, (row_count,)) finish_commit(connection, strategy) IMPLEMENTATIONS = { "naive": insert_naive, "one_transaction": insert_one_transaction, "executemany": insert_executemany, "multi_values": insert_multi_values, "set_based": insert_set_based, } def worker(strategy_key: str, db_path: Path, row_count: int) -> int: strategy = STRATEGY_BY_KEY[strategy_key] if db_path.exists(): db_path.unlink() connection = setup_connection(db_path, strategy) implementation = IMPLEMENTATIONS[strategy.implementation] gc_was_enabled = gc.isenabled() gc.disable() started_ns = time.perf_counter_ns() try: implementation(connection, row_count, strategy) except BaseException: if connection.in_transaction: connection.rollback() raise finally: if gc_was_enabled: gc.enable() elapsed_ns = time.perf_counter_ns() - started_ns # Snapshot peak before the untimed validation query. rss_bytes = peak_rss_bytes() digest = tuple(connection.execute(DIGEST_SQL).fetchone()) quick_check = connection.execute("PRAGMA quick_check").fetchone()[0] connection.close() print( json.dumps( { "strategy": strategy.key, "rows": row_count, "elapsed_seconds": elapsed_ns / 1_000_000_000, "peak_rss_bytes": rss_bytes, "digest": digest, "quick_check": quick_check, }, separators=(",", ":"), ) ) return 0 def format_seconds_list(results: Sequence[dict[str, object]]) -> str: return ",".join(f"{float(result['elapsed_seconds']):.3f}" for result in results) def format_rss_list(results: Sequence[dict[str, object]]) -> str: mib = 1024 * 1024 return ",".join(f"{int(result['peak_rss_bytes']) / mib:.1f}" for result in results) def median(values: Sequence[float]) -> float: ordered = sorted(values) middle = len(ordered) // 2 if len(ordered) % 2: return ordered[middle] return (ordered[middle - 1] + ordered[middle]) / 2 def print_table( grouped: dict[str, list[dict[str, object]]], total_rows: int, expected_by_count: dict[int, Digest], ) -> tuple[Strategy, Strategy]: headers = ( "Strategy", "Mode", "Rows/run", "Trial time(s)", "Time@1M(s)", "Peak RAM/run MiB", "Data", ) rows: list[tuple[str, ...]] = [] normalized_times: dict[str, float] = {} peak_by_strategy: dict[str, int] = {} for strategy in STRATEGIES: results = grouped[strategy.key] elapsed = [float(result["elapsed_seconds"]) for result in results] row_count = int(results[0]["rows"]) projected = median(elapsed) * total_rows / row_count normalized_times[strategy.key] = projected peak_by_strategy[strategy.key] = max( int(result["peak_rss_bytes"]) for result in results ) verified = all( tuple(result["digest"]) == expected_by_count[int(result["rows"])] and result["quick_check"] == "ok" for result in results ) projection_marker = "*" if strategy.sampled else "" rows.append( ( strategy.label, f"{strategy.journal_mode}/{strategy.synchronous}", f"{row_count:,}", format_seconds_list(results), f"{projected:.3f}{projection_marker}", format_rss_list(results), "OK" if verified else "FAIL", ) ) widths = [len(header) for header in headers] for row in rows: for index, value in enumerate(row): widths[index] = max(widths[index], len(value)) def render(row: Sequence[str]) -> str: return " | ".join(value.ljust(widths[i]) for i, value in enumerate(row)) print(render(headers)) print("-+-".join("-" * width for width in widths)) for row in rows: print(render(row)) speed_key = min(normalized_times, key=normalized_times.get) # The sampled baseline is not eligible for the memory title: its peak RSS # was measured honestly but it did not execute the full row count. full_keys = [strategy.key for strategy in STRATEGIES if not strategy.sampled] memory_key = min(full_keys, key=peak_by_strategy.get) return STRATEGY_BY_KEY[speed_key], STRATEGY_BY_KEY[memory_key] def orchestrator(args: argparse.Namespace) -> int: if args.rows <= 0 or args.baseline_rows <= 0 or args.repeats <= 0: raise SystemExit("--rows, --baseline-rows, and --repeats must be positive") baseline_rows = min(args.baseline_rows, args.rows) print("SQLite bulk insert benchmark") print(f"Identity: {IDENTITY}") print(f"Python: {platform.python_version()} ({sys.executable})") print(f"SQLite: {sqlite3.sqlite_version}") print(f"Platform: {platform.platform()}") print(f"Rows: {args.rows:,}") print( f"Autocommit sample: {baseline_rows:,} rows; its Time@1M is a linear projection" ) print(f"Full-strategy trials: {args.repeats}") print( "Timed interval: first BEGIN/INSERT through COMMIT; WAL trials also include " "TRUNCATE checkpoint" ) print("Peak RAM: per-worker process ru_maxrss; every trial uses a fresh process") print() print("Computing independent expected data digests...") expected_by_count = { baseline_rows: expected_digest(baseline_rows), args.rows: expected_digest(args.rows), } jobs: list[tuple[Strategy, int, int]] = [] for strategy in STRATEGIES: trial_count = 1 if strategy.sampled else args.repeats row_count = baseline_rows if strategy.sampled else args.rows for trial in range(1, trial_count + 1): jobs.append((strategy, trial, row_count)) random.Random(0x5A17E).shuffle(jobs) grouped: dict[str, list[dict[str, object]]] = { strategy.key: [] for strategy in STRATEGIES } script_path = Path(__file__).resolve() child_environment = os.environ.copy() child_environment["PYTHONHASHSEED"] = "0" with tempfile.TemporaryDirectory(prefix=f"sqlite-bench-{IDENTITY}-") as temp_name: temp_dir = Path(temp_name) for job_number, (strategy, trial, row_count) in enumerate(jobs, start=1): db_path = temp_dir / f"{IDENTITY}-{strategy.key}-trial-{trial}.sqlite3" print( f"[{job_number:02d}/{len(jobs):02d}] {strategy.label}, " f"trial {trial}, {row_count:,} rows...", flush=True, ) completed = subprocess.run( [ sys.executable, str(script_path), "--worker", strategy.key, "--db", str(db_path), "--worker-rows", str(row_count), ], check=False, capture_output=True, text=True, env=child_environment, timeout=args.worker_timeout, ) if completed.returncode != 0: raise RuntimeError( f"worker failed for {strategy.key}:\n" f"stdout:\n{completed.stdout}\n" f"stderr:\n{completed.stderr}" ) result = json.loads(completed.stdout.strip().splitlines()[-1]) grouped[strategy.key].append(result) for results in grouped.values(): results.sort(key=lambda item: float(item["elapsed_seconds"])) print("\nResults") speed_winner, memory_winner = print_table( grouped, args.rows, expected_by_count ) print( "\n* naive autocommit Time@1M is measured-time × " f"{args.rows:,}/{baseline_rows:,}; its RAM is measured, not projected." ) print( "OFF/OFF rows are semantically identical but the database is rebuildable-only; " "a crash can corrupt it." ) print( "INSERT-SELECT is applicable when rows can be generated in SQL or selected " "from an attached SQLite source." ) print(f"\nSpeed Winner: {speed_winner.label}") print(f"Memory Winner: {memory_winner.label}") return 0 def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--rows", type=int, default=DEFAULT_ROWS) parser.add_argument("--baseline-rows", type=int, default=DEFAULT_BASELINE_ROWS) parser.add_argument("--repeats", type=int, default=DEFAULT_REPEATS) parser.add_argument("--worker-timeout", type=float, default=180.0) parser.add_argument("--worker", choices=tuple(STRATEGY_BY_KEY)) parser.add_argument("--db", type=Path) parser.add_argument("--worker-rows", type=int) args = parser.parse_args(argv) if args.worker and (args.db is None or args.worker_rows is None): parser.error("--worker requires --db and --worker-rows") return args def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv) if args.worker: return worker(args.worker, args.db, args.worker_rows) return orchestrator(args) if __name__ == "__main__": raise SystemExit(main())