1pub 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
37pub const MAX_FOLLOWUP_DEPTH: u32 = 8;
39pub const REPLY_TIMEOUT: Duration = Duration::from_secs(120);
42const MAX_REPLY_TIMEOUT_SECS: u64 = 3600;
43const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
45pub const DEFAULT_PRODUCT_ID: &str = "DNC10170";
47pub const DEFAULT_AUTOCOMMIT: bool = false;
48pub const DEFAULT_PORT: u16 = 50000;
50const MAX_UOW_STATEMENTS: usize = 16;
52const UOW_STATEMENT_BYTES: usize = 200;
54const TURNS: usize = 64;
56const MAX_READ_BUFFER: usize = wire::MAX_DSS_BYTES + 64 * 1024;
58
59const FATAL_REPLIES: &[u16] = &[cp::SYNTAXRM, cp::PRCCNVRM, cp::AGNPRMRM, cp::MGRLVLRM];
61
62#[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
71pub 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
83pub 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 pub async fn exchange(&mut self, parts: Vec<(u8, Vec<u8>)>) -> Result<Vec<Object>> {
124 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 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 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 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 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 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 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 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
736fn 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 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
920async 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
1019async 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 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}