#![allow(clippy::result_large_err)]
use std::future::Future;
use std::pin::Pin;
pub mod backoffice;
pub mod diff;
mod error;
pub mod introspect;
pub mod meta;
mod migrate;
mod plugin;
pub mod pool;
pub mod query;
pub mod schema;
pub mod service;
pub mod solver;
mod traced;
pub use backoffice::SquelServiceImpl;
pub use dibs_jsonb::Jsonb;
pub use diff::{Change, SchemaDiff, TableDiff};
pub use error::{Error, MigrationError, SqlErrorContext};
pub use meta::{create_meta_tables_sql, record_migration_sql, sync_tables_sql};
pub use migrate::{
AppliedMigration, Migration, MigrationContext, MigrationRunner, MigrationStatus, RanMigration,
};
pub use pool::ConnectionProvider;
pub use service::{DibsServiceImpl, run_service};
pub use traced::{Connection, ConnectionExt, TracedConn, TracedObject, TracedPool};
pub use dibs_db_schema::{
__attr, __parse_attr, Attr, Check, CheckConstraint, Column, CompositeIndex, CompositeUnique,
ForeignKey, Index, IndexColumn, NullsOrder, PgType, Schema, SortOrder, SourceLocation, Table,
TableDef, TriggerCheck, TriggerCheckConstraint,
};
pub use dibs_proto::*;
pub use inventory;
pub use dibs_macros::migration;
pub use dibs_qgen::{GeneratedCode, QueryFile, generate_rust_code, parse_query_file};
pub fn quote_ident(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
pub fn index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
format!("idx_{}_{}", table, cols.join("_"))
}
pub fn unique_index_name(table: &str, columns: &[impl AsRef<str>]) -> String {
let cols: Vec<&str> = columns.iter().map(|c| c.as_ref()).collect();
format!("uq_{}_{}", table, cols.join("_"))
}
pub fn check_constraint_name(table: &str, expr: &str) -> String {
let normalized = normalize_sql_expr_for_hash(expr);
let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
let suffix = &hex[..16];
const PG_IDENT_MAX: usize = 63;
let prefix_overhead = "ck__".len(); let suffix_len = suffix.len();
let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
let table_part = if table.len() <= max_table_len {
table
} else {
let mut len = max_table_len.min(table.len());
while len > 0 && !table.is_char_boundary(len) {
len -= 1;
}
&table[..len]
};
format!("ck_{}_{}", table_part, suffix)
}
pub fn trigger_check_name(table: &str, expr: &str) -> String {
let normalized = normalize_sql_expr_for_hash(expr);
let hex = blake3::hash(normalized.as_bytes()).to_hex().to_string();
let suffix = &hex[..16];
const PG_IDENT_MAX: usize = 63;
let prefix_overhead = "trgck__".len(); let suffix_len = suffix.len();
let max_table_len = PG_IDENT_MAX.saturating_sub(prefix_overhead + suffix_len);
let table_part = if table.len() <= max_table_len {
table
} else {
let mut len = max_table_len.min(table.len());
while len > 0 && !table.is_char_boundary(len) {
len -= 1;
}
&table[..len]
};
format!("trgck_{}_{}", table_part, suffix)
}
pub fn trigger_check_function_name(trigger_name: &str) -> String {
let hex = blake3::hash(trigger_name.as_bytes()).to_hex().to_string();
format!("trgfn_{}", &hex[..20])
}
fn normalize_sql_expr_for_hash(expr: &str) -> String {
let mut out = String::with_capacity(expr.len());
let mut pending_space = false;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut chars = expr.chars().peekable();
while let Some(ch) = chars.next() {
if in_single_quote {
out.push(ch);
if ch == '\'' {
if matches!(chars.peek(), Some('\'')) {
out.push(chars.next().expect("peeked"));
} else {
in_single_quote = false;
}
}
continue;
}
if in_double_quote {
out.push(ch);
if ch == '"' {
if matches!(chars.peek(), Some('"')) {
out.push(chars.next().expect("peeked"));
} else {
in_double_quote = false;
}
}
continue;
}
match ch {
'\'' => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push('\'');
in_single_quote = true;
}
'"' => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push('"');
in_double_quote = true;
}
c if c.is_whitespace() => {
pending_space = true;
}
c => {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push(c);
}
}
}
out.trim().to_string()
}
#[doc(hidden)]
pub const fn __derive_migration_version(filename: &str) -> &str {
let bytes = filename.as_bytes();
let len = bytes.len();
let without_ext_len =
if len > 3 && bytes[len - 3] == b'.' && bytes[len - 2] == b'r' && bytes[len - 1] == b's' {
len - 3
} else {
len
};
let (start, version_len) = if without_ext_len > 2 && bytes[0] == b'm' && bytes[1] == b'_' {
(2, without_ext_len - 2)
} else {
(0, without_ext_len)
};
unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
bytes.as_ptr().add(start),
version_len,
))
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub type MigrationResult<T> = std::result::Result<T, MigrationError>;
pub type MigrationFn = for<'a> fn(
&'a mut MigrationContext<'a>,
)
-> Pin<Box<dyn Future<Output = MigrationResult<()>> + Send + 'a>>;
inventory::collect!(Migration);
pub fn build_queries(queries_path: impl AsRef<std::path::Path>) {
let queries_path = queries_path.as_ref();
println!("cargo::rerun-if-changed={}", queries_path.display());
let dibs_schema = schema::collect_schema();
eprintln!(
"cargo::warning=dibs: found {} tables in schema",
dibs_schema.tables.len()
);
for table in dibs_schema.tables.values() {
eprintln!(
"cargo::warning=dibs: table '{}' with {} columns, {} FKs",
table.name,
table.columns.len(),
table.foreign_keys.len()
);
}
let source = std::fs::read_to_string(queries_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", queries_path.display(), e));
let filename = camino::Utf8Path::new(queries_path.to_str().expect("path must be UTF-8"));
let (file, qsource) = parse_query_file(filename, &source).unwrap();
let generated =
generate_rust_code(&file, &dibs_schema, qsource).expect("query code generation failed");
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest_path = std::path::Path::new(&out_dir).join("queries.rs");
std::fs::write(&dest_path, &generated.code)
.unwrap_or_else(|e| panic!("Failed to write {}: {}", dest_path.display(), e));
println!("cargo::rustc-env=QUERIES_PATH={}", dest_path.display());
}