1pub mod admin;
62pub mod agent_contract;
63pub mod auth_scram;
64pub mod backend;
65pub mod batch;
66pub mod branch;
67pub mod client_tls;
68pub mod config;
69pub mod connection_pool;
70pub mod failover_controller;
71pub mod health_checker;
72pub mod http_gateway;
73pub(crate) mod http_util;
74pub mod load_balancer;
75pub mod mcp;
76pub mod mirror;
77pub mod pipeline;
78pub mod plugin_registry;
79pub mod primary_tracker;
80pub mod protocol;
81pub mod request;
82pub mod server;
83pub mod switchover_buffer;
84
85#[cfg(feature = "pool-modes")]
87pub mod pool;
88
89#[cfg(feature = "ha-tr")]
91pub mod cursor_restore;
92#[cfg(feature = "ha-tr")]
93pub mod failover_replay;
94#[cfg(feature = "ha-tr")]
95pub mod replay;
96#[cfg(feature = "ha-tr")]
97pub mod session_migrate;
98#[cfg(feature = "ha-tr")]
99pub mod transaction_journal;
100
101#[cfg(feature = "ha-tr")]
103pub mod upgrade_orchestrator;
104
105#[cfg(feature = "ha-tr")]
107pub mod shadow_execute;
108
109#[cfg(feature = "query-cache")]
111pub mod cache;
112
113#[cfg(feature = "routing-hints")]
115pub mod routing;
116
117#[cfg(feature = "lag-routing")]
119pub mod lag;
120
121#[cfg(feature = "rate-limiting")]
123pub mod rate_limit;
124
125#[cfg(feature = "circuit-breaker")]
127pub mod circuit_breaker;
128
129#[cfg(feature = "query-analytics")]
131pub mod analytics;
132
133#[cfg(feature = "anomaly-detection")]
136pub mod anomaly;
137
138pub mod edge;
144
145#[cfg(feature = "multi-tenancy")]
147pub mod multi_tenancy;
148
149#[cfg(feature = "auth-proxy")]
151pub mod auth;
152
153#[cfg(feature = "query-rewriting")]
155pub mod rewriter;
156
157#[cfg(feature = "wasm-plugins")]
159pub mod plugins;
160
161#[cfg(feature = "graphql-gateway")]
163pub mod graphql;
164#[cfg(feature = "graphql-gateway")]
165pub mod graphql_gateway;
166
167#[cfg(feature = "schema-routing")]
169pub mod schema_routing;
170
171#[cfg(feature = "distribcache")]
173pub mod distribcache;
174
175pub mod skills;
181
182use thiserror::Error;
183use uuid::Uuid;
184
185#[derive(Debug, Error)]
187pub enum ProxyError {
188 #[error("Configuration error: {0}")]
189 Config(String),
190
191 #[error("Network error: {0}")]
192 Network(String),
193
194 #[error("Connection error: {0}")]
195 Connection(String),
196
197 #[error("Protocol error: {0}")]
198 Protocol(String),
199
200 #[error("Pool error: {0}")]
201 Pool(String),
202
203 #[error("Health check error: {0}")]
204 HealthCheck(String),
205
206 #[error("Failover error: {0}")]
207 Failover(String),
208
209 #[error("Failover failed: {0}")]
210 FailoverFailed(String),
211
212 #[error("Transaction replay failed: {0}")]
213 ReplayFailed(String),
214
215 #[error("Session migration failed: {0}")]
216 SessionMigration(String),
217
218 #[error("Cursor restore failed: {0}")]
219 CursorRestore(String),
220
221 #[error("Routing error: {0}")]
222 Routing(String),
223
224 #[error("Authentication error: {0}")]
225 Auth(String),
226
227 #[error("Pool exhausted: {0}")]
228 PoolExhausted(String),
229
230 #[error("Timeout: {0}")]
231 Timeout(String),
232
233 #[error("Configuration error: {0}")]
234 Configuration(String),
235
236 #[error("No healthy nodes available")]
237 NoHealthyNodes,
238
239 #[error("IO error: {0}")]
240 Io(#[from] std::io::Error),
241
242 #[error("JSON error: {0}")]
243 Json(#[from] serde_json::Error),
244
245 #[error("Internal error: {0}")]
246 Internal(String),
247}
248
249pub type Result<T> = std::result::Result<T, ProxyError>;
250
251pub const VERSION: &str = env!("CARGO_PKG_VERSION");
253
254pub const DEFAULT_PORT: u16 = 5432;
256
257pub const DEFAULT_ADMIN_PORT: u16 = 9090;
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
262pub struct NodeId(pub Uuid);
263
264impl NodeId {
265 pub fn new() -> Self {
266 Self(Uuid::new_v4())
267 }
268}
269
270impl Default for NodeId {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub enum NodeRole {
279 Primary,
281 Standby,
283 ReadReplica,
285 Unknown,
287}
288
289#[derive(Debug, Clone)]
291pub struct NodeEndpoint {
292 pub id: NodeId,
294 pub host: String,
296 pub port: u16,
298 pub role: NodeRole,
300 pub weight: u32,
302 pub enabled: bool,
304}
305
306impl NodeEndpoint {
307 pub fn new(host: impl Into<String>, port: u16) -> Self {
308 Self {
309 id: NodeId::new(),
310 host: host.into(),
311 port,
312 role: NodeRole::Unknown,
313 weight: 100,
314 enabled: true,
315 }
316 }
317
318 pub fn with_role(mut self, role: NodeRole) -> Self {
319 self.role = role;
320 self
321 }
322
323 pub fn with_weight(mut self, weight: u32) -> Self {
324 self.weight = weight;
325 self
326 }
327
328 pub fn address(&self) -> String {
329 format!("{}:{}", self.host, self.port)
330 }
331}
332
333#[cfg(test)]
339mod postgresql_compat_tests {
340 use super::*;
341
342 #[test]
344 fn test_pg_node_endpoints() {
345 let primary =
346 NodeEndpoint::new("pg-primary.example.com", 5432).with_role(NodeRole::Primary);
347 let standby =
348 NodeEndpoint::new("pg-standby-1.example.com", 5432).with_role(NodeRole::Standby);
349 let replica = NodeEndpoint::new("pg-replica-1.example.com", 5433)
350 .with_role(NodeRole::ReadReplica)
351 .with_weight(50);
352
353 assert_eq!(primary.role, NodeRole::Primary);
354 assert_eq!(standby.role, NodeRole::Standby);
355 assert_eq!(replica.weight, 50);
356 assert_eq!(replica.address(), "pg-replica-1.example.com:5433");
357 }
358
359 #[test]
361 fn test_pg_load_balancer_config() {
362 use load_balancer::*;
363
364 let config = LoadBalancerConfig {
365 read_write_split: true,
366 read_strategy: RoutingStrategy::RoundRobin,
367 write_strategy: RoutingStrategy::PrimaryOnly,
368 ..Default::default()
369 };
370
371 assert!(config.read_write_split);
372 assert_eq!(config.read_strategy, RoutingStrategy::RoundRobin);
373 assert_eq!(config.write_strategy, RoutingStrategy::PrimaryOnly);
374
375 let _lb = LoadBalancer::new(config);
377 }
378
379 #[test]
381 fn test_pg_health_config() {
382 use health_checker::*;
383
384 let config = HealthConfig {
385 check_query: "SELECT 1".to_string(),
386 detailed_checks: true,
387 ..Default::default()
388 };
389
390 assert_eq!(config.check_query, "SELECT 1");
391 assert!(config.detailed_checks);
392 }
393
394 #[tokio::test]
396 async fn test_pg_failover() {
397 use failover_controller::*;
398
399 let controller = FailoverController::new(FailoverConfig {
400 auto_failover: true,
401 prefer_sync_standby: true,
402 ..Default::default()
403 });
404
405 let primary = NodeId::new();
406 controller.set_primary(primary).await;
407 assert_eq!(controller.get_primary().await, Some(primary));
408
409 let sync_standby = NodeId::new();
411 controller
412 .register_candidate(FailoverCandidate {
413 node_id: sync_standby,
414 endpoint: NodeEndpoint::new("pg-sync", 5432).with_role(NodeRole::Standby),
415 is_sync: true,
416 lag_bytes: 0,
417 priority: 1,
418 last_heartbeat: None,
419 })
420 .await;
421
422 let async_standby = NodeId::new();
423 controller
424 .register_candidate(FailoverCandidate {
425 node_id: async_standby,
426 endpoint: NodeEndpoint::new("pg-async", 5432).with_role(NodeRole::Standby),
427 is_sync: false,
428 lag_bytes: 1024,
429 priority: 2,
430 last_heartbeat: None,
431 })
432 .await;
433
434 assert_eq!(controller.state().await, FailoverState::Normal);
436 assert_eq!(controller.failover_count(), 0);
437 }
438
439 #[test]
441 fn test_pg_connection_pool() {
442 use connection_pool::*;
443
444 let config = PoolConfig {
445 min_connections: 2,
446 max_connections: 20,
447 test_on_acquire: true,
448 ..Default::default()
449 };
450
451 assert_eq!(config.min_connections, 2);
452 assert_eq!(config.max_connections, 20);
453 assert!(config.test_on_acquire);
454
455 let _pool = ConnectionPool::new(config);
456 }
457
458 #[tokio::test]
460 async fn test_pg_switchover_buffer() {
461 use switchover_buffer::*;
462
463 let buffer = SwitchoverBuffer::new(BufferConfig {
464 buffer_timeout: std::time::Duration::from_secs(5),
465 max_buffered_queries: 1000,
466 ..Default::default()
467 });
468
469 assert_eq!(buffer.state(), BufferState::Passthrough);
470 assert!(!buffer.is_buffering());
471
472 buffer.start_buffering();
474 assert!(buffer.is_buffering());
475
476 let rx = buffer
477 .buffer_query("INSERT INTO orders VALUES (1)".to_string(), vec![], 1)
478 .unwrap();
479
480 buffer.stop_buffering();
482 buffer.drain(|_sql, _params| async { Ok(()) }).await;
483
484 let result = rx.await.unwrap();
485 assert!(matches!(result, BufferResult::Success));
486 }
487
488 #[test]
490 fn test_pg_primary_tracker_standalone() {
491 use primary_tracker::*;
492
493 let tracker = PrimaryTracker::new_standalone();
494
495 let pg_primary = uuid::Uuid::new_v4();
497 tracker.set_primary(pg_primary, "pg-primary.local:5432".to_string());
498 tracker.confirm_primary();
499
500 assert!(tracker.has_primary());
501 assert!(tracker.get_primary().unwrap().is_confirmed);
502
503 tracker.clear_primary();
505 assert!(!tracker.has_primary());
506
507 let pg_new_primary = uuid::Uuid::new_v4();
509 tracker.set_primary(pg_new_primary, "pg-standby.local:5432".to_string());
510 tracker.confirm_primary();
511 assert_eq!(
512 tracker.get_primary_address(),
513 Some("pg-standby.local:5432".to_string())
514 );
515 }
516
517 #[cfg(feature = "ha-tr")]
519 #[tokio::test]
520 async fn test_pg_transaction_replay() {
521 use transaction_journal::*;
522
523 let journal = TransactionJournal::new();
524 let tx_id = uuid::Uuid::new_v4();
525 let session_id = uuid::Uuid::new_v4();
526 let node = NodeId::new();
527
528 journal
530 .begin_transaction(tx_id, session_id, node, 0)
531 .await
532 .unwrap();
533 journal
534 .log_statement(tx_id, "BEGIN".to_string(), vec![], None, None, 1)
535 .await
536 .unwrap();
537 journal
538 .log_statement(
539 tx_id,
540 "INSERT INTO accounts (id, balance) VALUES ($1, $2)".to_string(),
541 vec![JournalValue::Int64(1), JournalValue::Float64(100.0)],
542 Some(12345),
543 Some(1),
544 5,
545 )
546 .await
547 .unwrap();
548 journal
549 .log_statement(
550 tx_id,
551 "UPDATE accounts SET balance = balance - $1 WHERE id = $2".to_string(),
552 vec![JournalValue::Float64(25.0), JournalValue::Int64(1)],
553 Some(67890),
554 Some(1),
555 3,
556 )
557 .await
558 .unwrap();
559
560 let j = journal.get_journal(&tx_id).await.unwrap();
561 assert_eq!(j.entries.len(), 3);
562 assert!(j.has_mutations);
563
564 assert_eq!(j.entries[0].statement_type, StatementType::Transaction);
566 assert_eq!(j.entries[1].statement_type, StatementType::Insert);
567 assert_eq!(j.entries[2].statement_type, StatementType::Update);
568
569 journal.commit_transaction(tx_id).await.unwrap();
571 assert!(journal.get_journal(&tx_id).await.is_none());
572 }
573
574 #[cfg(feature = "ha-tr")]
576 #[tokio::test]
577 async fn test_pg_session_migration() {
578 use session_migrate::*;
579
580 let migrate = SessionMigrate::new();
581 let session_id = uuid::Uuid::new_v4();
582 let node = NodeId::new();
583
584 let mut state =
585 SessionState::new(session_id, "postgres".to_string(), "mydb".to_string(), node);
586
587 state.set_parameter("timezone".to_string(), "America/New_York".to_string());
589 state.set_parameter("search_path".to_string(), "public, app_schema".to_string());
590 state.set_parameter("statement_timeout".to_string(), "30000".to_string());
591 state.set_parameter("work_mem".to_string(), "256MB".to_string());
592
593 state.add_prepared_statement(PreparedStatementInfo {
595 name: "get_user".to_string(),
596 query: "SELECT * FROM users WHERE id = $1".to_string(),
597 param_types: vec!["integer".to_string()],
598 created_at: chrono::Utc::now(),
599 });
600
601 migrate.register_session(state).await.unwrap();
602
603 let session = migrate.get_session(&session_id).await.unwrap();
605 let restore_stmts = session.generate_restore_statements();
606
607 assert!(restore_stmts.iter().any(|s| s.contains("America/New_York")));
608 assert!(restore_stmts.iter().any(|s| s.contains("search_path")));
609 assert!(restore_stmts
610 .iter()
611 .any(|s| s.contains("statement_timeout")));
612 assert!(restore_stmts.iter().any(|s| s.contains("PREPARE get_user")));
613 }
614
615 #[tokio::test]
617 async fn test_pg_pipelining() {
618 use pipeline::*;
619
620 let pipeline = RequestPipeline::new(PipelineConfig {
621 max_depth: 16,
622 enabled: true,
623 ..Default::default()
624 });
625
626 let conn_id = 1;
627
628 let t1 = pipeline
630 .submit(conn_id, b"Parse: SELECT $1::int".to_vec())
631 .unwrap();
632 let _t2 = pipeline.submit(conn_id, b"Bind: [42]".to_vec()).unwrap();
633 let _t3 = pipeline.submit(conn_id, b"Execute".to_vec()).unwrap();
634
635 assert_eq!(pipeline.depth(conn_id), 3);
636
637 pipeline.complete_next(conn_id, b"ParseComplete".to_vec(), true, None);
639 pipeline.complete_next(conn_id, b"BindComplete".to_vec(), true, None);
640 pipeline.complete_next(conn_id, b"DataRow: 42".to_vec(), true, None);
641
642 assert_eq!(pipeline.depth(conn_id), 0);
643
644 let r1 = t1.wait().await.unwrap();
645 assert!(r1.success);
646 }
647
648 #[tokio::test]
650 async fn test_pg_batch_insert() {
651 use batch::*;
652
653 let config = BatchConfig {
654 max_batch_size: 3,
655 ..Default::default()
656 };
657 let batcher = std::sync::Arc::new(InsertBatcher::new(config));
658
659 batcher
660 .add(
661 "orders".to_string(),
662 vec!["id".to_string(), "total".to_string()],
663 vec![vec!["1".to_string(), "99.99".to_string()]],
664 "INSERT INTO orders (id, total) VALUES (1, 99.99)".to_string(),
665 )
666 .unwrap();
667
668 assert_eq!(batcher.batch_size("orders"), 1);
669
670 let stats = batcher.stats();
671 assert_eq!(stats.inserts_received, 1);
672 assert_eq!(stats.rows_received, 1);
673 }
674}