From 8ba84faf823f88d7874a2507ed481ae726f6d48d Mon Sep 17 00:00:00 2001 From: Rasmus Viitanen Date: Sun, 3 Oct 2021 16:59:15 +0200 Subject: [PATCH 1/3] make int bounds non-empty if low == high and either is included --- core/src/graph/number.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/graph/number.rs b/core/src/graph/number.rs index 14ec9a27..fe29355c 100644 --- a/core/src/graph/number.rs +++ b/core/src/graph/number.rs @@ -20,7 +20,7 @@ macro_rules! any_range_int_impl { fn is_empty(&self) -> bool { match (&self.low, &self.high) { (Bound::Excluded(low), Bound::Included(high)) - | (Bound::Included(low), Bound::Excluded(high)) => low >= high, + | (Bound::Included(low), Bound::Excluded(high)) => low > high, (Bound::Included(low), Bound::Included(high)) => low > high, (Bound::Excluded(low), Bound::Excluded(high)) => *low + 1 >= *high, _ => false @@ -30,16 +30,15 @@ macro_rules! any_range_int_impl { impl SampleRange<$target> for AnyRange<$target> { fn sample_single(self, rng: &mut R) -> $target { - let low = match self.low { - Bound::Unbounded => panic!("cannot sample {} range unbounded on the left", stringify!($target)), - Bound::Included(low) => low, - Bound::Excluded(low) => low + 1 - }; - - match self.high { - Bound::Excluded(high) => rng.gen_range(low..high), - Bound::Included(high) => rng.gen_range(low..=high), - Bound::Unbounded => panic!("cannot sample {} range unbounded on the right", stringify!($target)) + match (self.low, self.high) { + (Bound::Included(low), Bound::Included(high) | Bound::Excluded(high)) if low == high => low, + (Bound::Included(low) | Bound::Excluded(low), Bound::Included(high)) if low == high => high, + (Bound::Included(low), Bound::Excluded(high)) => {rng.gen_range(low..high)}, + (Bound::Included(low), Bound::Included(high)) => {rng.gen_range(low..=high)}, + (Bound::Excluded(low), Bound::Included(high)) => {rng.gen_range(low + 1..=high)}, + (Bound::Excluded(low), Bound::Excluded(high)) => {rng.gen_range(low + 1..high)}, + (Bound::Unbounded, _) => panic!("cannot sample {} range unbounded on the left", stringify!($target)), + (_, Bound::Unbounded) => panic!("cannot sample {} range unbounded on the right", stringify!($target)), } } From 85ca11b9065b01db6c3e4d5e0f2016cfb12d2532 Mon Sep 17 00:00:00 2001 From: Rasmus Viitanen Date: Sun, 3 Oct 2021 17:00:10 +0200 Subject: [PATCH 2/3] allow imports to empty dirs --- synth/src/cli/mod.rs | 4 ++-- synth/src/cli/store.rs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/synth/src/cli/mod.rs b/synth/src/cli/mod.rs index 20790000..16579777 100644 --- a/synth/src/cli/mod.rs +++ b/synth/src/cli/mod.rs @@ -137,9 +137,9 @@ impl Cli { self.store.save_collection_path(&path, collection, content)?; Ok(()) } - } else if self.store.ns_exists(&path) { + } else if self.store.ns_exists(&path) && !self.store.ns_is_empty_dir(&path) { Err(anyhow!( - "The directory at `{}` already exists. Will not import into an existing directory.", + "The directory at `{}` already exists and is not empty. Will not import into an existing directory.", path.display() )) } else { diff --git a/synth/src/cli/store.rs b/synth/src/cli/store.rs index 1910242d..7c6189be 100644 --- a/synth/src/cli/store.rs +++ b/synth/src/cli/store.rs @@ -64,6 +64,11 @@ impl Store { self.ns_path(namespace).exists() } + pub fn ns_is_empty_dir(&self, namespace: &Path) -> bool { + self.ns_path(namespace).is_dir() + && self.ns_path(namespace).read_dir().map(|mut dir| dir.next().is_none()).unwrap_or_default() + } + pub fn collection_exists(&self, namespace: &Path, collection: &Name) -> bool { self.collection_path(namespace, collection).exists() } From 9c9bcabbe2c8bc57c87294ec1c052c3bdfed4c4b Mon Sep 17 00:00:00 2001 From: Rasmus Viitanen Date: Sun, 3 Oct 2021 17:00:23 +0200 Subject: [PATCH 3/3] add support for sqlite --- Cargo.lock | 12 + core/Cargo.toml | 2 +- core/src/graph/mod.rs | 100 +++++++- synth/Cargo.toml | 2 +- synth/src/cli/export.rs | 7 +- synth/src/cli/import.rs | 6 + synth/src/cli/mod.rs | 1 + synth/src/cli/sqlite.rs | 46 ++++ synth/src/datasource/mod.rs | 1 + synth/src/datasource/sqlite_datasource.rs | 300 ++++++++++++++++++++++ 10 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 synth/src/cli/sqlite.rs create mode 100644 synth/src/datasource/sqlite_datasource.rs diff --git a/Cargo.lock b/Cargo.lock index 35cb97ac..0f2da190 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1648,6 +1648,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libssh2-sys" version = "0.2.21" @@ -2907,6 +2918,7 @@ dependencies = [ "hmac 0.10.1", "itoa", "libc", + "libsqlite3-sys", "log", "md-5", "memchr", diff --git a/core/Cargo.toml b/core/Cargo.toml index e2874a66..52b1c748 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -35,4 +35,4 @@ bimap = { version = "0.6.0", features = [ "std" ] } humantime-serde = "1.0.1" bloomfilter = "1.0.5" dynfmt = { version = "0.1.5", features = [ "curly" ] } -sqlx = { version = "0.5.7", features = [ "postgres", "mysql", "runtime-async-std-native-tls", "decimal", "chrono" ] } \ No newline at end of file +sqlx = { version = "0.5.7", features = [ "sqlite", "postgres", "mysql", "runtime-async-std-native-tls", "decimal", "chrono" ] } \ No newline at end of file diff --git a/core/src/graph/mod.rs b/core/src/graph/mod.rs index 19822709..534b3b60 100644 --- a/core/src/graph/mod.rs +++ b/core/src/graph/mod.rs @@ -4,7 +4,8 @@ use anyhow::{Context, Result}; use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use sqlx::mysql::MySqlTypeInfo; -use sqlx::{MySql, Postgres}; +use sqlx::sqlite::SqliteTypeInfo; +use sqlx::{Sqlite, MySql, Postgres}; use sqlx::postgres::{PgArgumentBuffer, PgTypeInfo}; use sqlx::{Type, Encode, encode::IsNull}; @@ -202,6 +203,16 @@ impl Type for Value { } } +impl Type for Value { + fn type_info() -> SqliteTypeInfo { + >::type_info() + } + + fn compatible(_ty: &SqliteTypeInfo) -> bool { + unreachable!("This should never happen. Please reach out to https://github.com/getsynth/synth/issues if it does.") + } +} + impl Encode<'_, Postgres> for Value { fn encode_by_ref( &self, @@ -322,6 +333,93 @@ impl Encode<'_, MySql> for Value { } } +impl Encode<'_, Sqlite> for Value { + fn encode_by_ref( + &self, + buf: &mut Vec + ) -> IsNull { + match self { + Value::Null(_) => IsNull::Yes, + Value::Bool(b) => >::encode_by_ref(b, buf), + Value::Number(num) => { + match *num { + Number::I8(i) => >::encode_by_ref(&i, buf), + Number::I16(i) => >::encode_by_ref(&i, buf), + Number::I32(i) => >::encode_by_ref(&i, buf), + Number::I64(i) => >::encode_by_ref(&i, buf), + Number::I128(_i) => unreachable!("not supported in sqlite"), + Number::U8(i) => >::encode_by_ref(&(i as i8), buf), + Number::U16(i) => >::encode_by_ref(&(i as i16), buf), + Number::U32(i) => >::encode_by_ref(&i, buf), + Number::U64(i) => { + if let Ok(i) = u32::try_from(i) { + >::encode_by_ref(&i, buf) + } else { + panic!("sqlx (the lib for Sqlite) does not support u64. As a workaround we currently use u32 as a fallback, \ + but it appears that the supplied number `{}` could not fit in an u32.", i); + } + }, + Number::U128(_i) => unreachable!("not supported in sqlite"), + Number::F32(f) => >::encode_by_ref(&f, buf), + Number::F64(f) => >::encode_by_ref(&f, buf), + } + }, + Value::String(s) => >::encode_by_ref(s, buf), + Value::DateTime(ChronoValueAndFormat { value, .. }) => { + match value { + ChronoValue::NaiveDate(nd) => >::encode_by_ref(nd, buf), + ChronoValue::NaiveTime(nt) => >::encode_by_ref(nt, buf), + ChronoValue::NaiveDateTime(ndt) => >::encode_by_ref(ndt, buf), + ChronoValue::DateTime(dt) => as Encode<'_, Sqlite>>::encode_by_ref(&dt.with_timezone(&Utc), buf), + } + } + Value::Object(_) => { + >::encode(json::synth_val_to_json(self.clone()), buf) + }, + Value::Array(_arr) => todo!()// as Encode<'_, Sqlite>>::encode_by_ref(arr, buf), //TODO special-case for u8 arrays? + } + } + + fn produces(&self) -> Option { + Some(match self { + Value::Null(_) => return >::produces(&serde_json::Value::Null), + Value::Bool(_) => >::type_info(), + Value::Number(num) => match num { + Number::I8(_) => >::type_info(), + Number::I16(_) => >::type_info(), + Number::I32(_) => >::type_info(), + Number::I64(_) => >::type_info(), + Number::I128(_) => unreachable!("not supported in sqlite"), + Number::U8(_) => >::type_info(), + Number::U16(_) => >::type_info(), + Number::U32(_) => >::type_info(), + // FIXME:(rasvi) [2021-10-03] + Number::U64(_) => >::type_info(), + Number::U128(_) => unreachable!("not supported in sqlite"), + Number::F32(_) => >::type_info(), + Number::F64(_) => >::type_info(), + }, + Value::DateTime(ChronoValueAndFormat { value, .. }) => { + match value { + ChronoValue::NaiveDate(_) => >::type_info(), + ChronoValue::NaiveTime(_) => >::type_info(), + ChronoValue::NaiveDateTime(_) => >::type_info(), + ChronoValue::DateTime(_) => as Type>::type_info(), + } + }, + Value::String(_) => >::type_info(), + Value::Object(_) => return None, //TODO: Use JSON here? + Value::Array(elems) => if elems.is_empty() { + return None + } else if let Value::Number(Number::U8(_) | Number::I8(_)) = elems[0] { + as Type>::type_info() + } else { + return None //TODO: other variants that would make sense? + } + }) + } +} + #[allow(unused)] impl Value { pub fn is_null(&self) -> bool { diff --git a/synth/Cargo.toml b/synth/Cargo.toml index 9a7bc0fc..de3dc85e 100644 --- a/synth/Cargo.toml +++ b/synth/Cargo.toml @@ -73,7 +73,7 @@ indicatif = "0.15.0" dirs = "3.0.2" mongodb = {version = "2.0.0-beta.3", features = ["sync", "bson-chrono-0_4"] , default-features = false} -sqlx = { version = "0.5.7", features = [ "postgres", "mysql", "runtime-async-std-native-tls", "decimal", "chrono" ] } +sqlx = { version = "0.5.7", features = [ "sqlite", "postgres", "mysql", "runtime-async-std-native-tls", "decimal", "chrono" ] } beau_collector = "0.2.1" diff --git a/synth/src/cli/export.rs b/synth/src/cli/export.rs index 51f23288..5f8c1798 100644 --- a/synth/src/cli/export.rs +++ b/synth/src/cli/export.rs @@ -6,6 +6,7 @@ use std::convert::TryFrom; use crate::cli::mongo::MongoExportStrategy; use crate::cli::mysql::MySqlExportStrategy; +use crate::cli::sqlite::SqliteExportStrategy; use crate::datasource::DataSource; use crate::sampler::{Sampler, SamplerOutput}; use async_std::task; @@ -47,9 +48,13 @@ impl TryFrom for Box { Box::new(MySqlExportStrategy { uri }) + } else if uri.starts_with("sqlite://") { + Box::new(SqliteExportStrategy { + uri + }) } else { return Err(anyhow!( - "Data sink not recognized. Was expecting one of 'mongodb' or 'postgres' or 'mysql' or 'mariadb'" + "Data sink not recognized. Was expecting one of 'mongodb' or 'postgres' or 'mysql' or 'sqlite' or 'mariadb'" )); }; Ok(export_strategy) diff --git a/synth/src/cli/import.rs b/synth/src/cli/import.rs index 99db2c56..c380da81 100644 --- a/synth/src/cli/import.rs +++ b/synth/src/cli/import.rs @@ -12,9 +12,11 @@ use synth_core::schema::Namespace; use crate::cli::db_utils::DataSourceParams; use crate::cli::mongo::MongoImportStrategy; use crate::cli::mysql::MySqlImportStrategy; +use crate::cli::sqlite::SqliteImportStrategy; use crate::cli::postgres::PostgresImportStrategy; use crate::cli::stdf::{FileImportStrategy, StdinImportStrategy}; + pub trait ImportStrategy { fn import(&self) -> Result { ns_from_value(self.as_value()?) @@ -45,6 +47,10 @@ impl TryFrom for Box { Box::new(MySqlImportStrategy { uri, }) + } else if uri.starts_with("sqlite://") { + Box::new(SqliteImportStrategy { + uri, + }) } else if let Ok(path) = PathBuf::from_str(&uri) { Box::new(FileImportStrategy { from_file: path, diff --git a/synth/src/cli/mod.rs b/synth/src/cli/mod.rs index 16579777..3a49e8f4 100644 --- a/synth/src/cli/mod.rs +++ b/synth/src/cli/mod.rs @@ -3,6 +3,7 @@ mod import; mod import_utils; mod mongo; mod mysql; +mod sqlite; mod postgres; mod stdf; mod store; diff --git a/synth/src/cli/sqlite.rs b/synth/src/cli/sqlite.rs new file mode 100644 index 00000000..98bd2b0d --- /dev/null +++ b/synth/src/cli/sqlite.rs @@ -0,0 +1,46 @@ +use crate::cli::export::{create_and_insert_values, ExportParams, ExportStrategy}; +use crate::cli::import::ImportStrategy; +use crate::cli::import_utils::build_namespace_import; +use crate::datasource::sqlite_datasource::SqliteDataSource; +use crate::datasource::DataSource; +use anyhow::Result; +use serde_json::Value; +use synth_core::schema::Namespace; +use synth_core::{Content, Name}; + +#[derive(Clone, Debug)] +pub struct SqliteExportStrategy { + pub uri: String, +} + +impl ExportStrategy for SqliteExportStrategy { + fn export(&self, params: ExportParams) -> Result<()> { + let datasource = SqliteDataSource::new(&self.uri)?; + + create_and_insert_values(params, &datasource) + } +} + +#[derive(Clone, Debug)] +pub struct SqliteImportStrategy { + pub uri: String, +} + +impl ImportStrategy for SqliteImportStrategy { + fn import(&self) -> Result { + let datasource = SqliteDataSource::new(&self.uri)?; + + build_namespace_import(&datasource) + } + + fn import_collection(&self, name: &Name) -> Result { + self.import()? + .collections + .remove(name) + .ok_or_else(|| anyhow!("Could not find table '{}' in Sqlite database.", name)) + } + + fn as_value(&self) -> Result { + bail!("Sqlite import doesn't support conversion into value") + } +} diff --git a/synth/src/datasource/mod.rs b/synth/src/datasource/mod.rs index 40046446..92b8dd30 100644 --- a/synth/src/datasource/mod.rs +++ b/synth/src/datasource/mod.rs @@ -2,6 +2,7 @@ use anyhow::Result; use async_trait::async_trait; use synth_core::Value; +pub(crate) mod sqlite_datasource; pub(crate) mod mysql_datasource; pub(crate) mod postgres_datasource; pub(crate) mod relational_datasource; diff --git a/synth/src/datasource/sqlite_datasource.rs b/synth/src/datasource/sqlite_datasource.rs new file mode 100644 index 00000000..656af7b7 --- /dev/null +++ b/synth/src/datasource/sqlite_datasource.rs @@ -0,0 +1,300 @@ +use crate::datasource::relational_datasource::{ + ColumnInfo, ForeignKey, PrimaryKey, RelationalDataSource, ValueWrapper, +}; +use crate::datasource::DataSource; +use anyhow::{Context, Result}; +use async_std::task; +use async_trait::async_trait; +use rust_decimal::prelude::ToPrimitive; +use sqlx::sqlite::{SqliteColumn, SqlitePoolOptions, SqliteQueryResult, SqliteRow}; +use sqlx::{Column, Pool, Row, Sqlite, TypeInfo}; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::prelude::rust_2015::Result::Ok; +use synth_core::schema::number_content::{F64, I64}; +use synth_core::schema::{ + BoolContent, ChronoValueType, DateTimeContent, NullContent, NumberContent, RangeStep, + RegexContent, StringContent, +}; +use synth_core::{Content, Value}; +use synth_gen::prelude::*; + +/// TODO +/// Known issues: +/// - Sqlite's random implementation does not support a seed argument. We currently use `random` directly. +/// This makes the sampling not behave as intended. + +pub struct SqliteDataSource { + pool: Pool, +} + +#[async_trait] +impl DataSource for SqliteDataSource { + type ConnectParams = String; + + fn new(connect_params: &Self::ConnectParams) -> Result { + task::block_on(async { + use sqlx::migrate::MigrateDatabase; + if !sqlx::Sqlite::database_exists(connect_params.as_str()).await? { + sqlx::Sqlite::create_database(connect_params.as_str()).await?; + } + + let pool = SqlitePoolOptions::new() + .max_connections(3) //TODO expose this as a user config? + .connect(connect_params.as_str()) + .await?; + + Ok::(SqliteDataSource { pool }) + }) + } + + async fn insert_data(&self, collection_name: String, collection: &[Value]) -> Result<()> { + self.insert_relational_data(collection_name, collection) + .await + } +} + +#[async_trait] +impl RelationalDataSource for SqliteDataSource { + type QueryResult = SqliteQueryResult; + + async fn execute_query( + &self, + query: String, + query_params: Vec<&Value>, + ) -> Result { + let mut query = sqlx::query(query.as_str()); + + for param in query_params { + query = query.bind(param); + } + + let result = query.execute(&self.pool).await?; + + Ok(result) + } + + async fn get_table_names(&self) -> Result> { + let query = r"SELECT name FROM sqlite_master + WHERE type='table'"; + + let table_names: Vec = sqlx::query(query) + .fetch_all(&self.pool) + .await? + .iter() + .map(|row| row.get::(0)) + .collect(); + + Ok(table_names) + } + + async fn get_columns_infos(&self, table_name: &str) -> Result> { + let query = r"SELECT * FROM PRAGMA_TABLE_INFO(?)"; + + let column_infos = sqlx::query(query) + .bind(table_name) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(ColumnInfo::try_from) + .collect::>>()?; + + Ok(column_infos) + } + + async fn get_primary_keys(&self, table_name: &str) -> Result> { + let query: &str = r"SELECT name, type FROM pragma_table_info(?) WHERE pk = 1"; + + sqlx::query(query) + .bind(table_name) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(PrimaryKey::try_from) + .collect::>>() + } + + async fn get_foreign_keys(&self) -> Result> { + let query: &str = r"SELECT * FROM pragma_table_info(?)"; + + sqlx::query(query) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(ForeignKey::try_from) + .collect::>>() + } + + async fn set_seed(&self) -> Result<()> { + // Sqlite doesn't set seed in a separate query + Ok(()) + } + + async fn get_deterministic_samples(&self, table_name: &str) -> Result> { + // FIXME:(rasviitanen) [2021-10-03] The random implementation doesn't take a seed + // in Sqlite, should we use rust's rand instead? + let query: &str = &format!("SELECT * FROM {} ORDER BY random() LIMIT 10", table_name); + + let values = sqlx::query(query) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(ValueWrapper::try_from) + .map(|v| match v { + Ok(wrapper) => Ok(wrapper.0), + Err(e) => bail!( + "Failed to convert to value wrapper from query results: {:?}", + e + ), + }) + .collect::>>()?; + + Ok(values) + } + + fn decode_to_content(&self, data_type: &str, char_max_len: Option) -> Result { + let content = match data_type.to_lowercase().as_str() { + "boolean" => { + Content::Bool(BoolContent::default()) + } + "integer" => { + Content::Number(NumberContent::I64(I64::Range(RangeStep::default()))) + } + "int8" | "bigint" => { + // FIXME:(rasviitanen) [2021-10-03] this should be i128, but is fine for now as u64 is not supported yet + Content::Number(NumberContent::I64(I64::Range(RangeStep::default()))) + } + "real" => { + Content::Number(NumberContent::F64(F64::Range(RangeStep::default()))) + } + "datetime" => { + Content::String(StringContent::DateTime(DateTimeContent { + format: "%Y-%m-%d %H:%M:%S".to_string(), + type_: ChronoValueType::NaiveDateTime, + begin: None, + end: None, + })) + } + "blob" | "text" => { + let pattern = + "[a-zA-Z0-9]{0, {}}".replace("{}", &format!("{}", char_max_len.unwrap_or(1))); + Content::String(StringContent::Pattern( + RegexContent::pattern(pattern).context("pattern will always compile")?, + )) + } + "null" => Content::Null(NullContent), + _ => unreachable!( + "Missing converter implementation for `{}`, but synth's Sqlite decoder should cover all types. \ + Please reach out to https://github.com/getsynth/synth/issues if encountered.", + data_type + ), + }; + + Ok(content) + } + + fn extend_parameterised_query(query: &mut String, _curr_index: usize, extend: usize) { + query.push('('); + for i in 0..extend { + query.push('?'); + if i != extend - 1 { + query.push(','); + } + } + query.push(')'); + } +} + +impl TryFrom for ColumnInfo { + type Error = anyhow::Error; + + fn try_from(row: SqliteRow) -> Result { + Ok(ColumnInfo { + column_name: row.try_get::(1)?, + ordinal_position: row.try_get::(0)? as i32, + is_nullable: !row.try_get::(3)?, + data_type: row.try_get::(2)?, + character_maximum_length: None, + }) + } +} + +impl TryFrom for PrimaryKey { + type Error = anyhow::Error; + + fn try_from(row: SqliteRow) -> Result { + Ok(PrimaryKey { + column_name: row.try_get::(0)?, + type_name: row.try_get::(1)?, + }) + } +} + +impl TryFrom for ForeignKey { + type Error = anyhow::Error; + + fn try_from(row: SqliteRow) -> Result { + Ok(ForeignKey { + from_table: row.try_get::(0)?, + from_column: row.try_get::(1)?, + to_table: row.try_get::(2)?, + to_column: row.try_get::(3)?, + }) + } +} + +impl TryFrom for ValueWrapper { + type Error = anyhow::Error; + + fn try_from(row: SqliteRow) -> Result { + let mut kv = BTreeMap::new(); + + for column in row.columns() { + let value = try_match_value(&row, column).unwrap_or(Value::Null(())); + kv.insert(column.name().to_string(), value); + } + + Ok(ValueWrapper(Value::Object(kv))) + } +} + +fn try_match_value(row: &SqliteRow, column: &SqliteColumn) -> Result { + let value = match column.type_info().name().to_lowercase().as_str() { + "boolean" => { + Value::Bool(row.try_get::(column.name())? == 1) + } + "integer" => { + Value::Number(Number::from(row.try_get::(column.name())?)) + } + "int8" | "bigint" => { + // FIXME:(rasviitanen) [2021-10-03] this should be i128, but is fine for now as u64 is not supported yet + Value::Number(Number::from(row.try_get::(column.name())?)) + } + "real" => { + let as_decimal = row.try_get::(column.name())?; + + if let Some(truncated) = as_decimal.to_f64() { + return Ok(Value::Number(Number::from(truncated))); + } + + bail!("Failed to convert Sqlite real data type to 64 bit float") + } + "datetime" => Value::String(format!( + "{}", + row.try_get::(column.name())? + )), + "blob" | "text" => { + Value::String(row.try_get::(column.name())?) + } + "null" => Value::Null(()), + _ => { + bail!( + "Could not convert value. Converter not implemented for {}, but Synth's Sqlite converter should cover all types. \ + Please reach out to https://github.com/getsynth/synth/issues if encountered.", + column.type_info().name() + ); + } + }; + + Ok(value) +}