Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 71 additions & 8 deletions src/catalog/constraint_populator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use regex::Regex;

// Pre-compiled regex patterns for constraint parsing
static PK_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\w+)\s+[^,\)]*\bPRIMARY\s+KEY\b").unwrap()
Regex::new(r"(?i)\b(\w+)\s+[^,()]*(?:\([^)]*\)[^,()]*)?\s*\bPRIMARY\s+KEY\b").unwrap()
});

static TABLE_PK_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)PRIMARY\s+KEY\s*\(\s*([^)]+)\s*\)").unwrap()
});

static UNIQUE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\w+)\s+[^,\)]*\bUNIQUE\b").unwrap()
Regex::new(r"(?i)\b(\w+)\s+[^,()]*(?:\([^)]*\)[^,()]*)?\s*\bUNIQUE\b").unwrap()
});

static TABLE_UNIQUE_REGEX: Lazy<Regex> = Lazy::new(|| {
Expand All @@ -26,23 +26,23 @@ static CHECK_REGEX: Lazy<Regex> = Lazy::new(|| {
});

static NOT_NULL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\w+)\s+[^,\)]*\bNOT\s+NULL\b").unwrap()
Regex::new(r"(?i)\b(\w+)\s+[^,()]*(?:\([^)]*\)[^,()]*)?\s*\bNOT\s+NULL\b").unwrap()
});

static DEFAULT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\w+)\s+[^,\)]*\bDEFAULT\s+([^,\)]+)").unwrap()
Regex::new(r"(?i)\b(\w+)\s+[^,()]*(?:\([^)]*\)[^,()]*)?\s*\bDEFAULT\s+([^,()]+)").unwrap()
});

static FOREIGN_KEY_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)FOREIGN\s+KEY\s*\(\s*([^)]+)\s*\)\s+REFERENCES\s+(\w+)\s*\(\s*([^)]+)\s*\)").unwrap()
});

static INLINE_FOREIGN_KEY_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\w+)\s+[^,\)]*\bREFERENCES\s+(\w+)\s*\(\s*([^)]+)\s*\)").unwrap()
Regex::new(r"(?i)\b(\w+)\s+[^,()]*(?:\([^)]*\)[^,()]*)?\s*\bREFERENCES\s+(\w+)\s*\(\s*([^)]+)\s*\)").unwrap()
});

static TABLE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)CREATE\s+TABLE\s+[^(]+\(\s*(.+)\s*\)").unwrap()
Regex::new(r"(?is)CREATE\s+TABLE\s+[^(]+\(\s*(.+)\s*\)").unwrap()
});

/// Populate PostgreSQL catalog tables with constraint information for a newly created table
Expand Down Expand Up @@ -115,7 +115,7 @@ fn get_referenced_table_oid(_conn: &Connection, definition: &str) -> Result<Stri
}

