Skip to content

Commit aee4a08

Browse files
russellromneyclaude
andcommitted
feat: Tiered S3 support in loadable extension via env vars
Extension now checks TURBOLITE_BUCKET at load time. If set, registers a TieredVfs with config from TURBOLITE_* env vars (prefix, cache_dir, endpoint, region, prefetch threads, etc.). Falls back to local CompressedVfs when no bucket is set. - make ext now builds with zstd,tiered by default - make ext-local for local-only builds - CI release builds include tiered feature - Fix test version assertions for 0.2.19 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a025da commit aee4a08

4 files changed

Lines changed: 107 additions & 17 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
5454
run: |
5555
cargo build --release --lib --target ${{ matrix.target }} \
56-
--no-default-features --features loadable-extension,zstd
56+
--no-default-features --features loadable-extension,zstd,tiered
5757
5858
- name: Determine library filename
5959
id: lib

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,24 @@ lib-bundled: ## Build shared library with bundled SQLite (self-contained)
3939

4040
# ── Loadable extension ────────────────────────────────────────────
4141

42-
EXT_FEATURES ?= zstd
42+
EXT_FEATURES ?= zstd,tiered
4343

4444
.PHONY: ext
45-
ext: ## Build SQLite loadable extension (.so / .dylib) for load_extension()
45+
ext: ## Build loadable extension with S3 tiered support (default)
4646
cargo build --release --lib --no-default-features --features loadable-extension,$(EXT_FEATURES)
4747
@cp $(TARGET_DIR)/$(LIB_FILE) $(TARGET_DIR)/turbolite.$(LIB_EXT)
4848
@echo ""
4949
@echo "Built loadable extension: $(TARGET_DIR)/turbolite.$(LIB_EXT)"
5050
@ls -lh $(TARGET_DIR)/turbolite.$(LIB_EXT)
5151

52+
.PHONY: ext-local
53+
ext-local: ## Build loadable extension (local compression only, no S3)
54+
cargo build --release --lib --no-default-features --features loadable-extension,zstd
55+
@cp $(TARGET_DIR)/$(LIB_FILE) $(TARGET_DIR)/turbolite.$(LIB_EXT)
56+
@echo ""
57+
@echo "Built loadable extension (local only): $(TARGET_DIR)/turbolite.$(LIB_EXT)"
58+
@ls -lh $(TARGET_DIR)/turbolite.$(LIB_EXT)
59+
5260
# ── C header ───────────────────────────────────────────────────────
5361

5462
.PHONY: header

src/ext.rs

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,117 @@
66
//! The C shim provides symbol shims for `sqlite3_vfs_register` etc. that route
77
//! through the extension API table, so `sqlite_vfs::register()` works correctly
88
//! inside a loadable extension.
9+
//!
10+
//! ## VFS selection
11+
//!
12+
//! If `TURBOLITE_BUCKET` is set, registers a **tiered S3 VFS** (requires the
13+
//! `tiered` feature). Otherwise, registers a **local compressed VFS**.
14+
//!
15+
//! ### Environment variables (tiered mode)
16+
//!
17+
//! | Variable | Required | Default | Description |
18+
//! |---|---|---|---|
19+
//! | `TURBOLITE_BUCKET` | yes | — | S3 bucket name |
20+
//! | `TURBOLITE_PREFIX` | no | `"turbolite"` | S3 key prefix |
21+
//! | `TURBOLITE_CACHE_DIR` | no | `"/tmp/turbolite"` | Local cache directory |
22+
//! | `TURBOLITE_ENDPOINT_URL` | no | — | Custom S3 endpoint (Tigris, MinIO) |
23+
//! | `TURBOLITE_REGION` | no | — | AWS region |
24+
//! | `TURBOLITE_PREFETCH_THREADS` | no | `num_cpus + 1` | Prefetch worker threads |
25+
//! | `TURBOLITE_COMPRESSION_LEVEL` | no | `3` | Zstd level 1-22 |
26+
//! | `TURBOLITE_READ_ONLY` | no | `false` | Open in read-only mode |
27+
//!
28+
//! Falls back to `AWS_ENDPOINT_URL` / `AWS_REGION` if the `TURBOLITE_` variants
29+
//! are not set.
930
10-
use std::path::PathBuf;
1131
use std::sync::atomic::{AtomicBool, Ordering};
1232

13-
use crate::CompressedVfs;
14-
15-
/// Track whether the VFS has already been registered (idempotent load).
1633
static VFS_REGISTERED: AtomicBool = AtomicBool::new(false);
1734

