Skip to main content

netget/client/db2/
mod.rs

1//! Db2 client: NetGet speaks DRDA to IBM Db2 or Apache Derby and the model decides what SQL to
2//! run. See `AGENTS.md` beside this file.
3//!
4//! Two tasks per connection, both registered with the client so `stop_client` ends them:
5//!
6//! * the **session** owns the socket. It runs the handshake before `connect` returns, then
7//!   executes one action at a time — the model's, or one injected from the dashboard or MCP —
8//!   as a DRDA request and its whole reply, and turns the reply into an event.
9//! * the **turns** task asks the model about each event and hands the actions it answers with
10//!   back to the session. Each event carries the number of model turns that led to it; past
11//!   [`MAX_FOLLOWUP_DEPTH`] the model is not asked again.
12//!
13//! Fail closed: a refused login fails `connect` and the model is never asked anything; an LLM
14//! failure sends nothing; a reply this client cannot read, a timeout or a server that hangs up
15//! ends the connection with an error rather than inventing a result.
16pub mod actions;
17pub mod wire;
18
19use std::net::SocketAddr;
20use std::time::Duration;
21
22use anyhow::{anyhow, bail, ensure, Context, Result};
23use serde_json::{json, Value};
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use tokio::net::TcpStream;
26use tokio::sync::mpsc;
27
28use crate::client::llm_budget::call_llm_for_client;
29use crate::llm::actions::client_trait::{Client, ClientActionResult};
30use crate::logging::emit::Log;
31use crate::protocol::{ConnectContext, Event, EventType};
32use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
33use crate::state::{AccessLogOwner, ClientStatus};
34pub use actions::Db2ClientProtocol;
35use wire::{cp, Object, Sqlca};
36
37/// How many model turns may follow from one another before the chain stops.
38pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
39/// How long a whole reply may take. A statement can wait on a lock (Derby's default lock
40/// timeout is a minute), so this is generous; past it the connection is given up.
41pub const REPLY_TIMEOUT: Duration = Duration::from_secs(120);
42const MAX_REPLY_TIMEOUT_SECS: u64 = 3600;
43/// How long the TCP connect may take.
44const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
45/// Apache Derby admits only product ids containing its own client's `DNC`; Db2 accepts any.
46pub const DEFAULT_PRODUCT_ID: &str = "DNC10170";
47pub const DEFAULT_AUTOCOMMIT: bool = false;
48/// The port a remote address without one is given: Db2's default instance port.
49pub const DEFAULT_PORT: u16 = 50000;
50/// Statements remembered for the unit of work reported on commit or rollback.
51const MAX_UOW_STATEMENTS: usize = 16;
52/// Each remembered statement is cut to this many bytes.
53const UOW_STATEMENT_BYTES: usize = 200;
54/// Events waiting for the model.
55const TURNS: usize = 64;
56/// The read buffer never holds more than one DSS at its bound plus one read.
57const MAX_READ_BUFFER: usize = wire::MAX_DSS_BYTES + 64 * 1024;
58
59/// Reply messages after which the conversation cannot continue.
60const FATAL_REPLIES: &[u16] = &[cp::SYNTAXRM, cp::PRCCNVRM, cp::AGNPRMRM, cp::MGRLVLRM];
61
62/// What the server said about itself during the handshake.
63#[derive(Debug, Clone, Default)]
64pub struct ServerInfo {
65    pub server_class: String,
66    pub server_name: String,
67    pub server_release: String,
68    pub product_id: String,
69}
70
71/// One DRDA connection, after or during the handshake.
72pub struct Conn {
73    stream: TcpStream,
74    buf: Vec<u8>,
75    pub repr: wire::Repr,
76    timeout: Duration,
77    pub bytes_out: usize,
78    package: Option<wire::Package>,
79    uow: Vec<String>,
80    autocommit: bool,
81}
82
83/// What one action produced: the event to raise.
84pub type Done = (&'static EventType, Value);
85
86fn find(objs: &[Object], codepoint: u16) -> Option<&Object> {
87    objs.iter().find(|o| o.codepoint == codepoint)
88}
89
90fn text_param(data: &[u8], codepoint: u16) -> Result<String> {
91    Ok(wire::param(data, codepoint)?
92        .map(wire::from_ebcdic)
93        .map(|s| crate::utils::sanitize::line_field(&s))
94        .unwrap_or_default())
95}
96
97fn remember(uow: &mut Vec<String>, sql: &str) {
98    if uow.len() == MAX_UOW_STATEMENTS {
99        uow.remove(0);
100    }
101    uow.push(crate::utils::truncate::truncate_with_suffix(
102        sql,
103        UOW_STATEMENT_BYTES,
104        "...",
105    ));
106}
107
108impl Conn {
109    pub fn new(stream: TcpStream, timeout: Duration, autocommit: bool) -> Self {
110        Self {
111            stream,
112            buf: Vec::with_capacity(32 * 1024),
113            repr: wire::Repr::default(),
114            timeout,
115            bytes_out: 0,
116            package: None,
117            uow: Vec::new(),
118            autocommit,
119        }
120    }
121
122    /// Send one command and read every DSS of its reply chain.
123    pub async fn exchange(&mut self, parts: Vec<(u8, Vec<u8>)>) -> Result<Vec<Object>> {
124        // Correlators distinguish the commands of one request chain. Each chain here is one
125        // command, so each starts at 1, as Derby's and IBM's clients do; Derby answers a chain
126        // that starts anywhere else with correlator 1 regardless.
127        let corr = 1;
128        let bytes = wire::command(corr, &parts);
129        let timeout = self.timeout;
130        tokio::time::timeout(timeout, async {
131            self.stream.write_all(&bytes).await?;
132            self.stream.flush().await?;
133            self.bytes_out += bytes.len();
134            let mut out = Vec::new();
135            let mut total = 0usize;
136            for count in 1.. {
137                let dss = self.next_dss().await?;
138                ensure!(
139                    count <= wire::MAX_REPLY_DSS,
140                    "the reply runs past {} DSSes; refused",
141                    wire::MAX_REPLY_DSS
142                );
143                total += dss.payload.len();
144                ensure!(
145                    total <= wire::MAX_REPLY_BYTES,
146                    "the reply runs past {} bytes; refused",
147                    wire::MAX_REPLY_BYTES
148                );
149                ensure!(
150                    dss.correlator == corr,
151                    "a reply DSS carries correlator {} where the request had {corr}",
152                    dss.correlator
153                );
154                ensure!(
155                    matches!(dss.dss_type, wire::DSS_RPYDSS | wire::DSS_OBJDSS),
156                    "a reply DSS of type {} (neither a reply nor an object)",
157                    dss.dss_type
158                );
159                for obj in wire::objects(&dss.payload)? {
160                    match obj.codepoint {
161                        cp::TYPDEFNAM => self.repr.set_name(&obj.data)?,
162                        cp::TYPDEFOVR => self.repr.set_overrides(&obj.data)?,
163                        _ => out.push(obj),
164                    }
165                }
166                if !dss.chained {
167                    break;
168                }
169            }
170            Ok(out)
171        })
172        .await
173        .map_err(|_| anyhow!("no complete reply within {}s", timeout.as_secs()))?
174    }
175
176    async fn next_dss(&mut self) -> Result<wire::Dss> {
177        let mut chunk = vec![0u8; 32 * 1024];
178        loop {
179            if let Some(dss) = wire::take_dss(&mut self.buf)? {
180                return Ok(dss);
181            }
182            let n = self.stream.read(&mut chunk).await?;
183            ensure!(n > 0, "the server closed the connection mid-reply");
184            self.buf.extend_from_slice(&chunk[..n]);
185            ensure!(
186                self.buf.len() <= MAX_READ_BUFFER,
187                "the server sent more than {MAX_READ_BUFFER} bytes without completing a DSS"
188            );
189        }
190    }
191
192    /// The first reply message at severity ERROR or worse, as a sentence.
193    fn refusal(objs: &[Object]) -> Result<Option<(u16, String)>> {
194        for o in objs {
195            let is_reply_message = matches!(o.codepoint >> 8, 0x12 | 0x22);
196            if !is_reply_message {
197                continue;
198            }
199            let svrcod = wire::severity(&o.data)?;
200            if svrcod < 8 {
201                continue;
202            }
203            let mut msg = format!("{} at severity {svrcod}", wire::name_of(o.codepoint));
204            if let Some([code]) = wire::param(&o.data, cp::SYNERRCD)? {
205                msg.push_str(&format!(", syntax error code 0x{code:02X}"));
206            }
207            if let Some([a, b]) = wire::param(&o.data, cp::CODPNT)? {
208                let c = u16::from_be_bytes([*a, *b]);
209                msg.push_str(&format!(", about {}", wire::name_of(c)));
210            }
211            return Ok(Some((o.codepoint, msg)));
212        }
213        Ok(None)
214    }
215
216    fn sqlca(&self, objs: &[Object]) -> Result<Option<Sqlca>> {
217        let mut found = None;
218        for o in objs.iter().filter(|o| o.codepoint == cp::SQLCARD) {
219            if let Some(ca) = wire::parse_sqlcard(&o.data, &self.repr)? {
220                // The most serious SQLCA wins: an error over a warning over success.
221                let worse = match &found {
222                    None => true,
223                    Some(Sqlca { sqlcode, .. }) => ca.sqlcode < 0 && *sqlcode >= 0,
224                };
225                if worse {
226                    found = Some(ca);
227                }
228            }
229        }
230        Ok(found)
231    }
232
233    /// The SQL or DRDA failure in a reply, as a `db2_error` event; `Err` when the conversation
234    /// cannot go on.
235    fn failure(&self, objs: &[Object], operation: &str, sql: Option<&str>) -> Result<Option<Done>> {
236        let rolled_back = find(objs, cp::ABNUOWRM).is_some();
237        if let Some(ca) = self.sqlca(objs)? {
238            if ca.sqlcode < 0 {
239                let mut data = json!({
240                    "operation": operation,
241                    "sqlcode": ca.sqlcode,
242                    "sqlstate": ca.sqlstate,
243                    "message_tokens": ca.tokens,
244                    "message": format!("the server refused it with SQLCODE {} (SQLSTATE {})", ca.sqlcode, ca.sqlstate),
245                    "rolled_back": rolled_back,
246                });
247                if let Some(sql) = sql {
248                    data["sql"] = json!(sql);
249                }
250                return Ok(Some((&actions::ERROR_EVENT, data)));
251            }
252        }
253        if let Some((codepoint, message)) = Self::refusal(objs)? {
254            if FATAL_REPLIES.contains(&codepoint) {
255                bail!(
256                    "{operation}: the server answered {message}; the conversation cannot continue"
257                );
258            }
259            let mut data = json!({
260                "operation": operation,
261                "reply_message": wire::name_of(codepoint),
262                "message": format!("the server answered {message}"),
263                "rolled_back": rolled_back,
264            });
265            if let Some(sql) = sql {
266                data["sql"] = json!(sql);
267            }
268            return Ok(Some((&actions::ERROR_EVENT, data)));
269        }
270        Ok(None)
271    }
272
273    fn error(operation: &str, sql: Option<&str>, message: String) -> Done {
274        let mut data = json!({ "operation": operation, "message": message });
275        if let Some(sql) = sql {
276            data["sql"] = json!(sql);
277        }
278        (&actions::ERROR_EVENT, data)
279    }
280
281    /// EXCSAT, ACCSEC, SECCHK, ACCRDB. Any refusal is an error: the caller fails `connect`.
282    pub async fn handshake(
283        &mut self,
284        database: &str,
285        user: &str,
286        password: Option<&str>,
287        product_id: &str,
288        crrtkn: Vec<u8>,
289    ) -> Result<ServerInfo> {
290        let mut info = ServerInfo::default();
291
292        let objs = self
293            .exchange(vec![(wire::DSS_RQSDSS, wire::excsat(product_id)?)])
294            .await?;
295        let rd = find(&objs, cp::EXCSATRD).ok_or_else(|| match Self::refusal(&objs) {
296            Ok(Some((_, m))) => anyhow!("EXCSAT refused: {m}"),
297            _ => anyhow!("the server did not answer EXCSAT with EXCSATRD"),
298        })?;
299        info.server_class = text_param(&rd.data, cp::SRVCLSNM)?;
300        info.server_name = text_param(&rd.data, cp::SRVNAM)?;
301        info.server_release = text_param(&rd.data, cp::SRVRLSLV)?;
302        if let Some(levels) = wire::param(&rd.data, cp::MGRLVLLS)? {
303            for pair in levels.chunks_exact(4) {
304                let manager = u16::from_be_bytes([pair[0], pair[1]]);
305                let level = u16::from_be_bytes([pair[2], pair[3]]);
306                if manager == cp::SQLAM {
307                    ensure!(
308                        level >= wire::SQLAM_LEVEL,
309                        "the server speaks SQLAM level {level}; this client reads level {}",
310                        wire::SQLAM_LEVEL
311                    );
312                }
313            }
314        }
315
316        let secmec = if password.is_some() {
317            wire::SECMEC_USRIDPWD
318        } else {
319            wire::SECMEC_USRIDONL
320        };
321        let objs = self
322            .exchange(vec![(wire::DSS_RQSDSS, wire::accsec(secmec, database)?)])
323            .await?;
324        let rd = find(&objs, cp::ACCSECRD).ok_or_else(|| match Self::refusal(&objs) {
325            Ok(Some((_, m))) => anyhow!("ACCSEC refused: {m}"),
326            _ => anyhow!("the server did not answer ACCSEC with ACCSECRD"),
327        })?;
328        let offered: Vec<u16> = wire::params(&rd.data)?
329            .into_iter()
330            .filter(|(c, _)| *c == cp::SECMEC)
331            .flat_map(|(_, v)| {
332                v.chunks_exact(2)
333                    .map(|c| u16::from_be_bytes([c[0], c[1]]))
334                    .collect::<Vec<_>>()
335            })
336            .collect();
337        ensure!(
338            offered.contains(&secmec),
339            "the server does not accept security mechanism {secmec} ({}); it offered {offered:?}. \
340             This client implements USRIDPWD (3) and USRIDONL (4) only",
341            if secmec == 3 {
342                "user id and password"
343            } else {
344                "user id only"
345            }
346        );
347        if let Some([code]) = wire::param(&rd.data, cp::SECCHKCD)? {
348            ensure!(
349                *code == 0,
350                "ACCSEC refused: SECCHKCD 0x{code:02X} ({})",
351                wire::secchkcd_meaning(*code)
352            );
353        }
354
355        let objs = self
356            .exchange(vec![(
357                wire::DSS_RQSDSS,
358                wire::secchk(secmec, database, user, password)?,
359            )])
360            .await?;
361        let rm = find(&objs, cp::SECCHKRM).ok_or_else(|| match Self::refusal(&objs) {
362            Ok(Some((_, m))) => anyhow!("SECCHK refused: {m}"),
363            _ => anyhow!("the server did not answer SECCHK with SECCHKRM"),
364        })?;
365        let code = match wire::param(&rm.data, cp::SECCHKCD)? {
366            Some([c]) => *c,
367            _ => bail!("SECCHKRM carries no SECCHKCD"),
368        };
369        let svrcod = wire::severity(&rm.data)?;
370        ensure!(
371            code == 0 && svrcod < 8,
372            "login refused: SECCHKCD 0x{code:02X} ({}) at severity {svrcod} decision=refused",
373            wire::secchkcd_meaning(code)
374        );
375
376        let objs = self
377            .exchange(vec![(
378                wire::DSS_RQSDSS,
379                wire::accrdb(database, product_id, &crrtkn)?,
380            )])
381            .await?;
382        let Some(rm) = find(&objs, cp::ACCRDBRM) else {
383            let why = match (Self::refusal(&objs)?, self.sqlca(&objs)?) {
384                (Some((_, m)), Some(ca)) => format!(
385                    "{m}; SQLCODE {} SQLSTATE {} {:?}",
386                    ca.sqlcode, ca.sqlstate, ca.tokens
387                ),
388                (Some((_, m)), None) => m,
389                (None, _) => "no ACCRDBRM".to_string(),
390            };
391            bail!("opening database {database:?} refused: {why} decision=refused");
392        };
393        let svrcod = wire::severity(&rm.data)?;
394        ensure!(
395            svrcod < 8,
396            "opening database {database:?} refused: ACCRDBRM at severity {svrcod} decision=refused"
397        );
398        info.product_id = text_param(&rm.data, cp::PRDID)?;
399        if let Some(name) = wire::param(&rm.data, cp::TYPDEFNAM)? {
400            self.repr.set_name(name)?;
401        }
402        if let Some(ovr) = wire::param(&rm.data, cp::TYPDEFOVR)? {
403            self.repr.set_overrides(ovr)?;
404        }
405        if let Some(ca) = self.sqlca(&objs)? {
406            ensure!(
407                ca.sqlcode >= 0,
408                "opening database {database:?} failed: SQLCODE {} SQLSTATE {}",
409                ca.sqlcode,
410                ca.sqlstate
411            );
412        }
413        self.package = Some(wire::Package::new(database)?);
414        Ok(info)
415    }
416
417    fn package(&self) -> Result<&wire::Package> {
418        self.package.as_ref().context("the database is not open")
419    }
420
421    /// Run one action. `Ok` carries the event to raise (including SQL errors); `Err` means
422    /// the connection is unusable.
423    pub async fn perform(&mut self, name: &str, data: &Value) -> Result<Done> {
424        let sql = data.get("sql").and_then(Value::as_str).unwrap_or_default();
425        match name {
426            "execute_statement" => self.execute_statement(sql).await,
427            "execute_query" => {
428                let max_rows =
429                    data.get("max_rows")
430                        .and_then(Value::as_u64)
431                        .unwrap_or(wire::DEFAULT_MAX_ROWS as u64) as usize;
432                self.execute_query(sql, max_rows.min(wire::MAX_ROWS_LIMIT))
433                    .await
434            }
435            "commit" => self.end_unit_of_work(true).await,
436            "rollback" => self.end_unit_of_work(false).await,
437            other => bail!("no such Db2 operation {other}"),
438        }
439    }
440
441    async fn execute_statement(&mut self, sql: &str) -> Result<Done> {
442        let parts = wire::excsqlimm(self.package()?, sql)?;
443        let objs = self.exchange(parts).await?;
444        if let Some(fail) = self.failure(&objs, "execute_statement", Some(sql))? {
445            if find(&objs, cp::ABNUOWRM).is_some() {
446                self.uow.clear();
447            }
448            return Ok(fail);
449        }
450        let ca = self.sqlca(&objs)?.unwrap_or_default();
451        remember(&mut self.uow, sql);
452        let mut data = json!({
453            "sql": sql,
454            "sqlcode": ca.sqlcode,
455            "sqlstate": ca.sqlstate.trim(),
456            "row_count": ca.row_count(),
457            "message_tokens": ca.tokens,
458            "committed": false,
459        });
460        if self.autocommit {
461            match self.end_unit_of_work(true).await? {
462                (event, _) if std::ptr::eq(event, &*actions::TRANSACTION_RESULT_EVENT) => {
463                    data["committed"] = json!(true)
464                }
465                failed => return Ok(failed),
466            }
467        }
468        Ok((&actions::STATEMENT_RESULT_EVENT, data))
469    }
470
471    async fn execute_query(&mut self, sql: &str, max_rows: usize) -> Result<Done> {
472        let parts = wire::prpsqlstt(self.package()?, sql)?;
473        let objs = self.exchange(parts).await?;
474        if let Some(fail) = self.failure(&objs, "prepare", Some(sql))? {
475            return Ok(fail);
476        }
477        let Some(dard) = find(&objs, cp::SQLDARD) else {
478            return Ok(Self::error(
479                "prepare",
480                Some(sql),
481                "the server described no result (no SQLDARD)".into(),
482            ));
483        };
484        let (ca, columns) = match wire::parse_sqldard(&dard.data, &self.repr) {
485            Ok(v) => v,
486            Err(e) => return Ok(Self::error("prepare", Some(sql), format!("{e:#}"))),
487        };
488        if let Some(ca) = ca.filter(|ca| ca.sqlcode < 0) {
489            return Ok((
490                &actions::ERROR_EVENT,
491                json!({"operation": "prepare", "sql": sql, "sqlcode": ca.sqlcode,
492                       "sqlstate": ca.sqlstate, "message_tokens": ca.tokens,
493                       "message": format!("the server refused it with SQLCODE {}", ca.sqlcode)}),
494            ));
495        }
496        if columns.is_empty() {
497            return Ok(Self::error(
498                "prepare",
499                Some(sql),
500                "the statement returns no rows; run it with execute_statement".into(),
501            ));
502        }
503        remember(&mut self.uow, sql);
504
505        let objs = self.exchange(wire::opnqry(self.package()?)).await?;
506        if let Some(fail) = self.failure(&objs, "open_query", Some(sql))? {
507            return Ok(fail);
508        }
509        let Some(rm) = find(&objs, cp::OPNQRYRM) else {
510            return Ok(Self::error(
511                "open_query",
512                Some(sql),
513                "the server did not open the query (no OPNQRYRM)".into(),
514            ));
515        };
516        let qryinsid = wire::param(&rm.data, cp::QRYINSID)?
517            .map(<[u8]>::to_vec)
518            .unwrap_or_default();
519        let dsc: Vec<u8> = objs
520            .iter()
521            .filter(|o| o.codepoint == cp::QRYDSC)
522            .flat_map(|o| o.data.iter().copied())
523            .collect();
524        let fields = match wire::parse_qrydsc(&dsc) {
525            Ok(f) if f.len() == columns.len() => f,
526            Ok(f) => {
527                self.close_query(&qryinsid).await?;
528                return Ok(Self::error(
529                    "open_query",
530                    Some(sql),
531                    format!(
532                        "the row layout has {} fields where the description has {} columns",
533                        f.len(),
534                        columns.len()
535                    ),
536                ));
537            }
538            Err(e) => {
539                self.close_query(&qryinsid).await?;
540                return Ok(Self::error("open_query", Some(sql), format!("{e:#}")));
541            }
542        };
543
544        let mut data: Vec<u8> = Vec::new();
545        let mut received = 0usize;
546        let mut ended = false;
547        let mut failed: Option<Sqlca> = None;
548        absorb(
549            &objs,
550            &mut data,
551            &mut received,
552            &mut ended,
553            &mut failed,
554            &self.repr,
555        )?;
556
557        let mut rows: Vec<Value> = Vec::new();
558        let mut warnings: Vec<Value> = Vec::new();
559        let mut truncated = false;
560        let mut done = false;
561        let mut continues = 0usize;
562        let mut decode_error: Option<String> = None;
563        'fetch: loop {
564            let mut pos = 0;
565            while pos < data.len() {
566                match wire::decode_row(&data[pos..], &fields, &self.repr) {
567                    Err(e) => {
568                        decode_error = Some(format!("{e:#}"));
569                        break 'fetch;
570                    }
571                    Ok(None) => break,
572                    Ok(Some((item, used))) => {
573                        pos += used;
574                        match item {
575                            wire::RowItem::Row(values, warning) => {
576                                if rows.len() == max_rows {
577                                    truncated = true;
578                                    break 'fetch;
579                                }
580                                rows.push(wire::row_object(&columns, values));
581                                if let Some(w) = warning {
582                                    warnings.push(w.to_json());
583                                }
584                            }
585                            wire::RowItem::End(_) => {
586                                done = true;
587                                break 'fetch;
588                            }
589                            wire::RowItem::Failed(ca) => {
590                                failed = Some(ca);
591                                break 'fetch;
592                            }
593                            wire::RowItem::Empty => {}
594                        }
595                    }
596                }
597            }
598            data.drain(..pos);
599            if failed.is_some() {
600                break;
601            }
602            if ended {
603                if !data.is_empty() {
604                    decode_error = Some(format!(
605                        "the query ended with {} bytes of an unfinished row",
606                        data.len()
607                    ));
608                }
609                done = true;
610                break;
611            }
612            continues += 1;
613            if continues > wire::MAX_CONTINUES {
614                decode_error = Some(format!(
615                    "the answer set took more than {} query blocks; refused",
616                    wire::MAX_CONTINUES
617                ));
618                break;
619            }
620            let objs = self
621                .exchange(wire::cntqry(self.package()?, &qryinsid))
622                .await?;
623            if find(&objs, cp::QRYNOPRM).is_some() {
624                // The server already closed it: nothing more will come.
625                done = true;
626                break;
627            }
628            if let Some(fail) = self.failure(&objs, "fetch", Some(sql))? {
629                return Ok(fail);
630            }
631            absorb(
632                &objs,
633                &mut data,
634                &mut received,
635                &mut ended,
636                &mut failed,
637                &self.repr,
638            )?;
639            if received > wire::MAX_RESULT_BYTES {
640                decode_error = Some(format!(
641                    "the answer set passed {} bytes; refused",
642                    wire::MAX_RESULT_BYTES
643                ));
644                break;
645            }
646        }
647        if !done && failed.is_none() {
648            self.close_query(&qryinsid).await?;
649        }
650        if let Some(ca) = failed {
651            return Ok((
652                &actions::ERROR_EVENT,
653                json!({"operation": "fetch", "sql": sql, "sqlcode": ca.sqlcode,
654                       "sqlstate": ca.sqlstate, "message_tokens": ca.tokens,
655                       "message": format!("the query failed after {} rows with SQLCODE {}", rows.len(), ca.sqlcode)}),
656            ));
657        }
658        if let Some(message) = decode_error {
659            return Ok(Self::error("fetch", Some(sql), message));
660        }
661        let data = json!({
662            "sql": sql,
663            "columns": columns.iter().map(wire::Column::to_json).collect::<Vec<_>>(),
664            "row_count": rows.len(),
665            "rows": rows,
666            "truncated": truncated,
667            "warnings": warnings,
668        });
669        if self.autocommit {
670            let (event, failed) = self.end_unit_of_work(true).await?;
671            if !std::ptr::eq(event, &*actions::TRANSACTION_RESULT_EVENT) {
672                return Ok((event, failed));
673            }
674        }
675        Ok((&actions::QUERY_RESULT_EVENT, data))
676    }
677
678    /// CLSQRY, tolerating a query the server already closed.
679    async fn close_query(&mut self, qryinsid: &[u8]) -> Result<()> {
680        if qryinsid.is_empty() {
681            return Ok(());
682        }
683        let objs = self
684            .exchange(wire::clsqry(self.package()?, qryinsid))
685            .await?;
686        if let Some((codepoint, message)) = Self::refusal(&objs)? {
687            ensure!(
688                !FATAL_REPLIES.contains(&codepoint),
689                "closing the query: {message}"
690            );
691        }
692        Ok(())
693    }
694
695    async fn end_unit_of_work(&mut self, commit: bool) -> Result<Done> {
696        let operation = if commit { "commit" } else { "rollback" };
697        let parts = if commit {
698            wire::rdbcmm()
699        } else {
700            wire::rdbrllbck()
701        };
702        let objs = self.exchange(parts).await?;
703        if let Some(fail) = self.failure(&objs, operation, None)? {
704            return Ok(fail);
705        }
706        let disposition = match find(&objs, cp::ENDUOWRM) {
707            Some(rm) => match wire::param(&rm.data, cp::UOWDSP)? {
708                Some([1]) => "committed",
709                Some([2]) => "rolled_back",
710                _ => operation_outcome(commit),
711            },
712            None => operation_outcome(commit),
713        };
714        let ca = self.sqlca(&objs)?.unwrap_or_default();
715        let statements = std::mem::take(&mut self.uow);
716        Ok((
717            &actions::TRANSACTION_RESULT_EVENT,
718            json!({
719                "operation": operation,
720                "outcome": disposition,
721                "statements": statements,
722                "sqlcode": ca.sqlcode,
723            }),
724        ))
725    }
726}
727
728fn operation_outcome(commit: bool) -> &'static str {
729    if commit {
730        "committed"
731    } else {
732        "rolled_back"
733    }
734}
735
736/// Take a reply's answer-set data: QRYDTA bytes, and whether the query ended.
737fn absorb(
738    objs: &[Object],
739    data: &mut Vec<u8>,
740    received: &mut usize,
741    ended: &mut bool,
742    failed: &mut Option<Sqlca>,
743    repr: &wire::Repr,
744) -> Result<()> {
745    for o in objs {
746        match o.codepoint {
747            cp::QRYDTA => {
748                *received += o.data.len();
749                ensure!(
750                    *received <= wire::MAX_RESULT_BYTES,
751                    "the answer set passed {} bytes; refused",
752                    wire::MAX_RESULT_BYTES
753                );
754                data.extend_from_slice(&o.data);
755            }
756            cp::ENDQRYRM => *ended = true,
757            cp::SQLCARD if *ended => {
758                if let Some(ca) = wire::parse_sqlcard(&o.data, repr)? {
759                    if ca.sqlcode < 0 {
760                        *failed = Some(ca);
761                    }
762                }
763            }
764            cp::EXTDTA => {
765                bail!("the answer set carries LOB data (EXTDTA), which this client does not read")
766            }
767            _ => {}
768        }
769    }
770    Ok(())
771}
772
773async fn resolve(raw: &str) -> Result<String> {
774    let raw = raw.trim();
775    ensure!(
776        !raw.is_empty(),
777        "the Db2 client needs a remote_addr (host:port); refusing to connect without one"
778    );
779    if raw.parse::<SocketAddr>().is_ok() {
780        return Ok(raw.to_string());
781    }
782    if raw
783        .rsplit_once(':')
784        .is_some_and(|(h, p)| !h.contains(':') && p.parse::<u16>().is_ok())
785    {
786        return Ok(raw.to_string());
787    }
788    Ok(format!("{}:{DEFAULT_PORT}", raw.trim_matches(['[', ']'])))
789}
790
791pub async fn connect(ctx: ConnectContext) -> Result<SocketAddr> {
792    let params = ctx.startup_params.as_ref();
793    let string = |key: &str| -> Result<Option<String>> {
794        Ok(params
795            .map(|p| p.get_optional_string(key))
796            .transpose()?
797            .flatten())
798    };
799    let database = string("database")?
800        .filter(|d| !d.trim().is_empty())
801        .context("the Db2 client needs the database startup parameter (the RDB name)")?;
802    let user = string("username")?
803        .filter(|u| !u.is_empty())
804        .context("the Db2 client needs the username startup parameter")?;
805    let password = string("password")?;
806    let product_id = string("product_id")?.unwrap_or_else(|| DEFAULT_PRODUCT_ID.to_string());
807    let autocommit = params
808        .map(|p| p.get_optional_bool("autocommit"))
809        .transpose()?
810        .flatten()
811        .unwrap_or(DEFAULT_AUTOCOMMIT);
812    let timeout_secs = params
813        .map(|p| p.get_optional_u64("reply_timeout_secs"))
814        .transpose()?
815        .flatten()
816        .unwrap_or(REPLY_TIMEOUT.as_secs());
817    ensure!(
818        (1..=MAX_REPLY_TIMEOUT_SECS).contains(&timeout_secs),
819        "reply_timeout_secs is 1..={MAX_REPLY_TIMEOUT_SECS}"
820    );
821
822    let target = resolve(&ctx.remote_addr).await?;
823    let stream = tokio::time::timeout(CONNECT_TIMEOUT, TcpStream::connect(&target))
824        .await
825        .map_err(|_| {
826            anyhow!(
827                "connecting to {target} took more than {}s",
828                CONNECT_TIMEOUT.as_secs()
829            )
830        })?
831        .with_context(|| format!("cannot connect to Db2 at {target}"))?;
832    let local = stream.local_addr()?;
833    let peer = stream.peer_addr()?;
834    let mut conn = Conn::new(stream, Duration::from_secs(timeout_secs), autocommit);
835    let token = wire::correlation_token(local, rand::random());
836    let log = Log::new(Some(&ctx.status_tx));
837    let info = match conn
838        .handshake(&database, &user, password.as_deref(), &product_id, token)
839        .await
840    {
841        Ok(info) => info,
842        Err(e) => {
843            log.warn(format!(
844                "Db2 client {}: handshake with {peer} failed: {e:#} decision=fail_closed_handshake",
845                ctx.client_id
846            ));
847            return Err(e);
848        }
849    };
850    ctx.state
851        .update_client_status(ctx.client_id, ClientStatus::Connected)
852        .await;
853    log.info(format!(
854        "Db2 client {} connected to {peer} database {database:?} as {user} ({} {}, {})",
855        ctx.client_id, info.server_class, info.server_release, conn.repr.typdefnam
856    ));
857    let _ = ctx.status_tx.send("__UPDATE_UI__".into());
858
859    // Registered before the connected turn can run, so the operator can reach the client
860    // while that turn is parked on a manual rule.
861    let external =
862        crate::client::command_support::register_command_channel(&ctx.state, ctx.client_id).await;
863    let (event_tx, event_rx) = mpsc::channel::<(Event, u32)>(TURNS);
864    let (internal_tx, internal) = mpsc::channel::<(Value, u32)>(TURNS);
865    let _ = event_tx.try_send((
866        Event::new(
867            &actions::CONNECTED_EVENT,
868            json!({
869                "remote_addr": peer.to_string(),
870                "database": database,
871                "user": user,
872                "server_class": info.server_class,
873                "server_release": info.server_release,
874                "server_product_id": info.product_id,
875                "data_representation": conn.repr.typdefnam,
876            }),
877        ),
878        0,
879    ));
880    let dispatcher = tokio::spawn(run_turns(ctx.clone(), event_rx, internal_tx));
881    let dispatcher_abort = dispatcher.abort_handle();
882    ctx.state
883        .register_client_task(ctx.client_id, dispatcher)
884        .await;
885    let session_ctx = ctx.clone();
886    let task = tokio::spawn(async move {
887        let result = run_session(&mut conn, &session_ctx, external, internal, event_tx).await;
888        dispatcher_abort.abort();
889        let _ = conn.stream.shutdown().await;
890        let status = match result {
891            Ok(()) => ClientStatus::Disconnected,
892            Err(e) => {
893                Log::new(Some(&session_ctx.status_tx)).warn(format!(
894                    "Db2 client {} ended: {e:#} decision=connection_failed",
895                    session_ctx.client_id
896                ));
897                ClientStatus::Error(e.to_string())
898            }
899        };
900        session_ctx
901            .state
902            .update_client_status(session_ctx.client_id, status)
903            .await;
904        session_ctx
905            .state
906            .remove_client_handle(session_ctx.client_id)
907            .await;
908        let _ = session_ctx.status_tx.send("__UPDATE_UI__".into());
909    });
910    ctx.state.register_client_task(ctx.client_id, task).await;
911    Ok(local)
912}
913
914fn reply(command: Option<ClientCommand>, outcome: ClientSendOutcome) {
915    if let Some(c) = command {
916        crate::client::command_support::reply(c, Ok(outcome));
917    }
918}
919
920/// Execute actions one at a time until the connection ends.
921async fn run_session(
922    conn: &mut Conn,
923    ctx: &ConnectContext,
924    mut external: mpsc::Receiver<ClientCommand>,
925    mut internal: mpsc::Receiver<(Value, u32)>,
926    events: mpsc::Sender<(Event, u32)>,
927) -> Result<()> {
928    let protocol = Db2ClientProtocol;
929    let mut idle = [0u8; 1024];
930    loop {
931        let (action, depth, command) = tokio::select! {
932            c = external.recv() => match c {
933                Some(c) => {
934                    ctx.state.record_access_log(AccessLogOwner::Client(ctx.client_id.as_u32()), "Db2",
935                        None, "injected_action", c.action.clone(), vec![]).await;
936                    (c.action.clone(), 0, Some(c))
937                }
938                None => return Ok(()),
939            },
940            a = internal.recv() => match a {
941                Some((a, d)) => (a, d, None),
942                None => return Ok(()),
943            },
944            r = conn.stream.read(&mut idle) => {
945                return match r {
946                    Ok(0) => Err(anyhow!("the server closed the connection")),
947                    Ok(n) => Err(anyhow!("the server sent {n} bytes with no request outstanding")),
948                    Err(e) => Err(e.into()),
949                };
950            }
951        };
952        let (name, data) = match protocol.execute_action(action) {
953            Ok(ClientActionResult::Disconnect) => {
954                reply(command, ClientSendOutcome::Disconnected);
955                return Ok(());
956            }
957            Ok(ClientActionResult::Custom { name, data }) => (name, data),
958            Ok(_) => {
959                reply(
960                    command,
961                    ClientSendOutcome::Executed {
962                        detail: "nothing to send".into(),
963                    },
964                );
965                continue;
966            }
967            Err(e) => {
968                Log::new(Some(&ctx.status_tx)).warn(format!(
969                    "Db2 client {}: action refused: {e:#}",
970                    ctx.client_id
971                ));
972                reply(
973                    command,
974                    ClientSendOutcome::Rejected {
975                        error: e.to_string(),
976                    },
977                );
978                continue;
979            }
980        };
981        let before = conn.bytes_out;
982        let (event_type, payload) = match conn.perform(&name, &data).await {
983            Ok(done) => done,
984            Err(e) => {
985                reply(
986                    command,
987                    ClientSendOutcome::Rejected {
988                        error: format!("{e:#}"),
989                    },
990                );
991                return Err(e);
992            }
993        };
994        reply(
995            command,
996            ClientSendOutcome::Sent {
997                bytes_sent: conn.bytes_out - before,
998            },
999        );
1000        tracing::info!(
1001            "Db2 client {} {name} -> {} (depth {depth})",
1002            ctx.client_id,
1003            event_type.id
1004        );
1005        if events
1006            .try_send((Event::new(event_type, payload), depth))
1007            .is_err()
1008        {
1009            Log::new(Some(&ctx.status_tx)).warn(format!(
1010                "Db2 client {}: {} events are waiting for the model; this one is dropped \
1011                 decision=turn_queue_full",
1012                ctx.client_id, TURNS
1013            ));
1014        }
1015        let _ = ctx.status_tx.send("__UPDATE_UI__".into());
1016    }
1017}
1018
1019/// Ask the model about each event, in order, and hand its actions to the session.
1020async fn run_turns(
1021    ctx: ConnectContext,
1022    mut events: mpsc::Receiver<(Event, u32)>,
1023    internal: mpsc::Sender<(Value, u32)>,
1024) {
1025    let protocol = Db2ClientProtocol;
1026    while let Some((event, depth)) = events.recv().await {
1027        if depth >= MAX_FOLLOWUP_DEPTH {
1028            Log::new(Some(&ctx.status_tx)).warn(format!(
1029                "Db2 client {} not asking the model about {}: {depth} turns deep \
1030                 decision=followup_depth",
1031                ctx.client_id,
1032                event.id()
1033            ));
1034            continue;
1035        }
1036        let instruction = ctx
1037            .state
1038            .get_instruction_for_client(ctx.client_id)
1039            .await
1040            .unwrap_or_default();
1041        let memory = ctx
1042            .state
1043            .get_memory_for_client(ctx.client_id)
1044            .await
1045            .unwrap_or_default();
1046        match call_llm_for_client(
1047            &ctx.llm_client,
1048            &ctx.state,
1049            ctx.client_id.to_string(),
1050            &instruction,
1051            &memory,
1052            Some(&event),
1053            &protocol,
1054            &ctx.status_tx,
1055        )
1056        .await
1057        {
1058            Ok(result) => {
1059                if let Some(memory) = result.memory_updates {
1060                    ctx.state.set_memory_for_client(ctx.client_id, memory).await;
1061                }
1062                tracing::info!(
1063                    "Db2 client {} {} decision={} actions={}",
1064                    ctx.client_id,
1065                    event.id(),
1066                    if result.actions.is_empty() {
1067                        "model_silent"
1068                    } else {
1069                        "model_actions"
1070                    },
1071                    result.actions.len()
1072                );
1073                for action in result.actions {
1074                    if internal.send((action, depth + 1)).await.is_err() {
1075                        return;
1076                    }
1077                }
1078            }
1079            // Fail closed: nothing is sent on the model's behalf when it cannot be asked.
1080            Err(e) => Log::new(Some(&ctx.status_tx)).warn(format!(
1081                "Db2 client {} {}: {e:#} decision=fail_closed_llm_error",
1082                ctx.client_id,
1083                event.id()
1084            )),
1085        }
1086    }
1087}