Compile-time-safe SQL for Rust β PostgreSQL and SQLite, async and sync.
Write real SQL. It is checked at cargo build against the schema your migration
files define β table names, column names, types, nullability. If it compiles,
the query is correct. No DSL, no method chains, no runtime "column not found".
1.0.0-alpha.5 β stable in shape, early in life; expect a few more alpha iterations before a full 1.0.
// migrations/0001_init.sql β CREATE TABLE users (id int PRIMARY KEY, email text NOT NULL);
// Typed at build time against the migration above.
// `SELECT nope FROM users` would be a compile error, not a runtime surprise.
bsql::query!(UserById, "SELECT id, email FROM users WHERE id = $1");
let user = conn.query_one::<UserById>((42_i32,)).await?;
// user.id: i32 user.email: String (a nullable column would be Option<T>)- If it compiles, the SQL is correct. Every query is validated at build time
against the schema your migration
*.sqlfiles describe β names, types, and nullability. Wrong column, wrong type, typo β the build fails. - One query function, and it always checks β no accidental unchecked path.
In sqlx a missing
!(query()vsquery!()) silently skips validation. bsql has a raw-SQL escape hatch too, but it is deliberate and named apart: the unchecked verbs carry a distinct suffix (query_rawfor raw text,query_paramsfor raw text plus runtime parameters), so you opt into unchecked SQL on purpose β never by forgetting a!. - Pure SQL text β no builder. CTEs, JOINs, window functions, subqueries β no
.filter().eq()method chains to learn. If PostgreSQL or SQLite supports it, you just write it. - Async and sync are both first-class. Both drivers plug the same socket into
ONE transport-generic core, so parity is a compiler guarantee, not
hand-maintained twins. The sync driver drops tokio entirely β pure
fn, no async runtime. Switch asyncβsync by swapping one feature line (and dropping.await). - PostgreSQL and SQLite, same macro. The same
query!carrier runs on both for typed reads, decoding into the same records β and SQLite verifies each value's storage class at runtime (a mismatch is a classified error, never a silent coercion). Typed writes throughquery!(execute/execute_batch/query_batch) are a PostgreSQL capability; on SQLite the typed flagship is read-only, and dynamic writes (execute_raw/execute_params) work on both. - Tiny footprint. ~1.7β1.8 MB peak memory for a real PostgreSQL workload β the leanest client measured there (and a near-tie with raw C on SQLite), ~8Γ under a C/libpq client, ~10Γ under Go/pgx, and ~3.6Γ under the nearest Rust client β and the whole TLS/SCRAM stack is feature-gated, so a localhost / trust-auth build is a handful of crates.
#![forbid(unsafe_code)]on every shipped crate. Nounwrap/expectin production code. NULL isOption<NonZeroU32>, not a sentinel. The hot decode path is proven panic-free and byte-stable by a machine-checked codegen gate.- Things nobody else does β automatic N+1 detection, a build-time
destructive-migration gate, Rust types generated from your migrations, typed
binary
COPY, and sub-millisecond schema-per-test isolation. See What makes it different.
Explore the Full Cross-Language Benchmark Suite & Methodology π«’ Seven clients across four languages (bsql, C/libpq, C/sqlite3, Go/pgx, Go/mattn, tokio-postgres, sqlx, diesel).
Point read latency (SELECT by-PK, 1 connection, lower is better):
bsql (sync) [ββββ ] 24.6 Β΅s (1.0x) 1.69 MB
bsql (async) [βββββ ] 26.2 Β΅s (1.1x) 1.80 MB
C / libpq [ββββ ] 25.5 Β΅s (1.0x) 13.25 MB
sqlx [βββββ ] 27.9 Β΅s (1.1x) 6.73 MB
tokio-postgres [βββββββ ] 39.8 Β΅s (1.6x) 6.50 MB
diesel [βββββββ ] 41.3 Β΅s (1.7x) 7.01 MB
Go / pgx [βββββββββ ] 52.1 Β΅s (2.1x) 16.81 MB
Point read latency (SELECT by-PK prepared, lower is better):
C / sqlite3 [ββ ] 1.51 Β΅s (1.0x) 3.83 MB
bsql [ββ ] 1.58 Β΅s (1.0x) 3.86 MB
diesel [βββ ] 1.87 Β΅s (1.2x) 4.08 MB
Go / mattn [ββββ ] 3.25 Β΅s (2.2x) 16.94 MB
sqlx [ββββββββ ] 6.16 Β΅s (4.1x) 4.59 MB
- On 10β100 row queries and batch inserts (
751 Β΅svs909 Β΅s),bsqlis 15β20% faster than raw C. - On 10,000 rows,
bsql(1.02 ms) is 14Γ faster than sqlx (14.19 ms), which hops threads on every row. - Peak RSS is a virtual tie with raw C (3.86 MB vs 3.83 MB).
Where the Real Multipliers Are (Orders of Magnitude, Not Fractions of a Percent)
In microbenchmarks (single point reads on loopback TCP), modern OS kernel TCP stacks and DB query planning consume ~85β90% of the ~25 Β΅s round trip. Driver micro-optimizations shave ~200β300 ns (~1β2% on loopback, invisible over a 1 ms network ping). Anyone promising 10Γ on a single SELECT is selling snake oil.
Where bsql delivers 10Γ to 1,000Γ architectural multipliers:
-
Aggregated Binary
COPY IN(~1,250Γ fewer wire frames & syscalls): Rows are packed into adaptive 64 KiB chunks: 5,000 rows stream in 4 TCP frames instead of 5,000 separate frames. - Constant-Memory Streaming (~524Γ less RAM than tokio-postgres, ~254Γ less than libpq): 5,000,000 rows stream in a flat 1.75 MB peak RSS with 5 total heap allocations across the entire stream (0.000001 allocs/row). Materializing competitors consume 918 MB (tokio-postgres) and 445 MB (libpq).
-
Zero-Allocation Column Slots (
SlotsScratch): Stack-allocated slots ([ColSlot; 16]) mean queries with$\le 16$ columns perform 0 heap allocations for slot management. - Instant Early-Break Recovery (>30,000Γ faster client reclaim): Terminating early from a 10M-row stream drops the socket via TCP RST after 128 frames, recovering the connection in <1 ms instead of hanging for 30+ seconds.
-
High-Concurrency Tail Latency (p99 / p99.9):
At 128 concurrent connections,
bsqldelivers 129,219 QPS with lowest p99 (1.58 ms) and ~32% lower extreme tail latency (p99.9 = 2.25 ms vs tokio-pg 3.33 ms). -
Connection Pool Thundering Herd Defense:
Rate-limited dials (
max_concurrent_handshakes) and double-checked idle reuse prevent DB CPU collapse on fleet reconnection. - Machine-Checked Hardware & Allocator Invariants: Hot path pinned at 761 instructions (0 panics, 0 unwinds), eager query allocations pinned at 14, session reset at 11.
Full PostgreSQL & SQLite Benchmark Tables
| scenario | bsql (sync) | bsql | C/libpq | sqlx | tokio-pg | diesel | Go/pgx |
|---|---|---|---|---|---|---|---|
| SELECT by-PK (1) | 24.6 x1 | 26.2 x1.1 | 25.5 x1.0 | 27.9 x1.1 | 39.8 x1.6 | 41.3 x1.7 | 52.1 x2.1 |
| SELECT 10 rows | 37.2 x1 | 40.0 x1.1 | 39.7 x1.1 | 42.6 x1.1 | 53.0 x1.4 | 55.4 x1.5 | 74.8 x2.0 |
| SELECT 100 rows | 49.7 x1 | 52.6 x1.1 | 54.7 x1.1 | 70.1 x1.4 | 72.0 x1.4 | 78.6 x1.6 | 78.5 x1.6 |
| SELECT 1000 rows | 232.7 x1 | 234.0 x1.0 | 250.3 x1.1 | 287.2 x1.2 | 279.9 x1.2 | 295.5 x1.3 | 259.5 x1.1 |
| INSERT single | 37.5 x1.0 | 43.1 x1.2 | 37.4 x1 | 45.3 x1.2 | 43.2 x1.2 | 44.0 x1.2 | 58.3 x1.6 |
| JOIN + GROUP BY | 2.99 ms x1 | 3.01 ms x1.0 | 3.04 ms x1.0 | 3.03 ms x1.0 | 3.05 ms x1.0 | 3.03 ms x1.0 | 3.05 ms x1.0 |
| scenario | bsql | C/sqlite3 | diesel | Go/mattn | sqlx |
|---|---|---|---|---|---|
| by-PK (prepared) | 1.58 x1.0 | 1.51 x1 | 1.87 x1.2 | 3.25 x2.2 | 6.16 x4.1 |
| 10 rows (prepared) | 4.69 x1 | 5.87 x1.3 | 9.91 x2.1 | 9.96 x2.1 | 14.05 x3.0 |
| 100 rows (prepared) | 13.48 x1 | 14.41 x1.1 | 29.70 x2.2 | 71.81 x5.3 | 112.37 x8.3 |
| 1000 rows (prepared) | 100.3 x1.0 | 97.7 x1 | 226.7 x2.3 | 671.2 x6.9 | 1.39 ms x14.2 |
| 10000 rows (prepared) | 1.02 ms x1.1 | 943.9 x1 | 2.19 ms x2.3 | 6.82 ms x7.2 | 14.19 ms x15.0 |
| INSERT single (prepared) | 18.43 x1 | 18.65 x1.0 | 21.01 x1.1 | 23.65 x1.3 | 27.89 x1.5 |
| INSERT batch (100) | 751 x1 | 909 x1.2 | 1.07 ms x1.4 | 1.07 ms x1.4 | 1.54 ms x2.1 |
| Subquery (prepared) | 28.58 x1.0 | 27.48 x1 | 42.87 x1.6 | 69.22 x2.5 | 113.97 x4.1 |
| JOIN + aggregate | 34.64 ms x1.0 | 34.77 ms x1.0 | 34.36 ms x1.0 | 34.12 ms x1 | 34.52 ms x1.0 |
PostgreSQL
# Cargo.toml (alpha is a pre-release, so pin the exact version)
[dependencies]
bsql = { version = "1.0.0-alpha.5", features = ["macros", "postgres-async"] }
[build-dependencies]
bsql-build = "1.0.0-alpha.5"// build.rs β replays migrations/ into the schema catalog query! reads.
fn main() -> Result<(), bsql_build::BuildError> {
bsql_build::emit("migrations")
}use bsql::pg;
bsql::query!(UserById, "SELECT id, email FROM users WHERE id = $1");
#[tokio::main]
async fn main() -> Result<(), bsql::pg::DriverError> {
let cfg = pg::ConnectConfig::from_dsn("postgres://user@localhost/mydb")?;
let mut conn = pg::Connection::connect(cfg).await?;
let user = conn.query_one::<UserById>((42_i32,)).await?;
println!("{} <{}>", user.id, user.email);
Ok(())
}Blocking driver? Swap the feature to postgres-sync, use bsql::pg_sync, and
drop the async/.await β the query! carriers and record types are identical.
SQLite
Enable features = ["macros", "sqlite", "macros-sqlite"] (the last one is the
build-time conformance oracle β it re-checks each query! against a real SQLite
replay of your migrations). The same carrier runs on bsql::sqlite::Connection
with the same query / query_one / query_opt / query_each verbs and the
same typed records. A PostgreSQL-only query (a uuid column, say) simply does
not implement the SQLite trait, so running it on SQLite is a compile error.
Runnable examples. The examples/ directory is a heavily-commented
tour that doubles as a usage guide β one focused program per feature (basic CRUD,
cross-backend, params vs raw, joins/aggregates, migrations, generated types,
external-type bridges, N+1 detection, typed COPY, pipelining/batch, streaming,
transactions, pooling, schema-per-test). The SQLite ones run with zero setup:
cargo run -p bsql-examples --bin basic_sqlite.
N+1 detection, for free (feature n1-detect)
The bug ORMs quietly encourage β one query per row of a previous result. bsql catches it for you:
let authors = conn.query::<AllAuthors>(()).await?;
for a in authors.iter() {
// one query per author, from the same source line β the anti-pattern
let books = conn.query::<BooksByAuthor>((a.id,)).await?;
}
if let Some(n1) = conn.n1_report() {
// N1Report { sql: "SELECT β¦ FROM books WHERE author_id = $1",
// file: "src/catalog.rs", line: 42, count: 250 }
eprintln!("N+1 at {}:{} ran {}Γ", n1.file, n1.line, n1.count);
}The same query! run 25+ times from the same call site within one logical
operation is flagged and attributed to the exact source line (via
#[track_caller]), with the SQL and repeat count. Diagnostics-only β it never
batches, blocks, or alters a result, so a false positive is at most a spurious log.
Zero-cost when off (the default): a production build compiles no detector field,
no branch, no #[track_caller] cost β the query verbs stay byte-identical. One
shared detector across PostgreSQL and SQLite.
A migration that destroys data won't compile silently
A DROP TABLE, ALTER TABLE β¦ DROP COLUMN, DROP SCHEMA β¦ CASCADE, TRUNCATE, or
DROP DATABASE in a migration is a build error β refused unless you acknowledge
the data loss on the line above it:
-- migrations/0007_drop_legacy.sql
-- bsql:ack-destructive
DROP TABLE legacy_sessions; -- compiles ONLY because of the ack line aboveWithout the -- bsql:ack-destructive line, cargo build fails and names the
offending statement. So a DROP that slipped in during a rebase can't reach
production silently β you either meant it (and said so) or you find out at build
time. The check runs on the same migration AST the schema catalog is built from, and
covers both directory migrations and a set baked into the binary with
embed_migrations!().
Rust types generated from your migrations
Declare a type in SQL, get a Rust type for it β no derive, no hand-written name, no drift:
-- migrations/0003_types.sql
CREATE TYPE mood AS ENUM ('happy', 'sad');bsql::user_types!(); // generates: pub enum Mood { Happy, Sad }
bsql::query!(GetMood, "SELECT mood FROM users WHERE id = $1");
let m: Mood = conn.query_one::<GetMood>((1_i32,)).await?.mood;Rename or delete a variant in a later migration and every use of the old name
stops compiling β drift is a build error, by construction. Enum evolution
(ALTER TYPE β¦ ADD VALUE / RENAME VALUE) is replayed so the generated enum always
matches the files, and variant order mirrors PostgreSQL's, so the derived Ord
matches the server's sort. Composites (CREATE TYPE addr AS (...)) generate a
struct; domains are transparent to their base type. No other Rust SQL library does
this β only bsql parses your migration set at build time.
Safe-by-construction bulk load β typed binary COPY
The raw text COPY path is the classic footgun: you hand-format the data, and one
mis-escaped tab or newline silently corrupts a row. copy! removes the text
entirely:
copy!(LoadUsers, "users", (id, email)); // validated against the catalog
conn.copy_in_typed::<LoadUsers>(
people.iter().map(|p| (p.id, p.email.as_str())) // one typed tuple per row
).await?;copy! checks the target table, columns and their types against the same catalog
query! reads; copy_in_typed then streams each row as a binary COPY in
constant memory. A wrong column type or arity is a compile error; an embedded
tab / newline / quote rides the binary field verbatim (nothing to mis-escape); and
it is faster than the text path on both client and server. The raw copy_in stays
as the escape hatch for pre-formatted data.
Schema-per-test isolation in sub-millisecond (feature test-harness)
#[bsql::test]
async fn creates_a_user(conn: &mut bsql::pg::Connection) {
conn.query_raw("CREATE TABLE users (id int)").await.unwrap(); // in an ISOLATED schema
} // schema auto-dropped, even if the test panicsEach test runs in its own freshly-created PostgreSQL schema (a CREATE SCHEMA, not a
whole database β so it's sub-millisecond, not a per-test database spin-up), so
cargo test's default parallelism never leaks state between tests. The isolation
rides the connect-time search_path, which survives a pool's RESET ALL, so a
pooled connection can't escape its schema. Teardown runs even if the test panics
(the schema is dropped in a catch_unwind, then the panic re-raised, so
#[should_panic] still works). Write a plain fn taking &mut bsql::pg_sync::Connection
and the same attribute gives you the blocking driver β it picks by async-ness.
Migrations, applied at runtime β atomic, ordered, exactly-once
let report = conn.run_migrations(
bsql::MigrationSource::directory("migrations")
).await?;
// report.applied β the migrations newly applied this run, in orderApplies your set to a live database on all three drivers, exactly once and in
lexicographic order, tracked in a _bsql_migrations ledger. Each migration + its
ledger row is one transaction β a failure rolls back and stops with a classified
error naming it, later migrations untouched. An edited-after-apply migration
(checksum drift), a reorder, or a deletion is a classified error, never silently
re-run. Concurrent boots serialize: PostgreSQL via a non-blocking advisory-lock poll
that stays deadlock-free even with CREATE INDEX CONCURRENTLY; SQLite via
BEGIN IMMEDIATE + an in-transaction re-check. Or bake the set into the binary with
embed_migrations!() β no filesystem at run time.
Production-grade connection lifecycle
let pool = pg::Pool::builder(cfg, 16)
.max_lifetime(Some(Duration::from_secs(1800)))
.idle_timeout(Some(Duration::from_secs(300)))
.slow_query_threshold(Some(Duration::from_millis(100)))
.on_diagnostic(|ev| tracing::info!(?ev)) // RAISE NOTICE, slow queries, pool statsβ¦
.build();
match conn.query_one::<GetUser>((id,)).await {
Err(e) if e.is_disconnect() => reconnect(), // connection died β reconnect
Err(e) => return Err(e), // query error β the connection is fine
Ok(u) => u,
}Health-gated checkout (a peer that died while idle is swapped transparently, and
get() stays bounded even on a half-open socket, never the ~15-minute kernel
hang), graceful shutdown (Pool::close sends a clean Terminate β no server
error-log flood), TCP keepalive, max_lifetime / idle_timeout reaping,
is_disconnect() reconnect-vs-retry classification, server-side statement_timeout,
and a client-side liveness window so a black-holed in-flight query is bounded too.
Diagnostics ride a dep-free DiagEvent sink β zero-cost when none is installed
(no clock read, no event built, hot path untouched).
Bring your own types β external-crate bridges
Decode a column straight into a foreign crate's type, with bsql depending on and forcing nothing:
// build.rs
bsql_build::Catalog::from_migrations("migrations")
.bridge("timestamptz", "chrono::DateTime<chrono::Utc>", "crate::to_chrono")
.emit()?;// your one infallible converter β the orphan-proof seam
pub fn to_chrono(ts: bsql::Timestamptz) -> chrono::DateTime<chrono::Utc> { /* β¦ */ }Now a timestamptz column in any query! decodes as chrono::DateTime<Utc>. The
target type and converter travel as strings, so bsql-build gains no dependency on
chrono. You can't impl bsql::Cell for chrono::DateTime (both are foreign β E0117),
but a free function compiles for any foreign target β so this works for uuid::Uuid,
serde_json::Value, rust_decimal, anything.
Speed is half the story; the other half is what the driver lets you do. An honest side-by-side with the Rust field β competitors get credit for what they have.
Capability matrix β bsql vs tokio-postgres / sqlx / diesel
Legend: β full Β· β partial Β· β none.
| capability | bsql | tokio-postgres | sqlx | diesel |
|---|---|---|---|---|
| Compile-time SQL check vs real schema | β
query! replays migrations |
β unchecked string | β
query! |
β typed DSL only |
| β¦with no live DB / cache at build | β offline from migration files | β | β needs DB or committed cache | β schema.rs usually from a DB |
| Plain SQL text (not a DSL) | β | β (unchecked) | β | β query-builder DSL |
| N+1 query detection | β
conn.n1_report(), zero-cost off |
β | β | β |
| Typed/safe binary COPY | β
copy_in_typed, compile-checked |
β hand-wired binary | β text COPY only | β copy_from, not compile-checked |
| Atomic typed pipelining / bulk batch | β
pipeline / execute_batch, all-or-nothing, ~1 RTT |
β untyped pipelining | β | β |
| Build-time migration safety gate | β destructive-op ack + checksum drift | β | β checksum drift | β up/down by version |
| First-class sync AND async, one API | β
shared Core<S> |
β async only | β async only | β separate diesel + diesel-async |
| Zero-per-row-alloc streaming | β
query_each, O(1) RAM, 0 alloc/row |
β streams, allocs/row | β streams, allocs/row | β default materializes |
| Out-of-band query cancellation | β
detached CancelToken |
β
cancel_token() |
β cancel-on-drop | β |
#![forbid(unsafe_code)] (shipped crates) |
β every crate | β some unsafe |
β some unsafe |
β links libpq (C FFI) |
Unix-domain socket β connect to a same-machine DB without the network layer (faster than localhost) |
β ~2.4β2.9Γ vs loopback TCP | β | β | β (libpq) |
Same query! on PostgreSQL and SQLite |
β one carrier, both backends | β PG only | β separate Sqlite/Pg types |
β separate backends |
#![forbid(unsafe_code)]on every shipped crate;deny(unwrap_used, expect_used).- The compile-checked path pins each column's PostgreSQL OID at build time β a wrong Rust type for a column is a compile error, and a wrong parameter type is rejected loudly on every binding surface (typed at compile, dynamic at the server, prepared at the client, SQLite by storage class) β never a silent byte-for-byte reinterpretation.
- The build-time check is against your committed migration files (the
version-controlled source of truth), not a live-DB introspection that can go
stale. An out-of-band
ALTER TABLEapplied by hand with no migration file is invisible at build time β but a runtime OID guard catches such drift as a classified error on the wire, never a silent wrong value. - What the checker asks of you in return: name your result columns (
SELECT *is rejected β the shape must be explicit), and add a cast where an expression's type is genuinely ambiguous (a bareSUM(x)βSUM(x)::int8, an uninferable expression βexpr::type). This is the ergonomic price of typing every column; a plain column, a join,COALESCE, and a cast-annotated aggregate all infer with no annotation. - What "correct" means, precisely. The compile check proves the query's
shape: relation and column names, result-column types, nullability, and the
parameter type OIDs. It does not type-check the SQL's operand / cast / value
semantics β a
WHERE bool_col = 5, a nonsensicalCAST, or a value-domain violation is caught by PostgreSQL itself at runtime, loudly and classified (42883/42846/22P02), never a silent wrong result. So "if it compiles, the query is correct" means its shape and types are correct against your schema; the server stays the authority on operand semantics. - Non-UTF-8 text is a classified error, never a panic. A
text/varcharcolumn carrying non-UTF-8 bytes (e.g. aSQL_ASCIIdatabase) decodes to a loudDecodeError(NonUtf8), not a panic and not a lossy substitution β read such a column asbyteaif it actually holds binary data. - Large results are bounded and streamable. An eager
query/query_rawreads the whole result into one arena bounded at 4 GiB (a larger one is the loudRowTooLarge, never an unbounded OOM); for a colossal result, stream it row-by-row in constant memory withquery_each/query_each_raw. - The wire decoders are proven total (no panic on any input) by a dep-free
fuzz gate; the inbound hot dispatch is proven panic-free and byte-stable by a
codegen gate. NULL is
Option<NonZeroU32>; SQL identifiers spliced into DDL go through a validate-onlySafeIdentnewtype. - Over TLS, SCRAM-SHA-256-PLUS channel binding (opt-in-strict) anchors auth to the server's certificate, closing the valid-cert relay/MITM gap that cert + hostname verification alone leaves.
- 64-bit Linux / macOS / Windows. TCP everywhere; unix sockets on unix.
No CI β by design. All gates run locally via cargo and a bsql-devgates crate
(dependency-frontier pin, runtime-vs-build boundary pin, intra-doc-link wall,
cross-platform check, hot-path codegen ceiling, decoder fuzz):
cargo clippy --workspace --all-targets # lint wall β 0 warnings
cargo test --workspace # unit + integration (offline)
cargo test -p bsql-devgates # the pins + walls
# live suites are #[ignore] and need a local PostgreSQL:
cargo test -p bsql-postgres-async --test sq_live -- --ignoredBenchmarks + their methodology live on the bench branch
(git switch bench && cargo bench), kept off the code branch so a
normal clone stays lean.
Built with Claude and Gemini in adversarial dual-LLM pair engineering. Both systems collaborated continuously across architectural synthesis, red-team chaos auditing, adversarial judging, and zero-overhead implementation β every slice challenged and verified before it landed.
2,640+ tests across the workspace β unit, integration, compile-fail (trybuild),
live-database, dependency-free fuzz, and machine-checked codegen gates. Not just
tests that the code works, but tests that broken code is rejected at compile
time. Hardened by a real-load fault-injection pass (TLS byte-fragmentation,
mid-stream faults, million-row streams, concurrency) β the sort of thing that
catches what green unit tests hide.
Don't follow the author's name. Don't assume an older library is automatically the safer bet. Run the benchmarks yourself, read the tests, check the code.
CLAUDE.md is the exhaustive engineering reference (conventions, layout, every
gate and safety invariant) β read it if you want the full picture.
MIT OR Apache-2.0, at your option.