feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,448 @@
|
||||
mod support;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use dever_core::source::SourceMap;
|
||||
use dever_runtime::database::{Field, Model, Schema, SchemaSql};
|
||||
use dever_runtime::orm::{Row, Value};
|
||||
use dever_runtime::sqlite::Database;
|
||||
|
||||
const GUARD_MODEL: &str = r#"
|
||||
database z
|
||||
|
||||
public type Guard {
|
||||
code: Text(16)
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ConnectionState {
|
||||
tables: Vec<Row>,
|
||||
history: Vec<Row>,
|
||||
data: Vec<Vec<Row>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CommittedConnections {
|
||||
a: ConnectionState,
|
||||
default: ConnectionState,
|
||||
report: ConnectionState,
|
||||
root: ConnectionState,
|
||||
}
|
||||
|
||||
fn model_source(name: &str, database: Option<&str>, seed: &str) -> String {
|
||||
let database = database
|
||||
.map(|name| format!("database {name}\n\n"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{database}public type {name} {{\n code: Text(16) unique\n}}\n\nseed {{\n {{\n code = \"{seed}\"\n }}\n}}\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn sources() -> SourceMap {
|
||||
let mut sources = SourceMap::default();
|
||||
// The z-bound Model is deliberately first. Runtime migration order must not follow this order.
|
||||
sources.add("zfirst/model/guard.dever", GUARD_MODEL);
|
||||
sources.add(
|
||||
"later/model/omega.dever",
|
||||
model_source("Omega", Some("a"), "omega"),
|
||||
);
|
||||
sources.add(
|
||||
"earlier/model/beta.dever",
|
||||
model_source("Beta", Some("a"), "beta"),
|
||||
);
|
||||
sources.add(
|
||||
"fallback/model/defaulted.dever",
|
||||
model_source("Defaulted", None, "default"),
|
||||
);
|
||||
sources.add(
|
||||
"root/model/rooted.dever",
|
||||
model_source("Rooted", None, "root"),
|
||||
);
|
||||
sources.add(
|
||||
"analytics/model/reported.dever",
|
||||
model_source("Reported", Some("report"), "report"),
|
||||
);
|
||||
sources.add("main.dever", "public main() () {}\n");
|
||||
sources
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_connection_models_are_valid_without_opening_a_database() {
|
||||
let sources = sources();
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
assert!(
|
||||
program
|
||||
.model_snapshot()
|
||||
.contains("model zfirst.model.guard name=Guard")
|
||||
);
|
||||
}
|
||||
|
||||
fn compile() -> (dever_core::native::NativeProgram, String, String) {
|
||||
let sources = sources();
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
let snapshot = program.model_snapshot();
|
||||
let guard = snapshot
|
||||
.lines()
|
||||
.find(|line| line.starts_with("model zfirst.model.guard "))
|
||||
.expect("guard Model snapshot");
|
||||
let revision = snapshot_value(guard, "revision");
|
||||
let seed_revision = snapshot_value(guard, "seed");
|
||||
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
|
||||
let native = dever_core::native::compile_application(
|
||||
&program,
|
||||
&sources,
|
||||
"main.main",
|
||||
&rustc,
|
||||
dever_runtime::config::RuntimeProfile {
|
||||
sqlite: true,
|
||||
postgres: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
(native, revision, seed_revision)
|
||||
}
|
||||
|
||||
fn snapshot_value(line: &str, name: &str) -> String {
|
||||
line.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix(&format!("{name}=")))
|
||||
.expect("Model snapshot value")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn configure(root: &Path) {
|
||||
fs::create_dir_all(root.join("config")).unwrap();
|
||||
fs::create_dir_all(root.join("data/db")).unwrap();
|
||||
fs::write(
|
||||
root.join("config/setting.json"),
|
||||
r#"{
|
||||
"database": {
|
||||
"default": {"type":"sqlite","path":"data/db/default.db","max_connections":1,"max_page_size":20},
|
||||
"a": {"type":"sqlite","path":"data/db/a.db","max_connections":1,"max_page_size":20},
|
||||
"analytics": {"type":"sqlite","path":"data/db/analytics.db","max_connections":1,"max_page_size":20},
|
||||
"report": {"type":"sqlite","path":"data/db/report.db","max_connections":1,"max_page_size":20},
|
||||
"root": {"type":"sqlite","path":"data/db/root.db","max_connections":1,"max_page_size":20},
|
||||
"z": {"type":"sqlite","path":"data/db/z.db","max_connections":1,"max_page_size":20}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn database_path(root: &Path, name: &str) -> PathBuf {
|
||||
root.join("data/db").join(format!("{name}.db"))
|
||||
}
|
||||
|
||||
fn run(native: &dever_core::native::NativeProgram) -> Output {
|
||||
Command::new(native.executable()).output().unwrap()
|
||||
}
|
||||
|
||||
async fn execute(path: &Path, sql: String) {
|
||||
let database = Database::open(path, 1, 20).unwrap();
|
||||
database.execute_batch(sql).await.unwrap();
|
||||
}
|
||||
|
||||
fn guard_table(table: &str) -> String {
|
||||
format!(
|
||||
"CREATE TABLE \"{table}\" (\n \"id\" INTEGER PRIMARY KEY AUTOINCREMENT,\n \"code\" TEXT NOT NULL CHECK (length(\"code\") <= 16),\n \"created_at\" INTEGER NOT NULL DEFAULT (unixepoch() * 1000)\n)"
|
||||
)
|
||||
}
|
||||
|
||||
async fn prepare_guard_catalog_drift(path: &Path, revision: String, seed_revision: String) {
|
||||
let database = Database::open(path, 1, 20).unwrap();
|
||||
database
|
||||
.migrate(Model {
|
||||
schema: Schema {
|
||||
model: "zfirst.model.guard".into(),
|
||||
table: "guard".into(),
|
||||
revision,
|
||||
seed_revision,
|
||||
fields: vec![
|
||||
Field {
|
||||
name: "id".into(),
|
||||
ty: "id:zfirst.model.guard".into(),
|
||||
nullable: false,
|
||||
generated: true,
|
||||
default: None,
|
||||
rename_from: None,
|
||||
},
|
||||
Field {
|
||||
name: "code".into(),
|
||||
ty: "Text(16)".into(),
|
||||
nullable: false,
|
||||
generated: false,
|
||||
default: None,
|
||||
rename_from: None,
|
||||
},
|
||||
Field {
|
||||
name: "created_at".into(),
|
||||
ty: "DateTime".into(),
|
||||
nullable: false,
|
||||
generated: true,
|
||||
default: Some("generated:created_at".into()),
|
||||
rename_from: None,
|
||||
},
|
||||
],
|
||||
indexes: vec![],
|
||||
migrations: vec![],
|
||||
},
|
||||
create_table: SchemaSql {
|
||||
sqlite: guard_table("guard"),
|
||||
postgres: String::new(),
|
||||
},
|
||||
create_temporary_table: guard_table("_dever_guard_test"),
|
||||
create_indexes: vec![],
|
||||
postgres_columns: vec![],
|
||||
postgres_constraints: vec![],
|
||||
seeds: vec![],
|
||||
data_migrations: vec![],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
database
|
||||
.execute_batch("ALTER TABLE guard ADD COLUMN rogue TEXT".into())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn connection_state(path: &Path, queries: &[&'static str]) -> ConnectionState {
|
||||
if !path.exists() {
|
||||
return ConnectionState {
|
||||
tables: vec![],
|
||||
history: vec![],
|
||||
data: vec![],
|
||||
};
|
||||
}
|
||||
let database = Database::open(path, 1, 20).unwrap();
|
||||
let tables = database
|
||||
.query(
|
||||
"SELECT name, sql FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != '_dever_history' ORDER BY name",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let has_history = database
|
||||
.query(
|
||||
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = '_dever_history'",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let history = if has_history.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
database
|
||||
.query(
|
||||
"SELECT model, kind, name, revision, definition FROM _dever_history ORDER BY model, kind, name",
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
let mut data = Vec::new();
|
||||
for query in queries {
|
||||
data.push(database.query(query, vec![]).await.unwrap());
|
||||
}
|
||||
ConnectionState {
|
||||
tables,
|
||||
history,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
async fn committed_connections(
|
||||
a: &Path,
|
||||
default: &Path,
|
||||
report: &Path,
|
||||
root: &Path,
|
||||
) -> CommittedConnections {
|
||||
CommittedConnections {
|
||||
a: connection_state(
|
||||
a,
|
||||
&[
|
||||
"SELECT code FROM omega ORDER BY id",
|
||||
"SELECT code FROM beta ORDER BY id",
|
||||
],
|
||||
)
|
||||
.await,
|
||||
default: connection_state(default, &["SELECT code FROM defaulted ORDER BY id"]).await,
|
||||
report: connection_state(report, &["SELECT code FROM reported ORDER BY id"]).await,
|
||||
root: connection_state(root, &["SELECT code FROM rooted ORDER BY id"]).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn text(row: &Row, index: usize) -> &str {
|
||||
match row.get(index).unwrap() {
|
||||
Value::Text(value) => value,
|
||||
value => panic!("expected text at column {index}, got {value:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn table_names(state: &ConnectionState) -> Vec<&str> {
|
||||
state.tables.iter().map(|row| text(row, 0)).collect()
|
||||
}
|
||||
|
||||
fn history_models(state: &ConnectionState) -> Vec<&str> {
|
||||
state
|
||||
.history
|
||||
.iter()
|
||||
.filter(|row| text(row, 1) == "schema")
|
||||
.map(|row| text(row, 0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assert_complete_history(state: &ConnectionState, models: &[&str]) {
|
||||
let expected = models
|
||||
.iter()
|
||||
.flat_map(|model| ["catalog", "schema", "seed"].map(|kind| (*model, kind, "current")))
|
||||
.collect::<Vec<_>>();
|
||||
let actual = state
|
||||
.history
|
||||
.iter()
|
||||
.map(|row| (text(row, 0), text(row, 1), text(row, 2)))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_sqlite_connections_bind_and_recover_in_stable_order() {
|
||||
let (native, guard_revision, guard_seed_revision) = compile();
|
||||
let root = native.executable().parent().unwrap();
|
||||
configure(root);
|
||||
let a = database_path(root, "a");
|
||||
let default = database_path(root, "default");
|
||||
let analytics = database_path(root, "analytics");
|
||||
let report = database_path(root, "report");
|
||||
let root_database = database_path(root, "root");
|
||||
let z = database_path(root, "z");
|
||||
|
||||
execute(
|
||||
&a,
|
||||
"CREATE TABLE beta (id INTEGER PRIMARY KEY, code INTEGER NOT NULL)".into(),
|
||||
)
|
||||
.await;
|
||||
prepare_guard_catalog_drift(&z, guard_revision, guard_seed_revision).await;
|
||||
|
||||
let first = run(&native);
|
||||
assert!(!first.status.success());
|
||||
let first_error = String::from_utf8(first.stderr).unwrap();
|
||||
assert!(
|
||||
first_error.contains("table 'beta' exists without Dever schema history"),
|
||||
"{first_error}"
|
||||
);
|
||||
let first_a = connection_state(&a, &["SELECT code FROM omega ORDER BY id"]).await;
|
||||
assert_eq!(table_names(&first_a), ["beta", "omega"]);
|
||||
assert_eq!(history_models(&first_a), ["later.model.omega"]);
|
||||
assert_complete_history(&first_a, &["later.model.omega"]);
|
||||
assert_eq!(text(&first_a.data[0][0], 0), "omega");
|
||||
for pending in [&default, &report, &root_database] {
|
||||
assert_eq!(
|
||||
connection_state(pending, &[]).await,
|
||||
ConnectionState {
|
||||
tables: vec![],
|
||||
history: vec![],
|
||||
data: vec![],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
execute(&a, "DROP TABLE beta".into()).await;
|
||||
let second = run(&native);
|
||||
assert!(!second.status.success());
|
||||
let second_error = String::from_utf8(second.stderr).unwrap();
|
||||
assert!(
|
||||
second_error.contains("table 'guard' does not match its recorded Dever schema"),
|
||||
"{second_error}"
|
||||
);
|
||||
|
||||
let second_connections = committed_connections(&a, &default, &report, &root_database).await;
|
||||
assert_eq!(table_names(&second_connections.a), ["beta", "omega"]);
|
||||
assert_eq!(
|
||||
history_models(&second_connections.a),
|
||||
["earlier.model.beta", "later.model.omega"]
|
||||
);
|
||||
assert_complete_history(
|
||||
&second_connections.a,
|
||||
&["earlier.model.beta", "later.model.omega"],
|
||||
);
|
||||
assert_eq!(table_names(&second_connections.default), ["defaulted"]);
|
||||
assert_eq!(
|
||||
history_models(&second_connections.default),
|
||||
["fallback.model.defaulted"]
|
||||
);
|
||||
assert_complete_history(&second_connections.default, &["fallback.model.defaulted"]);
|
||||
assert_eq!(table_names(&second_connections.report), ["reported"]);
|
||||
assert_eq!(
|
||||
history_models(&second_connections.report),
|
||||
["analytics.model.reported"]
|
||||
);
|
||||
assert_complete_history(&second_connections.report, &["analytics.model.reported"]);
|
||||
assert_eq!(table_names(&second_connections.root), ["rooted"]);
|
||||
assert_eq!(
|
||||
history_models(&second_connections.root),
|
||||
["root.model.rooted"]
|
||||
);
|
||||
assert_complete_history(&second_connections.root, &["root.model.rooted"]);
|
||||
assert_eq!(text(&second_connections.a.data[0][0], 0), "omega");
|
||||
assert_eq!(text(&second_connections.a.data[1][0], 0), "beta");
|
||||
assert_eq!(text(&second_connections.default.data[0][0], 0), "default");
|
||||
assert_eq!(text(&second_connections.report.data[0][0], 0), "report");
|
||||
assert_eq!(text(&second_connections.root.data[0][0], 0), "root");
|
||||
assert_eq!(
|
||||
connection_state(&analytics, &[]).await,
|
||||
ConnectionState {
|
||||
tables: vec![],
|
||||
history: vec![],
|
||||
data: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
execute(&z, format!("DROP TABLE guard; {}", guard_table("guard"))).await;
|
||||
let third = run(&native);
|
||||
assert!(
|
||||
third.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&third.stderr)
|
||||
);
|
||||
let third_connections = committed_connections(&a, &default, &report, &root_database).await;
|
||||
let third_z = connection_state(&z, &["SELECT code FROM guard ORDER BY id"]).await;
|
||||
assert_eq!(third_connections, second_connections);
|
||||
assert_eq!(table_names(&third_z), ["guard"]);
|
||||
assert_eq!(history_models(&third_z), ["zfirst.model.guard"]);
|
||||
assert_complete_history(&third_z, &["zfirst.model.guard"]);
|
||||
assert!(third_z.data[0].is_empty());
|
||||
|
||||
let fourth = run(&native);
|
||||
assert!(
|
||||
fourth.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&fourth.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
committed_connections(&a, &default, &report, &root_database).await,
|
||||
third_connections
|
||||
);
|
||||
assert_eq!(
|
||||
connection_state(&z, &["SELECT code FROM guard ORDER BY id"]).await,
|
||||
third_z
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user