1835
/// Called from C entry point (`sqlite3_turbolite_init` in ext_entry.c).
19-
///
20-
/// Registers a compressed VFS named "turbolite" with zstd level 3.
21-
/// The base directory is "." — SQLite passes full paths to the VFS, so
22-
/// relative base dir means paths are used as-given by the host application.
23-
///
2436
/// Returns 0 on success, 1 on error. Idempotent: second call is a no-op.
2537
#[no_mangle]
2638
pub extern "C" fn turbolite_ext_register_vfs() -> std::os::raw::c_int {
2739
if VFS_REGISTERED.swap(true, Ordering::SeqCst) {
28-
// Already registered — idempotent success.
2940
return 0;
3041
}
3142

32-
let vfs = CompressedVfs::new(PathBuf::from("."), 3);
33-
match crate::register("turbolite", vfs) {
43+
let result = if std::env::var("TURBOLITE_BUCKET").is_ok() {
44+
register_tiered()
45+
} else {
46+
register_local()
47+
};
48+
49+
match result {
3450
Ok(()) => 0,
3551
Err(_) => {
3652
VFS_REGISTERED.store(false, Ordering::SeqCst);
3753
1
3854
}
3955
}
4056
}
57+
58+
fn register_local() -> Result<(), std::io::Error> {
59+
use std::path::PathBuf;
60+
let level = std::env::var("TURBOLITE_COMPRESSION_LEVEL")
61+
.ok()
62+
.and_then(|s| s.parse().ok())
63+
.unwrap_or(3);
64+
let vfs = crate::CompressedVfs::new(PathBuf::from("."), level);
65+
crate::register("turbolite", vfs)
66+
}
67+
68+
#[cfg(feature = "tiered")]
69+
fn register_tiered() -> Result<(), std::io::Error> {
70+
use std::path::PathBuf;
71+
use crate::tiered::{TieredConfig, TieredVfs};
72+
73+
let bucket = std::env::var("TURBOLITE_BUCKET")
74+
.expect("TURBOLITE_BUCKET must be set for tiered mode");
75+
let prefix = std::env::var("TURBOLITE_PREFIX")
76+
.unwrap_or_else(|_| "turbolite".into());
77+
let cache_dir = std::env::var("TURBOLITE_CACHE_DIR")
78+
.map(PathBuf::from)
79+
.unwrap_or_else(|_| PathBuf::from("/tmp/turbolite"));
80+
let endpoint_url = std::env::var("TURBOLITE_ENDPOINT_URL")
81+
.or_else(|_| std::env::var("AWS_ENDPOINT_URL"))
82+
.ok();
83+
let region = std::env::var("TURBOLITE_REGION")
84+
.or_else(|_| std::env::var("AWS_REGION"))
85+
.ok();
86+
let prefetch_threads = std::env::var("TURBOLITE_PREFETCH_THREADS")
87+
.ok()
88+
.and_then(|s| s.parse().ok())
89+
.unwrap_or(0); // 0 = use default (num_cpus + 1)
90+
let compression_level = std::env::var("TURBOLITE_COMPRESSION_LEVEL")
91+
.ok()
92+
.and_then(|s| s.parse().ok())
93+
.unwrap_or(3);
94+
let read_only = std::env::var("TURBOLITE_READ_ONLY")
95+
.map(|s| s == "1" || s == "true")
96+
.unwrap_or(false);
97+
98+
let mut config = TieredConfig {
99+
bucket,
100+
prefix,
101+
cache_dir,
102+
endpoint_url,
103+
region,
104+
compression_level,
105+
read_only,
106+
..Default::default()
107+
};
108+
if prefetch_threads > 0 {
109+
config.prefetch_threads = prefetch_threads;
110+
}
111+
112+
let vfs = TieredVfs::new(config)?;
113+
crate::tiered::register("turbolite", vfs)
114+
}
115+
116+
#[cfg(not(feature = "tiered"))]
117+
fn register_tiered() -> Result<(), std::io::Error> {
118+
Err(std::io::Error::new(
119+
std::io::ErrorKind::Unsupported,
120+
"TURBOLITE_BUCKET is set but this extension was built without the 'tiered' feature",
121+
))
122+
}

tests/test_loadable_ext.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def _():
6060
conn = sqlite3.connect(":memory:")
6161
load_ext(conn)
6262
version = conn.execute("SELECT turbolite_version()").fetchone()[0]
63-
assert version == "0.1.0", f"expected 0.1.0, got {version}"
63+
assert version == "0.2.19", f"expected 0.2.19, got {version}"
6464
conn.close()
6565

6666

@@ -255,7 +255,7 @@ def _():
255255
# Loading again should not crash — VFS already registered, just no-op
256256
load_ext(conn)
257257
version = conn.execute("SELECT turbolite_version()").fetchone()[0]
258-
assert version == "0.1.0"
258+
assert version == "0.2.19"
259259
conn.close()
260260

261261

0 commit comments

Comments
 (0)