Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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", &[¶meter])
|
||||
.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)", &[¶meter])
|
||||
.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, &[¶meter]).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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user