1use quarb::{AstAdapter, NodeId, Value};
25use quarb_relational::{RelationalModel, RowSpec, TableSpec};
26use tokio_postgres::types::Type;
27use tokio_postgres::{Client, NoTls, Row};
28
29#[derive(Debug, thiserror::Error)]
31pub enum PostgresError {
32 #[error("postgres: {0}")]
33 Postgres(#[from] tokio_postgres::Error),
34 #[error("postgres runtime: {0}")]
35 Runtime(#[from] std::io::Error),
36}
37
38pub struct PostgresAdapter {
40 model: RelationalModel,
41}
42
43fn native_type(data_type: &str) -> bool {
46 matches!(
47 data_type,
48 "boolean"
49 | "smallint"
50 | "integer"
51 | "bigint"
52 | "real"
53 | "double precision"
54 | "text"
55 | "character varying"
56 | "character"
57 )
58}
59
60fn cell(row: &Row, i: usize) -> Result<Value, tokio_postgres::Error> {
61 let ty = row.columns()[i].type_();
62 let value = match *ty {
63 Type::BOOL => row
64 .get::<_, Option<bool>>(i)
65 .map_or(Value::Null, Value::Bool),
66 Type::INT2 => row
67 .get::<_, Option<i16>>(i)
68 .map_or(Value::Null, |n| Value::Int(n as i64)),
69 Type::INT4 => row
70 .get::<_, Option<i32>>(i)
71 .map_or(Value::Null, |n| Value::Int(n as i64)),
72 Type::INT8 => row.get::<_, Option<i64>>(i).map_or(Value::Null, Value::Int),
73 Type::FLOAT4 => row
74 .get::<_, Option<f32>>(i)
75 .map_or(Value::Null, |f| Value::Float(f as f64)),
76 Type::FLOAT8 => row
77 .get::<_, Option<f64>>(i)
78 .map_or(Value::Null, Value::Float),
79 _ => row
87 .try_get::<_, Option<String>>(i)?
88 .map_or(Value::Null, Value::Str),
89 };
90 Ok(value)
91}
92
93impl PostgresAdapter {
94 pub fn connect(config: &str) -> Result<Self, PostgresError> {
99 Self::connect_impl(config, None)
100 }
101
102 pub fn connect_filtered(
106 config: &str,
107 table: &str,
108 where_sql: &str,
109 ) -> Result<Self, PostgresError> {
110 Self::connect_impl(config, Some((table.to_string(), where_sql.to_string())))
111 }
112
113 fn connect_impl(config: &str, filter: Option<(String, String)>) -> Result<Self, PostgresError> {
114 let rt = tokio::runtime::Builder::new_current_thread()
115 .enable_all()
116 .build()?;
117 let (client, specs, types) = rt.block_on(async {
121 let (client, connection) = tokio_postgres::connect(config, NoTls).await?;
122 tokio::spawn(connection);
123 let (specs, types) = Self::introspect(&client).await?;
124 Ok::<_, tokio_postgres::Error>((client, specs, types))
125 })?;
126 let model = RelationalModel::lazy(
127 specs,
128 Box::new(move |t, spec| {
129 let w = filter
130 .as_ref()
131 .filter(|(tn, _)| *tn == spec.name)
132 .map(|(_, w)| w.as_str());
133 rt.block_on(Self::fetch_rows(&client, spec, &types[t], w))
134 .map_err(|e| e.to_string())
135 }),
136 );
137 Ok(PostgresAdapter { model })
138 }
139
140 async fn introspect(
143 client: &Client,
144 ) -> Result<(Vec<TableSpec>, Vec<Vec<String>>), tokio_postgres::Error> {
145 let names: Vec<String> = client
146 .query(
147 "SELECT table_name FROM information_schema.tables \
148 WHERE table_schema = 'public' AND table_type = 'BASE TABLE' \
149 ORDER BY table_name",
150 &[],
151 )
152 .await?
153 .iter()
154 .map(|r| r.get(0))
155 .collect();
156
157 let mut specs = Vec::new();
158 let mut all_types = Vec::new();
159 for name in names {
160 let cols = client
162 .query(
163 "SELECT column_name, data_type \
164 FROM information_schema.columns \
165 WHERE table_schema = 'public' AND table_name = $1 \
166 ORDER BY ordinal_position",
167 &[&name],
168 )
169 .await?;
170 let columns: Vec<String> = cols.iter().map(|r| r.get(0)).collect();
171 let types: Vec<String> = cols.iter().map(|r| r.get(1)).collect();
172
173 let pk_rows = client
175 .query(
176 "SELECT kcu.column_name \
177 FROM information_schema.table_constraints tc \
178 JOIN information_schema.key_column_usage kcu \
179 ON tc.constraint_name = kcu.constraint_name \
180 AND tc.table_schema = kcu.table_schema \
181 AND tc.table_name = kcu.table_name \
182 WHERE tc.table_schema = 'public' AND tc.table_name = $1 \
183 AND tc.constraint_type = 'PRIMARY KEY' \
184 ORDER BY kcu.ordinal_position",
185 &[&name],
186 )
187 .await?;
188 let pk = (pk_rows.len() == 1).then(|| {
189 let col: String = pk_rows[0].get(0);
190 columns.iter().position(|c| *c == col)
191 });
192 let pk = pk.flatten();
193
194 let fk_rows = client
203 .query(
204 "SELECT a.attname, cf.relname, af.attname \
205 FROM pg_catalog.pg_constraint con \
206 JOIN pg_catalog.pg_class c ON c.oid = con.conrelid \
207 JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace \
208 JOIN pg_catalog.pg_class cf ON cf.oid = con.confrelid \
209 JOIN LATERAL unnest(con.conkey, con.confkey) \
210 WITH ORDINALITY AS k(conkey, confkey, ord) ON true \
211 JOIN pg_catalog.pg_attribute a \
212 ON a.attrelid = con.conrelid AND a.attnum = k.conkey \
213 JOIN pg_catalog.pg_attribute af \
214 ON af.attrelid = con.confrelid AND af.attnum = k.confkey \
215 WHERE con.contype = 'f' AND n.nspname = 'public' \
216 AND c.relname = $1 \
217 ORDER BY con.oid, k.ord",
218 &[&name],
219 )
220 .await?;
221 let mut fks = Vec::new();
222 for r in &fk_rows {
223 let from: String = r.get(0);
224 let target: String = r.get(1);
225 let to: String = r.get(2);
226 if let Some(idx) = columns.iter().position(|c| *c == from) {
227 fks.push((idx, target, to));
228 }
229 }
230
231 specs.push(TableSpec {
232 name,
233 columns,
234 pk,
235 fks,
236 });
237 all_types.push(types);
238 }
239 Ok((specs, all_types))
240 }
241
242 async fn fetch_rows(
245 client: &Client,
246 spec: &TableSpec,
247 types: &[String],
248 where_sql: Option<&str>,
249 ) -> Result<Vec<RowSpec>, tokio_postgres::Error> {
250 let select: Vec<String> = spec
251 .columns
252 .iter()
253 .zip(types)
254 .map(|(c, t)| {
255 if native_type(t) {
256 format!("\"{c}\"")
257 } else {
258 format!("\"{c}\"::text")
259 }
260 })
261 .collect();
262 let order = match spec.pk {
268 Some(i) => format!(" ORDER BY \"{}\".\"{}\"", spec.name, spec.columns[i]),
269 None => String::new(),
270 };
271 let filter = match where_sql {
272 Some(w) => format!(" WHERE {w}"),
273 None => String::new(),
274 };
275 let rows = client
276 .query(
277 &format!(
278 "SELECT {} FROM \"{}\"{filter}{order}",
279 select.join(", "),
280 spec.name
281 ),
282 &[],
283 )
284 .await?;
285 rows.iter()
286 .enumerate()
287 .map(|(i, r)| {
288 let values = (0..spec.columns.len())
289 .map(|c| cell(r, c))
290 .collect::<Result<Vec<_>, _>>()?;
291 Ok(RowSpec {
292 rowid: i as i64 + 1,
293 values,
294 })
295 })
296 .collect()
297 }
298
299 pub fn locator(&self, node: NodeId) -> String {
301 self.model.locator(node)
302 }
303}
304
305pub fn raw_query(
309 config: &str,
310 sql: &str,
311 order_table: Option<&str>,
312) -> Result<(Vec<String>, Vec<Vec<Value>>), PostgresError> {
313 let rt = tokio::runtime::Builder::new_current_thread()
314 .enable_all()
315 .build()?;
316 rt.block_on(async {
317 let (client, connection) = tokio_postgres::connect(config, NoTls).await?;
318 tokio::spawn(connection);
319 let sql = match order_table {
320 Some(t) => {
321 let (specs, _) = PostgresAdapter::introspect(&client).await?;
322 let key = specs
323 .into_iter()
324 .find(|s| s.name == t)
325 .and_then(|s| s.pk.map(|i| s.columns[i].clone()));
326 match key {
327 Some(k) => format!("{sql} ORDER BY \"{t}\".\"{k}\""),
328 None => sql.to_string(),
329 }
330 }
331 None => sql.to_string(),
332 };
333 let rows = client.query(&sql, &[]).await?;
334 let cols: Vec<String> = rows
335 .first()
336 .map(|r| r.columns().iter().map(|c| c.name().to_string()).collect())
337 .unwrap_or_default();
338 let out = rows
339 .iter()
340 .map(|r| {
341 (0..r.columns().len())
342 .map(|i| cell(r, i))
343 .collect::<Result<Vec<_>, _>>()
344 })
345 .collect::<Result<Vec<_>, _>>()?;
346 Ok((cols, out))
347 })
348}
349
350impl AstAdapter for PostgresAdapter {
351 fn root(&self) -> NodeId {
352 self.model.root()
353 }
354 fn children(&self, node: NodeId) -> Vec<NodeId> {
355 self.model.children(node)
356 }
357 fn name(&self, node: NodeId) -> Option<String> {
358 self.model.name(node)
359 }
360 fn parent(&self, node: NodeId) -> Option<NodeId> {
361 self.model.parent(node)
362 }
363 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
364 self.model.property(node, name)
365 }
366 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
367 self.model.metadata(node, key)
368 }
369 fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
370 self.model.resolve(node, property, hint)
371 }
372 fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
373 self.model.links(node)
374 }
375 fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
376 self.model.backlinks(node)
377 }
378}