Skip to main content

wp_knowledge/
runtime.rs

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, &params)
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, &params)
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 &param.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 &param.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(&params), stable_params_hash(&params));
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 &params[0].value {
738            QueryValue::Text(value) => assert_eq!(value, "令狐冲"),
739            other => panic!("unexpected param value: {other:?}"),
740        }
741        let roundtrip = params_to_fields(&params);
742        assert!(matches!(roundtrip[0].get_value(), Value::Chars(_)));
743    }
744}