Skip to main content

netget/server/postgresql/
actions.rs

1//! PostgreSQL protocol actions implementation
2
3use 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
17/// PostgreSQL protocol action handler
18pub 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
40// Implement Protocol trait (common functionality)
41impl 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        // No user-triggered actions. (A `list_postgresql_connections` action used to be declared
57        // here; its executor returned a hardcoded empty list, so it only ever misled the model.)
58        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            // Beta: exercised against two independent clients that share no code with each
85            // other or with pgwire — tokio-postgres (Rust) and psql/libpq (C). Not Stable:
86            // Stable additionally wants spec compliance and scripting support reviewed, which
87            // has not been done here.
88            .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        // Deterministic answers for a version probe and generic SELECTs, with an
127        // empty command-complete tag for writes. Real column/row shapes.
128        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            // LLM mode: reason about the SQL to synthesise coherent rows.
150            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            // Script mode: fixed answers for known queries, no LLM call.
157            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            // Static mode: Fixed responses
171            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
191// Implement Server trait (server-specific functionality)
192impl 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            // `send_first` stays declared so a caller that passes `false` still validates - an
202            // undeclared key is refused outright - but `true` is refused rather than ignored.
203            // It was read here, threaded through spawn, and dropped at the far end as
204            // `_send_first`: a knob advertised to the model, plumbed through three hops, and
205            // silently doing nothing. PostgreSQL: the frontend sends the StartupMessage first, and a byte before it is a protocol violation the client cannot parse.
206            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        // Extract columns and rows from the action
252        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
337/// Action definition: Send PostgreSQL query response
338pub 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
381/// Action definition: Send PostgreSQL error response
382pub 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
420/// Action definition: Send PostgreSQL command complete response
421pub 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
448/// Action definition: Close current PostgreSQL connection
449pub 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
463// ============================================================================
464// PostgreSQL Action Constants
465// ============================================================================
466
467/// PostgreSQL query response action constant
468pub 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
510/// PostgreSQL error response action constant
511pub 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
549/// PostgreSQL OK response action constant
550pub 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
576/// PostgreSQL close connection action constant
577pub 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
590// ============================================================================
591// PostgreSQL Event Type Constants
592// ============================================================================
593
594/// PostgreSQL query event - triggered when client sends a query
595pub 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
631/// The `answer_with` event field for `sql`, or `None` when its first keyword does not say.
632///
633/// See `crate::utils::sql`: the real-model eval answered `SELECT current_user` with the command
634/// tag `INSERT 0 1`, copied from the ok action's example, so the client printed a tag instead of
635/// the value it asked for.
636pub 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
651/// Get PostgreSQL event types
652pub fn get_postgresql_event_types() -> Vec<EventType> {
653    vec![POSTGRESQL_QUERY_EVENT.clone()]
654}