Merge 'Fix two issues in simulator' from Jussi Saurio

## 1. select equal number of columns per compound subselect in simulator
#2609 fixed compound selects incorrectly, introducing another bug: now
the sim can SELECT a different number of columns per subselect, which is
illegal. this PR forces the sim to select the same number of cols per
subselect.
Depends on #2614 , otherwise the sim constantly fails whenever it
decides to do a compound select with DISTINCT.
Closes #2611
## 2. introduce TableContext for the simulator to properly generate
predicates for Joins
The simulator has a construct called `JoinTable` which is really a
`JoinContext` that includes all the columns from the so-far joined
tables, so that the next table to be joined can refer to columns from
any of those tables. @pedrocarlo 's commit fixes an issue where
`JoinTable` would incorrectly generate predicates with empty table
names. Related: #2618

Closes #2616
This commit is contained in:
Jussi Saurio
2025-08-15 23:49:29 +03:00
committed by GitHub
7 changed files with 214 additions and 168 deletions

View File

@@ -11,7 +11,7 @@ use crate::{
},
model::{
query::predicate::Predicate,
table::{SimValue, Table},
table::{SimValue, Table, TableContext},
},
};
@@ -241,39 +241,30 @@ impl Predicate {
}
impl SimplePredicate {
/// Generates a true [ast::Expr::Binary] [SimplePredicate] from a [Table] for a row in the table
pub fn true_binary<R: rand::Rng>(rng: &mut R, table: &Table, row: &[SimValue]) -> Self {
/// Generates a true [ast::Expr::Binary] [SimplePredicate] from a [TableContext] for a row in the table
pub fn true_binary<R: rand::Rng, T: TableContext>(
rng: &mut R,
table: &T,
row: &[SimValue],
) -> Self {
// Pick a random column
let column_index = rng.gen_range(0..table.columns.len());
let mut column = table.columns[column_index].clone();
let columns = table.columns().collect::<Vec<_>>();
let column_index = rng.gen_range(0..columns.len());
let column = columns[column_index];
let column_value = &row[column_index];
let mut table_name = table.name.clone();
let table_name = column.table_name;
// Avoid creation of NULLs
if row.is_empty() {
return SimplePredicate(Predicate(Expr::Literal(SimValue::TRUE.into())));
}
if table.name.is_empty() {
// If the table name is empty, we cannot create a qualified expression
// so we use the column name directly
let mut splitted = column.name.split('.');
table_name = splitted
.next()
.expect("Column name should have a table prefix for a joined table")
.to_string();
column.name = splitted
.next()
.expect("Column name should have a column suffix for a joined table")
.to_string();
}
let expr = one_of(
vec![
Box::new(|_rng| {
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::Equals,
Box::new(Expr::Literal(column_value.into())),
@@ -283,8 +274,8 @@ impl SimplePredicate {
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(lt_value.into())),
@@ -294,8 +285,8 @@ impl SimplePredicate {
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(gt_value.into())),
@@ -307,42 +298,30 @@ impl SimplePredicate {
SimplePredicate(Predicate(expr))
}
/// Generates a false [ast::Expr::Binary] [SimplePredicate] from a [Table] for a row in the table
pub fn false_binary<R: rand::Rng>(rng: &mut R, table: &Table, row: &[SimValue]) -> Self {
/// Generates a false [ast::Expr::Binary] [SimplePredicate] from a [TableContext] for a row in the table
pub fn false_binary<R: rand::Rng, T: TableContext>(
rng: &mut R,
table: &T,
row: &[SimValue],
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..table.columns.len());
// println!("column_index: {}", column_index);
// println!("table.columns: {:?}", table.columns);
// println!("row: {:?}", row);
let mut column = table.columns[column_index].clone();
let column_index = rng.gen_range(0..columns.len());
let column = columns[column_index];
let column_value = &row[column_index];
let mut table_name = table.name.clone();
let table_name = column.table_name;
// Avoid creation of NULLs
if row.is_empty() {
return SimplePredicate(Predicate(Expr::Literal(SimValue::FALSE.into())));
}
if table.name.is_empty() {
// If the table name is empty, we cannot create a qualified expression
// so we use the column name directly
let mut splitted = column.name.split('.');
table_name = splitted
.next()
.expect("Column name should have a table prefix for a joined table")
.to_string();
column.name = splitted
.next()
.expect("Column name should have a column suffix for a joined table")
.to_string();
}
let expr = one_of(
vec![
Box::new(|_rng| {
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::NotEquals,
Box::new(Expr::Literal(column_value.into())),
@@ -352,8 +331,8 @@ impl SimplePredicate {
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(gt_value.into())),
@@ -363,8 +342,8 @@ impl SimplePredicate {
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(lt_value.into())),
@@ -381,27 +360,21 @@ impl CompoundPredicate {
/// Decide if you want to create an AND or an OR
///
/// Creates a Compound Predicate that is TRUE or FALSE for at least a single row
pub fn from_table_binary<R: rand::Rng>(
pub fn from_table_binary<R: rand::Rng, T: TableContext>(
rng: &mut R,
table: &Table,
table: &T,
predicate_value: bool,
) -> Self {
// Cannot pick a row if the table is empty
if table.rows.is_empty() {
let rows = table.rows();
if rows.is_empty() {
return Self(if predicate_value {
Predicate::true_()
} else {
Predicate::false_()
});
}
let row = pick(&table.rows, rng);
tracing::trace!(
"Creating a {} CompoundPredicate for table: {} and row: {:?}",
if predicate_value { "true" } else { "false" },
table.name,
row
);
let row = pick(rows, rng);
let predicate = if rng.gen_bool(0.7) {
// An AND for true requires each of its children to be true

View File

@@ -3,7 +3,7 @@ use turso_sqlite3_parser::ast::{self, Expr};
use crate::model::{
query::predicate::Predicate,
table::{SimValue, Table},
table::{SimValue, Table, TableContext},
};
use super::{one_of, ArbitraryFrom};
@@ -17,11 +17,8 @@ struct CompoundPredicate(Predicate);
#[derive(Debug)]
struct SimplePredicate(Predicate);
impl<A: AsRef<[SimValue]>> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(
rng: &mut R,
(table, row, predicate_value): (&Table, A, bool),
) -> Self {
impl<A: AsRef<[SimValue]>, T: TableContext> ArbitraryFrom<(&T, A, bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, row, predicate_value): (&T, A, bool)) -> Self {
let row = row.as_ref();
// Pick an operator
let choice = rng.gen_range(0..2);
@@ -41,21 +38,21 @@ impl<A: AsRef<[SimValue]>> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate
}
}
impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self {
impl<T: TableContext> ArbitraryFrom<(&T, bool)> for CompoundPredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&T, bool)) -> Self {
CompoundPredicate::from_table_binary(rng, table, predicate_value)
}
}
impl ArbitraryFrom<&Table> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &Table) -> Self {
impl<T: TableContext> ArbitraryFrom<&T> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &T) -> Self {
let predicate_value = rng.gen_bool(0.5);
Predicate::arbitrary_from(rng, (table, predicate_value)).parens()
}
}
impl ArbitraryFrom<(&Table, bool)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self {
impl<T: TableContext> ArbitraryFrom<(&T, bool)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&T, bool)) -> Self {
CompoundPredicate::arbitrary_from(rng, (table, predicate_value)).0
}
}

View File

@@ -8,7 +8,7 @@ use crate::{
generation::{backtrack, pick, predicate::SimplePredicate, ArbitraryFromMaybe},
model::{
query::predicate::Predicate,
table::{SimValue, Table},
table::{SimValue, TableContext},
},
};
@@ -99,10 +99,15 @@ impl ArbitraryFromMaybe<(&Vec<&SimValue>, bool)> for BitNotValue {
// TODO: have some more complex generation with columns names here as well
impl SimplePredicate {
/// Generates a true [ast::Expr::Unary] [SimplePredicate] from a [Table] for some values in the table
pub fn true_unary<R: rand::Rng>(rng: &mut R, table: &Table, row: &[SimValue]) -> Self {
/// Generates a true [ast::Expr::Unary] [SimplePredicate] from a [TableContext] for some values in the table
pub fn true_unary<R: rand::Rng, T: TableContext>(
rng: &mut R,
table: &T,
row: &[SimValue],
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..table.columns.len());
let column_index = rng.gen_range(0..columns.len());
let column_value = &row[column_index];
let num_retries = row.len();
// Avoid creation of NULLs
@@ -160,10 +165,15 @@ impl SimplePredicate {
))
}
/// Generates a false [ast::Expr::Unary] [SimplePredicate] from a [Table] for a row in the table
pub fn false_unary<R: rand::Rng>(rng: &mut R, table: &Table, row: &[SimValue]) -> Self {
/// Generates a false [ast::Expr::Unary] [SimplePredicate] from a [TableContext] for a row in the table
pub fn false_unary<R: rand::Rng, T: TableContext>(
rng: &mut R,
table: &T,
row: &[SimValue],
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..table.columns.len());
let column_index = rng.gen_range(0..columns.len());
let column_value = &row[column_index];
let num_retries = row.len();
// Avoid creation of NULLs

View File

@@ -1,12 +1,12 @@
use crate::generation::{Arbitrary, ArbitraryFrom, ArbitrarySizedFrom, Shadow};
use crate::model::query::predicate::Predicate;
use crate::model::query::select::{
CompoundOperator, CompoundSelect, Distinctness, FromClause, JoinTable, JoinType, JoinedTable,
OrderBy, ResultColumn, SelectBody, SelectInner,
CompoundOperator, CompoundSelect, Distinctness, FromClause, OrderBy, ResultColumn, SelectBody,
SelectInner,
};
use crate::model::query::update::Update;
use crate::model::query::{Create, Delete, Drop, Insert, Query, Select};
use crate::model::table::{SimValue, Table};
use crate::model::table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext};
use crate::SimulatorEnv;
use itertools::Itertools;
use rand::Rng;
@@ -39,27 +39,32 @@ impl ArbitraryFrom<&Vec<Table>> for FromClause {
let name = table.name.clone();
let mut table_context = JoinTable {
tables: Vec::new(),
rows: Vec::new(),
};
let joins: Vec<_> = (0..num_joins)
.filter_map(|_| {
if tables.is_empty() {
return None;
}
let join_table = pick(&tables, rng).clone();
let joined_table_name = join_table.name.clone();
tables.retain(|t| t.name != join_table.name);
table = JoinTable {
tables: vec![table.clone(), join_table.clone()],
rows: table
.rows
.iter()
.cartesian_product(join_table.rows.iter())
.map(|(t_row, j_row)| {
let mut row = t_row.clone();
row.extend(j_row.clone());
row
})
.collect(),
}
.into_table();
table_context.rows = table_context
.rows
.iter()
.cartesian_product(join_table.rows.iter())
.map(|(t_row, j_row)| {
let mut row = t_row.clone();
row.extend(j_row.clone());
row
})
.collect();
// TODO: inneficient. use a Deque to push_front?
table_context.tables.insert(0, join_table);
for row in &mut table.rows {
assert_eq!(
row.len(),
@@ -70,7 +75,7 @@ impl ArbitraryFrom<&Vec<Table>> for FromClause {
let predicate = Predicate::arbitrary_from(rng, &table);
Some(JoinedTable {
table: join_table.name.clone(),
table: joined_table_name,
join_type: JoinType::Inner,
on: predicate,
})
@@ -87,8 +92,8 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
// todo: this is a temporary hack because env is not separated from the tables
let join_table = from
.shadow(&mut tables)
.expect("Failed to shadow FromClause")
.into_table();
.expect("Failed to shadow FromClause");
let cuml_col_count = join_table.columns().count();
let order_by = 'order_by: {
if rng.gen_bool(0.3) {
@@ -98,7 +103,6 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
.map(|j| j.table.clone())
.chain(std::iter::once(from.table.clone()))
.collect::<Vec<_>>();
let cuml_col_count = join_table.columns.len();
let order_by_col_count =
(rng.gen::<f64>() * rng.gen::<f64>() * (cuml_col_count as f64)) as usize; // skew towards 0
if order_by_col_count == 0 {
@@ -144,6 +148,43 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
}
}
impl ArbitrarySizedFrom<&SimulatorEnv> for SelectInner {
fn arbitrary_sized_from<R: Rng>(
rng: &mut R,
env: &SimulatorEnv,
num_result_columns: usize,
) -> Self {
let mut select_inner = SelectInner::arbitrary_from(rng, env);
let select_from = &select_inner.from.as_ref().unwrap();
let table_names = select_from
.joins
.iter()
.map(|j| j.table.clone())
.chain(std::iter::once(select_from.table.clone()))
.collect::<Vec<_>>();
let flat_columns_names = table_names
.iter()
.flat_map(|t| {
env.tables
.iter()
.find(|table| table.name == *t)
.unwrap()
.columns
.iter()
.map(|c| format!("{}.{}", t.clone(), c.name))
})
.collect::<Vec<_>>();
let selected_columns = pick_unique(&flat_columns_names, num_result_columns, rng);
let mut columns = Vec::new();
for column_name in selected_columns {
columns.push(ResultColumn::Column(column_name.clone()));
}
select_inner.columns = columns;
select_inner
}
}
impl Arbitrary for Distinctness {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
match rng.gen_range(0..=5) {
@@ -191,10 +232,15 @@ impl ArbitraryFrom<&SimulatorEnv> for Select {
0
};
let first = SelectInner::arbitrary_from(rng, env);
let min_column_count_across_tables =
env.tables.iter().map(|t| t.columns.len()).min().unwrap();
let num_result_columns = rng.gen_range(1..=min_column_count_across_tables);
let first = SelectInner::arbitrary_sized_from(rng, env, num_result_columns);
let rest: Vec<SelectInner> = (0..num_compound_selects)
.map(|_| SelectInner::arbitrary_from(rng, env))
.map(|_| SelectInner::arbitrary_sized_from(rng, env, num_result_columns))
.collect();
Self {

View File

@@ -3,7 +3,7 @@ use std::fmt::Display;
use serde::{Deserialize, Serialize};
use turso_sqlite3_parser::ast::{self, fmt::ToTokens};
use crate::model::table::{SimValue, Table};
use crate::model::table::{SimValue, Table, TableContext};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Predicate(pub ast::Expr);
@@ -78,7 +78,7 @@ impl Predicate {
expr_to_value(&self.0, row, table)
}
pub(crate) fn test(&self, row: &[SimValue], table: &Table) -> bool {
pub(crate) fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
let value = expr_to_value(&self.0, row, table);
value.is_some_and(|value| value.as_bool())
}
@@ -88,19 +88,23 @@ impl Predicate {
// This function attempts to convert an simpler easily computable expression into values
// TODO: In the future, we can try to expand this computation if we want to support harder properties that require us
// to already know more values before hand
pub fn expr_to_value(expr: &ast::Expr, row: &[SimValue], table: &Table) -> Option<SimValue> {
pub fn expr_to_value<T: TableContext>(
expr: &ast::Expr,
row: &[SimValue],
table: &T,
) -> Option<SimValue> {
match expr {
ast::Expr::DoublyQualified(_, _, ast::Name::Ident(col_name))
| ast::Expr::DoublyQualified(_, _, ast::Name::Quoted(col_name))
| ast::Expr::Qualified(_, ast::Name::Ident(col_name))
| ast::Expr::Qualified(_, ast::Name::Quoted(col_name))
| ast::Expr::Id(ast::Name::Ident(col_name)) => {
assert_eq!(row.len(), table.columns.len());
table
.columns
let columns = table.columns().collect::<Vec<_>>();
assert_eq!(row.len(), columns.len());
columns
.iter()
.zip(row.iter())
.find(|(column, _)| column.name == *col_name)
.find(|(column, _)| column.column.name == *col_name)
.map(|(_, value)| value)
.cloned()
}

View File

@@ -10,7 +10,7 @@ use crate::{
generation::Shadow,
model::{
query::EmptyContext,
table::{SimValue, Table},
table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext},
},
runner::env::SimulatorTables,
};
@@ -239,51 +239,6 @@ impl FromClause {
deps
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct JoinedTable {
/// table name
pub table: String,
/// `JOIN` type
pub join_type: JoinType,
/// `ON` clause
pub on: Predicate,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum JoinType {
Inner,
Left,
Right,
Full,
Cross,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JoinTable {
pub tables: Vec<Table>,
pub rows: Vec<Vec<SimValue>>,
}
impl JoinTable {
pub(crate) fn into_table(self) -> Table {
let t = Table {
name: "".to_string(),
columns: self
.tables
.iter()
.flat_map(|t| {
t.columns.iter().map(|c| {
let mut c = c.clone();
c.name = format!("{}.{}", t.name, c.name);
c
})
})
.collect(),
rows: self.rows,
};
t
}
}
impl Shadow for FromClause {
type Result = anyhow::Result<JoinTable>;
@@ -327,8 +282,7 @@ impl Shadow for FromClause {
for (row1, row2) in all_row_pairs {
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
let as_table = join_table.clone().into_table();
let is_in = join.on.test(&row, &as_table);
let is_in = join.on.test(&row, &join_table);
if is_in {
join_table.rows.push(row);
@@ -348,18 +302,19 @@ impl Shadow for SelectInner {
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
if let Some(from) = &self.from {
let mut join_table = from.shadow(env)?;
let as_table = join_table.clone().into_table();
let col_count = join_table.columns().count();
for row in &mut join_table.rows {
assert_eq!(
row.len(),
as_table.columns.len(),
col_count,
"Row length does not match column length after join"
);
}
let join_clone = join_table.clone();
join_table
.rows
.retain(|row| self.where_clause.test(row, &as_table));
.retain(|row| self.where_clause.test(row, &join_clone));
if self.distinctness == Distinctness::Distinct {
join_table.rows.sort_unstable();
@@ -414,7 +369,7 @@ impl Shadow for Select {
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let first_result = self.body.select.shadow(env)?;
let mut rows = first_result.into_table().rows;
let mut rows = first_result.rows;
for compound in self.body.compounds.iter() {
let compound_results = compound.select.shadow(env)?;
@@ -422,7 +377,7 @@ impl Shadow for Select {
match compound.operator {
CompoundOperator::Union => {
// Union means we need to combine the results, removing duplicates
let mut new_rows = compound_results.into_table().rows;
let mut new_rows = compound_results.rows;
new_rows.extend(rows.clone());
new_rows.sort_unstable();
new_rows.dedup();

View File

@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use turso_core::{numeric::Numeric, types};
use turso_sqlite3_parser::ast;
use crate::model::query::predicate::Predicate;
pub(crate) struct Name(pub(crate) String);
impl Deref for Name {
@@ -14,11 +16,35 @@ impl Deref for Name {
}
}
#[derive(Debug, Clone, Copy)]
pub struct ContextColumn<'a> {
pub table_name: &'a str,
pub column: &'a Column,
}
pub trait TableContext {
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>>;
fn rows(&self) -> &Vec<Vec<SimValue>>;
}
impl TableContext for Table {
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>> {
self.columns.iter().map(|col| ContextColumn {
column: col,
table_name: &self.name,
})
}
fn rows(&self) -> &Vec<Vec<SimValue>> {
&self.rows
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Table {
pub(crate) rows: Vec<Vec<SimValue>>,
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
pub(crate) rows: Vec<Vec<SimValue>>,
}
impl Table {
@@ -73,6 +99,41 @@ impl Display for ColumnType {
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct JoinedTable {
/// table name
pub table: String,
/// `JOIN` type
pub join_type: JoinType,
/// `ON` clause
pub on: Predicate,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum JoinType {
Inner,
Left,
Right,
Full,
Cross,
}
impl TableContext for JoinTable {
fn columns<'a>(&'a self) -> impl Iterator<Item = ContextColumn<'a>> {
self.tables.iter().flat_map(|table| table.columns())
}
fn rows(&self) -> &Vec<Vec<SimValue>> {
&self.rows
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JoinTable {
pub tables: Vec<Table>,
pub rows: Vec<Vec<SimValue>>,
}
fn float_to_string<S>(float: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,