1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::num::NonZeroUsize;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, OnceLock, RwLock};
6use std::time::{Duration, Instant};
7
8use lru::LruCache;
9use orion_error::{ToStructError, UvsFrom};
10use wp_error::{KnowledgeReason, KnowledgeResult};
11use wp_log::{debug_kdb, warn_kdb};
12use wp_model_core::model::{DataField, DataType, Value};
13
14use crate::loader::ProviderKind;
15use crate::mem::RowData;
16use crate::telemetry::{
17 CacheLayer, CacheOutcome, CacheTelemetryEvent, QueryTelemetryEvent, ReloadOutcome,
18 ReloadTelemetryEvent, telemetry,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct DatasourceId(pub String);
23
24impl DatasourceId {
25 pub fn from_seed(kind: ProviderKind, seed: &str) -> Self {
26 let mut hasher = DefaultHasher::new();
27 seed.hash(&mut hasher);
28 let kind_str = match kind {
29 ProviderKind::SqliteAuthority => "sqlite",
30 ProviderKind::Postgres => "postgres",
31 ProviderKind::Mysql => "mysql",
32 };
33 Self(format!("{kind_str}:{:016x}", hasher.finish()))
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct Generation(pub u64);
39
40#[derive(Debug, Clone)]
41pub enum QueryMode {
42 Many,
43 FirstRow,
44}
45
46#[derive(Debug, Clone, Copy)]
47pub enum CachePolicy {
48 Bypass,
49 UseGlobal,
50 UseCallScope,
51}
52
53#[derive(Debug, Clone)]
54pub enum QueryValue {
55 Null,
56 Bool(bool),
57 Int(i64),
58 Float(f64),
59 Text(String),
60}
61
62#[derive(Debug, Clone)]
63pub struct QueryParam {
64 pub name: String,
65 pub value: QueryValue,
66}
67
68#[derive(Debug, Clone)]
69pub struct QueryRequest {
70 pub sql: String,
71 pub params: Vec<QueryParam>,
72 pub mode: QueryMode,
73 pub cache_policy: CachePolicy,
74}
75
76impl QueryRequest {
77 pub fn many(
78 sql: impl Into<String>,
79 params: Vec<QueryParam>,
80 cache_policy: CachePolicy,
81 ) -> Self {
82 Self {
83 sql: sql.into(),
84 params,
85 mode: QueryMode::Many,
86 cache_policy,
87 }
88 }
89
90 pub fn first_row(
91 sql: impl Into<String>,
92 params: Vec<QueryParam>,
93 cache_policy: CachePolicy,
94 ) -> Self {
95 Self {
96 sql: sql.into(),
97 params,
98 mode: QueryMode::FirstRow,
99 cache_policy,
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
105pub enum QueryResponse {
106 Rows(Vec<RowData>),
107 Row(RowData),
108}
109
110impl QueryResponse {
111 pub fn into_rows(self) -> Vec<RowData> {
112 match self {
113 QueryResponse::Rows(rows) => rows,
114 QueryResponse::Row(row) => vec![row],
115 }
116 }
117
118 pub fn into_row(self) -> RowData {
119 match self {
120 QueryResponse::Rows(rows) => rows.into_iter().next().unwrap_or_default(),
121 QueryResponse::Row(row) => row,
122 }
123 }
124}
125
126pub trait ProviderExecutor: Send + Sync {
127 fn query(&self, sql: &str) -> KnowledgeResult<Vec<RowData>>;
128 fn query_fields(&self, sql: &str, params: &[DataField]) -> KnowledgeResult<Vec<RowData>>;
129 fn query_row(&self, sql: &str) -> KnowledgeResult<RowData>;
130 fn query_named_fields(&self, sql: &str, params: &[DataField]) -> KnowledgeResult<RowData>;
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134pub enum QueryModeTag {
135 Many,
136 FirstRow,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub struct ResultCacheKey {
141 pub datasource_id: DatasourceId,
142 pub generation: Generation,
143 pub query_hash: u64,
144 pub params_hash: u64,
145 pub mode: QueryModeTag,
146}
147
148pub struct ProviderHandle {
149 pub provider: Arc<dyn ProviderExecutor>,
150 pub datasource_id: DatasourceId,
151 pub generation: Generation,
152 pub kind: ProviderKind,
153}
154
155#[derive(Debug, Clone)]
156pub struct RuntimeSnapshot {
157 pub provider_kind: Option<ProviderKind>,
158 pub datasource_id: Option<DatasourceId>,
159 pub generation: Option<Generation>,
160 pub result_cache_enabled: bool,
161 pub result_cache_len: usize,
162 pub result_cache_capacity: usize,
163 pub result_cache_ttl_ms: u64,
164 pub metadata_cache_len: usize,
165 pub metadata_cache_capacity: usize,
166 pub result_cache_hits: u64,
167 pub result_cache_misses: u64,
168 pub metadata_cache_hits: u64,
169 pub metadata_cache_misses: u64,
170 pub local_cache_hits: u64,
171 pub local_cache_misses: u64,
172 pub reload_successes: u64,
173 pub reload_failures: u64,
174}
175
176#[derive(Debug, Clone)]
177pub struct MetadataCacheScope {
178 pub datasource_id: DatasourceId,
179 pub generation: Generation,
180}
181
182#[derive(Debug, Clone, Copy)]
183pub struct ResultCacheConfig {
184 pub enabled: bool,
185 pub capacity: usize,
186 pub ttl: Duration,
187}
188
189impl Default for ResultCacheConfig {
190 fn default() -> Self {
191 Self {
192 enabled: true,
193 capacity: 1024,
194 ttl: Duration::from_millis(30_000),
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
200struct CachedQueryResponse {
201 response: Arc<QueryResponse>,
202 cached_at: Instant,
203}
204
205pub struct KnowledgeRuntime {
206 provider: RwLock<Option<Arc<ProviderHandle>>>,
207 next_generation: AtomicU64,
208 result_cache_config: RwLock<ResultCacheConfig>,
209 result_cache: RwLock<LruCache<ResultCacheKey, CachedQueryResponse>>,
210 result_cache_hits: AtomicU64,
211 result_cache_misses: AtomicU64,
212 metadata_cache_hits: AtomicU64,
213 metadata_cache_misses: AtomicU64,
214 local_cache_hits: AtomicU64,
215 local_cache_misses: AtomicU64,
216 reload_successes: AtomicU64,
217 reload_failures: AtomicU64,
218}
219
220impl KnowledgeRuntime {
221 pub fn new(result_cache_capacity: usize) -> Self {
222 let config = ResultCacheConfig {
223 capacity: result_cache_capacity.max(1),
224 ..ResultCacheConfig::default()
225 };
226 let capacity = NonZeroUsize::new(config.capacity).expect("non-zero capacity");
227 Self {
228 provider: RwLock::new(None),
229 next_generation: AtomicU64::new(0),
230 result_cache_config: RwLock::new(config),
231 result_cache: RwLock::new(LruCache::new(capacity)),
232 result_cache_hits: AtomicU64::new(0),
233 result_cache_misses: AtomicU64::new(0),
234 metadata_cache_hits: AtomicU64::new(0),
235 metadata_cache_misses: AtomicU64::new(0),
236 local_cache_hits: AtomicU64::new(0),
237 local_cache_misses: AtomicU64::new(0),
238 reload_successes: AtomicU64::new(0),
239 reload_failures: AtomicU64::new(0),
240 }
241 }
242
243 pub fn install_provider<F>(
244 &self,
245 kind: ProviderKind,
246 datasource_id: DatasourceId,
247 build: F,
248 ) -> KnowledgeResult<Generation>
249 where
250 F: FnOnce(Generation) -> KnowledgeResult<Arc<dyn ProviderExecutor>>,
251 {
252 let generation = Generation(self.next_generation.fetch_add(1, Ordering::SeqCst) + 1);
253 let previous = self
254 .provider
255 .read()
256 .ok()
257 .and_then(|guard| guard.as_ref().cloned());
258 debug_kdb!(
259 "[kdb] reload provider start kind={kind:?} datasource_id={} target_generation={} previous_generation={}",
260 datasource_id.0,
261 generation.0,
262 previous
263 .as_ref()
264 .map(|handle| handle.generation.0.to_string())
265 .unwrap_or_else(|| "none".to_string())
266 );
267 let provider = match build(generation) {
268 Ok(provider) => provider,
269 Err(err) => {
270 self.reload_failures.fetch_add(1, Ordering::Relaxed);
271 warn_kdb!(
272 "[kdb] reload provider failed kind={kind:?} datasource_id={} target_generation={} err={}",
273 datasource_id.0,
274 generation.0,
275 err
276 );
277 telemetry().on_reload(&ReloadTelemetryEvent {
278 outcome: ReloadOutcome::Failure,
279 provider_kind: kind.clone(),
280 });
281 return Err(err);
282 }
283 };
284 debug_kdb!(
285 "[kdb] install provider kind={kind:?} datasource_id={} generation={}",
286 datasource_id.0,
287 generation.0
288 );
289 let kind_for_handle = kind.clone();
290 let datasource_id_for_handle = datasource_id.clone();
291 let handle = Arc::new(ProviderHandle {
292 provider,
293 datasource_id: datasource_id_for_handle,
294 generation,
295 kind: kind_for_handle,
296 });
297 {
298 let mut guard = self
299 .provider
300 .write()
301 .expect("runtime provider lock poisoned");
302 *guard = Some(handle);
303 }
304 self.reload_successes.fetch_add(1, Ordering::Relaxed);
305 telemetry().on_reload(&ReloadTelemetryEvent {
306 outcome: ReloadOutcome::Success,
307 provider_kind: kind.clone(),
308 });
309 debug_kdb!(
310 "[kdb] reload provider success kind={kind:?} datasource_id={} generation={}",
311 datasource_id.0,
312 generation.0
313 );
314 Ok(generation)
315 }
316
317 pub fn configure_result_cache(&self, enabled: bool, capacity: usize, ttl: Duration) {
318 let new_config = ResultCacheConfig {
319 enabled,
320 capacity: capacity.max(1),
321 ttl: ttl.max(Duration::from_millis(1)),
322 };
323 let mut should_reset_cache = false;
324 {
325 let mut guard = self
326 .result_cache_config
327 .write()
328 .expect("runtime result cache config lock poisoned");
329 if guard.capacity != new_config.capacity || (!new_config.enabled && guard.enabled) {
330 should_reset_cache = true;
331 }
332 *guard = new_config;
333 }
334
335 if should_reset_cache {
336 let mut cache = self
337 .result_cache
338 .write()
339 .expect("runtime result cache lock poisoned");
340 *cache = LruCache::new(
341 NonZeroUsize::new(new_config.capacity).expect("non-zero result cache capacity"),
342 );
343 }
344 }
345
346 pub fn current_generation(&self) -> Option<Generation> {
347 self.provider
348 .read()
349 .ok()
350 .and_then(|guard| guard.as_ref().map(|handle| handle.generation))
351 }
352
353 pub fn snapshot(&self) -> RuntimeSnapshot {
354 let provider = self
355 .provider
356 .read()
357 .ok()
358 .and_then(|guard| guard.as_ref().cloned());
359 let result_cache_config = self
360 .result_cache_config
361 .read()
362 .map(|guard| *guard)
363 .unwrap_or_default();
364 let (result_cache_len, result_cache_capacity) = self
365 .result_cache
366 .read()
367 .map(|cache| (cache.len(), cache.cap().get()))
368 .unwrap_or((0, 0));
369 let (metadata_cache_len, metadata_cache_capacity) =
370 crate::mem::query_util::column_metadata_cache_snapshot();
371 RuntimeSnapshot {
372 provider_kind: provider.as_ref().map(|handle| handle.kind.clone()),
373 datasource_id: provider.as_ref().map(|handle| handle.datasource_id.clone()),
374 generation: provider.as_ref().map(|handle| handle.generation),
375 result_cache_enabled: result_cache_config.enabled,
376 result_cache_len,
377 result_cache_capacity,
378 result_cache_ttl_ms: result_cache_config.ttl.as_millis() as u64,
379 metadata_cache_len,
380 metadata_cache_capacity,
381 result_cache_hits: self.result_cache_hits.load(Ordering::Relaxed),
382 result_cache_misses: self.result_cache_misses.load(Ordering::Relaxed),
383 metadata_cache_hits: self.metadata_cache_hits.load(Ordering::Relaxed),
384 metadata_cache_misses: self.metadata_cache_misses.load(Ordering::Relaxed),
385 local_cache_hits: self.local_cache_hits.load(Ordering::Relaxed),
386 local_cache_misses: self.local_cache_misses.load(Ordering::Relaxed),
387 reload_successes: self.reload_successes.load(Ordering::Relaxed),
388 reload_failures: self.reload_failures.load(Ordering::Relaxed),
389 }
390 }
391
392 pub fn current_metadata_scope(&self) -> MetadataCacheScope {
393 self.provider
394 .read()
395 .ok()
396 .and_then(|guard| guard.as_ref().cloned())
397 .map(|handle| MetadataCacheScope {
398 datasource_id: handle.datasource_id.clone(),
399 generation: handle.generation,
400 })
401 .unwrap_or_else(|| MetadataCacheScope {
402 datasource_id: DatasourceId("sqlite:standalone".to_string()),
403 generation: Generation(0),
404 })
405 }
406
407 pub fn current_provider_kind(&self) -> Option<ProviderKind> {
408 self.provider
409 .read()
410 .ok()
411 .and_then(|guard| guard.as_ref().map(|handle| handle.kind.clone()))
412 }
413
414 pub fn record_result_cache_hit(&self) {
415 self.result_cache_hits.fetch_add(1, Ordering::Relaxed);
416 }
417
418 pub fn record_result_cache_miss(&self) {
419 self.result_cache_misses.fetch_add(1, Ordering::Relaxed);
420 }
421
422 pub fn record_metadata_cache_hit(&self) {
423 self.metadata_cache_hits.fetch_add(1, Ordering::Relaxed);
424 }
425
426 pub fn record_metadata_cache_miss(&self) {
427 self.metadata_cache_misses.fetch_add(1, Ordering::Relaxed);
428 }
429
430 pub fn record_local_cache_hit(&self) {
431 self.local_cache_hits.fetch_add(1, Ordering::Relaxed);
432 }
433
434 pub fn record_local_cache_miss(&self) {
435 self.local_cache_misses.fetch_add(1, Ordering::Relaxed);
436 }
437
438 pub fn execute(&self, req: &QueryRequest) -> KnowledgeResult<QueryResponse> {
439 let handle = self.current_handle()?;
440 let result_cache_config = self
441 .result_cache_config
442 .read()
443 .map(|guard| *guard)
444 .unwrap_or_default();
445 let use_global_cache =
446 matches!(req.cache_policy, CachePolicy::UseGlobal) && result_cache_config.enabled;
447 if use_global_cache && let Some(hit) = self.fetch_result_cache(&handle, req) {
448 self.record_result_cache_hit();
449 telemetry().on_cache(&CacheTelemetryEvent {
450 layer: CacheLayer::Result,
451 outcome: CacheOutcome::Hit,
452 provider_kind: Some(handle.kind.clone()),
453 });
454 debug_kdb!(
455 "[kdb] global result cache hit kind={:?} generation={}",
456 handle.kind,
457 handle.generation.0
458 );
459 return Ok(hit);
460 }
461 if use_global_cache {
462 self.record_result_cache_miss();
463 telemetry().on_cache(&CacheTelemetryEvent {
464 layer: CacheLayer::Result,
465 outcome: CacheOutcome::Miss,
466 provider_kind: Some(handle.kind.clone()),
467 });
468 debug_kdb!(
469 "[kdb] global result cache miss kind={:?} generation={}",
470 handle.kind,
471 handle.generation.0
472 );
473 }
474
475 let params = params_to_fields(&req.params);
476 let mode_tag = query_mode_tag(&req.mode);
477 let started = Instant::now();
478 debug_kdb!(
479 "[kdb] execute query kind={:?} generation={} mode={:?} cache_policy={:?}",
480 handle.kind,
481 handle.generation.0,
482 req.mode,
483 req.cache_policy
484 );
485 let response = match match req.mode {
486 QueryMode::Many => {
487 if params.is_empty() {
488 handle.provider.query(&req.sql).map(QueryResponse::Rows)
489 } else {
490 handle
491 .provider
492 .query_fields(&req.sql, ¶ms)
493 .map(QueryResponse::Rows)
494 }
495 }
496 QueryMode::FirstRow => {
497 if params.is_empty() {
498 handle.provider.query_row(&req.sql).map(QueryResponse::Row)
499 } else {
500 handle
501 .provider
502 .query_named_fields(&req.sql, ¶ms)
503 .map(QueryResponse::Row)
504 }
505 }
506 } {
507 Ok(response) => {
508 telemetry().on_query(&QueryTelemetryEvent {
509 provider_kind: handle.kind.clone(),
510 mode: mode_tag,
511 success: true,
512 elapsed: started.elapsed(),
513 });
514 response
515 }
516 Err(err) => {
517 telemetry().on_query(&QueryTelemetryEvent {
518 provider_kind: handle.kind.clone(),
519 mode: mode_tag,
520 success: false,
521 elapsed: started.elapsed(),
522 });
523 return Err(err);
524 }
525 };
526
527 if use_global_cache {
528 self.save_result_cache(&handle, req, response.clone());
529 debug_kdb!(
530 "[kdb] global result cache store kind={:?} generation={}",
531 handle.kind,
532 handle.generation.0
533 );
534 }
535
536 Ok(response)
537 }
538
539 fn current_handle(&self) -> KnowledgeResult<Arc<ProviderHandle>> {
540 self.provider
541 .read()
542 .expect("runtime provider lock poisoned")
543 .clone()
544 .ok_or_else(|| {
545 KnowledgeReason::from_logic()
546 .to_err()
547 .with_detail("knowledge provider not initialized")
548 })
549 }
550
551 fn fetch_result_cache(
552 &self,
553 handle: &ProviderHandle,
554 req: &QueryRequest,
555 ) -> Option<QueryResponse> {
556 let config = self
557 .result_cache_config
558 .read()
559 .map(|guard| *guard)
560 .unwrap_or_default();
561 if !config.enabled {
562 return None;
563 }
564 let key = result_cache_key(handle, req);
565 let cached = self
566 .result_cache
567 .read()
568 .ok()
569 .and_then(|cache| cache.peek(&key).cloned())?;
570 if cached.cached_at.elapsed() > config.ttl {
571 if let Ok(mut cache) = self.result_cache.write() {
572 let _ = cache.pop(&key);
573 }
574 return None;
575 }
576 Some((*cached.response).clone())
577 }
578
579 fn save_result_cache(
580 &self,
581 handle: &ProviderHandle,
582 req: &QueryRequest,
583 response: QueryResponse,
584 ) {
585 if let Ok(mut cache) = self.result_cache.write() {
586 cache.put(
587 result_cache_key(handle, req),
588 CachedQueryResponse {
589 response: Arc::new(response),
590 cached_at: Instant::now(),
591 },
592 );
593 }
594 }
595}
596
597pub fn runtime() -> &'static KnowledgeRuntime {
598 static RUNTIME: OnceLock<KnowledgeRuntime> = OnceLock::new();
599 RUNTIME.get_or_init(|| KnowledgeRuntime::new(1024))
600}
601
602#[cfg(test)]
603pub(crate) fn runtime_test_guard() -> &'static std::sync::Mutex<()> {
604 static GUARD: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
605 GUARD.get_or_init(|| std::sync::Mutex::new(()))
606}
607
608fn result_cache_key(handle: &ProviderHandle, req: &QueryRequest) -> ResultCacheKey {
609 ResultCacheKey {
610 datasource_id: handle.datasource_id.clone(),
611 generation: handle.generation,
612 query_hash: stable_hash(&req.sql),
613 params_hash: stable_params_hash(&req.params),
614 mode: match req.mode {
615 QueryMode::Many => QueryModeTag::Many,
616 QueryMode::FirstRow => QueryModeTag::FirstRow,
617 },
618 }
619}
620
621fn query_mode_tag(mode: &QueryMode) -> QueryModeTag {
622 match mode {
623 QueryMode::Many => QueryModeTag::Many,
624 QueryMode::FirstRow => QueryModeTag::FirstRow,
625 }
626}
627
628fn stable_hash(value: &str) -> u64 {
629 let mut hasher = DefaultHasher::new();
630 value.hash(&mut hasher);
631 hasher.finish()
632}
633
634fn stable_params_hash(params: &[QueryParam]) -> u64 {
635 let mut hasher = DefaultHasher::new();
636 for param in params {
637 param.name.hash(&mut hasher);
638 match ¶m.value {
639 QueryValue::Null => 0u8.hash(&mut hasher),
640 QueryValue::Bool(value) => {
641 1u8.hash(&mut hasher);
642 value.hash(&mut hasher);
643 }
644 QueryValue::Int(value) => {
645 2u8.hash(&mut hasher);
646 value.hash(&mut hasher);
647 }
648 QueryValue::Float(value) => {
649 3u8.hash(&mut hasher);
650 value.to_bits().hash(&mut hasher);
651 }
652 QueryValue::Text(value) => {
653 4u8.hash(&mut hasher);
654 value.hash(&mut hasher);
655 }
656 }
657 }
658 hasher.finish()
659}
660
661pub fn fields_to_params(params: &[DataField]) -> Vec<QueryParam> {
662 params
663 .iter()
664 .map(|field| {
665 let value = match field.get_value() {
666 Value::Null | Value::Ignore(_) => QueryValue::Null,
667 Value::Bool(value) => QueryValue::Bool(*value),
668 Value::Digit(value) => QueryValue::Int(*value),
669 Value::Float(value) => QueryValue::Float(*value),
670 Value::Chars(value) => QueryValue::Text(value.to_string()),
671 Value::Symbol(value) => QueryValue::Text(value.to_string()),
672 Value::Time(value) => QueryValue::Text(value.to_string()),
673 Value::Hex(value) => QueryValue::Text(value.to_string()),
674 Value::IpNet(value) => QueryValue::Text(value.to_string()),
675 Value::IpAddr(value) => QueryValue::Text(value.to_string()),
676 Value::Obj(value) => QueryValue::Text(format!("{:?}", value)),
677 Value::Array(value) => QueryValue::Text(format!("{:?}", value)),
678 Value::Domain(value) => QueryValue::Text(value.0.to_string()),
679 Value::Url(value) => QueryValue::Text(value.0.to_string()),
680 Value::Email(value) => QueryValue::Text(value.0.to_string()),
681 Value::IdCard(value) => QueryValue::Text(value.0.to_string()),
682 Value::MobilePhone(value) => QueryValue::Text(value.0.to_string()),
683 };
684 QueryParam {
685 name: field.get_name().to_string(),
686 value,
687 }
688 })
689 .collect()
690}
691
692pub fn params_to_fields(params: &[QueryParam]) -> Vec<DataField> {
693 params
694 .iter()
695 .map(|param| match ¶m.value {
696 QueryValue::Null => {
697 DataField::new(DataType::default(), param.name.clone(), Value::Null)
698 }
699 QueryValue::Bool(value) => {
700 DataField::new(DataType::default(), param.name.clone(), Value::Bool(*value))
701 }
702 QueryValue::Int(value) => DataField::from_digit(param.name.clone(), *value),
703 QueryValue::Float(value) => DataField::from_float(param.name.clone(), *value),
704 QueryValue::Text(value) => DataField::from_chars(param.name.clone(), value.clone()),
705 })
706 .collect()
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use wp_model_core::model::Value;
713
714 #[test]
715 fn query_param_hash_is_stable() {
716 let params = vec![
717 QueryParam {
718 name: ":id".to_string(),
719 value: QueryValue::Int(7),
720 },
721 QueryParam {
722 name: ":name".to_string(),
723 value: QueryValue::Text("abc".to_string()),
724 },
725 ];
726 assert_eq!(stable_params_hash(¶ms), stable_params_hash(¶ms));
727 }
728
729 #[test]
730 fn fields_to_params_preserves_raw_chars_value() {
731 let fields = [DataField::from_chars(
732 ":name".to_string(),
733 "令狐冲".to_string(),
734 )];
735 let params = fields_to_params(&fields);
736 assert_eq!(params.len(), 1);
737 match ¶ms[0].value {
738 QueryValue::Text(value) => assert_eq!(value, "令狐冲"),
739 other => panic!("unexpected param value: {other:?}"),
740 }
741 let roundtrip = params_to_fields(¶ms);
742 assert!(matches!(roundtrip[0].get_value(), Value::Chars(_)));
743 }
744}