1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::server::connection::ConnectionId;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::{Arc, LazyLock};
14use tokio::sync::mpsc;
15use tracing::debug;
16
17pub struct PostgresqlProtocol {
19 #[allow(dead_code)]
20 connection_id: ConnectionId,
21 #[allow(dead_code)]
22 app_state: Arc<AppState>,
23 status_tx: mpsc::UnboundedSender<String>,
24}
25
26impl PostgresqlProtocol {
27 pub fn new(
28 connection_id: ConnectionId,
29 app_state: Arc<AppState>,
30 status_tx: mpsc::UnboundedSender<String>,
31 ) -> Self {
32 Self {
33 connection_id,
34 app_state,
35 status_tx,
36 }
37 }
38}
39
40impl Protocol for PostgresqlProtocol {
42 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
43 vec![
44 crate::llm::actions::ParameterDefinition {
45 name: "send_first".to_string(),
46 type_hint: "boolean".to_string(),
47 description: "Unsupported by this server and refused if set to true. PostgreSQL: the frontend sends the StartupMessage first, and a byte before it is a protocol violation the client cannot parse."
48 .to_string(),
49 required: false,
50 example: json!(false),
51 default: None,
52 },
53 ]
54 }
55 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
56 vec![]
59 }
60 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
61 vec![
62 postgresql_query_response_action(),
63 postgresql_error_response_action(),
64 postgresql_ok_response_action(),
65 close_this_connection_action(),
66 ]
67 }
68 fn protocol_name(&self) -> &'static str {
69 "PostgreSQL"
70 }
71 fn get_event_types(&self) -> Vec<EventType> {
72 get_postgresql_event_types()
73 }
74 fn stack_name(&self) -> &'static str {
75 "ETH>IP>TCP>PostgreSQL"
76 }
77 fn keywords(&self) -> Vec<&'static str> {
78 vec!["postgres", "psql"]
79 }
80 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
81 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
82
83 ProtocolMetadataV2::builder()
84 .state(DevelopmentState::Beta)
89 .well_known_port(5432)
90 .implementation("pgwire v0.35 protocol library")
91 .llm_control("Query responses (columns, rows, types)")
92 .e2e_testing(
93 "Two independent clients. tokio-postgres (tests/server/postgresql/test.rs, \
94 extended_query_test.rs, llm_failure_test.rs): simple query, and \
95 Parse/Bind/Describe/Execute with binary result columns decoded into real Rust \
96 types. psql 14 / libpq (real_client_test.rs, hard-fails when the binary is \
97 absent): the sslmode=prefer SSLRequest negotiation tokio-postgres+NoTls never \
98 sends, the startup handshake, a four-column three-row SELECT asserted on the \
99 exact text psql rendered (t/f booleans, a real NULL distinguished from an \
100 empty string), a CommandComplete tag, and an ErrorResponse whose SQLSTATE psql \
101 prints under VERBOSITY verbose. \
102 NOT proven: psql 14 sends every statement as a simple Query (no \\bind), so the \
103 extended protocol and binary formats rest on tokio-postgres alone; no client \
104 here exercises authentication or TLS (neither is implemented), bound parameter \
105 values, COPY, LISTEN/NOTIFY, cursors, or multi-statement simple queries.",
106 )
107 .notes(
108 "No authentication, no TLS; simple and extended query protocols, text format only",
109 )
110 .build()
111 }
112 fn description(&self) -> &'static str {
113 "PostgreSQL database server"
114 }
115 fn example_prompt(&self) -> &'static str {
116 "Start a PostgreSQL server on port 5432"
117 }
118 fn group_name(&self) -> &'static str {
119 "Database"
120 }
121
122 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
123 use crate::llm::actions::StartupExamples;
124 use serde_json::json;
125
126 let script = r#"import json, sys
129data = json.load(sys.stdin)
130event = data["event"]
131if data["event_type_id"] == "postgresql_query":
132 q = event.get("query", "").strip().rstrip(";").upper()
133 if q.startswith("SELECT VERSION()"):
134 actions = [{"type": "postgresql_query_response",
135 "columns": [{"name": "version", "type": "text"}],
136 "rows": [["PostgreSQL 16.2"]]}]
137 elif q.startswith("SELECT"):
138 actions = [{"type": "postgresql_query_response",
139 "columns": [{"name": "id", "type": "int4"},
140 {"name": "name", "type": "text"}],
141 "rows": [[1, "alice"], [2, "bob"]]}]
142 else:
143 actions = [{"type": "postgresql_ok_response", "tag": "OK"}]
144else:
145 actions = []
146print(json.dumps({"actions": actions}))"#;
147
148 StartupExamples::new(
149 json!({
151 "type": "open_server",
152 "port": 5432,
153 "base_stack": "postgresql",
154 "instruction": "Act as a PostgreSQL server for a 'users' database. Answer SELECT queries by generating plausible rows that satisfy the WHERE clause and requested columns; treat INSERT/UPDATE/DELETE as succeeding with a suitable command tag. Keep results consistent across queries in the same session."
155 }),
156 json!({
158 "type": "open_server",
159 "port": 5432,
160 "base_stack": "postgresql",
161 "event_handlers": [{
162 "event_pattern": "postgresql_query",
163 "handler": {
164 "type": "script",
165 "language": "python",
166 "code": script
167 }
168 }]
169 }),
170 json!({
172 "type": "open_server",
173 "port": 5432,
174 "base_stack": "postgresql",
175 "event_handlers": [{
176 "event_pattern": "postgresql_query",
177 "handler": {
178 "type": "static",
179 "actions": [{
180 "type": "postgresql_query_response",
181 "columns": [{"name": "result", "type": "text"}],
182 "rows": [["OK"]]
183 }]
184 }
185 }]
186 }),
187 )
188 }
189}
190
191impl Server for PostgresqlProtocol {
193 fn spawn(
194 &self,
195 ctx: crate::protocol::SpawnContext,
196 ) -> std::pin::Pin<
197 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
198 > {
199 Box::pin(async move {
200 use crate::server::postgresql::PostgresqlServer;
201 let send_first = ctx
207 .startup_params
208 .as_ref()
209 .map(|p| p.get_optional_bool("send_first"))
210 .transpose()?
211 .flatten()
212 .unwrap_or(false);
213 if send_first {
214 return Err(anyhow::anyhow!(
215 "send_first is not supported by the PostgreSQL server: the frontend sends the StartupMessage first, and a byte before it is a protocol violation the client cannot parse"
216 ));
217 }
218
219 PostgresqlServer::spawn_with_llm_actions(
220 ctx.legacy_listen_addr(),
221 ctx.llm_client,
222 ctx.state,
223 ctx.status_tx,
224 send_first,
225 ctx.server_id,
226 )
227 .await
228 })
229 }
230 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
231 let action_type = action
232 .get("type")
233 .and_then(|v| v.as_str())
234 .context("Missing 'type' field in action")?;
235
236 match action_type {
237 "postgresql_query_response" => self.execute_postgresql_query_response(action),
238 "postgresql_error_response" => self.execute_postgresql_error_response(action),
239 "postgresql_ok_response" => self.execute_postgresql_ok_response(action),
240 "close_this_connection" => Ok(ActionResult::CloseConnection),
241 _ => Err(anyhow::anyhow!(
242 "Unknown PostgreSQL action: {}",
243 action_type
244 )),
245 }
246 }
247}
248
249impl PostgresqlProtocol {
250 fn execute_postgresql_query_response(&self, action: serde_json::Value) -> Result<ActionResult> {
251 let columns = action
253 .get("columns")
254 .and_then(|v| v.as_array())
255 .context("Missing 'columns' array")?;
256
257 let rows = action
258 .get("rows")
259 .and_then(|v| v.as_array())
260 .context("Missing 'rows' array")?;
261
262 debug!(
263 "PostgreSQL query response: {} columns, {} rows",
264 columns.len(),
265 rows.len()
266 );
267
268 let _ = self.status_tx.send(format!(
269 "[DEBUG] PostgreSQL → Result set: {} columns, {} rows",
270 columns.len(),
271 rows.len()
272 ));
273
274 Ok(ActionResult::Custom {
275 name: "postgresql_query_response".to_string(),
276 data: json!({
277 "columns": columns,
278 "rows": rows
279 }),
280 })
281 }
282
283 fn execute_postgresql_error_response(&self, action: serde_json::Value) -> Result<ActionResult> {
284 let severity = action
285 .get("severity")
286 .and_then(|v| v.as_str())
287 .unwrap_or("ERROR");
288
289 let code = action
290 .get("code")
291 .and_then(|v| v.as_str())
292 .unwrap_or("XX000");
293
294 let message = action
295 .get("message")
296 .and_then(|v| v.as_str())
297 .unwrap_or("Unknown error");
298
299 debug!(
300 "PostgreSQL error response: {} {} - {}",
301 severity, code, message
302 );
303
304 let _ = self.status_tx.send(format!(
305 "[DEBUG] PostgreSQL ✗ {} {}: {}",
306 severity, code, message
307 ));
308
309 Ok(ActionResult::Custom {
310 name: "postgresql_error".to_string(),
311 data: json!({
312 "severity": severity,
313 "code": code,
314 "message": message
315 }),
316 })
317 }
318
319 fn execute_postgresql_ok_response(&self, action: serde_json::Value) -> Result<ActionResult> {
320 let tag = action.get("tag").and_then(|v| v.as_str()).unwrap_or("OK");
321
322 debug!("PostgreSQL OK response: {}", tag);
323
324 let _ = self
325 .status_tx
326 .send(format!("[DEBUG] PostgreSQL → OK: {}", tag));
327
328 Ok(ActionResult::Custom {
329 name: "postgresql_ok".to_string(),
330 data: json!({
331 "tag": tag
332 }),
333 })
334 }
335}
336
337pub fn postgresql_query_response_action() -> ActionDefinition {
339 ActionDefinition {
340 name: "postgresql_query_response".to_string(),
341 description: "The answer to any statement that returns rows - SELECT (including \
342 SELECT 1, SELECT current_user, SELECT version()), SHOW, VALUES, WITH. \
343 A single value is one column and one row. Never answer a SELECT with \
344 postgresql_ok_response: a command tag carries no rows, so the client \
345 prints the tag and never sees the value. One response per query."
346 .to_string(),
347 parameters: vec![
348 Parameter {
349 name: "columns".to_string(),
350 type_hint: "array".to_string(),
351 description: "Array of column definitions. Each column needs 'name' and 'type'. \
352 Recognised types: int2/smallint, int4/int/integer, int8/bigint, \
353 float4/real, float8/double, bool/boolean, date, time, timestamp, \
354 text, varchar (anything else is sent as varchar). Rows shorter than \
355 the column list are padded with NULLs; extra values are dropped"
356 .to_string(),
357 required: true,
358 },
359 Parameter {
360 name: "rows".to_string(),
361 type_hint: "array".to_string(),
362 description:
363 "Array of rows. Each row is an array of values matching the column order"
364 .to_string(),
365 required: true,
366 },
367 ],
368 example: json!({
369 "type": "postgresql_query_response",
370 "columns": [{"name": "id", "type": "int4"}, {"name": "name", "type": "text"}],
371 "rows": [[1, "Alice"], [2, "Bob"]]
372 }),
373 log_template: Some(
374 LogTemplate::new()
375 .with_info("-> PostgreSQL {columns_len} cols, {rows_len} rows")
376 .with_debug("PostgreSQL query_response: {columns_len} columns, {rows_len} rows"),
377 ),
378 }
379}
380
381pub fn postgresql_error_response_action() -> ActionDefinition {
383 ActionDefinition {
384 name: "postgresql_error_response".to_string(),
385 description: "Send an error response to the client".to_string(),
386 parameters: vec![
387 Parameter {
388 name: "severity".to_string(),
389 type_hint: "string".to_string(),
390 description: "Error severity (ERROR, FATAL, PANIC, WARNING, NOTICE, DEBUG, INFO, LOG)".to_string(),
391 required: false,
392 },
393 Parameter {
394 name: "code".to_string(),
395 type_hint: "string".to_string(),
396 description: "PostgreSQL error code (e.g. '42P01' for undefined_table, '42601' for syntax_error)".to_string(),
397 required: false,
398 },
399 Parameter {
400 name: "message".to_string(),
401 type_hint: "string".to_string(),
402 description: "Error message to display to the client".to_string(),
403 required: true,
404 },
405 ],
406 example: json!({
407 "type": "postgresql_error_response",
408 "severity": "ERROR",
409 "code": "42P01",
410 "message": "relation \"table_name\" does not exist"
411 }),
412 log_template: Some(
413 LogTemplate::new()
414 .with_info("-> PostgreSQL {severity} {code}: {message}")
415 .with_debug("PostgreSQL error_response: {severity} {code}"),
416 ),
417 }
418}
419
420pub fn postgresql_ok_response_action() -> ActionDefinition {
422 ActionDefinition {
423 name: "postgresql_ok_response".to_string(),
424 description: "Command-complete tag for a statement that returns no rows: INSERT, \
425 UPDATE, DELETE, CREATE, SET, BEGIN. Never for SELECT or SHOW - those \
426 are answered with postgresql_query_response. The tag names the statement \
427 that ran (a SELECT answered here would print 'INSERT 0 1')."
428 .to_string(),
429 parameters: vec![Parameter {
430 name: "tag".to_string(),
431 type_hint: "string".to_string(),
432 description: "Command tag (e.g. 'INSERT 0 1', 'UPDATE 3', 'DELETE 2', 'CREATE TABLE')"
433 .to_string(),
434 required: true,
435 }],
436 example: json!({
437 "type": "postgresql_ok_response",
438 "tag": "INSERT 0 1"
439 }),
440 log_template: Some(
441 LogTemplate::new()
442 .with_info("-> PostgreSQL OK: {tag}")
443 .with_debug("PostgreSQL ok_response: {tag}"),
444 ),
445 }
446}
447
448pub fn close_this_connection_action() -> ActionDefinition {
450 ActionDefinition {
451 name: "close_this_connection".to_string(),
452 description: "Close the current PostgreSQL connection".to_string(),
453 parameters: vec![],
454 example: json!({"type": "close_this_connection"}),
455 log_template: Some(
456 LogTemplate::new()
457 .with_info("PostgreSQL connection closed")
458 .with_debug("PostgreSQL close_this_connection"),
459 ),
460 }
461}
462
463pub static POSTGRESQL_QUERY_RESPONSE_ACTION: LazyLock<ActionDefinition> =
469 LazyLock::new(|| ActionDefinition {
470 name: "postgresql_query_response".to_string(),
471 description: "The answer to any statement that returns rows - SELECT (including \
472 SELECT 1, SELECT current_user, SELECT version()), SHOW, VALUES, WITH. \
473 A single value is one column and one row. Never answer a SELECT with \
474 postgresql_ok_response: a command tag carries no rows, so the client \
475 prints the tag and never sees the value. One response per query."
476 .to_string(),
477 parameters: vec![
478 Parameter {
479 name: "columns".to_string(),
480 type_hint: "array".to_string(),
481 description: "Array of column definitions. Each column needs 'name' and 'type'. \
482 Recognised types: int2/smallint, int4/int/integer, int8/bigint, \
483 float4/real, float8/double, bool/boolean, date, time, timestamp, \
484 text, varchar (anything else is sent as varchar). Rows shorter than \
485 the column list are padded with NULLs; extra values are dropped"
486 .to_string(),
487 required: true,
488 },
489 Parameter {
490 name: "rows".to_string(),
491 type_hint: "array".to_string(),
492 description:
493 "Array of rows. Each row is an array of values matching the column order"
494 .to_string(),
495 required: true,
496 },
497 ],
498 example: json!({
499 "type": "postgresql_query_response",
500 "columns": [{"name": "id", "type": "int4"}, {"name": "name", "type": "text"}],
501 "rows": [[1, "Alice"], [2, "Bob"]]
502 }),
503 log_template: Some(
504 LogTemplate::new()
505 .with_info("-> PostgreSQL {columns_len} cols, {rows_len} rows")
506 .with_debug("PostgreSQL query_response: {columns_len} columns, {rows_len} rows"),
507 ),
508 });
509
510pub static POSTGRESQL_ERROR_RESPONSE_ACTION: LazyLock<ActionDefinition> = LazyLock::new(|| {
512 ActionDefinition {
513 name: "postgresql_error_response".to_string(),
514 description: "Send an error response to the client".to_string(),
515 parameters: vec![
516 Parameter {
517 name: "severity".to_string(),
518 type_hint: "string".to_string(),
519 description: "Error severity (ERROR, FATAL, PANIC, WARNING, NOTICE, DEBUG, INFO, LOG)".to_string(),
520 required: false,
521 },
522 Parameter {
523 name: "code".to_string(),
524 type_hint: "string".to_string(),
525 description: "PostgreSQL error code (e.g. '42P01' for undefined_table, '42601' for syntax_error)".to_string(),
526 required: false,
527 },
528 Parameter {
529 name: "message".to_string(),
530 type_hint: "string".to_string(),
531 description: "Error message to display to the client".to_string(),
532 required: true,
533 },
534 ],
535 example: json!({
536 "type": "postgresql_error_response",
537 "severity": "ERROR",
538 "code": "42P01",
539 "message": "relation \"table_name\" does not exist"
540 }),
541 log_template: Some(
542 LogTemplate::new()
543 .with_info("-> PostgreSQL {severity} {code}: {message}")
544 .with_debug("PostgreSQL error_response: {severity} {code}"),
545 ),
546 }
547});
548
549pub static POSTGRESQL_OK_RESPONSE_ACTION: LazyLock<ActionDefinition> =
551 LazyLock::new(|| ActionDefinition {
552 name: "postgresql_ok_response".to_string(),
553 description: "Command-complete tag for a statement that returns no rows: INSERT, \
554 UPDATE, DELETE, CREATE, SET, BEGIN. Never for SELECT or SHOW - those \
555 are answered with postgresql_query_response. The tag names the statement \
556 that ran (a SELECT answered here would print 'INSERT 0 1')."
557 .to_string(),
558 parameters: vec![Parameter {
559 name: "tag".to_string(),
560 type_hint: "string".to_string(),
561 description: "Command tag (e.g. 'INSERT 0 1', 'UPDATE 3', 'DELETE 2', 'CREATE TABLE')"
562 .to_string(),
563 required: true,
564 }],
565 example: json!({
566 "type": "postgresql_ok_response",
567 "tag": "INSERT 0 1"
568 }),
569 log_template: Some(
570 LogTemplate::new()
571 .with_info("-> PostgreSQL OK: {tag}")
572 .with_debug("PostgreSQL ok_response: {tag}"),
573 ),
574 });
575
576pub static POSTGRESQL_CLOSE_CONNECTION_ACTION: LazyLock<ActionDefinition> =
578 LazyLock::new(|| ActionDefinition {
579 name: "close_this_connection".to_string(),
580 description: "Close the current PostgreSQL connection".to_string(),
581 parameters: vec![],
582 example: json!({"type": "close_this_connection"}),
583 log_template: Some(
584 LogTemplate::new()
585 .with_info("PostgreSQL connection closed")
586 .with_debug("PostgreSQL close_this_connection"),
587 ),
588 });
589
590pub static POSTGRESQL_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
596 EventType::new(
597 "postgresql_query",
598 "PostgreSQL query received from client",
599 json!({"type": "placeholder", "event_id": "postgresql_query"}),
600 )
601 .with_parameters(vec![
602 Parameter {
603 name: "query".to_string(),
604 type_hint: "string".to_string(),
605 description: "The SQL query string sent by the client".to_string(),
606 required: true,
607 },
608 Parameter {
609 name: "answer_with".to_string(),
610 type_hint: "string".to_string(),
611 description: "Which response shape the statement needs, from its first keyword. \
612 Absent when the server cannot tell."
613 .to_string(),
614 required: false,
615 },
616 ])
617 .with_actions(vec![
618 POSTGRESQL_QUERY_RESPONSE_ACTION.clone(),
619 POSTGRESQL_ERROR_RESPONSE_ACTION.clone(),
620 POSTGRESQL_OK_RESPONSE_ACTION.clone(),
621 POSTGRESQL_CLOSE_CONNECTION_ACTION.clone(),
622 ])
623 .with_log_template(
624 LogTemplate::new()
625 .with_info("PostgreSQL {client_ip}: {preview(query,80)}")
626 .with_debug("PostgreSQL query from {client_ip}:{client_port}")
627 .with_trace("PostgreSQL: {json_pretty(.)}"),
628 )
629});
630
631pub fn answer_with_for_query(sql: &str) -> Option<&'static str> {
637 use crate::utils::sql::{statement_shape, StatementShape};
638 match statement_shape(sql) {
639 StatementShape::Rows => Some(
640 "postgresql_query_response: this statement returns rows - columns and rows, one \
641 column and one row for a single value - never a command tag",
642 ),
643 StatementShape::NoRows => Some(
644 "postgresql_ok_response: this statement returns no rows - the command tag of what \
645 ran, e.g. 'INSERT 0 1', 'UPDATE 3', 'SET'",
646 ),
647 StatementShape::Unknown => None,
648 }
649}
650
651pub fn get_postgresql_event_types() -> Vec<EventType> {
653 vec![POSTGRESQL_QUERY_EVENT.clone()]
654}