use macrame::schema::ddl;
async fn try_weight(conn: &libsql::Connection, label: &str, sql: &str) {
match conn.execute(sql, ()).await {
Ok(_) => println!(" {label:<28} ACCEPTED"),
Err(e) => {
let msg = e.to_string();
let short = msg
.split(':')
.next_back()
.unwrap_or(&msg)
.trim()
.to_string();
println!(" {label:<28} refused ({short})");
}
}
}
#[tokio::main]
async fn main() {
println!("\n1. CHECK (weight >= 0.0), values offered directly:");
let db = libsql::Builder::new_local(":memory:")
.build()
.await
.unwrap();
let conn = db.connect().unwrap();
conn.execute(
"CREATE TABLE w (id INTEGER PRIMARY KEY, weight REAL NOT NULL CHECK (weight >= 0.0))",
(),
)
.await
.unwrap();
for (label, expr) in [
("1.0", "1.0"),
("0.0", "0.0"),
("-0.0", "-0.0"),
("-1.5", "-1.5"),
("+inf (9e999)", "9e999"),
("-inf (-9e999)", "-9e999"),
("NaN (0.0/0.0)", "0.0/0.0"),
("integer 3", "3"),
("text '5'", "'5'"),
("text 'abc'", "'abc'"),
] {
try_weight(
&conn,
label,
&format!("INSERT INTO w (weight) VALUES ({expr})"),
)
.await;
}
println!("\n2. rebuilding `links` with the CHECK, in one transaction:");
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("m.db");
let db = libsql::Builder::new_local(&path).build().await.unwrap();
let conn = db.connect().unwrap();
conn.execute("PRAGMA foreign_keys = ON", ()).await.unwrap();
macrame::schema::migrations::run(&conn).await.unwrap();
for id in ["c0", "c1"] {
conn.execute(
"INSERT INTO concepts (id, title, valid_from, recorded_at) \
VALUES (?1, 'N', '2026-01-01T00:00:00.000000Z', '2026-01-01T00:00:00.000000Z')",
libsql::params![id],
)
.await
.unwrap();
}
conn.execute(
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES ('c0','c1','A', \
'2026-01-01T00:00:00.000000Z','9999-12-31T23:59:59.999999Z',2.5,'{}', \
'2026-01-01T00:00:00.000000Z')",
(),
)
.await
.unwrap();
let new_ddl = ddl::CREATE_LINKS_TABLE
.replacen("links", "links_rebuild", 1)
.replace(
"weight REAL NOT NULL DEFAULT 1.0,",
"weight REAL NOT NULL DEFAULT 1.0 CHECK (weight >= 0.0),",
);
let mut ok = true;
let step = |label: &'static str| label;
for (label, sql) in [
("BEGIN IMMEDIATE", "BEGIN IMMEDIATE".to_string()),
("create links_rebuild", new_ddl.clone()),
(
"copy rows",
"INSERT INTO links_rebuild (source_id, target_id, edge_type, valid_from, \
recorded_at, valid_to, weight, properties) SELECT source_id, target_id, \
edge_type, valid_from, recorded_at, valid_to, weight, properties FROM links"
.to_string(),
),
(
"drop links (takes its triggers)",
"DROP TABLE links".to_string(),
),
(
"rename",
"ALTER TABLE links_rebuild RENAME TO links".to_string(),
),
] {
match conn.execute(&sql, ()).await {
Ok(_) => println!(" {:<38} OK", step(label)),
Err(e) => {
println!(" {:<38} ERR: {e}", step(label));
ok = false;
}
}
}
if ok {
for trigger in ddl::CREATE_TRIGGERS {
if trigger.contains("ON links\n") || trigger.contains("ON links\r\n") {
if let Err(e) = conn.execute(trigger, ()).await {
println!(" recreate trigger ERR: {e}");
ok = false;
}
}
}
println!(" recreate links triggers OK={ok}");
}
let commit = conn.execute("COMMIT", ()).await;
println!(" COMMIT {commit:?}");
println!("\n3. after the rebuild:");
let n: i64 = conn
.query("SELECT COUNT(*) FROM links", ())
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap()
.get(0)
.unwrap();
println!(" links holds {n} row(s)");
let mut triggers = Vec::new();
let mut rows = conn
.query(
"SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='links' \
ORDER BY name",
(),
)
.await
.unwrap();
while let Some(r) = rows.next().await.unwrap() {
triggers.push(r.get::<String>(0).unwrap());
}
println!(" triggers on links: {triggers:?}");
try_weight(
&conn,
"negative weight now",
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES ('c0','c1','NEG', \
'2026-01-01T00:00:00.000000Z','2027-01-01T00:00:00.000000Z',-1.0,'{}', \
'2026-02-01T00:00:00.000000Z')",
)
.await;
try_weight(
&conn,
"edge to a missing concept",
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES ('c0','nope','FK', \
'2026-01-01T00:00:00.000000Z','2027-01-01T00:00:00.000000Z',1.0,'{}', \
'2026-03-01T00:00:00.000000Z')",
)
.await;
println!("\n4. a text weight, which the CHECK admits:");
try_weight(
&conn,
"weight = 'abc'",
"INSERT INTO links (source_id, target_id, edge_type, valid_from, valid_to, \
weight, properties, recorded_at) VALUES ('c0','c1','TXT', \
'2026-01-01T00:00:00.000000Z','9999-12-31T23:59:59.999999Z','abc','{}', \
'2026-04-01T00:00:00.000000Z')",
)
.await;
let mut rows = conn
.query(
"SELECT typeof(weight), weight FROM links WHERE edge_type = 'TXT'",
(),
)
.await
.unwrap();
if let Some(r) = rows.next().await.unwrap() {
println!(" stored typeof: {:?}", r.get::<String>(0).unwrap());
println!(" as a Value: {:?}", r.get_value(1));
println!(" read as f64: panics (libsql rows.rs: invalid value type)");
}
println!("\n5. CHECK (weight >= 0.0 AND typeof(weight) = 'real'):");
let db3 = libsql::Builder::new_local(":memory:")
.build()
.await
.unwrap();
let conn3 = db3.connect().unwrap();
conn3
.execute(
"CREATE TABLE w2 (id INTEGER PRIMARY KEY, weight REAL NOT NULL \
CHECK (weight >= 0.0 AND typeof(weight) = 'real'))",
(),
)
.await
.unwrap();
for (label, expr) in [
("1.0", "1.0"),
("0.0", "0.0"),
("integer 3", "3"),
("text '5'", "'5'"),
("text 'abc'", "'abc'"),
("+inf (9e999)", "9e999"),
("-1.5", "-1.5"),
] {
try_weight(
&conn3,
label,
&format!("INSERT INTO w2 (weight) VALUES ({expr})"),
)
.await;
}
}