Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ path = "src/main.rs"
name = "generate-schemas"
path = "src/bin/generate_schemas.rs"

[[example]]
name = "seed_duckdb"
required-features = ["duckdb"]

[dependencies]
# CLI framework
clap = { version = "4.5", features = ["derive"] }
Expand Down
58 changes: 58 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Plenum developer targets.
#
# `make duckdb-test` is the full DuckDB verification: the offline parity
# suite followed by an end-to-end evidence run (seed a real .duckdb file,
# then drive the release binary against it, printing every JSON envelope
# to stdout — including the rejected write).

TARGET_DIR := $(or $(CARGO_TARGET_DIR),target)
PLENUM := $(TARGET_DIR)/release/plenum
DEMO_DB := $(TARGET_DIR)/duckdb-demo/demo.duckdb
DEMO_DSN := duckdb:$(DEMO_DB)

.PHONY: help duckdb-test duckdb-parity duckdb-evidence duckdb-seed clean-duckdb-demo

help:
@echo "Targets:"
@echo " duckdb-test full DuckDB test: parity suite + end-to-end evidence run"
@echo " duckdb-parity offline DuckDB parity suite (cargo test --test duckdb_parity)"
@echo " duckdb-evidence seed a demo .duckdb file and run plenum against it (evidence on stdout)"
@echo " duckdb-seed (re)create the seeded demo database at $(DEMO_DB)"
@echo " clean-duckdb-demo remove the demo database"

duckdb-test: duckdb-parity duckdb-evidence

duckdb-parity:
cargo test --test duckdb_parity

$(PLENUM): Cargo.toml $(shell find src -name '*.rs')
cargo build --release

duckdb-seed:
cargo run --quiet --example seed_duckdb -- $(DEMO_DB)

duckdb-evidence: $(PLENUM) duckdb-seed
@echo "=== 1/5 introspect: list tables, then full detail for customers ==="
$(PLENUM) introspect --dsn "$(DEMO_DSN)" --list-tables
$(PLENUM) introspect --dsn "$(DEMO_DSN)" --table customers
@echo
@echo "=== 2/5 query: SELECT over customers (unicode round-trips) ==="
$(PLENUM) query --dsn "$(DEMO_DSN)" --sql "SELECT id, name, email FROM customers ORDER BY id"
@echo
@echo "=== 3/5 query: aggregate view v_order_totals (JOIN + SUM over DECIMAL) ==="
$(PLENUM) query --dsn "$(DEMO_DSN)" --sql "SELECT * FROM v_order_totals ORDER BY customer_id, order_no"
@echo
@echo "=== 4/5 query: max_rows truncation (1500-row table, --max-rows 5) ==="
$(PLENUM) query --dsn "$(DEMO_DSN)" --sql "SELECT n, label FROM bulk_rows ORDER BY n" --max-rows 5
@echo
@echo "=== 5/5 query: INSERT must be rejected (CAPABILITY_VIOLATION expected) ==="
@if $(PLENUM) query --dsn "$(DEMO_DSN)" --sql "INSERT INTO customers (id, name, email) VALUES (99, 'Eve', 'eve@example.com')"; then \
echo "FAIL: write was not rejected" >&2; exit 1; \
else \
echo "OK: write rejected before execution"; \
fi
@echo
@echo "duckdb-evidence: all 5 checks passed against $(DEMO_DB)"

clean-duckdb-demo:
rm -f $(DEMO_DB)
93 changes: 93 additions & 0 deletions examples/seed_duckdb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Dev-only seeder for the `DuckDB` evidence demo (`make duckdb-evidence`).
//!
//! Creates a seeded `.duckdb` file mirroring the logical dataset used by the
//! `MySQL` / `PostgreSQL` live seeds and the `duckdb_parity` fixture, so the
//! release `plenum` binary can be exercised end-to-end against it.
//!
//! This is an example (never shipped in release artifacts) because plenum
//! itself is strictly read-only and cannot seed a database.
//!
//! Usage: `cargo run --example seed_duckdb -- <path.duckdb>`

use duckdb::Connection;

fn main() {
let path = std::env::args().nth(1).expect("usage: seed_duckdb <path.duckdb>");
assert!(
path.ends_with(".duckdb"),
"refusing to touch a path that does not end in .duckdb: {path}"
);

// Deterministic: always rebuild from scratch.
let _ = std::fs::remove_file(&path);
if let Some(parent) = std::path::Path::new(&path).parent() {
std::fs::create_dir_all(parent).expect("create parent dir");
}

let conn = Connection::open(&path).expect("create demo DB");

conn.execute_batch(
"CREATE TABLE customers (
id INTEGER NOT NULL,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX uq_customers_email ON customers(email);
INSERT INTO customers (id, name, email) VALUES
(1, 'Ada Lovelace', 'ada@example.com'),
(2, 'Grace Hopper 🌟', 'grace@example.com'),
(3, 'Annie Easley', 'annie@example.com');

CREATE TABLE orders (
customer_id INTEGER NOT NULL,
order_no INTEGER NOT NULL,
status VARCHAR NOT NULL DEFAULT 'pending',
placed_at TIMESTAMP NOT NULL,
PRIMARY KEY (customer_id, order_no),
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
INSERT INTO orders (customer_id, order_no, status, placed_at) VALUES
(1, 1, 'shipped', TIMESTAMP '2024-02-01 09:00:00'),
(1, 2, 'pending', TIMESTAMP '2024-02-03 10:30:00'),
(2, 1, 'cancelled', TIMESTAMP '2024-02-05 16:45:00');

CREATE TABLE order_items (
customer_id INTEGER NOT NULL,
order_no INTEGER NOT NULL,
line_no INTEGER NOT NULL,
sku VARCHAR NOT NULL,
qty INTEGER NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (customer_id, order_no, line_no),
FOREIGN KEY (customer_id, order_no)
REFERENCES orders(customer_id, order_no)
);
CREATE INDEX idx_order_items_sku ON order_items(sku);
INSERT INTO order_items
(customer_id, order_no, line_no, sku, qty, unit_price)
VALUES
(1, 1, 1, 'SKU-0001', 2, 19.99),
(1, 1, 2, 'SKU-0002', 1, 5.00),
(1, 2, 1, 'SKU-0003', 4, 2.50),
(2, 1, 1, 'SKU-0001', 1, 19.99);

CREATE TABLE bulk_rows AS
SELECT CAST(range + 1 AS INTEGER) AS n,
printf('row-%04d', range + 1) AS label
FROM range(1500);

CREATE VIEW v_order_totals AS
SELECT o.customer_id,
o.order_no,
o.status,
SUM(i.qty * i.unit_price) AS total
FROM orders o
JOIN order_items i
ON i.customer_id = o.customer_id AND i.order_no = o.order_no
GROUP BY o.customer_id, o.order_no, o.status;",
)
.expect("seed demo DB");

eprintln!("seeded {path}");
}
Loading