From 3760d44c13f69bf3a3114ffc41abc08ed737bec8 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Fri, 15 Aug 2025 15:45:33 +0300 Subject: [PATCH 1/2] sim: force compound selects to have the same number of result columns --- simulator/generation/query.rs | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index c0b3ce7cc..7e07c529c 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -144,6 +144,43 @@ impl ArbitraryFrom<&SimulatorEnv> for SelectInner { } } +impl ArbitrarySizedFrom<&SimulatorEnv> for SelectInner { + fn arbitrary_sized_from( + 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::>(); + + 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::>(); + 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(rng: &mut R) -> Self { match rng.gen_range(0..=5) { @@ -191,10 +228,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 = (0..num_compound_selects) - .map(|_| SelectInner::arbitrary_from(rng, env)) + .map(|_| SelectInner::arbitrary_sized_from(rng, env, num_result_columns)) .collect(); Self { From 2bc6edc3d4caa138c4ba34cb8c7a4f6b6af63f6b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Fri, 15 Aug 2025 12:44:43 -0300 Subject: [PATCH 2/2] introduce `TableContext` for the simulator to properly generate predicates for Joins --- simulator/generation/predicate/binary.rs | 103 +++++++++-------------- simulator/generation/predicate/mod.rs | 21 ++--- simulator/generation/predicate/unary.rs | 24 ++++-- simulator/generation/query.rs | 46 +++++----- simulator/model/query/predicate.rs | 18 ++-- simulator/model/query/select.rs | 61 ++------------ simulator/model/table.rs | 63 +++++++++++++- 7 files changed, 170 insertions(+), 166 deletions(-) diff --git a/simulator/generation/predicate/binary.rs b/simulator/generation/predicate/binary.rs index 522b3fdce..f8ba27236 100644 --- a/simulator/generation/predicate/binary.rs +++ b/simulator/generation/predicate/binary.rs @@ -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(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( + 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::>(); + 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(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( + rng: &mut R, + table: &T, + row: &[SimValue], + ) -> Self { + let columns = table.columns().collect::>(); // 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( + pub fn from_table_binary( 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 diff --git a/simulator/generation/predicate/mod.rs b/simulator/generation/predicate/mod.rs index 12d28cf41..5c5887818 100644 --- a/simulator/generation/predicate/mod.rs +++ b/simulator/generation/predicate/mod.rs @@ -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> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate { - fn arbitrary_from( - rng: &mut R, - (table, row, predicate_value): (&Table, A, bool), - ) -> Self { +impl, T: TableContext> ArbitraryFrom<(&T, A, bool)> for SimplePredicate { + fn arbitrary_from(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> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate } } -impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate { - fn arbitrary_from(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { +impl ArbitraryFrom<(&T, bool)> for CompoundPredicate { + fn arbitrary_from(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(rng: &mut R, table: &Table) -> Self { +impl ArbitraryFrom<&T> for Predicate { + fn arbitrary_from(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(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { +impl ArbitraryFrom<(&T, bool)> for Predicate { + fn arbitrary_from(rng: &mut R, (table, predicate_value): (&T, bool)) -> Self { CompoundPredicate::arbitrary_from(rng, (table, predicate_value)).0 } } diff --git a/simulator/generation/predicate/unary.rs b/simulator/generation/predicate/unary.rs index 52f1d366c..f7f374b6e 100644 --- a/simulator/generation/predicate/unary.rs +++ b/simulator/generation/predicate/unary.rs @@ -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(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( + rng: &mut R, + table: &T, + row: &[SimValue], + ) -> Self { + let columns = table.columns().collect::>(); // 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(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( + rng: &mut R, + table: &T, + row: &[SimValue], + ) -> Self { + let columns = table.columns().collect::>(); // 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 diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 7e07c529c..4950540eb 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -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> 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
> 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::>(); - let cuml_col_count = join_table.columns.len(); let order_by_col_count = (rng.gen::() * rng.gen::() * (cuml_col_count as f64)) as usize; // skew towards 0 if order_by_col_count == 0 { diff --git a/simulator/model/query/predicate.rs b/simulator/model/query/predicate.rs index 36e1c362c..c55c45417 100644 --- a/simulator/model/query/predicate.rs +++ b/simulator/model/query/predicate.rs @@ -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(&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 { +pub fn expr_to_value( + expr: &ast::Expr, + row: &[SimValue], + table: &T, +) -> Option { 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::>(); + 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() } diff --git a/simulator/model/query/select.rs b/simulator/model/query/select.rs index c06bbc009..2dcc762a8 100644 --- a/simulator/model/query/select.rs +++ b/simulator/model/query/select.rs @@ -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
, - pub rows: Vec>, -} - -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; @@ -327,8 +282,7 @@ impl Shadow for FromClause { for (row1, row2) in all_row_pairs { let row = row1.iter().chain(row2.iter()).cloned().collect::>(); - 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(); diff --git a/simulator/model/table.rs b/simulator/model/table.rs index 6972cf6ac..b50fd84ea 100644 --- a/simulator/model/table.rs +++ b/simulator/model/table.rs @@ -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>; + fn rows(&self) -> &Vec>; +} + +impl TableContext for Table { + fn columns<'a>(&'a self) -> impl Iterator> { + self.columns.iter().map(|col| ContextColumn { + column: col, + table_name: &self.name, + }) + } + + fn rows(&self) -> &Vec> { + &self.rows + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Table { - pub(crate) rows: Vec>, pub(crate) name: String, pub(crate) columns: Vec, + pub(crate) rows: Vec>, } 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> { + self.tables.iter().flat_map(|table| table.columns()) + } + + fn rows(&self) -> &Vec> { + &self.rows + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JoinTable { + pub tables: Vec
, + pub rows: Vec>, +} + fn float_to_string(float: &f64, serializer: S) -> Result where S: serde::Serializer,