1use std::io::Write;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use alopex_embedded::{Database, TransactionManager as Transaction, TxnMode};
10use serde::{Deserialize, Serialize};
11
12use crate::batch::BatchMode;
13use crate::cli::{KvCommand, KvTxnCommand, OutputFormat};
14use crate::client::http::{ClientError, HttpClient};
15use crate::error::{CliError, Result};
16use crate::models::{Column, DataType, Row, Value};
17use crate::output::formatter::Formatter;
18use crate::output::RowCollector;
19use crate::streaming::{StreamingWriter, WriteStatus};
20use crate::tui::admin::{AdminBackend, AdminContext, AdminTarget, AuthCapabilities};
21use crate::tui::renderer::render_output;
22
23const DEFAULT_TXN_TIMEOUT_SECS: u64 = 60;
24
25#[derive(Debug, Serialize)]
26struct RemoteKvGetRequest {
27 key: String,
28}
29
30#[derive(Debug, Serialize)]
31struct RemoteKvPutRequest {
32 key: String,
33 value: Vec<u8>,
34}
35
36#[derive(Debug, Serialize)]
37struct RemoteKvDeleteRequest {
38 key: String,
39}
40
41#[derive(Debug, Serialize)]
42struct RemoteKvListRequest {
43 prefix: Option<String>,
44}
45
46#[derive(Debug, Serialize)]
47struct RemoteKvTxnBeginRequest {
48 timeout_secs: Option<u64>,
49}
50
51#[derive(Debug, Serialize)]
52struct RemoteKvTxnGetRequest {
53 txn_id: String,
54 key: String,
55}
56
57#[derive(Debug, Serialize)]
58struct RemoteKvTxnPutRequest {
59 txn_id: String,
60 key: String,
61 value: Vec<u8>,
62}
63
64#[derive(Debug, Serialize)]
65struct RemoteKvTxnDeleteRequest {
66 txn_id: String,
67 key: String,
68}
69
70#[derive(Debug, Serialize)]
71struct RemoteKvTxnCommitRequest {
72 txn_id: String,
73}
74
75#[derive(Debug, Serialize)]
76struct RemoteKvTxnRollbackRequest {
77 txn_id: String,
78}
79
80#[derive(Debug, Deserialize)]
81struct RemoteKvGetResponse {
82 value: Option<Vec<u8>>,
83}
84
85#[derive(Debug, Deserialize)]
86struct RemoteKvListEntry {
87 key: Vec<u8>,
88 value: Vec<u8>,
89}
90
91#[derive(Debug, Deserialize)]
92struct RemoteKvListResponse {
93 entries: Vec<RemoteKvListEntry>,
94}
95
96#[derive(Debug, Deserialize)]
97struct RemoteKvStatusResponse {
98 success: bool,
99}
100
101#[derive(Debug, Deserialize)]
102struct RemoteKvTxnBeginResponse {
103 txn_id: String,
104}
105
106pub fn execute<W: Write>(
114 db: &Database,
115 cmd: KvCommand,
116 writer: &mut StreamingWriter<W>,
117) -> Result<()> {
118 match cmd {
119 KvCommand::Get { key } => execute_get(db, &key, writer),
120 KvCommand::Put { key, value } => execute_put(db, &key, &value, writer),
121 KvCommand::Delete { key } => execute_delete(db, &key, writer),
122 KvCommand::List { prefix } => execute_list(db, prefix.as_deref(), writer),
123 KvCommand::Txn(cmd) => execute_txn_command(db, cmd, writer),
124 }
125}
126
127#[allow(clippy::too_many_arguments)]
128pub fn execute_tui(
129 db: &Database,
130 cmd: KvCommand,
131 batch_mode: &BatchMode,
132 output_format: OutputFormat,
133 columns: Vec<Column>,
134 limit: Option<usize>,
135 quiet: bool,
136 connection_label: impl Into<String>,
137 data_dir: Option<PathBuf>,
138) -> Result<()> {
139 let connection_label = connection_label.into();
140 let context_message = Some(kv_command_context(&cmd));
141 let admin_label = connection_label.clone();
142 let admin_data_dir = data_dir.clone();
143 let admin_launcher: Option<Box<dyn FnMut() -> Result<()> + '_>> = Some(Box::new(move || {
144 let connection_label = admin_label.clone();
145 let data_dir = admin_data_dir.clone();
146 crate::tui::admin::run_admin_ui(AdminContext {
147 connection_label,
148 auth: AuthCapabilities::full(),
149 backend: AdminBackend::Local {
150 db,
151 batch_mode,
152 output_format,
153 limit,
154 quiet,
155 data_dir,
156 },
157 initial_target: Some(AdminTarget::Kv),
158 })
159 }));
160 let collector = RowCollector::new();
161 let formatter = Box::new(collector.formatter());
162 let mut sink = std::io::sink();
163 let mut writer =
164 StreamingWriter::new(&mut sink, formatter, columns.clone(), limit).with_quiet(quiet);
165 execute(db, cmd, &mut writer)?;
166 let warning = collector.truncation_warning();
167 render_output(
168 columns,
169 collector.rows(),
170 connection_label,
171 context_message,
172 true,
173 warning,
174 output_format,
175 admin_launcher,
176 )
177}
178
179pub async fn execute_remote_with_formatter<W: Write>(
181 client: &HttpClient,
182 cmd: &KvCommand,
183 writer: &mut W,
184 formatter: Box<dyn Formatter>,
185 limit: Option<usize>,
186 quiet: bool,
187) -> Result<()> {
188 match cmd {
189 KvCommand::Get { key } => {
190 let request = RemoteKvGetRequest { key: key.clone() };
191 let response: RemoteKvGetResponse = client
192 .post_json("kv/get", &request)
193 .await
194 .map_err(map_client_error)?;
195 let Some(value) = response.value else {
196 return Err(CliError::InvalidArgument(format!("Key not found: {}", key)));
197 };
198
199 let columns = kv_columns();
200 let mut streaming_writer =
201 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
202 streaming_writer.prepare(Some(1))?;
203 let row = Row::new(vec![Value::Text(key.clone()), bytes_to_value(value)]);
204 streaming_writer.write_row(row)?;
205 streaming_writer.finish()
206 }
207 KvCommand::Put { key, value } => {
208 let request = RemoteKvPutRequest {
209 key: key.clone(),
210 value: value.as_bytes().to_vec(),
211 };
212 let response: RemoteKvStatusResponse = client
213 .post_json("kv/put", &request)
214 .await
215 .map_err(map_client_error)?;
216 if response.success {
217 if quiet {
218 return Ok(());
219 }
220 let columns = kv_status_columns();
221 let mut streaming_writer =
222 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
223 streaming_writer.prepare(Some(1))?;
224 let row = Row::new(vec![
225 Value::Text("OK".to_string()),
226 Value::Text(format!("Set key: {}", key)),
227 ]);
228 streaming_writer.write_row(row)?;
229 streaming_writer.finish()
230 } else {
231 Err(CliError::InvalidArgument("Failed to set key".to_string()))
232 }
233 }
234 KvCommand::Delete { key } => {
235 let request = RemoteKvDeleteRequest { key: key.clone() };
236 let response: RemoteKvStatusResponse = client
237 .post_json("kv/delete", &request)
238 .await
239 .map_err(map_client_error)?;
240 if response.success {
241 if quiet {
242 return Ok(());
243 }
244 let columns = kv_status_columns();
245 let mut streaming_writer =
246 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
247 streaming_writer.prepare(Some(1))?;
248 let row = Row::new(vec![
249 Value::Text("OK".to_string()),
250 Value::Text(format!("Deleted key: {}", key)),
251 ]);
252 streaming_writer.write_row(row)?;
253 streaming_writer.finish()
254 } else {
255 Err(CliError::InvalidArgument(
256 "Failed to delete key".to_string(),
257 ))
258 }
259 }
260 KvCommand::List { prefix } => {
261 let request = RemoteKvListRequest {
262 prefix: prefix.clone(),
263 };
264 let response: RemoteKvListResponse = client
265 .post_json("kv/list", &request)
266 .await
267 .map_err(map_client_error)?;
268 let columns = kv_columns();
269 let mut streaming_writer =
270 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
271 streaming_writer.prepare(Some(response.entries.len()))?;
272 for entry in response.entries {
273 let row = Row::new(vec![bytes_to_value(entry.key), bytes_to_value(entry.value)]);
274 match streaming_writer.write_row(row)? {
275 WriteStatus::LimitReached => break,
276 WriteStatus::Continue => {}
277 }
278 }
279 streaming_writer.finish()
280 }
281 KvCommand::Txn(txn_cmd) => {
282 execute_remote_txn_command(client, txn_cmd, writer, formatter, limit, quiet).await
283 }
284 }
285}
286
287#[allow(clippy::too_many_arguments)]
288pub async fn execute_remote_tui<'a>(
289 client: &HttpClient,
290 cmd: &KvCommand,
291 columns: Vec<Column>,
292 output_format: OutputFormat,
293 limit: Option<usize>,
294 quiet: bool,
295 connection_label: impl Into<String>,
296 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
297) -> Result<()> {
298 let collector = RowCollector::new();
299 let formatter = Box::new(collector.formatter());
300 let mut sink = std::io::sink();
301 execute_remote_with_formatter(client, cmd, &mut sink, formatter, limit, quiet).await?;
302 let warning = collector.truncation_warning();
303 render_output(
304 columns,
305 collector.rows(),
306 connection_label,
307 Some(kv_command_context(cmd)),
308 true,
309 warning,
310 output_format,
311 admin_launcher,
312 )
313}
314
315async fn execute_remote_txn_command<W: Write>(
316 client: &HttpClient,
317 cmd: &KvTxnCommand,
318 writer: &mut W,
319 formatter: Box<dyn Formatter>,
320 limit: Option<usize>,
321 quiet: bool,
322) -> Result<()> {
323 match cmd {
324 KvTxnCommand::Begin { timeout_secs } => {
325 let request = RemoteKvTxnBeginRequest {
326 timeout_secs: *timeout_secs,
327 };
328 let response: RemoteKvTxnBeginResponse = client
329 .post_json("kv/txn/begin", &request)
330 .await
331 .map_err(map_client_error)?;
332 let columns = kv_columns();
333 let mut streaming_writer =
334 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
335 streaming_writer.prepare(Some(1))?;
336 let row = Row::new(vec![
337 Value::Text("txn_id".to_string()),
338 Value::Text(response.txn_id),
339 ]);
340 streaming_writer.write_row(row)?;
341 streaming_writer.finish()
342 }
343 KvTxnCommand::Get { key, txn_id } => {
344 let request = RemoteKvTxnGetRequest {
345 txn_id: txn_id.clone(),
346 key: key.clone(),
347 };
348 let response: RemoteKvGetResponse = client
349 .post_json("kv/txn/get", &request)
350 .await
351 .map_err(map_client_error)?;
352 let Some(value) = response.value else {
353 return Err(CliError::InvalidArgument(format!("Key not found: {}", key)));
354 };
355 let columns = kv_columns();
356 let mut streaming_writer =
357 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
358 streaming_writer.prepare(Some(1))?;
359 let row = Row::new(vec![Value::Text(key.clone()), bytes_to_value(value)]);
360 streaming_writer.write_row(row)?;
361 streaming_writer.finish()
362 }
363 KvTxnCommand::Put { key, value, txn_id } => {
364 let request = RemoteKvTxnPutRequest {
365 txn_id: txn_id.clone(),
366 key: key.clone(),
367 value: value.as_bytes().to_vec(),
368 };
369 let response: RemoteKvStatusResponse = client
370 .post_json("kv/txn/put", &request)
371 .await
372 .map_err(map_client_error)?;
373 if response.success {
374 let columns = kv_status_columns();
375 let mut streaming_writer =
376 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
377 write_status_if_needed(&mut streaming_writer, &format!("Staged key: {}", key))
378 } else {
379 Err(CliError::InvalidArgument("Failed to stage key".to_string()))
380 }
381 }
382 KvTxnCommand::Delete { key, txn_id } => {
383 let request = RemoteKvTxnDeleteRequest {
384 txn_id: txn_id.clone(),
385 key: key.clone(),
386 };
387 let response: RemoteKvStatusResponse = client
388 .post_json("kv/txn/delete", &request)
389 .await
390 .map_err(map_client_error)?;
391 if response.success {
392 let columns = kv_status_columns();
393 let mut streaming_writer =
394 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
395 write_status_if_needed(&mut streaming_writer, &format!("Staged delete: {}", key))
396 } else {
397 Err(CliError::InvalidArgument(
398 "Failed to stage delete".to_string(),
399 ))
400 }
401 }
402 KvTxnCommand::Commit { txn_id } => {
403 let request = RemoteKvTxnCommitRequest {
404 txn_id: txn_id.clone(),
405 };
406 let response: RemoteKvStatusResponse = client
407 .post_json("kv/txn/commit", &request)
408 .await
409 .map_err(map_client_error)?;
410 if response.success {
411 let columns = kv_status_columns();
412 let mut streaming_writer =
413 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
414 write_status_if_needed(
415 &mut streaming_writer,
416 &format!("Committed transaction: {}", txn_id),
417 )
418 } else {
419 Err(CliError::InvalidArgument(
420 "Failed to commit transaction".to_string(),
421 ))
422 }
423 }
424 KvTxnCommand::Rollback { txn_id } => {
425 let request = RemoteKvTxnRollbackRequest {
426 txn_id: txn_id.clone(),
427 };
428 let response: RemoteKvStatusResponse = client
429 .post_json("kv/txn/rollback", &request)
430 .await
431 .map_err(map_client_error)?;
432 if response.success {
433 let columns = kv_status_columns();
434 let mut streaming_writer =
435 StreamingWriter::new(writer, formatter, columns, limit).with_quiet(quiet);
436 write_status_if_needed(
437 &mut streaming_writer,
438 &format!("Rolled back transaction: {}", txn_id),
439 )
440 } else {
441 Err(CliError::InvalidArgument(
442 "Failed to rollback transaction".to_string(),
443 ))
444 }
445 }
446 }
447}
448
449fn map_client_error(err: ClientError) -> CliError {
450 match err {
451 ClientError::Request { source, .. } => {
452 CliError::ServerConnection(format!("request failed: {source}"))
453 }
454 ClientError::InvalidUrl(message) => CliError::InvalidArgument(message),
455 ClientError::Build(message) => CliError::InvalidArgument(message),
456 ClientError::Auth(err) => CliError::InvalidArgument(err.to_string()),
457 ClientError::HttpStatus { status, body } => {
458 CliError::InvalidArgument(format!("Server error: HTTP {} - {}", status.as_u16(), body))
459 }
460 }
461}
462
463fn kv_command_context(cmd: &KvCommand) -> String {
464 match cmd {
465 KvCommand::Get { key } => format!("kv get {key}"),
466 KvCommand::Put { key, .. } => format!("kv put {key}"),
467 KvCommand::Delete { key } => format!("kv delete {key}"),
468 KvCommand::List { prefix } => match prefix {
469 Some(prefix) => format!("kv list --prefix {prefix}"),
470 None => "kv list".to_string(),
471 },
472 KvCommand::Txn(command) => match command {
473 KvTxnCommand::Begin { timeout_secs } => match timeout_secs {
474 Some(secs) => format!("kv txn begin --timeout-secs {secs}"),
475 None => "kv txn begin".to_string(),
476 },
477 KvTxnCommand::Get { key, txn_id } => {
478 format!("kv txn get {key} --txn-id {txn_id}")
479 }
480 KvTxnCommand::Put { key, txn_id, .. } => {
481 format!("kv txn put {key} --txn-id {txn_id}")
482 }
483 KvTxnCommand::Delete { key, txn_id } => {
484 format!("kv txn delete {key} --txn-id {txn_id}")
485 }
486 KvTxnCommand::Commit { txn_id } => format!("kv txn commit --txn-id {txn_id}"),
487 KvTxnCommand::Rollback { txn_id } => format!("kv txn rollback --txn-id {txn_id}"),
488 },
489 }
490}
491
492fn bytes_to_value(bytes: Vec<u8>) -> Value {
493 match std::str::from_utf8(&bytes) {
494 Ok(s) => Value::Text(s.to_string()),
495 Err(_) => Value::Bytes(bytes),
496 }
497}
498
499fn map_txn_error(txn_id: &str, err: alopex_embedded::Error) -> CliError {
500 match err {
501 alopex_embedded::Error::InvalidTransactionId(_) => {
502 CliError::InvalidTransactionId(txn_id.to_string())
503 }
504 other => CliError::Database(other),
505 }
506}
507
508fn map_txn_result<T>(
509 txn_id: &str,
510 result: std::result::Result<T, alopex_embedded::Error>,
511) -> Result<T> {
512 result.map_err(|err| map_txn_error(txn_id, err))
513}
514
515fn ensure_txn_not_expired(db: &Database, txn_id: &str) -> Result<()> {
516 let expired = map_txn_result(txn_id, Transaction::is_expired(db, txn_id))?;
517 if expired {
518 let _ = Transaction::rollback(db, txn_id);
519 return Err(CliError::TransactionTimeout(txn_id.to_string()));
520 }
521 Ok(())
522}
523
524fn execute_txn_command<W: Write>(
525 db: &Database,
526 cmd: KvTxnCommand,
527 writer: &mut StreamingWriter<W>,
528) -> Result<()> {
529 match cmd {
530 KvTxnCommand::Begin { timeout_secs } => execute_txn_begin(db, timeout_secs, writer),
531 KvTxnCommand::Get { key, txn_id } => execute_txn_get(db, &key, &txn_id, writer),
532 KvTxnCommand::Put { key, value, txn_id } => {
533 execute_txn_put(db, &key, &value, &txn_id, writer)
534 }
535 KvTxnCommand::Delete { key, txn_id } => execute_txn_delete(db, &key, &txn_id, writer),
536 KvTxnCommand::Commit { txn_id } => execute_txn_commit(db, &txn_id, writer),
537 KvTxnCommand::Rollback { txn_id } => execute_txn_rollback(db, &txn_id, writer),
538 }
539}
540
541fn execute_txn_begin<W: Write>(
542 db: &Database,
543 timeout_secs: Option<u64>,
544 writer: &mut StreamingWriter<W>,
545) -> Result<()> {
546 let timeout = Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TXN_TIMEOUT_SECS));
547 let txn_id = Transaction::begin_with_timeout(db, timeout)?;
548
549 writer.prepare(Some(1))?;
550 let row = Row::new(vec![Value::Text("txn_id".to_string()), Value::Text(txn_id)]);
551 writer.write_row(row)?;
552 writer.finish()?;
553 Ok(())
554}
555
556fn execute_txn_get<W: Write>(
557 db: &Database,
558 key: &str,
559 txn_id: &str,
560 writer: &mut StreamingWriter<W>,
561) -> Result<()> {
562 ensure_txn_not_expired(db, txn_id)?;
563 let value = map_txn_result(txn_id, Transaction::get(db, txn_id, key.as_bytes()))?;
564 write_kv_value(key, value, writer)
565}
566
567fn execute_txn_put<W: Write>(
568 db: &Database,
569 key: &str,
570 value: &str,
571 txn_id: &str,
572 writer: &mut StreamingWriter<W>,
573) -> Result<()> {
574 ensure_txn_not_expired(db, txn_id)?;
575 map_txn_result(
576 txn_id,
577 Transaction::put(db, txn_id, key.as_bytes(), value.as_bytes()),
578 )?;
579
580 write_status_if_needed(writer, &format!("Staged key: {}", key))
581}
582
583fn execute_txn_delete<W: Write>(
584 db: &Database,
585 key: &str,
586 txn_id: &str,
587 writer: &mut StreamingWriter<W>,
588) -> Result<()> {
589 ensure_txn_not_expired(db, txn_id)?;
590 map_txn_result(txn_id, Transaction::delete(db, txn_id, key.as_bytes()))?;
591
592 write_status_if_needed(writer, &format!("Staged delete: {}", key))
593}
594
595fn execute_txn_commit<W: Write>(
596 db: &Database,
597 txn_id: &str,
598 writer: &mut StreamingWriter<W>,
599) -> Result<()> {
600 ensure_txn_not_expired(db, txn_id)?;
601 map_txn_result(txn_id, Transaction::commit(db, txn_id))?;
602
603 write_status_if_needed(writer, &format!("Committed transaction: {}", txn_id))
604}
605
606fn execute_txn_rollback<W: Write>(
607 db: &Database,
608 txn_id: &str,
609 writer: &mut StreamingWriter<W>,
610) -> Result<()> {
611 map_txn_result(txn_id, Transaction::rollback(db, txn_id))?;
612
613 write_status_if_needed(writer, &format!("Rolled back transaction: {}", txn_id))
614}
615
616fn write_kv_value<W: Write>(
617 key: &str,
618 value: Option<Vec<u8>>,
619 writer: &mut StreamingWriter<W>,
620) -> Result<()> {
621 let Some(value) = value else {
622 return Err(CliError::InvalidArgument(format!("Key not found: {}", key)));
623 };
624
625 let value_display = match std::str::from_utf8(&value) {
626 Ok(s) => Value::Text(s.to_string()),
627 Err(_) => Value::Bytes(value),
628 };
629 writer.prepare(Some(1))?;
630 let row = Row::new(vec![Value::Text(key.to_string()), value_display]);
631 writer.write_row(row)?;
632 writer.finish()?;
633 Ok(())
634}
635
636fn write_status_if_needed<W: Write>(writer: &mut StreamingWriter<W>, message: &str) -> Result<()> {
637 if writer.is_quiet() {
638 return Ok(());
639 }
640
641 writer.prepare(Some(1))?;
642 let row = Row::new(vec![
643 Value::Text("OK".to_string()),
644 Value::Text(message.to_string()),
645 ]);
646 writer.write_row(row)?;
647 writer.finish()?;
648 Ok(())
649}
650
651fn execute_get<W: Write>(db: &Database, key: &str, writer: &mut StreamingWriter<W>) -> Result<()> {
653 let mut txn = db.begin(TxnMode::ReadOnly)?;
654 let result = txn.get(key.as_bytes())?;
655 txn.commit()?;
656
657 writer.prepare(Some(1))?;
659
660 match result {
661 Some(value) => {
662 let value_display = match std::str::from_utf8(&value) {
664 Ok(s) => Value::Text(s.to_string()),
665 Err(_) => Value::Bytes(value),
666 };
667
668 let row = Row::new(vec![Value::Text(key.to_string()), value_display]);
669 writer.write_row(row)?;
670 }
671 None => {
672 return Err(CliError::InvalidArgument(format!("Key not found: {}", key)));
673 }
674 }
675
676 writer.finish()?;
677 Ok(())
678}
679
680fn execute_put<W: Write>(
682 db: &Database,
683 key: &str,
684 value: &str,
685 writer: &mut StreamingWriter<W>,
686) -> Result<()> {
687 let mut txn = db.begin(TxnMode::ReadWrite)?;
688 txn.put(key.as_bytes(), value.as_bytes())?;
689 txn.commit()?;
690
691 if !writer.is_quiet() {
693 writer.prepare(Some(1))?;
694 let row = Row::new(vec![
695 Value::Text("OK".to_string()),
696 Value::Text(format!("Stored key: {}", key)),
697 ]);
698 writer.write_row(row)?;
699 writer.finish()?;
700 }
701
702 Ok(())
703}
704
705fn execute_delete<W: Write>(
707 db: &Database,
708 key: &str,
709 writer: &mut StreamingWriter<W>,
710) -> Result<()> {
711 let mut txn = db.begin(TxnMode::ReadWrite)?;
712
713 let exists = txn.get(key.as_bytes())?.is_some();
715
716 if !exists {
717 txn.rollback()?;
718 return Err(CliError::InvalidArgument(format!("Key not found: {}", key)));
719 }
720
721 txn.delete(key.as_bytes())?;
722 txn.commit()?;
723
724 if !writer.is_quiet() {
726 writer.prepare(Some(1))?;
727 let row = Row::new(vec![
728 Value::Text("OK".to_string()),
729 Value::Text(format!("Deleted key: {}", key)),
730 ]);
731 writer.write_row(row)?;
732 writer.finish()?;
733 }
734
735 Ok(())
736}
737
738fn execute_list<W: Write>(
740 db: &Database,
741 prefix: Option<&str>,
742 writer: &mut StreamingWriter<W>,
743) -> Result<()> {
744 let mut txn = db.begin(TxnMode::ReadOnly)?;
745
746 let prefix_bytes = prefix.map(|p| p.as_bytes()).unwrap_or(b"");
747 let iter = txn.scan_prefix(prefix_bytes)?;
748
749 writer.prepare(None)?;
751
752 for (key, value) in iter {
754 let key_display = match std::str::from_utf8(&key) {
756 Ok(s) => Value::Text(s.to_string()),
757 Err(_) => Value::Bytes(key),
758 };
759 let value_display = match std::str::from_utf8(&value) {
760 Ok(s) => Value::Text(s.to_string()),
761 Err(_) => Value::Bytes(value),
762 };
763
764 let row = Row::new(vec![key_display, value_display]);
765 match writer.write_row(row)? {
766 WriteStatus::LimitReached => break,
767 WriteStatus::Continue => {}
768 }
769 }
770
771 txn.commit()?;
772 writer.finish()?;
773 Ok(())
774}
775
776pub fn kv_columns() -> Vec<Column> {
778 vec![
779 Column::new("key", DataType::Text),
780 Column::new("value", DataType::Text),
781 ]
782}
783
784pub fn kv_status_columns() -> Vec<Column> {
786 vec![
787 Column::new("status", DataType::Text),
788 Column::new("message", DataType::Text),
789 ]
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::output::jsonl::JsonlFormatter;
796
797 fn create_test_db() -> Database {
798 Database::open_in_memory().unwrap()
799 }
800
801 fn create_test_writer(output: &mut Vec<u8>) -> StreamingWriter<&mut Vec<u8>> {
802 let formatter = Box::new(JsonlFormatter::new());
803 let columns = kv_columns();
804 StreamingWriter::new(output, formatter, columns, None)
805 }
806
807 fn create_status_writer(output: &mut Vec<u8>) -> StreamingWriter<&mut Vec<u8>> {
808 let formatter = Box::new(JsonlFormatter::new());
809 let columns = kv_status_columns();
810 StreamingWriter::new(output, formatter, columns, None)
811 }
812
813 #[test]
814 fn test_put_and_get() {
815 let db = create_test_db();
816
817 let mut output = Vec::new();
819 {
820 let mut writer = create_status_writer(&mut output);
821 execute_put(&db, "test_key", "test_value", &mut writer).unwrap();
822 }
823
824 let mut output = Vec::new();
826 {
827 let mut writer = create_test_writer(&mut output);
828 execute_get(&db, "test_key", &mut writer).unwrap();
829 }
830
831 let result = String::from_utf8(output).unwrap();
832 assert!(result.contains("test_key"));
833 assert!(result.contains("test_value"));
834 }
835
836 #[test]
837 fn test_get_not_found() {
838 let db = create_test_db();
839
840 let mut output = Vec::new();
841 let mut writer = create_test_writer(&mut output);
842
843 let result = execute_get(&db, "nonexistent", &mut writer);
844 assert!(result.is_err());
845 assert!(matches!(result.unwrap_err(), CliError::InvalidArgument(_)));
846 }
847
848 #[test]
849 fn test_delete() {
850 let db = create_test_db();
851
852 {
854 let mut output = Vec::new();
855 let mut writer = create_status_writer(&mut output);
856 execute_put(&db, "to_delete", "value", &mut writer).unwrap();
857 }
858
859 {
861 let mut output = Vec::new();
862 let mut writer = create_status_writer(&mut output);
863 execute_delete(&db, "to_delete", &mut writer).unwrap();
864 }
865
866 {
868 let mut output = Vec::new();
869 let mut writer = create_test_writer(&mut output);
870 let result = execute_get(&db, "to_delete", &mut writer);
871 assert!(result.is_err());
872 }
873 }
874
875 #[test]
876 fn test_delete_not_found() {
877 let db = create_test_db();
878
879 let mut output = Vec::new();
880 let mut writer = create_status_writer(&mut output);
881
882 let result = execute_delete(&db, "nonexistent", &mut writer);
883 assert!(result.is_err());
884 assert!(matches!(result.unwrap_err(), CliError::InvalidArgument(_)));
885 }
886
887 #[test]
888 fn test_list_all() {
889 let db = create_test_db();
890
891 {
893 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
894 txn.put(b"key1", b"value1").unwrap();
895 txn.put(b"key2", b"value2").unwrap();
896 txn.put(b"key3", b"value3").unwrap();
897 txn.commit().unwrap();
898 }
899
900 let mut output = Vec::new();
902 {
903 let mut writer = create_test_writer(&mut output);
904 execute_list(&db, None, &mut writer).unwrap();
905 }
906
907 let result = String::from_utf8(output).unwrap();
908 assert!(result.contains("key1"));
909 assert!(result.contains("key2"));
910 assert!(result.contains("key3"));
911 }
912
913 #[test]
914 fn test_list_with_prefix() {
915 let db = create_test_db();
916
917 {
919 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
920 txn.put(b"user:1", b"Alice").unwrap();
921 txn.put(b"user:2", b"Bob").unwrap();
922 txn.put(b"item:1", b"Widget").unwrap();
923 txn.commit().unwrap();
924 }
925
926 let mut output = Vec::new();
928 {
929 let mut writer = create_test_writer(&mut output);
930 execute_list(&db, Some("user:"), &mut writer).unwrap();
931 }
932
933 let result = String::from_utf8(output).unwrap();
934 assert!(result.contains("user:1"));
935 assert!(result.contains("user:2"));
936 assert!(!result.contains("item:1"));
937 }
938
939 #[test]
940 fn test_list_empty() {
941 let db = create_test_db();
942
943 let mut output = Vec::new();
944 {
945 let mut writer = create_test_writer(&mut output);
946 execute_list(&db, None, &mut writer).unwrap();
947 }
948
949 let result = String::from_utf8(output).unwrap();
951 assert!(result.is_empty() || result.lines().count() == 0);
953 }
954}