/// Populate pg_constraint table with constraint information
fn populate_table_constraints(conn: &Connection, table_name: &str, create_sql: &str, table_oid: &str) -> Result<()> {
pub fn populate_table_constraints(conn: &Connection, table_name: &str, create_sql: &str, table_oid: &str) -> Result<()> {
let constraints = parse_table_constraints(table_name, create_sql);
eprintln!("🔎 Found {} constraints for table {}", constraints.len(), table_name);
for c in &constraints {
Expand Down Expand Up @@ -615,4 +615,67 @@ fn generate_sequence_oid(table_name: &str, column_name: &str) -> u32 {
let sequence_name = format!("{}_{}_seq", table_name, column_name);
// Use the standard OID generator but offset for sequences to avoid conflicts
generate_oid(&sequence_name) + 50000
}
}

#[cfg(test)]
mod v25_column_regex_tests {
use super::*;

const CONFIG_DDL: &str = "CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '')";
const WISH_DDL: &str = r#"CREATE TABLE wish_goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
child_id INTEGER NOT NULL REFERENCES users(id),
item_id INTEGER NOT NULL REFERENCES shop_items(id),
active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"#;

#[test]
fn v25_pk_extracts_real_column_not_create() {
let cons = parse_table_constraints("config", CONFIG_DDL);
let pk: Vec<_> = cons.iter().filter(|c| c.contype == "p").collect();
assert_eq!(pk.len(), 1, "应恰好 1 个主键");
assert_eq!(pk[0].columns, vec!["key".to_string()],
"PK 列应为 key,而不是被 CREATE 劫持");
}

#[test]
fn v25_not_null_extracts_column() {
let cons = parse_table_constraints("config", CONFIG_DDL);
let nn: Vec<_> = cons.iter().filter(|c| c.contype == "c").collect();
assert!(!nn.is_empty());
assert!(nn.iter().any(|c| c.columns == vec!["value".to_string()]));
}

#[test]
fn v25_inline_fk_extracts_column() {
let cons = parse_table_constraints("wish_goals", WISH_DDL);
let fks: Vec<_> = cons.iter().filter(|c| c.contype == "f").collect();
assert_eq!(fks.len(), 2, "wish_goals 应 2 个外键");
assert_eq!(fks[0].columns, vec!["child_id".to_string()]);
assert_eq!(fks[1].columns, vec!["item_id".to_string()]);
}

#[test]
fn v25_pk_autoincrement_column() {
let cons = parse_table_constraints("wish_goals", WISH_DDL);
let pk: Vec<_> = cons.iter().filter(|c| c.contype == "p").collect();
assert_eq!(pk.len(), 1);
assert_eq!(pk[0].columns, vec!["id".to_string()]);
}

#[test]
fn v25_get_column_number_multiline_ddl() {
// 多行 DDL(wish_goals 真实形态)必须能定位列号
assert_eq!(get_column_number(WISH_DDL, "id"), Some(1), "id 应为第 1 列");
assert_eq!(get_column_number(WISH_DDL, "child_id"), Some(2), "child_id 应为第 2 列");
assert_eq!(get_column_number(WISH_DDL, "item_id"), Some(3), "item_id 应为第 3 列");
assert_eq!(get_column_number(WISH_DDL, "active"), Some(4), "active 应为第 4 列");
}

#[test]
fn v25_get_column_number_single_line_ddl() {
assert_eq!(get_column_number(CONFIG_DDL, "key"), Some(1), "key 应为第 1 列");
assert_eq!(get_column_number(CONFIG_DDL, "value"), Some(2), "value 应为第 2 列");
}
}
13 changes: 13 additions & 0 deletions src/catalog/pg_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ impl PgSettingsHandler {
("statement_timeout", "0", "ms", "Client Connection Defaults", "Statement timeout", "user", "integer"),
("lock_timeout", "0", "ms", "Client Connection Defaults", "Lock timeout", "user", "integer"),
("idle_in_transaction_session_timeout", "0", "ms", "Client Connection Defaults", "Idle transaction timeout", "user", "integer"),
// === v38: pgjdbc 通过 pg_catalog.pg_settings 读取这些 preset。
// 带 pg_catalog. 前缀的查询会被路由到本 handler(而不是 SQLite 视图),
// 此前列表里缺 max_index_keys,pgjdbc getMaxIndexKeys() 拿到 0 行后抛
// "Unable to determine a value for MaxIndexKeys due to missing system
// catalog data",导致 getImportedKeys / getExportedKeys(DBeaver 外键
// 标签页)整体不可用。取值与 SQLite 视图 (migration v33) 保持一致。
("max_index_keys", "32", "", "Preset Options", "Maximum number of index keys", "internal", "integer"),
("max_identifier_length", "63", "", "Preset Options", "Maximum identifier length", "internal", "integer"),
("max_function_args", "100", "", "Preset Options", "Maximum function arguments", "internal", "integer"),
("block_size", "8192", "", "Preset Options", "Database block size", "internal", "integer"),
("segment_size", "131072", "8kB", "Preset Options", "Segment size", "internal", "integer"),
("wal_block_size", "8192", "", "Preset Options", "WAL block size", "internal", "integer"),
("wal_segment_size", "16MB", "", "Preset Options", "WAL segment size", "internal", "integer"),
];

settings_data
Expand Down
Loading