Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
use super::*;
use crate::database::{Error, Options, Parameter};
use futures_util::FutureExt;
use std::panic::AssertUnwindSafe;
use tokio_postgres::Client;
mod cases;
mod codec;
mod constraints;
#[cfg(feature = "net")]
mod registry;
const ID: Column = Column {
constraints: &[("account_pkey", "p", "PRIMARY KEY (\"id\")")],
..column("id", "bigint")
};
const NAME: Column = Column {
constraints: &[("_dever_unique_account_1", "u", "UNIQUE (\"name\")")],
..column("name", "text")
};
const TITLE: Column = Column {
constraints: &[("_dever_unique_account_1", "u", "UNIQUE (\"title\")")],
..column("title", "text")
};
const UID: Column = Column {
generated: true,
constraints: &[("_dever_unique_account_3", "u", "UNIQUE (\"uid\")")],
..column("uid", "uuid")
};
const INITIAL: &str = "CREATE TABLE \"account\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL CONSTRAINT _dever_unique_account_1 UNIQUE, legacy bigint NOT NULL DEFAULT 4, amount numeric(6,2) NOT NULL DEFAULT 1.25);";
const FINAL: &str = "CREATE TABLE \"account\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text NOT NULL CONSTRAINT _dever_unique_account_1 UNIQUE, amount numeric(8,3) NOT NULL DEFAULT 1.25, uid uuid NOT NULL CONSTRAINT _dever_unique_account_3 UNIQUE);";
const INDEX: &str = "CREATE INDEX account_title ON account(title);";
const HISTORICAL: Migration = Migration {
name: "historical",
revision: "historical-one",
drops: &["ancient"],
before: &[statement("UPDATE account SET ancient = 1")],
after: &[statement("UPDATE account SET ancient = 2")],
};
const UPGRADE: Migration = Migration {
name: "rename",
revision: "rename-one",
drops: &["legacy"],
before: &[statement("UPDATE account SET name = name || '_before'")],
after: &[statement("UPDATE account SET title = title || '_after'")],
};
const INITIAL_PLAN: Plan = Plan {
create: Sql::same(INITIAL),
postgres_columns: &[
ID,
NAME,
Column {
default: Some("4"),
..column("legacy", "bigint")
},
Column {
default: Some("1.25"),
..column("amount", "numeric(6,2)")
},
],
migrations: &[HISTORICAL],
seeds: &[Statement {
sql: Sql::same("INSERT INTO account(name) VALUES ($1) ON CONFLICT(name) DO NOTHING"),
parameters: || Ok(vec![Parameter::Text("seed".into())]),
}],
seed_revision: "initial-seed",
..EMPTY
};
const FINAL_PLAN: Plan = Plan {
create: Sql::same(FINAL),
indexes: Sql::same(INDEX),
postgres_columns: &[
ID,
TITLE,
Column {
default: Some("1.25"),
..column("amount", "numeric(8,3)")
},
UID,
],
migrations: &[HISTORICAL, UPGRADE],
renames: &[("title", "name")],
seeds: &[Statement {
sql: Sql::same(
"INSERT INTO account(title,uid) VALUES ($1,$2::uuid) ON CONFLICT(title) DO NOTHING",
),
parameters: || {
Ok(vec![
Parameter::Text("new".into()),
Parameter::Uuid(*crate::database::scalar::generated_uuid()?.as_bytes()),
])
},
}],
seed_revision: "final-seed",
};
const fn statement(sql: &'static str) -> Statement {
Statement {
sql: Sql::same(sql),
parameters: || Ok(vec![]),
}
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn schema_migration_seed_uuid_and_rollback() {
fixture(cases::migrations);
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn catalog_drift_fails_before_data_or_history_mutation() {
fixture(cases::drift);
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn new_database_records_final_history_without_old_transforms() {
fixture(cases::fresh);
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn native_binary_codecs_validate_precision_and_parameter_types() {
fixture(codec::roundtrip);
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn foreign_keys_and_tightening_validate_existing_data() {
fixture(constraints::foreign_keys);
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn numeric_migrations_reject_precision_loss() {
fixture(constraints::numeric);
}
fn configuration() -> serde_json::Value {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../config/setting.json");
let document =
crate::adapter::Document::load(&path).expect("cannot read PostgreSQL test settings");
let root: serde_json::Value = document.decode().expect("invalid PostgreSQL test settings");
root.get("database")
.and_then(|database| database.get("postgres_test"))
.expect("database.postgres_test must explicitly select an isolated test database")
.clone()
}
fn options() -> Options {
let options: Options = serde_json::from_value(configuration())
.unwrap_or_else(|_| panic!("invalid PostgreSQL test options"));
assert_eq!(options.driver, "postgres");
options
}
fn fixture(check: impl AsyncFnOnce(&mut Client)) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(isolated(options(), check));
}
#[cfg(feature = "net")]
fn registry_fixture(check: impl AsyncFnOnce(&mut Client, &crate::task::Scope<'_>)) {
let runtime = crate::task::Runtime::new(crate::task::Limits {
threads: 1,
workers: 1,
tasks: 16,
})
.unwrap();
runtime
.run(async |scope| isolated(options(), async |client| check(client, scope).await).await)
.unwrap();
}
async fn isolated(options: Options, check: impl AsyncFnOnce(&mut Client)) {
let database = super::super::Database::open(options).await.unwrap();
let mut client = database
.pool
.get()
.await
.unwrap_or_else(|_| panic!("cannot acquire PostgreSQL test connection"));
let name = format!(
"dever_test_{}",
crate::database::scalar::generated_uuid()
.unwrap()
.to_string()
.replace('-', "")
);
let quoted = schema::quote(&name);
client
.batch_execute(&format!("CREATE SCHEMA {quoted}"))
.await
.map_err(Error::from)
.unwrap();
let result = AssertUnwindSafe(async {
client
.batch_execute(&format!("SET search_path TO {quoted}"))
.await
.map_err(Error::from)
.unwrap();
tokio::time::timeout(std::time::Duration::from_secs(30), check(&mut client))
.await
.expect("PostgreSQL test deadline exceeded");
})
.catch_unwind()
.await;
let cleanup = tokio::time::timeout(
std::time::Duration::from_secs(10),
client.batch_execute(&format!(
"ROLLBACK; SET search_path TO pg_catalog; DROP SCHEMA {quoted} CASCADE"
)),
)
.await;
database.close();
cleanup
.expect("PostgreSQL test cleanup timed out")
.map_err(Error::from)
.expect("PostgreSQL test schema cleanup failed");
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
@@ -0,0 +1,229 @@
use super::*;
pub(super) async fn fresh(client: &mut Client) {
let definition = format!("{FINAL}{INDEX}");
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
let row = client
.query_one("SELECT title,uid::text FROM account", &[])
.await
.unwrap();
assert_eq!(row.get::<_, String>(0), "new");
assert_eq!(row.get::<_, String>(1).as_bytes()[14], b'7');
assert_eq!(
count(client, "SELECT count(*) FROM _dever_migration").await,
2
);
assert_eq!(count(client, "SELECT count(*) FROM _dever_seed").await, 1);
}
pub(super) async fn migrations(client: &mut Client) {
schema::migrate(client, "account", INITIAL, Some(&INITIAL_PLAN))
.await
.unwrap();
assert_eq!(count(client, "SELECT count(*) FROM account").await, 1);
client
.execute("UPDATE account SET name='business'", &[])
.await
.unwrap();
schema::migrate(client, "account", INITIAL, Some(&INITIAL_PLAN))
.await
.unwrap();
assert_eq!(count(client, "SELECT count(*) FROM account").await, 1);
client.batch_execute("INSERT INTO account(name) VALUES ('removed-high-id'); DELETE FROM account WHERE name='removed-high-id'").await.unwrap();
client
.execute(
"INSERT INTO account(name) SELECT 'row_' || value FROM generate_series(1,257) AS value",
&[],
)
.await
.unwrap();
let definition = format!("{FINAL}{INDEX}");
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
let rows = client
.query(
"SELECT id,title,amount::text,uid::text FROM account ORDER BY id",
&[],
)
.await
.unwrap();
assert_eq!(rows.len(), 259);
assert_eq!(rows[0].get::<_, String>(1), "business_before_after");
assert_eq!(rows[0].get::<_, String>(2), "1.250");
assert!(rows.last().unwrap().get::<_, i64>(0) > 259);
let mut generated = std::collections::BTreeSet::new();
for row in &rows {
let uuid: String = row.get(3);
assert_eq!(uuid.as_bytes()[14], b'7');
assert!(generated.insert(uuid));
}
let first: String = rows[0].get(3);
let second: String = rows[1].get(3);
assert_eq!(first.as_bytes()[14], b'7');
assert_eq!(second.as_bytes()[14], b'7');
assert_ne!(first, second);
let removed = Plan {
migrations: &[HISTORICAL],
..FINAL_PLAN
};
assert_eq!(
schema::migrate(client, "account", &definition, Some(&removed))
.await
.unwrap_err()
.kind,
Kind::Migration
);
let changed = const {
Plan {
migrations: &[HISTORICAL, step("rename", "changed", &["legacy"])],
..FINAL_PLAN
}
};
assert_eq!(
schema::migrate(client, "account", &definition, Some(&changed))
.await
.unwrap_err()
.kind,
Kind::Migration
);
const FAILED: &str = "CREATE TABLE \"account\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text NOT NULL CONSTRAINT _dever_unique_account_1 UNIQUE, amount numeric(8,3) NOT NULL DEFAULT 1.25, uid uuid NOT NULL CONSTRAINT _dever_unique_account_3 UNIQUE, note text);";
let failed = const {
Plan {
create: Sql::same(FAILED),
postgres_columns: &[
ID,
TITLE,
Column {
default: Some("1.25"),
..column("amount", "numeric(8,3)")
},
UID,
Column {
nullable: true,
..column("note", "text")
},
],
migrations: &[
HISTORICAL,
UPGRADE,
Migration {
name: "fail",
revision: "fail-one",
drops: &[],
before: &[statement("UPDATE account SET title=title || '_rollback'")],
after: &[statement("UPDATE account SET title=NULL")],
},
],
..FINAL_PLAN
}
};
assert_eq!(
schema::migrate(
client,
"account",
&format!("{FAILED}{INDEX}"),
Some(&failed)
)
.await
.unwrap_err()
.kind,
Kind::Constraint
);
assert_eq!(count(client, "SELECT count(*) FROM pg_attribute WHERE attrelid='account'::regclass AND attname='note' AND NOT attisdropped").await, 0);
assert_eq!(
count(client, "SELECT count(*) FROM _dever_migration").await,
2
);
assert_eq!(
client
.query_one("SELECT title FROM account WHERE id=1", &[])
.await
.unwrap()
.get::<_, String>(0),
"business_before_after"
);
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
}
pub(super) async fn drift(client: &mut Client) {
let definition = format!("{FINAL}{INDEX}");
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
let original: String = client
.query_one(
"SELECT catalog FROM _dever_history WHERE name='account'",
&[],
)
.await
.unwrap()
.get(0);
for (change, restore) in [
("DROP INDEX account_title", INDEX),
(
"ALTER TABLE account ALTER COLUMN amount SET DEFAULT 2.25",
"ALTER TABLE account ALTER COLUMN amount SET DEFAULT 1.25",
),
(
"ALTER TABLE account ALTER COLUMN title DROP NOT NULL",
"ALTER TABLE account ALTER COLUMN title SET NOT NULL",
),
(
"ALTER TABLE account ADD CONSTRAINT _dever_manual CHECK (amount>0)",
"ALTER TABLE account DROP CONSTRAINT _dever_manual",
),
(
"ALTER TABLE account ALTER COLUMN id SET INCREMENT BY 2",
"ALTER TABLE account ALTER COLUMN id SET INCREMENT BY 1",
),
] {
client.batch_execute(change).await.unwrap();
assert_eq!(
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap_err()
.kind,
Kind::Migration
);
assert_eq!(
client
.query_one(
"SELECT catalog FROM _dever_history WHERE name='account'",
&[]
)
.await
.unwrap()
.get::<_, String>(0),
original
);
assert_eq!(count(client, "SELECT count(*) FROM account").await, 1);
client.batch_execute(restore).await.unwrap();
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap();
}
client.batch_execute("DROP TABLE account").await.unwrap();
assert_eq!(
schema::migrate(client, "account", &definition, Some(&FINAL_PLAN))
.await
.unwrap_err()
.kind,
Kind::Migration
);
}
async fn count(client: &Client, sql: &str) -> i64 {
client.query_one(sql, &[]).await.unwrap().get(0)
}
@@ -0,0 +1,120 @@
use super::*;
use crate::{database::Row, value::decimal::Decimal};
pub(super) async fn roundtrip(client: &mut Client) {
for text in [
"0",
"-0.0001",
"12345.6789",
"9.999999999999999999999999999999999E+6144",
"1E-6176",
] {
let expected = Decimal::parse(text).unwrap();
let parameter = Parameter::NativeDecimal(expected);
let row = client
.query_one("SELECT $1::numeric", &[&parameter])
.await
.unwrap();
assert_eq!(Row::Postgres(&row).native_decimal(0).unwrap(), expected);
}
let uuid = crate::database::scalar::generated_uuid().unwrap();
let integer = Decimal::parse("9999999999999999999999999999999999").unwrap();
let fraction = Decimal::parse("-0.9999999999999999999999999999999999").unwrap();
let parameters = [
Parameter::Bool(true),
Parameter::Integer(i64::MIN),
Parameter::Real(1.25),
Parameter::Uuid(*uuid.as_bytes()),
Parameter::Decimal {
value: integer,
precision: 34,
scale: 0,
},
Parameter::Decimal {
value: fraction,
precision: 34,
scale: 34,
},
Parameter::Null,
Parameter::Text("雪'\\native".into()),
Parameter::Blob(vec![0, 128, 255]),
];
let references = parameters
.iter()
.map(|value| value as &(dyn tokio_postgres::types::ToSql + Sync))
.collect::<Vec<_>>();
let result = client.query_one("SELECT $1::boolean,$2::bigint,$3::float8,$4::uuid,$5::numeric(34,0),$6::numeric(34,34),$7::uuid,$8::text,$9::bytea", &references).await.unwrap();
let row = Row::Postgres(&result);
assert!(row.get::<_, bool>(0).unwrap());
assert_eq!(row.get::<_, i64>(1).unwrap(), i64::MIN);
assert_eq!(row.get::<_, f64>(2).unwrap(), 1.25);
assert_eq!(row.uuid(3).unwrap(), *uuid.as_bytes());
assert_eq!(row.decimal(4, 34, 0).unwrap(), integer);
assert_eq!(row.decimal(5, 34, 34).unwrap(), fraction);
assert!(row.is_null(6).unwrap());
assert_eq!(row.text(7).unwrap(), "雪'\\native");
assert_eq!(row.bytes(8).unwrap(), [0, 128, 255]);
for value in [
"0",
"0.0000000000000000000000000000000001",
"0.1234000000000000000000000000000000",
] {
let expected = Decimal::parse(value).unwrap();
let parameter = Parameter::Decimal {
value: expected,
precision: 34,
scale: 34,
};
let result = client
.query_one("SELECT $1::numeric(34,34)", &[&parameter])
.await
.unwrap();
assert_eq!(Row::Postgres(&result).decimal(0, 34, 34).unwrap(), expected);
}
for (sql, parameter) in [
("SELECT $1::integer", Parameter::Integer(1)),
("SELECT $1::text::uuid", Parameter::Uuid(*uuid.as_bytes())),
("SELECT $1::float8", Parameter::Real(f64::INFINITY)),
("SELECT $1::float8", Parameter::Real(f64::NAN)),
(
"SELECT $1::numeric(5,2)",
Parameter::Decimal {
value: Decimal::parse("0.001").unwrap(),
precision: 5,
scale: 2,
},
),
(
"SELECT $1::numeric(5,2)",
Parameter::Decimal {
value: Decimal::parse("1000").unwrap(),
precision: 5,
scale: 2,
},
),
] {
assert!(client.query_one(sql, &[&parameter]).await.is_err(), "{sql}");
}
let invalid = client
.query_one("SELECT 0.001::numeric,1000::numeric,'NaN'::numeric", &[])
.await
.unwrap();
for position in 0..3 {
assert_eq!(
Row::Postgres(&invalid)
.decimal(position, 5, 2)
.unwrap_err()
.kind,
Kind::InvalidData
);
}
let result = client
.query_one(
"SELECT $1::bigint,$2::float8",
&[&Parameter::Integer(i64::MAX), &Parameter::Real(-f64::MAX)],
)
.await
.unwrap();
assert_eq!(result.get::<_, i64>(0), i64::MAX);
assert_eq!(result.get::<_, f64>(1), -f64::MAX);
}
@@ -0,0 +1,230 @@
use super::*;
pub(super) async fn foreign_keys(client: &mut Client) {
const PARENT: &str = "CREATE TABLE parent(id bigint PRIMARY KEY);";
schema::migrate(client, "parent", PARENT, None)
.await
.unwrap();
schema::migrate(client, "parent", PARENT, None)
.await
.unwrap();
assert_eq!(
schema::migrate(
client,
"parent",
"CREATE TABLE parent(id bigint PRIMARY KEY, extra text);",
None
)
.await
.unwrap_err()
.kind,
Kind::Migration
);
client
.execute("INSERT INTO parent VALUES (1)", &[])
.await
.unwrap();
const START: &str = "CREATE TABLE \"child\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, parent_id bigint NOT NULL CONSTRAINT _dever_fk_parent REFERENCES parent(id));";
const REFERENCE: Column = Column {
constraints: &[(
"_dever_fk_parent",
"f",
"FOREIGN KEY (parent_id) REFERENCES parent(id)",
)],
..column("parent_id", "bigint")
};
const START_PLAN: Plan = Plan {
create: Sql::same(START),
postgres_columns: &[ID, REFERENCE],
..EMPTY
};
schema::migrate(client, "child", START, Some(&START_PLAN))
.await
.unwrap();
client
.execute("INSERT INTO child(parent_id) VALUES (1)", &[])
.await
.unwrap();
const NEXT: &str = "CREATE TABLE \"child\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, parent_id bigint NOT NULL CONSTRAINT _dever_fk_parent REFERENCES parent(id), label text NOT NULL DEFAULT 'ok' CONSTRAINT _dever_check_label CHECK (char_length(label)<=4));";
const NEXT_PLAN: Plan = Plan {
create: Sql::same(NEXT),
postgres_columns: &[
ID,
REFERENCE,
Column {
default: Some("'ok'"),
constraints: &[("_dever_check_label", "c", "CHECK (char_length(label)<=4)")],
..column("label", "text")
},
],
..EMPTY
};
schema::migrate(client, "child", NEXT, Some(&NEXT_PLAN))
.await
.unwrap();
schema::migrate(client, "child", NEXT, Some(&NEXT_PLAN))
.await
.unwrap();
assert!(
client
.execute("INSERT INTO child(parent_id) VALUES (2)", &[])
.await
.is_err()
);
assert!(
client
.execute("UPDATE child SET label='too long'", &[])
.await
.is_err()
);
assert!(
client
.execute("DELETE FROM parent WHERE id=1", &[])
.await
.is_err()
);
const TIGHT: &str = "CREATE TABLE \"child\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, parent_id bigint NOT NULL CONSTRAINT _dever_fk_parent REFERENCES parent(id), label text NOT NULL DEFAULT 'ok' CONSTRAINT _dever_check_label CHECK (char_length(label)<=1));";
const TIGHT_PLAN: Plan = Plan {
create: Sql::same(TIGHT),
postgres_columns: &[
ID,
REFERENCE,
Column {
default: Some("'ok'"),
constraints: &[("_dever_check_label", "c", "CHECK (char_length(label)<=1)")],
..column("label", "text")
},
],
..EMPTY
};
assert_eq!(
schema::migrate(client, "child", TIGHT, Some(&TIGHT_PLAN))
.await
.unwrap_err()
.kind,
Kind::Constraint
);
schema::migrate(client, "child", NEXT, Some(&NEXT_PLAN))
.await
.unwrap();
assert_eq!(
client
.query_one("SELECT label FROM child", &[])
.await
.unwrap()
.get::<_, String>(0),
"ok"
);
}
pub(super) async fn numeric(client: &mut Client) {
const INTEGER: &str = "CREATE TABLE \"numbers\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, value bigint NOT NULL);";
const FLOAT: &str = "CREATE TABLE \"numbers\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, value double precision NOT NULL);";
const INTEGER_PLAN: Plan = Plan {
create: Sql::same(INTEGER),
postgres_columns: &[ID, column("value", "bigint")],
..EMPTY
};
const FLOAT_PLAN: Plan = Plan {
create: Sql::same(FLOAT),
postgres_columns: &[ID, column("value", "double precision")],
..EMPTY
};
schema::migrate(client, "numbers", INTEGER, Some(&INTEGER_PLAN))
.await
.unwrap();
client
.execute(
"INSERT INTO numbers(value) VALUES (9007199254740993), (9223372036854775807)",
&[],
)
.await
.unwrap();
assert_eq!(
schema::migrate(client, "numbers", FLOAT, Some(&FLOAT_PLAN))
.await
.unwrap_err()
.kind,
Kind::Migration
);
assert_eq!(
client
.query_one("SELECT value FROM numbers ORDER BY id LIMIT 1", &[])
.await
.unwrap()
.get::<_, i64>(0),
9007199254740993
);
client.batch_execute("DELETE FROM numbers; INSERT INTO numbers(value) VALUES (9007199254740992), (-9223372036854775808), (9223372036854774784)").await.unwrap();
assert_eq!(
schema::migrate(client, "numbers", FLOAT, Some(&FLOAT_PLAN))
.await
.unwrap_err()
.kind,
Kind::Migration
);
assert_eq!(
client
.query_one("SELECT value FROM numbers ORDER BY id LIMIT 1", &[])
.await
.unwrap()
.get::<_, i64>(0),
9007199254740992
);
const WIDE: &str = "CREATE TABLE \"decimal_numbers\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, value numeric(8,3) NOT NULL);";
const NARROW: &str = "CREATE TABLE \"decimal_numbers\" (id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, value numeric(5,2) NOT NULL);";
const WIDE_PLAN: Plan = Plan {
create: Sql::same(WIDE),
postgres_columns: &[ID, column("value", "numeric(8,3)")],
..EMPTY
};
const NARROW_PLAN: Plan = Plan {
create: Sql::same(NARROW),
postgres_columns: &[ID, column("value", "numeric(5,2)")],
..EMPTY
};
schema::migrate(client, "decimal_numbers", WIDE, Some(&WIDE_PLAN))
.await
.unwrap();
client
.execute("INSERT INTO decimal_numbers(value) VALUES (1.251)", &[])
.await
.unwrap();
assert_eq!(
schema::migrate(client, "decimal_numbers", NARROW, Some(&NARROW_PLAN))
.await
.unwrap_err()
.kind,
Kind::Migration
);
client
.execute("UPDATE decimal_numbers SET value=1000", &[])
.await
.unwrap();
assert_eq!(
schema::migrate(client, "decimal_numbers", NARROW, Some(&NARROW_PLAN))
.await
.unwrap_err()
.kind,
Kind::InvalidData
);
schema::migrate(client, "decimal_numbers", WIDE, Some(&WIDE_PLAN))
.await
.unwrap();
client
.execute("UPDATE decimal_numbers SET value=1.250", &[])
.await
.unwrap();
schema::migrate(client, "decimal_numbers", NARROW, Some(&NARROW_PLAN))
.await
.unwrap();
assert_eq!(
client
.query_one("SELECT value::text FROM decimal_numbers", &[])
.await
.unwrap()
.get::<_, String>(0),
"1.25"
);
}
@@ -0,0 +1,288 @@
use super::*;
use crate::database::{Binding, Context, Registry};
use std::{io::Write, os::unix::fs::OpenOptionsExt, path::PathBuf};
#[test]
#[ignore = "explicit release pagination performance probe with isolated PostgreSQL"]
fn postgres_page_cost() {
use crate::database::{Database, PageQuery};
use std::{hint::black_box, time::Instant};
registry_fixture(async |client, scope| {
let settings = options();
let timeout = settings.io_timeout();
let database = Database::open(std::path::Path::new("."), settings, scope)
.await
.unwrap();
let mut executor = database.executor().await.unwrap();
let count = "SELECT 1::bigint WHERE $1::text IS NOT NULL";
let select = "SELECT octet_length($1::text)::bigint LIMIT $2::bigint OFFSET $3::bigint";
let count_statement = client.prepare(count).await.unwrap();
let select_statement = client.prepare(select).await.unwrap();
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
for bytes in [64, 65536, 1048576] {
let source = "x".repeat(bytes);
for sample in 0..16 {
for offset in 0..2 {
let generated = (sample + offset) % 2 == 0;
let started = Instant::now();
let mut checksum = 0;
for _ in 0..20 {
let payload = black_box(source.clone());
let value = if generated {
let page = executor
.page(
scope,
PageQuery {
count: Sql::same(count),
select: Sql::same(select),
parameters: vec![Parameter::Text(payload)],
page: 1,
size: 1,
},
|row| row.get::<_, i64>(0),
)
.await
.unwrap();
assert_eq!((page.total, page.pages, page.items.len()), (1, 1, 1));
page.items[0]
} else {
// 原生驱动同样执行两个有独立期限的查询;载荷只持有一份。
let total = tokio::time::timeout(
timeout,
client.query_one(&count_statement, &[&payload]),
)
.await
.unwrap()
.unwrap();
assert_eq!(total.get::<_, i64>(0), 1);
tokio::time::timeout(
timeout,
client.query_one(&select_statement, &[&payload, &1i64, &0i64]),
)
.await
.unwrap()
.unwrap()
.get::<_, i64>(0)
};
assert_eq!(value, bytes as i64);
checksum += black_box(value);
}
let elapsed = started.elapsed().as_nanos();
if sample != 0 {
let implementation = if generated { "dever" } else { "rust" };
println!(
"postgres_page_{bytes},{},{implementation},20,{elapsed},{checksum}",
sample - 1
);
}
}
}
}
database.close();
});
}
const fn binding(
table: &'static str,
sql: &'static str,
database: Option<&'static str>,
references: &'static [usize],
) -> Binding {
Binding {
storage: crate::database::Storage::Global,
component: "demo",
database,
table,
schema: Sql::same(sql),
plan: None,
references,
}
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn first_create_orders_parent_before_alphabetically_earlier_child() {
registry_fixture(async |client, scope| {
let settings = SettingsFile::new(client).await;
let bindings = [
binding(
"a_child",
"CREATE TABLE a_child(id bigint PRIMARY KEY, parent_id bigint NOT NULL REFERENCES z_parent(id));",
None,
&[2],
),
binding(
"m_independent",
"CREATE TABLE m_independent(id bigint PRIMARY KEY);",
Some("secondary"),
&[],
),
binding(
"z_parent",
"CREATE TABLE z_parent(id bigint PRIMARY KEY);",
None,
&[],
),
binding(
"self_node",
"CREATE TABLE self_node(id bigint PRIMARY KEY, parent_id bigint REFERENCES self_node(id));",
None,
&[3],
),
];
let registry = Registry::load(&settings.path(), &bindings, scope)
.await
.unwrap();
registry.check_transactions(&[&[0, 2, 3]]).unwrap();
assert_eq!(
registry.check_transactions(&[&[0, 1]]).unwrap_err().kind,
Kind::Connection
);
let mut context = Context::new(registry);
context
.on(2, scope)
.await
.unwrap()
.execute(scope, Sql::same("INSERT INTO z_parent VALUES (42)"), vec![])
.await
.unwrap();
context
.on(0, scope)
.await
.unwrap()
.execute(
scope,
Sql::same("INSERT INTO a_child VALUES (1,42)"),
vec![],
)
.await
.unwrap();
context
.on(1, scope)
.await
.unwrap()
.execute(
scope,
Sql::same("INSERT INTO m_independent VALUES (1)"),
vec![],
)
.await
.unwrap();
context
.on(3, scope)
.await
.unwrap()
.execute(
scope,
Sql::same("INSERT INTO self_node VALUES (1,1)"),
vec![],
)
.await
.unwrap();
drop(context);
let repeated = Registry::load(&settings.path(), &bindings, scope)
.await
.unwrap();
repeated.check_transactions(&[&[0, 2]]).unwrap();
assert_eq!(
client
.query_one("SELECT parent_id FROM a_child", &[])
.await
.unwrap()
.get::<_, i64>(0),
42
);
assert_eq!(
client
.query_one("SELECT count(*) FROM _dever_history", &[])
.await
.unwrap()
.get::<_, i64>(0),
4
);
assert!(client.execute("DELETE FROM z_parent", &[]).await.is_err());
});
}
#[test]
#[ignore = "requires explicit isolated config/setting.json database.postgres_test"]
fn cyclic_postgres_bindings_fail_before_any_schema_or_history_ddl() {
registry_fixture(async |client, scope| {
let settings = SettingsFile::new(client).await;
let bindings = [
binding(
"independent",
"CREATE TABLE independent(id bigint PRIMARY KEY);",
None,
&[],
),
binding(
"cycle_left",
"CREATE TABLE cycle_left(id bigint PRIMARY KEY, parent_id bigint REFERENCES cycle_right(id));",
None,
&[2],
),
binding(
"cycle_right",
"CREATE TABLE cycle_right(id bigint PRIMARY KEY, parent_id bigint REFERENCES cycle_left(id));",
None,
&[1],
),
];
let error = Registry::load(&settings.path(), &bindings, scope)
.await
.err()
.unwrap();
assert_eq!(error.kind, Kind::Migration);
assert_eq!(client.query_one("SELECT count(*) FROM pg_class WHERE relnamespace=current_schema()::regnamespace", &[]).await.unwrap().get::<_, i64>(0), 0);
});
}
struct SettingsFile(PathBuf);
impl SettingsFile {
async fn new(client: &Client) -> Self {
let schema: String = client
.query_one("SELECT current_schema()", &[])
.await
.unwrap()
.get(0);
let mut connection = configuration();
let mut url = connection["url"].as_str().unwrap().to_owned();
let parsed: tokio_postgres::Config = url.parse().unwrap();
assert!(
parsed.get_options().is_none(),
"test connection cannot override search_path"
);
url.push(if url.contains('?') { '&' } else { '?' });
url.push_str(&format!("options=-csearch_path%3D{schema}"));
connection["url"] = serde_json::Value::String(url);
let directory = std::env::temp_dir().join(format!(
"dever_registry_{}",
crate::database::scalar::generated_uuid().unwrap()
));
std::fs::create_dir(&directory).unwrap();
let settings = Self(directory);
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(settings.path())
.unwrap();
let document =
serde_json::json!({"database":{"default":connection,"secondary":connection}});
file.write_all(&serde_json::to_vec(&document).unwrap())
.unwrap();
settings
}
fn path(&self) -> PathBuf {
self.0.join("setting.json")
}
}
impl Drop for SettingsFile {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).unwrap();
}
}
+156
View File
@@ -0,0 +1,156 @@
use super::{alter, history, schema};
use crate::database::{Column, Kind, Migration, Plan, Sql, Statement};
use std::collections::BTreeMap;
mod live;
mod registry;
const EMPTY: Plan = Plan {
create: Sql::same(""),
indexes: Sql::same(""),
postgres_columns: &[],
renames: &[],
migrations: &[],
seeds: &[],
seed_revision: "empty",
};
const fn column(name: &'static str, kind: &'static str) -> Column {
Column {
name,
kind,
default: None,
nullable: false,
generated: false,
constraints: &[],
}
}
const fn step(
name: &'static str,
revision: &'static str,
drops: &'static [&'static str],
) -> Migration {
Migration {
name,
revision,
drops,
before: &[],
after: &[],
}
}
#[test]
fn migration_history_is_an_immutable_contiguous_prefix() {
let plan = const {
Plan {
migrations: &[step("first", "one", &[]), step("second", "two", &[])],
..EMPTY
}
};
assert_eq!(history::validate(&[], &plan).unwrap(), 0);
assert_eq!(
history::validate(&[(0, "first".into(), "one".into())], &plan).unwrap(),
1
);
for records in [
vec![(1, "first".into(), "one".into())],
vec![(0, "second".into(), "two".into())],
vec![(0, "first".into(), "modified".into())],
vec![
(0, "first".into(), "one".into()),
(2, "second".into(), "two".into()),
],
] {
assert_eq!(
history::validate(&records, &plan).unwrap_err().kind,
Kind::Migration
);
}
assert_eq!(
history::validate(&[(0, "first".into(), "one".into())], &EMPTY)
.unwrap_err()
.kind,
Kind::Migration
);
}
#[test]
fn column_mapping_requires_exact_unused_drops_and_unambiguous_renames() {
let old = BTreeMap::from([
("id".into(), "bigint".into()),
("name".into(), "text".into()),
("removed".into(), "bigint".into()),
]);
let plan = const {
Plan {
postgres_columns: &[
column("id", "bigint"),
column("title", "text"),
column("new", "uuid"),
],
renames: &[("title", "name")],
migrations: &[step("remove", "one", &["removed"])],
..EMPTY
}
};
assert_eq!(
alter::sources(&old, &plan, 0).unwrap(),
[Some("id"), Some("name"), None]
);
assert_eq!(
alter::sources(&old, &plan, 1).unwrap_err().kind,
Kind::Migration
);
let mut ambiguous = old.clone();
ambiguous.insert("title".into(), "text".into());
assert_eq!(
alter::sources(&ambiguous, &plan, 0).unwrap_err().kind,
Kind::Migration
);
let missing = Plan {
renames: &[("title", "absent")],
..plan
};
assert_eq!(
alter::sources(&old, &missing, 0).unwrap_err().kind,
Kind::Migration
);
let duplicated = Plan {
postgres_columns: const {
&[
column("id", "bigint"),
column("title", "text"),
column("other", "text"),
]
},
renames: &[("title", "name"), ("other", "name")],
..missing
};
assert_eq!(
alter::sources(&old, &duplicated, 0).unwrap_err().kind,
Kind::Migration
);
}
#[test]
fn only_supported_storage_conversions_reach_postgres_ddl() {
for (old, next) in [
("text", "text"),
("numeric(6,2)", "numeric(8,3)"),
("numeric(8,3)", "numeric(6,2)"),
] {
assert!(alter::conversion(old, next));
}
for (old, next) in [
("double precision", "bigint"),
("bigint", "double precision"),
("text", "uuid"),
("bigint", "numeric(8,2)"),
("bytea", "text"),
("numeric(6,2)", "numeric(3,4)"),
] {
assert!(!alter::conversion(old, next));
}
assert_eq!(schema::quote("a\"b"), "\"a\"\"b\"");
}
@@ -0,0 +1,54 @@
use crate::database::{Binding, Kind, Sql, context::initialization_order};
fn binding(references: &'static [usize]) -> Binding {
Binding {
storage: crate::database::Storage::Global,
component: "demo",
database: None,
table: "unused",
schema: Sql::same(""),
plan: None,
references,
}
}
#[test]
fn postgres_dependencies_precede_children_without_changing_binding_indices() {
let bindings = [
binding(&[0, 1, 2, 2]),
binding(&[2]),
binding(&[]),
binding(&[]),
];
assert_eq!(
initialization_order(&bindings, &[true; 4]).unwrap(),
[2, 1, 0, 3]
);
}
#[test]
fn postgres_cycles_fail_before_initialization_but_self_references_are_valid() {
assert_eq!(
initialization_order(&[binding(&[1]), binding(&[0])], &[true, true])
.unwrap_err()
.kind,
Kind::Migration
);
assert_eq!(
initialization_order(&[binding(&[0])], &[true]).unwrap(),
[0]
);
}
#[test]
fn sqlite_keeps_source_order_and_accepts_forward_and_cyclic_references() {
assert_eq!(
initialization_order(&[binding(&[1]), binding(&[0])], &[false, false]).unwrap(),
[0, 1]
);
let bindings = [binding(&[2]), binding(&[3]), binding(&[]), binding(&[1])];
assert_eq!(
initialization_order(&bindings, &[true, false, true, false]).unwrap(),
[1, 2, 0, 3]
);
}