1use akar_extension::{Extension, ExtensionContext};
12use std::sync::Arc;
13
14#[cfg(feature = "native")]
21fn postgres_value_to_string(row: &tokio_postgres::Row, i: usize) -> String {
22 let type_name = row
23 .columns()
24 .get(i)
25 .map(|c| c.type_().name())
26 .unwrap_or("unknown")
27 .to_string();
28 if let Ok(Some(v)) = row.try_get::<_, Option<String>>(i) {
29 return v;
30 }
31 if let Ok(Some(v)) = row.try_get::<_, Option<i64>>(i) {
32 return v.to_string();
33 }
34 if let Ok(Some(v)) = row.try_get::<_, Option<i32>>(i) {
35 return v.to_string();
36 }
37 if let Ok(Some(v)) = row.try_get::<_, Option<f64>>(i) {
38 return v.to_string();
39 }
40 if let Ok(Some(v)) = row.try_get::<_, Option<bool>>(i) {
41 return v.to_string();
42 }
43 if row.try_get::<_, Option<i64>>(i).is_ok() {
45 "NULL".to_string()
46 } else {
47 format!("<{type_name}>")
48 }
49}
50
51#[cfg(feature = "native")]
58mod runtime {
59 use std::collections::HashMap;
60 use std::sync::{Arc, Mutex, OnceLock};
61 use std::time::Duration;
62
63 static RUNTIME: OnceLock<Result<tokio::runtime::Runtime, String>> = OnceLock::new();
64 static CONNECTIONS: OnceLock<Mutex<HashMap<String, Arc<tokio_postgres::Client>>>> = OnceLock::new();
65
66 fn runtime() -> Result<&'static tokio::runtime::Runtime, String> {
67 RUNTIME
68 .get_or_init(|| {
69 tokio::runtime::Builder::new_multi_thread()
70 .enable_all()
71 .build()
72 .map_err(|e| format!("Failed to create tokio runtime: {e}"))
73 })
74 .as_ref()
75 .map_err(|e| e.clone())
76 }
77
78 fn connections() -> &'static Mutex<HashMap<String, Arc<tokio_postgres::Client>>> {
79 CONNECTIONS.get_or_init(Default::default)
80 }
81
82 pub fn query(conn_str: &str, sql: &str) -> Result<Vec<tokio_postgres::Row>, String> {
83 let rt = runtime()?;
84
85 let mut config = conn_str
86 .parse::<tokio_postgres::Config>()
87 .map_err(|e| format!("Invalid PostgreSQL connection string: {e}"))?;
88
89 if config.get_ssl_mode() == tokio_postgres::config::SslMode::Require {
92 return Err(
93 "sslmode=require requested but TLS support is not compiled into akar-postgres \
94 (use sslmode=disable or sslmode=prefer)"
95 .into(),
96 );
97 }
98 config.connect_timeout(Duration::from_secs(10));
99
100 let mut cache = connections()
101 .lock()
102 .map_err(|_| "Connection cache lock poisoned".to_string())?;
103
104 let client = match cache.get(conn_str) {
105 Some(c) => Arc::clone(c),
106 None => {
107 let (client, connection) = rt
108 .block_on(async { config.connect(tokio_postgres::NoTls).await })
109 .map_err(|e| format!("PostgreSQL connect error: {e}"))?;
110 rt.spawn(async move {
111 if let Err(e) = connection.await {
112 tracing::warn!("PostgreSQL connection error: {e}");
113 }
114 });
115 let client = Arc::new(client);
116 cache.insert(conn_str.to_string(), Arc::clone(&client));
117 client
118 }
119 };
120
121 match rt.block_on(async { client.query(sql, &[]).await }) {
122 Ok(rows) => Ok(rows),
123 Err(e) => {
124 if e.is_closed() {
126 cache.remove(conn_str);
127 }
128 Err(format!("PostgreSQL query error: {e}"))
129 }
130 }
131 }
132}
133
134pub struct PostgresExtension;
136
137impl Default for PostgresExtension {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl PostgresExtension {
144 pub fn new() -> Self {
145 Self
146 }
147}
148
149impl Extension for PostgresExtension {
150 fn name(&self) -> &'static str {
151 "POSTGRES"
152 }
153
154 fn load(&self, context: &ExtensionContext) -> Result<(), String> {
155 use akar_function::registry::ScalarFunction;
156
157 #[cfg(feature = "native")]
158 {
159 use akar_function::Value;
160
161 let query_fn: Arc<dyn Fn(&[Value]) -> Result<Value, String> + Send + Sync> = Arc::new(|args| {
163 if args.len() < 2 {
164 return Err("sql_query requires (connection_string, sql) arguments".into());
165 }
166 let conn_str = match &args[0] {
167 Value::String(s) => s.clone(),
168 _ => return Err("sql_query: first argument must be a connection string".into()),
169 };
170 let sql = match &args[1] {
171 Value::String(s) => s.clone(),
172 _ => return Err("sql_query: second argument must be a SQL string".into()),
173 };
174
175 let rows = runtime::query(&conn_str, &sql)?;
176
177 let mut parts = Vec::new();
179 for row in &rows {
180 for i in 0..row.len() {
181 parts.push(postgres_value_to_string(row, i));
182 }
183 }
184 if parts.is_empty() {
185 Ok(Value::String("(empty)".into()))
186 } else {
187 Ok(Value::String(parts.join(",")))
188 }
189 });
190
191 context.register_scalar_function(
192 "sql_query",
193 ScalarFunction::CustomScalar {
194 name: "sql_query".into(),
195 execute: query_fn,
196 },
197 );
198
199 tracing::info!("PostgreSQL extension loaded: 1 function registered (tokio-postgres native)");
200 }
201
202 #[cfg(not(feature = "native"))]
203 {
204 context.register_scalar_function(
205 "sql_query",
206 ScalarFunction::CustomScalar {
207 name: "sql_query".into(),
208 execute: Arc::new(|_| Err("PostgreSQL not available (feature 'native' disabled)".into())),
209 },
210 );
211 tracing::info!("PostgreSQL extension loaded: 1 function registered (placeholder)");
212 }
213
214 Ok(())
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_postgres_extension_name() {
224 let ext = PostgresExtension::new();
225 assert_eq!(ext.name(), "POSTGRES");
226 }
227}