1use crate::result_map::RowData;
45use crate::value::Value;
46use std::collections::HashMap;
47use std::sync::RwLock;
48use std::time::{Duration, Instant};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
56pub enum HydrationMode {
57 #[default]
59 Object,
60 Array,
62 Scalar,
64 SingleScalar,
66 Column,
68}
69
70impl HydrationMode {
71 pub fn name(&self) -> &'static str {
73 match self {
74 HydrationMode::Object => "object",
75 HydrationMode::Array => "array",
76 HydrationMode::Scalar => "scalar",
77 HydrationMode::SingleScalar => "single_scalar",
78 HydrationMode::Column => "column",
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
89pub enum HydrationError {
90 SingleScalarRequiresSingleRow { actual_rows: usize },
92 ColumnNotFound { column: String },
94 EmptyRow,
96}
97
98impl std::fmt::Display for HydrationError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 HydrationError::SingleScalarRequiresSingleRow { actual_rows } => {
102 write!(
103 f,
104 "SingleScalar mode requires exactly 1 row, got {}",
105 actual_rows
106 )
107 }
108 HydrationError::ColumnNotFound { column } => {
109 write!(f, "column '{}' not found", column)
110 }
111 HydrationError::EmptyRow => write!(f, "row has no columns"),
112 }
113 }
114}
115
116impl std::error::Error for HydrationError {}
117
118pub type HydrationResult<T> = Result<T, HydrationError>;
120
121pub fn hydrate_object(rows: &[RowData]) -> HydrationResult<Vec<HashMap<String, Value>>> {
123 Ok(rows
124 .iter()
125 .map(|r| {
126 let mut map = HashMap::new();
127 for (k, v) in r.iter() {
128 map.insert(k.clone(), v.clone());
129 }
130 map
131 })
132 .collect())
133}
134
135pub fn hydrate_array(rows: &[RowData]) -> HydrationResult<Vec<Vec<Value>>> {
137 let mut result = Vec::with_capacity(rows.len());
138 for row in rows {
139 let sorted = row.sorted_columns();
141 let values: Vec<Value> = sorted.iter().map(|(_, v)| (*v).clone()).collect();
142 result.push(values);
143 }
144 Ok(result)
145}
146
147pub fn hydrate_scalar(rows: &[RowData]) -> HydrationResult<Vec<Value>> {
149 let mut result = Vec::with_capacity(rows.len());
150 for row in rows {
151 if row.is_empty() {
152 return Err(HydrationError::EmptyRow);
153 }
154 let sorted = row.sorted_columns();
155 let (_, first_value) = sorted
156 .first()
157 .expect("sorted_columns is non-empty after is_empty check");
158 result.push((*first_value).clone());
159 }
160 Ok(result)
161}
162
163pub fn hydrate_single_scalar(rows: &[RowData]) -> HydrationResult<Value> {
165 if rows.len() != 1 {
166 return Err(HydrationError::SingleScalarRequiresSingleRow {
167 actual_rows: rows.len(),
168 });
169 }
170 let row = &rows[0];
171 if row.is_empty() {
172 return Err(HydrationError::EmptyRow);
173 }
174 let sorted = row.sorted_columns();
175 let (_, first_value) = sorted
176 .first()
177 .expect("sorted_columns is non-empty after is_empty check");
178 Ok((*first_value).clone())
179}
180
181pub fn hydrate_column(rows: &[RowData], column: &str) -> HydrationResult<Vec<Value>> {
183 let mut result = Vec::with_capacity(rows.len());
184 for row in rows {
185 match row.get(column) {
186 Some(v) => result.push(v.clone()),
187 None => {
188 return Err(HydrationError::ColumnNotFound {
189 column: column.to_string(),
190 })
191 }
192 }
193 }
194 Ok(result)
195}
196
197pub fn hydrate(rows: &[RowData], mode: HydrationMode) -> HydrationResult<Vec<Value>> {
202 match mode {
203 HydrationMode::Scalar => hydrate_scalar(rows),
204 HydrationMode::SingleScalar => {
205 let v = hydrate_single_scalar(rows)?;
206 Ok(vec![v])
207 }
208 HydrationMode::Column => {
209 if rows.is_empty() {
210 return Ok(Vec::new());
211 }
212 let first_row = &rows[0];
213 if first_row.is_empty() {
214 return Err(HydrationError::EmptyRow);
215 }
216 let sorted = first_row.sorted_columns();
217 let first_col = sorted
218 .first()
219 .expect("sorted_columns is non-empty after is_empty check")
220 .0
221 .as_str();
222 hydrate_column(rows, first_col)
223 }
224 HydrationMode::Object | HydrationMode::Array => {
225 hydrate_scalar(rows)
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
239pub enum ExecutionStage {
240 BeforeQuery,
242 AfterQuery,
244 BeforeUpdate,
246 AfterUpdate,
248 BeforeCommit,
250 AfterCommit,
252 BeforeRollback,
254 AfterRollback,
256}
257
258impl ExecutionStage {
259 pub fn name(&self) -> &'static str {
261 match self {
262 ExecutionStage::BeforeQuery => "before_query",
263 ExecutionStage::AfterQuery => "after_query",
264 ExecutionStage::BeforeUpdate => "before_update",
265 ExecutionStage::AfterUpdate => "after_update",
266 ExecutionStage::BeforeCommit => "before_commit",
267 ExecutionStage::AfterCommit => "after_commit",
268 ExecutionStage::BeforeRollback => "before_rollback",
269 ExecutionStage::AfterRollback => "after_rollback",
270 }
271 }
272
273 pub fn is_before(&self) -> bool {
275 matches!(
276 self,
277 ExecutionStage::BeforeQuery
278 | ExecutionStage::BeforeUpdate
279 | ExecutionStage::BeforeCommit
280 | ExecutionStage::BeforeRollback
281 )
282 }
283
284 pub fn is_after(&self) -> bool {
286 !self.is_before()
287 }
288
289 pub fn is_query(&self) -> bool {
291 matches!(
292 self,
293 ExecutionStage::BeforeQuery | ExecutionStage::AfterQuery
294 )
295 }
296
297 pub fn is_update(&self) -> bool {
299 matches!(
300 self,
301 ExecutionStage::BeforeUpdate | ExecutionStage::AfterUpdate
302 )
303 }
304
305 pub fn is_transaction(&self) -> bool {
307 matches!(
308 self,
309 ExecutionStage::BeforeCommit
310 | ExecutionStage::AfterCommit
311 | ExecutionStage::BeforeRollback
312 | ExecutionStage::AfterRollback
313 )
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct PluginContext {
320 pub stage: ExecutionStage,
322 pub sql: String,
324 pub parameters: Vec<Value>,
326 pub started_at: Option<Instant>,
328 pub elapsed: Option<Duration>,
330 pub affected_rows: Option<usize>,
332 pub metadata: HashMap<String, Value>,
334}
335
336impl PluginContext {
337 pub fn new(stage: ExecutionStage, sql: impl Into<String>) -> Self {
339 Self {
340 stage,
341 sql: sql.into(),
342 parameters: Vec::new(),
343 started_at: None,
344 elapsed: None,
345 affected_rows: None,
346 metadata: HashMap::new(),
347 }
348 }
349
350 pub fn with_parameters(mut self, params: Vec<Value>) -> Self {
352 self.parameters = params;
353 self
354 }
355
356 pub fn with_start_time(mut self, instant: Instant) -> Self {
358 self.started_at = Some(instant);
359 self
360 }
361
362 pub fn with_elapsed(mut self, elapsed: Duration) -> Self {
364 self.elapsed = Some(elapsed);
365 self
366 }
367
368 pub fn with_affected_rows(mut self, rows: usize) -> Self {
370 self.affected_rows = Some(rows);
371 self
372 }
373
374 pub fn set_metadata(&mut self, key: impl Into<String>, value: Value) {
376 self.metadata.insert(key.into(), value);
377 }
378
379 pub fn get_metadata(&self, key: &str) -> Option<&Value> {
381 self.metadata.get(key)
382 }
383}
384
385#[derive(Debug, Clone, PartialEq)]
387pub enum PluginDecision {
388 Continue,
390 Skip,
392 Modified { sql: String, parameters: Vec<Value> },
394 Abort(String),
396 Kill,
398}
399
400pub trait Plugin: Send + Sync {
402 fn name(&self) -> &str;
404
405 fn stages(&self) -> Vec<ExecutionStage>;
407
408 fn intercept(&self, context: &mut PluginContext) -> PluginDecision;
410}
411
412#[derive(Default)]
418pub struct PluginChain {
419 plugins: RwLock<Vec<Box<dyn Plugin>>>,
420}
421
422impl PluginChain {
423 pub fn new() -> Self {
425 Self {
426 plugins: RwLock::new(Vec::new()),
427 }
428 }
429
430 pub fn register(&self, plugin: Box<dyn Plugin>) {
432 let mut plugins = self.plugins.write().unwrap();
433 plugins.push(plugin);
434 }
435
436 pub fn insert_at(&self, index: usize, plugin: Box<dyn Plugin>) {
438 let mut plugins = self.plugins.write().unwrap();
439 let len = plugins.len();
440 plugins.insert(index.min(len), plugin);
441 }
442
443 pub fn unregister(&self, name: &str) -> bool {
445 let mut plugins = self.plugins.write().unwrap();
446 if let Some(idx) = plugins.iter().position(|p| p.name() == name) {
447 plugins.remove(idx);
448 true
449 } else {
450 false
451 }
452 }
453
454 pub fn len(&self) -> usize {
456 self.plugins.read().unwrap().len()
457 }
458
459 pub fn is_empty(&self) -> bool {
461 self.len() == 0
462 }
463
464 pub fn plugin_names(&self) -> Vec<String> {
466 self.plugins
467 .read()
468 .unwrap()
469 .iter()
470 .map(|p| p.name().to_string())
471 .collect()
472 }
473
474 pub fn clear(&self) {
476 self.plugins.write().unwrap().clear();
477 }
478
479 pub fn execute(&self, context: &mut PluginContext) -> PluginDecision {
486 let plugins = self.plugins.read().unwrap();
487 let target_stages = [context.stage];
488
489 for plugin in plugins.iter() {
490 if !plugin.stages().iter().any(|s| target_stages.contains(s)) {
492 continue;
493 }
494 match plugin.intercept(context) {
495 PluginDecision::Continue => continue,
496 PluginDecision::Skip => return PluginDecision::Skip,
497 PluginDecision::Modified { sql, parameters } => {
498 context.sql = sql;
499 context.parameters = parameters;
500 continue;
501 }
502 PluginDecision::Abort(reason) => {
503 return PluginDecision::Abort(reason);
504 }
505 PluginDecision::Kill => return PluginDecision::Kill,
506 }
507 }
508 PluginDecision::Continue
509 }
510}
511
512impl std::fmt::Debug for PluginChain {
513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514 let plugins = self.plugins.read().unwrap();
515 let names: Vec<&str> = plugins.iter().map(|p| p.name()).collect();
516 f.debug_struct("PluginChain")
517 .field("plugins", &names)
518 .finish()
519 }
520}
521
522pub struct SqlLogPlugin {
528 logs: RwLock<Vec<String>>,
529}
530
531impl SqlLogPlugin {
532 pub fn new() -> Self {
533 Self {
534 logs: RwLock::new(Vec::new()),
535 }
536 }
537
538 pub fn logs(&self) -> Vec<String> {
539 self.logs.read().unwrap().clone()
540 }
541
542 pub fn clear(&self) {
543 self.logs.write().unwrap().clear();
544 }
545
546 pub fn count(&self) -> usize {
547 self.logs.read().unwrap().len()
548 }
549}
550
551impl Default for SqlLogPlugin {
552 fn default() -> Self {
553 Self::new()
554 }
555}
556
557impl Plugin for SqlLogPlugin {
558 fn name(&self) -> &str {
559 "sql_log"
560 }
561
562 fn stages(&self) -> Vec<ExecutionStage> {
563 vec![
564 ExecutionStage::BeforeQuery,
565 ExecutionStage::AfterQuery,
566 ExecutionStage::BeforeUpdate,
567 ExecutionStage::AfterUpdate,
568 ]
569 }
570
571 fn intercept(&self, context: &mut PluginContext) -> PluginDecision {
572 let mut logs = self.logs.write().unwrap();
573 let entry = match context.stage {
574 ExecutionStage::BeforeQuery => {
575 format!("[{}] QUERY: {}", context.stage.name(), context.sql)
576 }
577 ExecutionStage::AfterQuery => {
578 let elapsed_ms = context.elapsed.map(|d| d.as_millis()).unwrap_or(0);
579 format!(
580 "[{}] QUERY ({}ms): {}",
581 context.stage.name(),
582 elapsed_ms,
583 context.sql
584 )
585 }
586 ExecutionStage::BeforeUpdate => {
587 format!("[{}] UPDATE: {}", context.stage.name(), context.sql)
588 }
589 ExecutionStage::AfterUpdate => {
590 let rows = context.affected_rows.unwrap_or(0);
591 format!(
592 "[{}] UPDATE ({} rows): {}",
593 context.stage.name(),
594 rows,
595 context.sql
596 )
597 }
598 _ => return PluginDecision::Continue,
599 };
600 logs.push(entry);
601 PluginDecision::Continue
602 }
603}
604
605pub struct SlowQueryPlugin {
611 threshold: Duration,
612 slow_queries: RwLock<Vec<SlowQueryRecord>>,
613 kill_threshold: Option<Duration>,
615}
616
617#[derive(Debug, Clone)]
619pub struct SlowQueryRecord {
620 pub sql: String,
621 pub elapsed: Duration,
622 pub threshold: Duration,
623}
624
625impl SlowQueryPlugin {
626 pub fn new(threshold: Duration) -> Self {
628 Self {
629 threshold,
630 slow_queries: RwLock::new(Vec::new()),
631 kill_threshold: None,
632 }
633 }
634
635 pub fn default_threshold() -> Self {
637 Self::new(Duration::from_secs(1))
638 }
639
640 pub fn with_kill_threshold(mut self, kill_threshold: Duration) -> Self {
642 self.kill_threshold = Some(kill_threshold);
643 self
644 }
645
646 pub fn slow_queries(&self) -> Vec<SlowQueryRecord> {
648 self.slow_queries.read().unwrap().clone()
649 }
650
651 pub fn count(&self) -> usize {
653 self.slow_queries.read().unwrap().len()
654 }
655
656 pub fn clear(&self) {
658 self.slow_queries.write().unwrap().clear();
659 }
660
661 pub fn threshold(&self) -> Duration {
663 self.threshold
664 }
665}
666
667fn mask_sql(sql: &str) -> String {
672 const SENSITIVE_KEYS: &[&str] = &["password", "passwd", "secret", "token"];
673
674 let lower = sql.to_ascii_lowercase();
675 let bytes = sql.as_bytes();
676 let lower_bytes = lower.as_bytes();
677 let mut result = String::with_capacity(sql.len());
678 let mut i = 0;
679
680 while i < bytes.len() {
681 let mut matched = false;
682 for kw in SENSITIVE_KEYS {
683 let kw_b = kw.as_bytes();
684 if i + kw_b.len() <= bytes.len() && &lower_bytes[i..i + kw_b.len()] == kw_b {
685 let prev_ok = i == 0 || !is_ident_char(bytes[i - 1]);
687 let next = i + kw_b.len();
688 let next_ok = next >= bytes.len() || !is_ident_char(bytes[next]);
689 if !prev_ok || !next_ok {
690 continue;
691 }
692 result.push_str(&sql[i..next]);
694 i = next;
695 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
697 result.push(bytes[i] as char);
698 i += 1;
699 }
700 if i < bytes.len() && bytes[i] == b'=' {
702 result.push('=');
703 i += 1;
704 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
705 result.push(bytes[i] as char);
706 i += 1;
707 }
708 if i < bytes.len() && bytes[i] == b'\'' {
710 i += 1; while i < bytes.len() && bytes[i] != b'\'' {
712 i += 1;
713 }
714 if i < bytes.len() {
715 i += 1; }
717 result.push_str("'***'");
718 }
719 }
720 matched = true;
721 break;
722 }
723 }
724 if !matched {
725 let ch = sql[i..]
726 .chars()
727 .next()
728 .expect("i < bytes.len() guarantees non-empty slice");
729 result.push(ch);
730 i += ch.len_utf8();
731 }
732 }
733 result
734}
735
736fn is_ident_char(b: u8) -> bool {
738 b.is_ascii_alphanumeric() || b == b'_'
739}
740
741impl Plugin for SlowQueryPlugin {
742 fn name(&self) -> &str {
743 "slow_query"
744 }
745
746 fn stages(&self) -> Vec<ExecutionStage> {
747 vec![
750 ExecutionStage::BeforeQuery,
751 ExecutionStage::AfterQuery,
752 ExecutionStage::AfterUpdate,
753 ]
754 }
755
756 fn intercept(&self, context: &mut PluginContext) -> PluginDecision {
757 match context.stage {
758 ExecutionStage::BeforeQuery => {
759 if let Some(kill_threshold) = self.kill_threshold {
763 if let Some(elapsed) = context.elapsed {
764 if elapsed > kill_threshold {
765 return PluginDecision::Kill;
766 }
767 }
768 }
769 PluginDecision::Continue
770 }
771 ExecutionStage::AfterQuery | ExecutionStage::AfterUpdate => {
772 if let Some(elapsed) = context.elapsed {
773 if elapsed > self.threshold {
774 let masked_sql = mask_sql(&context.sql);
776 let mut records = self.slow_queries.write().unwrap();
777 records.push(SlowQueryRecord {
778 sql: masked_sql,
779 elapsed,
780 threshold: self.threshold,
781 });
782 }
783 if let Some(kill_threshold) = self.kill_threshold {
786 if elapsed > kill_threshold {
787 return PluginDecision::Kill;
788 }
789 }
790 }
791 PluginDecision::Continue
792 }
793 _ => PluginDecision::Continue,
794 }
795 }
796}
797
798pub struct AuditPlugin {
804 audit_log: RwLock<Vec<AuditRecord>>,
805}
806
807#[derive(Debug, Clone)]
809pub struct AuditRecord {
810 pub stage: ExecutionStage,
811 pub sql: String,
812 pub affected_rows: Option<usize>,
813}
814
815impl AuditPlugin {
816 pub fn new() -> Self {
817 Self {
818 audit_log: RwLock::new(Vec::new()),
819 }
820 }
821
822 pub fn records(&self) -> Vec<AuditRecord> {
823 self.audit_log.read().unwrap().clone()
824 }
825
826 pub fn count(&self) -> usize {
827 self.audit_log.read().unwrap().len()
828 }
829
830 pub fn clear(&self) {
831 self.audit_log.write().unwrap().clear();
832 }
833}
834
835impl Default for AuditPlugin {
836 fn default() -> Self {
837 Self::new()
838 }
839}
840
841impl Plugin for AuditPlugin {
842 fn name(&self) -> &str {
843 "audit"
844 }
845
846 fn stages(&self) -> Vec<ExecutionStage> {
847 vec![ExecutionStage::AfterUpdate]
848 }
849
850 fn intercept(&self, context: &mut PluginContext) -> PluginDecision {
851 let mut log = self.audit_log.write().unwrap();
852 log.push(AuditRecord {
853 stage: context.stage,
854 sql: context.sql.clone(),
855 affected_rows: context.affected_rows,
856 });
857 PluginDecision::Continue
858 }
859}
860
861pub struct SqlRewritePlugin {
869 pattern: String,
870 replacement: String,
871}
872
873impl SqlRewritePlugin {
874 pub fn new(pattern: impl Into<String>, replacement: impl Into<String>) -> Self {
875 Self {
876 pattern: pattern.into(),
877 replacement: replacement.into(),
878 }
879 }
880}
881
882impl Plugin for SqlRewritePlugin {
883 fn name(&self) -> &str {
884 "sql_rewrite"
885 }
886
887 fn stages(&self) -> Vec<ExecutionStage> {
888 vec![ExecutionStage::BeforeQuery, ExecutionStage::BeforeUpdate]
889 }
890
891 fn intercept(&self, context: &mut PluginContext) -> PluginDecision {
892 if context.sql.contains(&self.pattern) {
893 let new_sql = context.sql.replace(&self.pattern, &self.replacement);
894 PluginDecision::Modified {
895 sql: new_sql,
896 parameters: context.parameters.clone(),
897 }
898 } else {
899 PluginDecision::Continue
900 }
901 }
902}
903
904pub struct BlockPlugin {
912 blocked_keywords: Vec<String>,
913}
914
915impl BlockPlugin {
916 pub fn new(keywords: Vec<String>) -> Self {
917 Self {
918 blocked_keywords: keywords,
919 }
920 }
921
922 pub fn default_block_ddl() -> Self {
924 Self::new(vec![
925 "DROP TABLE".to_string(),
926 "TRUNCATE".to_string(),
927 "DROP DATABASE".to_string(),
928 ])
929 }
930}
931
932impl Plugin for BlockPlugin {
933 fn name(&self) -> &str {
934 "block"
935 }
936
937 fn stages(&self) -> Vec<ExecutionStage> {
938 vec![ExecutionStage::BeforeUpdate, ExecutionStage::BeforeQuery]
939 }
940
941 fn intercept(&self, context: &mut PluginContext) -> PluginDecision {
942 let upper_sql = context.sql.to_uppercase();
943 for kw in &self.blocked_keywords {
944 if upper_sql.contains(&kw.to_uppercase()) {
945 return PluginDecision::Abort(format!(
946 "blocked by BlockPlugin: SQL contains forbidden keyword '{}'",
947 kw
948 ));
949 }
950 }
951 PluginDecision::Continue
952 }
953}
954
955#[cfg(test)]
960mod tests {
961 use super::*;
962
963 #[test]
966 fn test_hydration_mode_default() {
967 assert_eq!(HydrationMode::default(), HydrationMode::Object);
968 }
969
970 #[test]
971 fn test_hydration_mode_name() {
972 assert_eq!(HydrationMode::Object.name(), "object");
973 assert_eq!(HydrationMode::Array.name(), "array");
974 assert_eq!(HydrationMode::Scalar.name(), "scalar");
975 assert_eq!(HydrationMode::SingleScalar.name(), "single_scalar");
976 assert_eq!(HydrationMode::Column.name(), "column");
977 }
978
979 #[test]
982 fn test_hydrate_object_basic() {
983 let mut row = RowData::empty();
984 row.set("id", Value::I64(1));
985 row.set("name", Value::String("Alice".to_string()));
986
987 let result = hydrate_object(&[row]).unwrap();
988 assert_eq!(result.len(), 1);
989 assert_eq!(result[0].get("id"), Some(&Value::I64(1)));
990 assert_eq!(
991 result[0].get("name"),
992 Some(&Value::String("Alice".to_string()))
993 );
994 }
995
996 #[test]
997 fn test_hydrate_object_multiple_rows() {
998 let rows = vec![
999 {
1000 let mut r = RowData::empty();
1001 r.set("id", Value::I64(1));
1002 r
1003 },
1004 {
1005 let mut r = RowData::empty();
1006 r.set("id", Value::I64(2));
1007 r
1008 },
1009 ];
1010
1011 let result = hydrate_object(&rows).unwrap();
1012 assert_eq!(result.len(), 2);
1013 }
1014
1015 #[test]
1016 fn test_hydrate_object_empty() {
1017 let result = hydrate_object(&[]).unwrap();
1018 assert!(result.is_empty());
1019 }
1020
1021 #[test]
1024 fn test_hydrate_array_basic() {
1025 let mut row = RowData::empty();
1026 row.set("id", Value::I64(1));
1027 row.set("name", Value::String("Alice".to_string()));
1028
1029 let result = hydrate_array(&[row]).unwrap();
1030 assert_eq!(result.len(), 1);
1031 assert_eq!(result[0].len(), 2);
1032 assert_eq!(result[0][0], Value::I64(1));
1034 assert_eq!(result[0][1], Value::String("Alice".to_string()));
1035 }
1036
1037 #[test]
1038 fn test_hydrate_array_empty() {
1039 let result = hydrate_array(&[]).unwrap();
1040 assert!(result.is_empty());
1041 }
1042
1043 #[test]
1046 fn test_hydrate_scalar_basic() {
1047 let mut row = RowData::empty();
1048 row.set("count", Value::I64(42));
1049
1050 let result = hydrate_scalar(&[row]).unwrap();
1051 assert_eq!(result.len(), 1);
1052 assert_eq!(result[0], Value::I64(42));
1053 }
1054
1055 #[test]
1056 fn test_hydrate_scalar_multiple_rows() {
1057 let rows = vec![
1058 {
1059 let mut r = RowData::empty();
1060 r.set("id", Value::I64(1));
1061 r
1062 },
1063 {
1064 let mut r = RowData::empty();
1065 r.set("id", Value::I64(2));
1066 r
1067 },
1068 ];
1069
1070 let result = hydrate_scalar(&rows).unwrap();
1071 assert_eq!(result, vec![Value::I64(1), Value::I64(2)]);
1072 }
1073
1074 #[test]
1075 fn test_hydrate_scalar_empty_row_error() {
1076 let row = RowData::empty();
1077 let err = hydrate_scalar(&[row]).unwrap_err();
1078 match err {
1079 HydrationError::EmptyRow => {}
1080 _ => panic!("expected EmptyRow error"),
1081 }
1082 }
1083
1084 #[test]
1087 fn test_hydrate_single_scalar_ok() {
1088 let mut row = RowData::empty();
1089 row.set("total", Value::I64(100));
1090
1091 let result = hydrate_single_scalar(&[row]).unwrap();
1092 assert_eq!(result, Value::I64(100));
1093 }
1094
1095 #[test]
1096 fn test_hydrate_single_scalar_no_rows() {
1097 let err = hydrate_single_scalar(&[]).unwrap_err();
1098 match err {
1099 HydrationError::SingleScalarRequiresSingleRow { actual_rows } => {
1100 assert_eq!(actual_rows, 0)
1101 }
1102 _ => panic!("expected SingleScalarRequiresSingleRow"),
1103 }
1104 }
1105
1106 #[test]
1107 fn test_hydrate_single_scalar_too_many_rows() {
1108 let rows = vec![
1109 {
1110 let mut r = RowData::empty();
1111 r.set("id", Value::I64(1));
1112 r
1113 },
1114 {
1115 let mut r = RowData::empty();
1116 r.set("id", Value::I64(2));
1117 r
1118 },
1119 ];
1120 let err = hydrate_single_scalar(&rows).unwrap_err();
1121 match err {
1122 HydrationError::SingleScalarRequiresSingleRow { actual_rows } => {
1123 assert_eq!(actual_rows, 2)
1124 }
1125 _ => panic!("expected SingleScalarRequiresSingleRow"),
1126 }
1127 }
1128
1129 #[test]
1130 fn test_hydrate_single_scalar_empty_row() {
1131 let row = RowData::empty();
1132 let err = hydrate_single_scalar(&[row]).unwrap_err();
1133 match err {
1134 HydrationError::EmptyRow => {}
1135 _ => panic!("expected EmptyRow"),
1136 }
1137 }
1138
1139 #[test]
1142 fn test_hydrate_column_basic() {
1143 let rows = vec![
1144 {
1145 let mut r = RowData::empty();
1146 r.set("id", Value::I64(1));
1147 r.set("name", Value::String("Alice".to_string()));
1148 r
1149 },
1150 {
1151 let mut r = RowData::empty();
1152 r.set("id", Value::I64(2));
1153 r.set("name", Value::String("Bob".to_string()));
1154 r
1155 },
1156 ];
1157
1158 let result = hydrate_column(&rows, "name").unwrap();
1159 assert_eq!(
1160 result,
1161 vec![
1162 Value::String("Alice".to_string()),
1163 Value::String("Bob".to_string()),
1164 ]
1165 );
1166 }
1167
1168 #[test]
1169 fn test_hydrate_column_missing() {
1170 let rows = vec![{
1171 let mut r = RowData::empty();
1172 r.set("id", Value::I64(1));
1173 r
1174 }];
1175
1176 let err = hydrate_column(&rows, "missing").unwrap_err();
1177 match err {
1178 HydrationError::ColumnNotFound { column } => assert_eq!(column, "missing"),
1179 _ => panic!("expected ColumnNotFound"),
1180 }
1181 }
1182
1183 #[test]
1184 fn test_hydrate_column_empty_rows() {
1185 let result = hydrate_column(&[], "name").unwrap();
1186 assert!(result.is_empty());
1187 }
1188
1189 #[test]
1192 fn test_hydrate_scalar_mode() {
1193 let mut row = RowData::empty();
1194 row.set("count", Value::I64(42));
1195
1196 let result = hydrate(&[row], HydrationMode::Scalar).unwrap();
1197 assert_eq!(result, vec![Value::I64(42)]);
1198 }
1199
1200 #[test]
1201 fn test_hydrate_single_scalar_mode() {
1202 let mut row = RowData::empty();
1203 row.set("total", Value::I64(100));
1204
1205 let result = hydrate(&[row], HydrationMode::SingleScalar).unwrap();
1206 assert_eq!(result, vec![Value::I64(100)]);
1207 }
1208
1209 #[test]
1210 fn test_hydrate_column_mode() {
1211 let rows = vec![
1212 {
1213 let mut r = RowData::empty();
1214 r.set("id", Value::I64(1));
1215 r
1216 },
1217 {
1218 let mut r = RowData::empty();
1219 r.set("id", Value::I64(2));
1220 r
1221 },
1222 ];
1223
1224 let result = hydrate(&rows, HydrationMode::Column).unwrap();
1225 assert_eq!(result, vec![Value::I64(1), Value::I64(2)]);
1226 }
1227
1228 #[test]
1231 fn test_execution_stage_name() {
1232 assert_eq!(ExecutionStage::BeforeQuery.name(), "before_query");
1233 assert_eq!(ExecutionStage::AfterQuery.name(), "after_query");
1234 assert_eq!(ExecutionStage::BeforeUpdate.name(), "before_update");
1235 assert_eq!(ExecutionStage::AfterCommit.name(), "after_commit");
1236 }
1237
1238 #[test]
1239 fn test_execution_stage_is_before() {
1240 assert!(ExecutionStage::BeforeQuery.is_before());
1241 assert!(ExecutionStage::BeforeUpdate.is_before());
1242 assert!(ExecutionStage::BeforeCommit.is_before());
1243 assert!(ExecutionStage::BeforeRollback.is_before());
1244 assert!(!ExecutionStage::AfterQuery.is_before());
1245 assert!(!ExecutionStage::AfterUpdate.is_before());
1246 }
1247
1248 #[test]
1249 fn test_execution_stage_is_after() {
1250 assert!(ExecutionStage::AfterQuery.is_after());
1251 assert!(!ExecutionStage::BeforeQuery.is_after());
1252 }
1253
1254 #[test]
1255 fn test_execution_stage_is_query() {
1256 assert!(ExecutionStage::BeforeQuery.is_query());
1257 assert!(ExecutionStage::AfterQuery.is_query());
1258 assert!(!ExecutionStage::BeforeUpdate.is_query());
1259 }
1260
1261 #[test]
1262 fn test_execution_stage_is_update() {
1263 assert!(ExecutionStage::BeforeUpdate.is_update());
1264 assert!(ExecutionStage::AfterUpdate.is_update());
1265 assert!(!ExecutionStage::BeforeQuery.is_update());
1266 }
1267
1268 #[test]
1269 fn test_execution_stage_is_transaction() {
1270 assert!(ExecutionStage::BeforeCommit.is_transaction());
1271 assert!(ExecutionStage::AfterCommit.is_transaction());
1272 assert!(ExecutionStage::BeforeRollback.is_transaction());
1273 assert!(ExecutionStage::AfterRollback.is_transaction());
1274 assert!(!ExecutionStage::BeforeQuery.is_transaction());
1275 }
1276
1277 #[test]
1280 fn test_plugin_context_new() {
1281 let ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1282 assert_eq!(ctx.stage, ExecutionStage::BeforeQuery);
1283 assert_eq!(ctx.sql, "SELECT 1");
1284 assert!(ctx.parameters.is_empty());
1285 assert!(ctx.started_at.is_none());
1286 assert!(ctx.elapsed.is_none());
1287 assert!(ctx.affected_rows.is_none());
1288 }
1289
1290 #[test]
1291 fn test_plugin_context_with_parameters() {
1292 let ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT ?")
1293 .with_parameters(vec![Value::I64(1)]);
1294 assert_eq!(ctx.parameters.len(), 1);
1295 }
1296
1297 #[test]
1298 fn test_plugin_context_with_elapsed() {
1299 let ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT 1")
1300 .with_elapsed(Duration::from_millis(50));
1301 assert_eq!(ctx.elapsed.unwrap().as_millis(), 50);
1302 }
1303
1304 #[test]
1305 fn test_plugin_context_with_affected_rows() {
1306 let ctx = PluginContext::new(ExecutionStage::AfterUpdate, "UPDATE users SET ...")
1307 .with_affected_rows(10);
1308 assert_eq!(ctx.affected_rows.unwrap(), 10);
1309 }
1310
1311 #[test]
1312 fn test_plugin_context_metadata() {
1313 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1314 ctx.set_metadata("user_id", Value::I64(42));
1315 assert_eq!(ctx.get_metadata("user_id"), Some(&Value::I64(42)));
1316 assert_eq!(ctx.get_metadata("missing"), None);
1317 }
1318
1319 #[test]
1322 fn test_plugin_chain_empty() {
1323 let chain = PluginChain::new();
1324 assert!(chain.is_empty());
1325 assert_eq!(chain.len(), 0);
1326 }
1327
1328 #[test]
1329 fn test_plugin_chain_register() {
1330 let chain = PluginChain::new();
1331 chain.register(Box::new(SqlLogPlugin::new()));
1332 assert_eq!(chain.len(), 1);
1333 }
1334
1335 #[test]
1336 fn test_plugin_chain_unregister() {
1337 let chain = PluginChain::new();
1338 chain.register(Box::new(SqlLogPlugin::new()));
1339 assert_eq!(chain.len(), 1);
1340
1341 let removed = chain.unregister("sql_log");
1342 assert!(removed);
1343 assert_eq!(chain.len(), 0);
1344 }
1345
1346 #[test]
1347 fn test_plugin_chain_unregister_missing() {
1348 let chain = PluginChain::new();
1349 let removed = chain.unregister("non_existent");
1350 assert!(!removed);
1351 }
1352
1353 #[test]
1354 fn test_plugin_chain_plugin_names() {
1355 let chain = PluginChain::new();
1356 chain.register(Box::new(SqlLogPlugin::new()));
1357 chain.register(Box::new(AuditPlugin::new()));
1358
1359 let names = chain.plugin_names();
1360 assert_eq!(names, vec!["sql_log", "audit"]);
1361 }
1362
1363 #[test]
1364 fn test_plugin_chain_clear() {
1365 let chain = PluginChain::new();
1366 chain.register(Box::new(SqlLogPlugin::new()));
1367 chain.clear();
1368 assert!(chain.is_empty());
1369 }
1370
1371 #[test]
1372 fn test_plugin_chain_insert_at() {
1373 let chain = PluginChain::new();
1374 chain.register(Box::new(SqlLogPlugin::new()));
1375 chain.insert_at(0, Box::new(AuditPlugin::new()));
1376
1377 let names = chain.plugin_names();
1378 assert_eq!(names, vec!["audit", "sql_log"]);
1379 }
1380
1381 #[test]
1382 fn test_plugin_chain_insert_at_end() {
1383 let chain = PluginChain::new();
1384 chain.register(Box::new(SqlLogPlugin::new()));
1385 chain.insert_at(99, Box::new(AuditPlugin::new()));
1386
1387 let names = chain.plugin_names();
1388 assert_eq!(names, vec!["sql_log", "audit"]);
1389 }
1390
1391 #[test]
1392 fn test_plugin_chain_execute_empty() {
1393 let chain = PluginChain::new();
1394 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1395 let decision = chain.execute(&mut ctx);
1396 assert_eq!(decision, PluginDecision::Continue);
1397 }
1398
1399 #[test]
1400 fn test_plugin_chain_execute_continue() {
1401 let chain = PluginChain::new();
1402 chain.register(Box::new(SqlLogPlugin::new()));
1403
1404 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1405 let decision = chain.execute(&mut ctx);
1406 assert_eq!(decision, PluginDecision::Continue);
1407 }
1408
1409 #[test]
1410 fn test_plugin_chain_execute_skip() {
1411 struct SkipPlugin;
1412 impl Plugin for SkipPlugin {
1413 fn name(&self) -> &str {
1414 "skip"
1415 }
1416 fn stages(&self) -> Vec<ExecutionStage> {
1417 vec![ExecutionStage::BeforeQuery]
1418 }
1419 fn intercept(&self, _ctx: &mut PluginContext) -> PluginDecision {
1420 PluginDecision::Skip
1421 }
1422 }
1423
1424 let chain = PluginChain::new();
1425 chain.register(Box::new(SkipPlugin));
1426
1427 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1428 let decision = chain.execute(&mut ctx);
1429 assert_eq!(decision, PluginDecision::Skip);
1430 }
1431
1432 #[test]
1433 fn test_plugin_chain_execute_modified() {
1434 let chain = PluginChain::new();
1435 chain.register(Box::new(SqlRewritePlugin::new(
1436 "SELECT",
1437 "SELECT /* hint */",
1438 )));
1439
1440 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1441 let decision = chain.execute(&mut ctx);
1442 assert_eq!(decision, PluginDecision::Continue);
1443 assert_eq!(ctx.sql, "SELECT /* hint */ 1");
1444 }
1445
1446 #[test]
1447 fn test_plugin_chain_execute_abort() {
1448 let chain = PluginChain::new();
1449 chain.register(Box::new(BlockPlugin::new(vec!["DROP".to_string()])));
1450
1451 let mut ctx = PluginContext::new(ExecutionStage::BeforeUpdate, "DROP TABLE users");
1452 let decision = chain.execute(&mut ctx);
1453 match decision {
1454 PluginDecision::Abort(reason) => assert!(reason.contains("DROP")),
1455 _ => panic!("expected Abort"),
1456 }
1457 }
1458
1459 #[test]
1460 fn test_plugin_chain_skip_unrelated_stages() {
1461 let chain = PluginChain::new();
1462 chain.register(Box::new(SqlLogPlugin::new()));
1464
1465 let mut ctx = PluginContext::new(ExecutionStage::BeforeCommit, "COMMIT");
1466 let decision = chain.execute(&mut ctx);
1467 assert_eq!(decision, PluginDecision::Continue);
1468 }
1470
1471 #[test]
1474 fn test_sql_log_plugin_basic() {
1475 let plugin = SqlLogPlugin::new();
1476 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1477 let decision = plugin.intercept(&mut ctx);
1478 assert_eq!(decision, PluginDecision::Continue);
1479 assert_eq!(plugin.count(), 1);
1480 }
1481
1482 #[test]
1483 fn test_sql_log_plugin_after_query_with_elapsed() {
1484 let plugin = SqlLogPlugin::new();
1485 let mut ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT 1")
1486 .with_elapsed(Duration::from_millis(50));
1487 let _ = plugin.intercept(&mut ctx);
1488
1489 let logs = plugin.logs();
1490 assert!(logs[0].contains("50ms"));
1491 }
1492
1493 #[test]
1494 fn test_sql_log_plugin_after_update_with_rows() {
1495 let plugin = SqlLogPlugin::new();
1496 let mut ctx = PluginContext::new(ExecutionStage::AfterUpdate, "UPDATE users SET ...")
1497 .with_affected_rows(10);
1498 let _ = plugin.intercept(&mut ctx);
1499
1500 let logs = plugin.logs();
1501 assert!(logs[0].contains("10 rows"));
1502 }
1503
1504 #[test]
1505 fn test_sql_log_plugin_clear() {
1506 let plugin = SqlLogPlugin::new();
1507 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT 1");
1508 let _ = plugin.intercept(&mut ctx);
1509 assert_eq!(plugin.count(), 1);
1510
1511 plugin.clear();
1512 assert_eq!(plugin.count(), 0);
1513 }
1514
1515 #[test]
1518 fn test_slow_query_plugin_below_threshold() {
1519 let plugin = SlowQueryPlugin::new(Duration::from_millis(100));
1520 let mut ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT 1")
1521 .with_elapsed(Duration::from_millis(50));
1522 let _ = plugin.intercept(&mut ctx);
1523
1524 assert_eq!(plugin.count(), 0);
1525 }
1526
1527 #[test]
1528 fn test_slow_query_plugin_above_threshold() {
1529 let plugin = SlowQueryPlugin::new(Duration::from_millis(100));
1530 let mut ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT * FROM big_table")
1531 .with_elapsed(Duration::from_millis(500));
1532 let _ = plugin.intercept(&mut ctx);
1533
1534 assert_eq!(plugin.count(), 1);
1535 let records = plugin.slow_queries();
1536 assert!(records[0].elapsed > records[0].threshold);
1537 }
1538
1539 #[test]
1540 fn test_slow_query_plugin_no_elapsed() {
1541 let plugin = SlowQueryPlugin::new(Duration::from_millis(100));
1542 let mut ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT 1");
1543 let _ = plugin.intercept(&mut ctx);
1544
1545 assert_eq!(plugin.count(), 0);
1546 }
1547
1548 #[test]
1549 fn test_slow_query_plugin_clear() {
1550 let plugin = SlowQueryPlugin::new(Duration::from_millis(100));
1551 let mut ctx = PluginContext::new(ExecutionStage::AfterQuery, "SELECT 1")
1552 .with_elapsed(Duration::from_millis(200));
1553 let _ = plugin.intercept(&mut ctx);
1554 assert_eq!(plugin.count(), 1);
1555
1556 plugin.clear();
1557 assert_eq!(plugin.count(), 0);
1558 }
1559
1560 #[test]
1561 fn test_slow_query_plugin_default_threshold() {
1562 let plugin = SlowQueryPlugin::default_threshold();
1563 assert_eq!(plugin.threshold(), Duration::from_secs(1));
1564 }
1565
1566 #[test]
1569 fn test_audit_plugin_basic() {
1570 let plugin = AuditPlugin::new();
1571 let mut ctx = PluginContext::new(ExecutionStage::AfterUpdate, "INSERT INTO users ...")
1572 .with_affected_rows(1);
1573 let _ = plugin.intercept(&mut ctx);
1574
1575 assert_eq!(plugin.count(), 1);
1576 let records = plugin.records();
1577 assert_eq!(records[0].stage, ExecutionStage::AfterUpdate);
1578 assert_eq!(records[0].affected_rows, Some(1));
1579 }
1580
1581 #[test]
1582 fn test_audit_plugin_clear() {
1583 let plugin = AuditPlugin::new();
1584 let mut ctx = PluginContext::new(ExecutionStage::AfterUpdate, "UPDATE users");
1585 let _ = plugin.intercept(&mut ctx);
1586 assert_eq!(plugin.count(), 1);
1587
1588 plugin.clear();
1589 assert_eq!(plugin.count(), 0);
1590 }
1591
1592 #[test]
1595 fn test_sql_rewrite_plugin_matches() {
1596 let plugin = SqlRewritePlugin::new("SELECT", "SELECT /* hint */");
1597 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT * FROM users");
1598 let decision = plugin.intercept(&mut ctx);
1599 match decision {
1600 PluginDecision::Modified { sql, .. } => {
1601 assert_eq!(sql, "SELECT /* hint */ * FROM users");
1602 }
1603 _ => panic!("expected Modified"),
1604 }
1605 }
1606
1607 #[test]
1608 fn test_sql_rewrite_plugin_no_match() {
1609 let plugin = SqlRewritePlugin::new("SELECT", "SELECT /* hint */");
1610 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SHOW TABLES");
1611 let decision = plugin.intercept(&mut ctx);
1612 assert_eq!(decision, PluginDecision::Continue);
1613 }
1614
1615 #[test]
1618 fn test_block_plugin_blocks_drop() {
1619 let plugin = BlockPlugin::default_block_ddl();
1620 let mut ctx = PluginContext::new(ExecutionStage::BeforeUpdate, "DROP TABLE users");
1621 let decision = plugin.intercept(&mut ctx);
1622 match decision {
1623 PluginDecision::Abort(reason) => {
1624 assert!(reason.contains("DROP TABLE"));
1625 }
1626 _ => panic!("expected Abort"),
1627 }
1628 }
1629
1630 #[test]
1631 fn test_block_plugin_blocks_truncate() {
1632 let plugin = BlockPlugin::default_block_ddl();
1633 let mut ctx = PluginContext::new(ExecutionStage::BeforeUpdate, "TRUNCATE TABLE logs");
1634 let decision = plugin.intercept(&mut ctx);
1635 assert!(matches!(decision, PluginDecision::Abort(_)));
1636 }
1637
1638 #[test]
1639 fn test_block_plugin_allows_safe_sql() {
1640 let plugin = BlockPlugin::default_block_ddl();
1641 let mut ctx = PluginContext::new(ExecutionStage::BeforeQuery, "SELECT * FROM users");
1642 let decision = plugin.intercept(&mut ctx);
1643 assert_eq!(decision, PluginDecision::Continue);
1644 }
1645
1646 #[test]
1647 fn test_block_plugin_case_insensitive() {
1648 let plugin = BlockPlugin::default_block_ddl();
1649 let mut ctx = PluginContext::new(ExecutionStage::BeforeUpdate, "drop table users");
1650 let decision = plugin.intercept(&mut ctx);
1651 assert!(matches!(decision, PluginDecision::Abort(_)));
1652 }
1653
1654 #[test]
1657 fn test_e2e_plugin_chain_workflow() {
1658 let chain = PluginChain::new();
1659 let sql_log = std::sync::Arc::new(SqlLogPlugin::new());
1660 let audit = std::sync::Arc::new(AuditPlugin::new());
1661 let slow = std::sync::Arc::new(SlowQueryPlugin::new(Duration::from_millis(100)));
1662
1663 chain.register(Box::new(SqlLogPlugin::new()));
1666 chain.register(Box::new(AuditPlugin::new()));
1667 chain.register(Box::new(SlowQueryPlugin::new(Duration::from_millis(100))));
1668
1669 assert_eq!(chain.len(), 3);
1670
1671 let mut before_ctx = PluginContext::new(
1673 ExecutionStage::BeforeQuery,
1674 "SELECT * FROM users WHERE id = ?",
1675 )
1676 .with_parameters(vec![Value::I64(1)]);
1677 let decision = chain.execute(&mut before_ctx);
1678 assert_eq!(decision, PluginDecision::Continue);
1679 let mut after_ctx = PluginContext::new(
1685 ExecutionStage::AfterQuery,
1686 "SELECT * FROM users WHERE id = ?",
1687 )
1688 .with_elapsed(Duration::from_millis(500));
1689 let decision = chain.execute(&mut after_ctx);
1690 assert_eq!(decision, PluginDecision::Continue);
1691
1692 let names = chain.plugin_names();
1694 assert_eq!(names, vec!["sql_log", "audit", "slow_query"]);
1695
1696 let _ = (sql_log, audit, slow); }
1698
1699 #[test]
1700 fn test_e2e_block_plugin_aborts_chain() {
1701 let chain = PluginChain::new();
1702 chain.register(Box::new(BlockPlugin::default_block_ddl()));
1704 chain.register(Box::new(SqlLogPlugin::new()));
1705
1706 let mut ctx = PluginContext::new(ExecutionStage::BeforeUpdate, "DROP TABLE users");
1707 let decision = chain.execute(&mut ctx);
1708 assert!(matches!(decision, PluginDecision::Abort(_)));
1709 }
1710
1711 #[test]
1712 fn test_e2e_hydrate_scalar_count_query() {
1713 let mut row = RowData::empty();
1715 row.set("cnt", Value::I64(42));
1716
1717 let result = hydrate_single_scalar(&[row]).unwrap();
1718 assert_eq!(result, Value::I64(42));
1719 }
1720
1721 #[test]
1722 fn test_e2e_hydrate_object_user_query() {
1723 let rows = vec![
1725 {
1726 let mut r = RowData::empty();
1727 r.set("id", Value::I64(1));
1728 r.set("name", Value::String("Alice".to_string()));
1729 r.set("email", Value::String("alice@example.com".to_string()));
1730 r
1731 },
1732 {
1733 let mut r = RowData::empty();
1734 r.set("id", Value::I64(2));
1735 r.set("name", Value::String("Bob".to_string()));
1736 r.set("email", Value::String("bob@example.com".to_string()));
1737 r
1738 },
1739 ];
1740
1741 let result = hydrate_object(&rows).unwrap();
1742 assert_eq!(result.len(), 2);
1743 assert_eq!(
1744 result[0].get("name"),
1745 Some(&Value::String("Alice".to_string()))
1746 );
1747 }
1748
1749 #[test]
1750 fn test_e2e_hydrate_array_multi_column() {
1751 let rows = vec![{
1752 let mut r = RowData::empty();
1753 r.set("a", Value::I64(1));
1754 r.set("b", Value::I64(2));
1755 r.set("c", Value::I64(3));
1756 r
1757 }];
1758
1759 let result = hydrate_array(&rows).unwrap();
1760 assert_eq!(result[0], vec![Value::I64(1), Value::I64(2), Value::I64(3)]);
1762 }
1763}