Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ Cargo.lock
**/*.rs.bk

/.idea
/example/target/
/.cl*
26 changes: 11 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "schema_guard"
version = "1.6.2"
name = "schema_guard_tokio"
version = "1.10.0"
authors = ["V.Krinitsyn <V.Krinitsyn@gmail.com>"]
edition = "2018"
description = "Schema Guard: Relation Database (Schema) Management tool"
Expand All @@ -11,17 +11,12 @@ repository = "https://github.com/vkrinitsyn/schema_guard"
license = "MIT"

[lib]
name = "schema_guard"
name = "schema_guard_tokio"
path = "src/lib.rs"

[dependencies]

slog = { version = "^2.7.0", features=["max_level_debug"] }
slog-async = "^2.6.0"
slog-envlogger = "^2.2.0"
slog-stdlog = "^4.1.0"
slog-term = "^2.8.0"
sloggers = "^2.0.0"
lazy_static = "^1.4.0"
postgres = { version = "^0.19.1", features=["with-chrono-0_4", "with-time-0_2"] }

Expand All @@ -35,13 +30,14 @@ yaml-rust = "^0.4.5"

yaml-validator = "0.2.0"

postgres-native-tls = { version = "^0.5.0", optional = true }
native-tls = { version = "^0.2.11", optional = true }

bb8 = { version = "0.8.3", optional = true }
bb8-postgres = {version = "0.8.0", optional = true}
tokio = { version = "^1.36.0", optional = true }
tokio-postgres = { version = "^0.7.1", optional = true }
tokio-postgres-rustls = { version = "^0.12.0" }
rustls = { version = "^0.23.0" }
bb8 = { version = "0.8.3"}
bb8-postgres = {version = "0.8.0", features = ["with-chrono-0_4","with-uuid-0_8"]}
tokio = { version = "^1.36.0", features = ["test-util", "full"]}
tokio-postgres = { version = "^0.7.1"}

[features]
#default = ["slog"]
# show extra messages when do SQL execution using slog logger
slog = []
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,21 @@ database:
- column:
name: test
type: varchar(250)
index: true
triggers:
- trigger:
name: uniq_name_of_trigger
event: before update
when: for each row
proc:
```
See [examples](test/example.yaml) and [template](test/example_template.yaml)

One line of code:

```rust
let _ = schema_guard::migrate1(schema_guard::load_schema_from_file("file.yaml").unwrap(), &mut db)?;
let _ = schema_guard::migrate1(schema_guard::load_schema_from_file("file.yaml").unwrap(), "postgresql://")?;

```

Will create or upgrade existing Postgres database schema with desired tables without extra table creation.
Expand All @@ -52,5 +55,3 @@ Will create or upgrade existing Postgres database schema with desired tables wit
Not recommended to integrate schema migrate into application for production use
as such violate security concern and best practices.

Please consider to use full-featured [SchemaGuard](https://www.dbinvent.com/rdbm/) (free for personal use)

11 changes: 0 additions & 11 deletions TODO.md

This file was deleted.

131 changes: 121 additions & 10 deletions src/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl Default for Column {
description: "".to_string(),
sql: "".to_string(),
index: None,
partition_by: None,
}
}
}
Expand All @@ -46,12 +47,34 @@ pub struct Column {
pub sql: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<Index>,
/// Partitioning method: RANGE, LIST, or HASH
#[serde(skip_serializing_if = "Option::is_none")]
pub partition_by: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Index {
#[serde(skip_serializing_if = "String::is_empty")]
pub name: String,
/// UNIQUE index
#[serde(skip_serializing_if = "Option::is_none")]
pub unique: Option<bool>,
/// CREATE INDEX CONCURRENTLY
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrently: Option<bool>,
/// Index method: btree, hash, gist, spgist, gin, brin
#[serde(skip_serializing_if = "String::is_empty")]
pub using: String,
/// ASC or DESC
#[serde(skip_serializing_if = "String::is_empty")]
pub order: String,
/// NULLS FIRST or NULLS LAST
#[serde(skip_serializing_if = "String::is_empty")]
pub nulls: String,
/// COLLATE collation
#[serde(skip_serializing_if = "String::is_empty")]
pub collate: String,
/// Additional SQL (deprecated, use specific fields instead)
#[serde(skip_serializing_if = "String::is_empty")]
pub sql: String,
}
Expand Down Expand Up @@ -112,6 +135,28 @@ impl Column {
None
};
let index = &input["index"];
// Parse index: can be boolean (true = default index) or object (full config)
// - index: true → create index with auto-generated name
// - index: { name: "+" } or { name: "my_idx" } → create index
// - index: false, index: {}, or not present → no index
let index = if index.is_null() {
None
} else if let Some(b) = index.as_bool() {
if b {
Some(Index::default()) // index: true creates index with auto-generated name
} else {
None // index: false means no index
}
} else {
Index::new(index) // index: { ... } returns Some only if name is set
};

let partition_by_val = crate::utils::as_str_esc(input, "partition_by");
let partition_by = if partition_by_val.is_empty() {
None
} else {
Some(partition_by_val.to_uppercase())
};

Column {
name: crate::utils::safe_sql_name(crate::utils::as_str_esc(input, "name")),
Expand All @@ -120,11 +165,8 @@ impl Column {
description: crate::utils::as_str_esc(input, "description"),
sql: crate::utils::as_str_esc(input, "sql"),
constraint,
index: if index.is_null() {
None
} else {
Some(Index::new(index))
},
index,
partition_by,
}
}

Expand All @@ -147,6 +189,7 @@ impl Column {
sql: "".to_string(),
constraint,
index: None,
partition_by: None,
}
}

Expand Down Expand Up @@ -210,13 +253,81 @@ impl Trig {
None
}
}

/// Convert to PgTrigger for storage in dbc
pub(crate) fn to_pg_trigger(&self) -> crate::loader::PgTrigger {
crate::loader::PgTrigger {
trigger_name: self.name.clone(),
event: self.event.to_uppercase(),
orientation: self.when.to_uppercase(),
proc: self.proc.clone(),
}
}

/// Check if this trigger matches an existing PgTrigger from the database
pub(crate) fn matches_pg_trigger(&self, existing: &crate::loader::PgTrigger) -> bool {
// Normalize for comparison
let self_event = self.event.to_uppercase();
let self_when = self.when.to_uppercase();

// Compare event (BEFORE INSERT, AFTER UPDATE, etc.)
if self_event != existing.event {
return false;
}

// Compare orientation (FOR EACH ROW, FOR EACH STATEMENT)
if self_when != existing.orientation {
return false;
}

// Compare procedure - existing includes schema, self might not
// Handle both "schema.func()" and "func()" formats
let self_proc = self.proc.trim();
let existing_proc = existing.proc.trim();

// If self_proc doesn't have schema prefix, check if existing ends with it
if self_proc.contains('.') {
self_proc == existing_proc
} else {
// self_proc is just "func()", existing is "schema.func()"
existing_proc.ends_with(self_proc) || existing_proc == self_proc
}
}
}

impl Index {
pub(crate) fn new(input: &Yaml) -> Self {
impl Default for Index {
fn default() -> Self {
Index {
name: crate::utils::as_str_esc(input, "name"),
sql: crate::utils::as_str_esc(input, "sql"),
name: "+".to_string(), // "+" triggers auto-generated index name
unique: None,
concurrently: None,
using: String::new(),
order: String::new(),
nulls: String::new(),
collate: String::new(),
sql: String::new(),
}
}
}

impl Index {
pub(crate) fn new(input: &Yaml) -> Option<Self> {
let name = crate::utils::as_str_esc(input, "name");
// If name is empty, no index is created (must explicitly set name or use index: true)
if name.is_empty() {
return None;
}
let unique_val = crate::utils::as_bool(input, "unique", false);
let concurrently_val = crate::utils::as_bool(input, "concurrently", false);
Some(Index {
name,
unique: if unique_val { Some(true) } else { None },
concurrently: if concurrently_val { Some(true) } else { None },
using: crate::utils::as_str_esc(input, "using").to_lowercase(),
order: crate::utils::as_str_esc(input, "order").to_uppercase(),
nulls: crate::utils::as_str_esc(input, "nulls").to_uppercase(),
collate: crate::utils::as_str_esc(input, "collate"),
sql: crate::utils::as_str_esc(input, "sql"),
})
}
}
Loading