diff --git a/wren-modeling-rs/.gitignore b/wren-modeling-rs/.gitignore index 1e7caa9ea..33cbfb15a 100644 --- a/wren-modeling-rs/.gitignore +++ b/wren-modeling-rs/.gitignore @@ -1,2 +1,3 @@ Cargo.lock target/ +sqllogictest/test_sql_files/scratch/ diff --git a/wren-modeling-rs/Cargo.toml b/wren-modeling-rs/Cargo.toml index 04c5d7ff3..5a06a712a 100644 --- a/wren-modeling-rs/Cargo.toml +++ b/wren-modeling-rs/Cargo.toml @@ -1,5 +1,6 @@ [workspace] -members = ["core"] +members = ["core", "sqllogictest"] +resolver = "2" [workspace.package] authors = ["Canner "] @@ -14,6 +15,9 @@ version = "0.1.0" [workspace.dependencies] arrow-schema = { version = "51.0.0", default-features = false } datafusion = { version = "38.0.0" } +log = { version = "0.4.14" } +petgraph = "0.6.5" +petgraph-evcxr = "*" serde = { version = "1.0.201", features = ["derive", "rc"] } serde_json = { version = "1.0.117" } tokio = { version = "1.4.0", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/wren-modeling-rs/core/Cargo.toml b/wren-modeling-rs/core/Cargo.toml index 51f78660c..57ad6642a 100644 --- a/wren-modeling-rs/core/Cargo.toml +++ b/wren-modeling-rs/core/Cargo.toml @@ -8,6 +8,10 @@ repository = { workspace = true } license = { workspace = true } authors = { workspace = true } +[lib] +name = "wren_core" +path = "src/lib.rs" + [dependencies] arrow-schema = { workspace = true } datafusion = { workspace = true } diff --git a/wren-modeling-rs/core/src/logical_plan/utils.rs b/wren-modeling-rs/core/src/logical_plan/utils.rs index 676c29563..07a0b3293 100644 --- a/wren-modeling-rs/core/src/logical_plan/utils.rs +++ b/wren-modeling-rs/core/src/logical_plan/utils.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, sync::Arc}; -use arrow_schema::{DataType, Field, Schema, SchemaRef}; +use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit}; use datafusion::logical_expr::{builder::LogicalTableSource, TableSource}; use crate::mdl::{ @@ -12,6 +12,9 @@ pub fn map_data_type(data_type: &str) -> DataType { match data_type { "integer" => DataType::Int32, "varchar" => DataType::Utf8, + "double" => DataType::Float64, + "timestamp" => DataType::Timestamp(TimeUnit::Nanosecond, None), + "date" => DataType::Date32, _ => unimplemented!("{}", &data_type), } } diff --git a/wren-modeling-rs/core/src/mdl/builder.rs b/wren-modeling-rs/core/src/mdl/builder.rs index 52e51ebab..8e0f338ff 100644 --- a/wren-modeling-rs/core/src/mdl/builder.rs +++ b/wren-modeling-rs/core/src/mdl/builder.rs @@ -10,6 +10,12 @@ pub struct ManifestBuilder { pub manifest: Manifest, } +impl Default for ManifestBuilder { + fn default() -> Self { + Self::new() + } +} + impl ManifestBuilder { pub fn new() -> Self { Self { diff --git a/wren-modeling-rs/core/src/mdl/mod.rs b/wren-modeling-rs/core/src/mdl/mod.rs index d9a7b973d..a4200048e 100644 --- a/wren-modeling-rs/core/src/mdl/mod.rs +++ b/wren-modeling-rs/core/src/mdl/mod.rs @@ -21,7 +21,7 @@ use crate::{ mdl::manifest::{Column, Manifest, Metric, Model}, }; -mod builder; +pub mod builder; pub mod lineage; pub mod manifest; pub mod utils; diff --git a/wren-modeling-rs/sqllogictest/Cargo.toml b/wren-modeling-rs/sqllogictest/Cargo.toml new file mode 100644 index 000000000..3f3f42524 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "wren-sqllogictest" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lib] +name = "wren_sqllogictest" +path = "src/lib.rs" + +[dependencies] +async-trait = "0.1.80" +bigdecimal = "0.4.3" +datafusion = { workspace = true } +half = { version = "2.4.1", default-features = true } +log = { workspace = true } +rust_decimal = { version = "1.27.0" } +sqllogictest = "0.20.4" +thiserror = "1.0.61" +tokio = { workspace = true } +wren-core = { path = "../core" } + +itertools = "0.13.0" +object_store = { version = "0.10.1", default-features = false } + +clap = { version = "4.4.8", features = ["derive", "env"] } +futures = "0.3.17" +tempfile = "3.10.1" + +[dev-dependencies] +env_logger = "0.11.3" +num_cpus = "1.16.0" +tokio = { workspace = true, features = ["rt-multi-thread"] } + +[[test]] +harness = false +name = "sqllogictests" +path = "bin/sqllogictests.rs" diff --git a/wren-modeling-rs/sqllogictest/bin/sqllogictests.rs b/wren-modeling-rs/sqllogictest/bin/sqllogictests.rs new file mode 100644 index 000000000..14d7ce8f9 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/bin/sqllogictests.rs @@ -0,0 +1,309 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; +#[cfg(target_family = "windows")] +use std::thread; + +use clap::Parser; +use futures::stream::StreamExt; +use log::info; +use sqllogictest::strict_column_validator; +use wren_sqllogictest::{engine::DataFusion, TestContext}; + +use datafusion::common::runtime::SpawnedTask; +use datafusion::common::{exec_err, DataFusionError, Result}; +use wren_sqllogictest::engine::utils::read_dir_recursive; + +const TEST_DIRECTORY: &str = "test_sql_files/"; + +#[cfg(target_family = "windows")] +pub fn main() { + // Tests from `tpch/tpch.slt` fail with stackoverflow with the default stack size. + thread::Builder::new() + .stack_size(2 * 1024 * 1024) // 2 MB + .spawn(move || { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { run_tests().await }) + .unwrap() + }) + .unwrap() + .join() + .unwrap(); +} + +#[tokio::main] +#[cfg(not(target_family = "windows"))] +pub async fn main() -> Result<()> { + run_tests().await +} + +/// Sets up an empty directory at test_sql_files/scratch/ +/// creating it if needed and clearing any file contents if it exists +/// This allows tests for inserting to external tables or copy to +/// to persist data to disk and have consistent state when running +/// a new test +fn setup_scratch_dir(name: &Path) -> Result<()> { + let file_stem = name.file_stem().expect("File should have a stem"); + let path = PathBuf::from(TEST_DIRECTORY) + .join("scratch") + .join(file_stem); + + info!("Creating scratch dir in {path:?}"); + if path.exists() { + fs::remove_dir_all(&path)?; + } + fs::create_dir_all(&path)?; + Ok(()) +} + +async fn run_tests() -> Result<()> { + // Enable logging (e.g. set RUST_LOG=debug to see debug logs) + env_logger::init(); + + let options: Options = Parser::parse(); + options.warn_on_ignored(); + + // Run all tests in parallel, reporting failures at the end + // + // Doing so is safe because each slt file runs with its own + // `SessionContext` and should not have side effects (like + // modifying shared state like `/tmp/`) + let errors: Vec<_> = futures::stream::iter(read_test_files(&options)?) + .map(|test_file| { + SpawnedTask::spawn(async move { + println!("Running {:?}", test_file.relative_path); + if options.complete { + run_complete_file(test_file).await?; + } else { + run_test_file(test_file).await?; + } + Ok(()) as Result<()> + }) + .join() + }) + // run up to num_cpus streams in parallel + .buffer_unordered(num_cpus::get()) + .flat_map(|result| { + // Filter out any Ok() leaving only the DataFusionErrors + futures::stream::iter(match result { + // Tokio panic error + Err(e) => Some(DataFusionError::External(Box::new(e))), + Ok(thread_result) => match thread_result { + // Test run error + Err(e) => Some(e), + // success + Ok(_) => None, + }, + }) + }) + .collect() + .await; + + // report on any errors + if !errors.is_empty() { + for e in &errors { + println!("{e}"); + } + exec_err!("{} failures", errors.len()) + } else { + Ok(()) + } +} + +async fn run_test_file(test_file: TestFile) -> Result<()> { + let TestFile { + path, + relative_path, + } = test_file; + info!("Running with DataFusion runner: {}", path.display()); + let Some(test_ctx) = TestContext::try_new_for_test_file(&relative_path).await else { + info!("Skipping: {}", path.display()); + return Ok(()); + }; + setup_scratch_dir(&relative_path)?; + let mut runner = sqllogictest::Runner::new(|| async { + Ok(DataFusion::new( + test_ctx.session_ctx().clone(), + relative_path.clone(), + )) + }); + runner.with_column_validator(strict_column_validator); + runner + .run_file_async(path) + .await + .map_err(|e| DataFusionError::External(Box::new(e))) +} + +async fn run_complete_file(test_file: TestFile) -> Result<()> { + let TestFile { + path, + relative_path, + } = test_file; + use sqllogictest::default_validator; + + info!("Using complete mode to complete: {}", path.display()); + let Some(test_ctx) = TestContext::try_new_for_test_file(&relative_path).await else { + info!("Skipping: {}", path.display()); + return Ok(()); + }; + setup_scratch_dir(&relative_path)?; + let mut runner = sqllogictest::Runner::new(|| async { + Ok(DataFusion::new( + test_ctx.session_ctx().clone(), + relative_path.clone(), + )) + }); + let col_separator = " "; + runner + .update_test_file( + path, + col_separator, + default_validator, + strict_column_validator, + ) + .await + // Can't use e directly because it isn't marked Send, so turn it into a string. + .map_err(|e| { + DataFusionError::Execution(format!("Error completing {relative_path:?}: {e}")) + }) +} + +/// Represents a parsed test file +#[derive(Debug)] +struct TestFile { + /// The absolute path to the file + pub path: PathBuf, + /// The relative path of the file (used for display) + pub relative_path: PathBuf, +} + +impl TestFile { + fn new(path: PathBuf) -> Self { + let relative_path = PathBuf::from( + path.to_string_lossy() + .strip_prefix(TEST_DIRECTORY) + .unwrap_or(""), + ); + + Self { + path, + relative_path, + } + } + + fn is_slt_file(&self) -> bool { + self.path.extension() == Some(OsStr::new("slt")) + } +} + +fn read_test_files<'a>( + options: &'a Options, +) -> Result + 'a>> { + Ok(Box::new( + read_dir_recursive(TEST_DIRECTORY)? + .into_iter() + .map(TestFile::new) + .filter(|f| options.check_test_file(&f.relative_path)) + .filter(|f| f.is_slt_file()), + )) +} + +/// Parsed command line options +/// +/// This structure attempts to mimic the command line options +/// accepted by IDEs such as CLion that pass arguments +/// +/// See for more details +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about= None)] +struct Options { + #[clap(long, help = "Auto complete mode to fill out expected results")] + complete: bool, + + #[clap(long, env = "INCLUDE_TPCH", help = "Include tpch files")] + include_tpch: bool, + + #[clap( + action, + help = "regex like arguments passed to the program which are treated as cargo test filter (substring match on filenames)" + )] + filters: Vec, + + #[clap( + long, + help = "IGNORED (for compatibility with built in rust test runner)" + )] + format: Option, + + #[clap( + short = 'Z', + long, + help = "IGNORED (for compatibility with built in rust test runner)" + )] + z_options: Option, + + #[clap( + long, + help = "IGNORED (for compatibility with built in rust test runner)" + )] + show_output: bool, +} + +impl Options { + /// Because this test can be run as a cargo test, commands like + /// + /// ```shell + /// cargo test foo + /// ``` + /// + /// Will end up passing `foo` as a command line argument. + /// + /// To be compatible with this, treat the command line arguments as a + /// filter and that does a substring match on each input. returns + /// true f this path should be run + fn check_test_file(&self, relative_path: &Path) -> bool { + if self.filters.is_empty() { + return true; + } + + // otherwise check if any filter matches + self.filters + .iter() + .any(|filter| relative_path.to_string_lossy().contains(filter)) + } + + /// Logs warning messages to stdout if any ignored options are passed + fn warn_on_ignored(&self) { + if self.format.is_some() { + println!("WARNING: Ignoring `--format` compatibility option"); + } + + if self.z_options.is_some() { + println!("WARNING: Ignoring `-Z` compatibility option"); + } + + if self.show_output { + println!("WARNING: Ignoring `--show-output` compatibility option"); + } + } +} diff --git a/wren-modeling-rs/sqllogictest/src/engine/conversion.rs b/wren-modeling-rs/sqllogictest/src/engine/conversion.rs new file mode 100644 index 000000000..85b5a11b4 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/conversion.rs @@ -0,0 +1,100 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use bigdecimal::BigDecimal; +use datafusion::arrow::datatypes::{i256, Decimal128Type, Decimal256Type, DecimalType}; +use half::f16; +use rust_decimal::prelude::*; + +/// Represents a constant for NULL string in your database. +pub const NULL_STR: &str = "NULL"; + +pub(crate) fn bool_to_str(value: bool) -> String { + if value { + "true".to_string() + } else { + "false".to_string() + } +} + +pub(crate) fn varchar_to_str(value: &str) -> String { + if value.is_empty() { + "(empty)".to_string() + } else { + value.trim_end_matches('\n').to_string() + } +} + +pub(crate) fn f16_to_str(value: f16) -> String { + if value.is_nan() { + // The sign of NaN can be different depending on platform. + // So the string representation of NaN ignores the sign. + "NaN".to_string() + } else if value == f16::INFINITY { + "Infinity".to_string() + } else if value == f16::NEG_INFINITY { + "-Infinity".to_string() + } else { + big_decimal_to_str(BigDecimal::from_str(&value.to_string()).unwrap()) + } +} + +pub(crate) fn f32_to_str(value: f32) -> String { + if value.is_nan() { + // The sign of NaN can be different depending on platform. + // So the string representation of NaN ignores the sign. + "NaN".to_string() + } else if value == f32::INFINITY { + "Infinity".to_string() + } else if value == f32::NEG_INFINITY { + "-Infinity".to_string() + } else { + big_decimal_to_str(BigDecimal::from_str(&value.to_string()).unwrap()) + } +} + +pub(crate) fn f64_to_str(value: f64) -> String { + if value.is_nan() { + // The sign of NaN can be different depending on platform. + // So the string representation of NaN ignores the sign. + "NaN".to_string() + } else if value == f64::INFINITY { + "Infinity".to_string() + } else if value == f64::NEG_INFINITY { + "-Infinity".to_string() + } else { + big_decimal_to_str(BigDecimal::from_str(&value.to_string()).unwrap()) + } +} + +pub(crate) fn i128_to_str(value: i128, precision: &u8, scale: &i8) -> String { + big_decimal_to_str( + BigDecimal::from_str(&Decimal128Type::format_decimal(value, *precision, *scale)) + .unwrap(), + ) +} + +pub(crate) fn i256_to_str(value: i256, precision: &u8, scale: &i8) -> String { + big_decimal_to_str( + BigDecimal::from_str(&Decimal256Type::format_decimal(value, *precision, *scale)) + .unwrap(), + ) +} + +pub(crate) fn big_decimal_to_str(value: BigDecimal) -> String { + value.round(12).normalized().to_string() +} diff --git a/wren-modeling-rs/sqllogictest/src/engine/error.rs b/wren-modeling-rs/sqllogictest/src/engine/error.rs new file mode 100644 index 000000000..0be29eb16 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/error.rs @@ -0,0 +1,50 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use datafusion::arrow::error::ArrowError; +use datafusion::common::DataFusionError; +use datafusion::sql::sqlparser::parser::ParserError; +use sqllogictest::TestError; +use thiserror::Error; + +pub type Result = std::result::Result; + +/// DataFusion sql-logicaltest error +#[derive(Debug, Error)] +pub enum DFSqlLogicTestError { + /// Error from sqllogictest-rs + #[error("SqlLogicTest error(from sqllogictest-rs crate): {0}")] + SqlLogicTest(#[from] TestError), + /// Error from datafusion + #[error("DataFusion error: {0}")] + DataFusion(#[from] DataFusionError), + /// Error returned when SQL is syntactically incorrect. + #[error("SQL Parser error: {0}")] + Sql(#[from] ParserError), + /// Error from arrow-rs + #[error("Arrow error: {0}")] + Arrow(#[from] ArrowError), + /// Generic error + #[error("Other Error: {0}")] + Other(String), +} + +impl From for DFSqlLogicTestError { + fn from(value: String) -> Self { + DFSqlLogicTestError::Other(value) + } +} diff --git a/wren-modeling-rs/sqllogictest/src/engine/mod.rs b/wren-modeling-rs/sqllogictest/src/engine/mod.rs new file mode 100644 index 000000000..b9afa5072 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/mod.rs @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// DataFusion engine implementation for sqllogictest. +mod error; +mod normalize; +mod runner; + +mod conversion; +mod output; +pub mod utils; + +pub use error::*; +pub use runner::*; +pub use DataFusion; diff --git a/wren-modeling-rs/sqllogictest/src/engine/normalize.rs b/wren-modeling-rs/sqllogictest/src/engine/normalize.rs new file mode 100644 index 000000000..1624548d6 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/normalize.rs @@ -0,0 +1,276 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use datafusion::arrow::datatypes::Fields; +use datafusion::arrow::util::display::ArrayFormatter; +use datafusion::arrow::{ + array, array::ArrayRef, datatypes::DataType, record_batch::RecordBatch, +}; +use datafusion::common::format::DEFAULT_FORMAT_OPTIONS; +use datafusion::common::DataFusionError; +use std::path::PathBuf; +use std::sync::OnceLock; + +use super::output::DFColumnType; + +use super::conversion::*; +use super::error::{DFSqlLogicTestError, Result}; + +/// Converts `batches` to a result as expected by sqllogicteset. +pub(crate) fn convert_batches(batches: Vec) -> Result>> { + if batches.is_empty() { + Ok(vec![]) + } else { + let schema = batches[0].schema(); + let mut rows = vec![]; + for batch in batches { + // Verify schema + if !schema.contains(&batch.schema()) { + return Err(DFSqlLogicTestError::DataFusion(DataFusionError::Internal( + format!( + "Schema mismatch. Previously had\n{:#?}\n\nGot:\n{:#?}", + &schema, + batch.schema() + ), + ))); + } + + let new_rows = convert_batch(batch)? + .into_iter() + .flat_map(expand_row) + .map(normalize_paths); + rows.extend(new_rows); + } + Ok(rows) + } +} + +/// special case rows that have newlines in them (like explain plans) +// +/// Transform inputs like: +/// ```text +/// [ +/// "logical_plan", +/// "Sort: d.b ASC NULLS LAST\n Projection: d.b, MAX(d.a) AS max_a", +/// ] +/// ``` +/// +/// Into one cell per line, adding lines if necessary +/// ```text +/// [ +/// "logical_plan", +/// ] +/// [ +/// "Sort: d.b ASC NULLS LAST", +/// ] +/// [ <--- newly added row +/// "|-- Projection: d.b, MAX(d.a) AS max_a", +/// ] +/// ``` +fn expand_row(mut row: Vec) -> impl Iterator> { + use itertools::Either; + use std::iter::once; + + // check last cell + if let Some(cell) = row.pop() { + let lines: Vec<_> = cell.split('\n').collect(); + + // no newlines in last cell + if lines.len() < 2 { + row.push(cell); + return Either::Left(once(row)); + } + + // form new rows with each additional line + let new_lines: Vec<_> = lines + .into_iter() + .enumerate() + .map(|(idx, l)| { + // replace any leading spaces with '-' as + // `sqllogictest` ignores whitespace differences + // + // See https://github.com/apache/datafusion/issues/6328 + let content = l.trim_start(); + let new_prefix = "-".repeat(l.len() - content.len()); + // maintain for each line a number, so + // reviewing explain result changes is easier + let line_num = idx + 1; + vec![format!("{line_num:02}){new_prefix}{content}")] + }) + .collect(); + + Either::Right(once(row).chain(new_lines)) + } else { + Either::Left(once(row)) + } +} + +/// normalize path references +/// +/// ```text +/// CsvExec: files={1 group: [[path/to/datafusion/testing/data/csv/aggregate_test_100.csv]]}, ... +/// ``` +/// +/// into: +/// +/// ```text +/// CsvExec: files={1 group: [[WORKSPACE_ROOT/testing/data/csv/aggregate_test_100.csv]]}, ... +/// ``` +fn normalize_paths(mut row: Vec) -> Vec { + row.iter_mut().for_each(|s| { + let workspace_root: &str = workspace_root().as_ref(); + if s.contains(workspace_root) { + *s = s.replace(workspace_root, "WORKSPACE_ROOT"); + } + }); + row +} + +/// return the location of the datafusion checkout +fn workspace_root() -> &'static object_store::path::Path { + static WORKSPACE_ROOT_LOCK: OnceLock = OnceLock::new(); + WORKSPACE_ROOT_LOCK.get_or_init(|| { + // e.g. /Software/datafusion/datafusion/core + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + // e.g. /Software/datafusion/datafusion + let workspace_root = dir + .parent() + .expect("Can not find parent of datafusion/core") + // e.g. /Software/datafusion + .parent() + .expect("parent of datafusion") + .to_string_lossy(); + + let sanitized_workplace_root = if cfg!(windows) { + // Object store paths are delimited with `/`, e.g. `/datafusion/datafusion/testing/data/csv/aggregate_test_100.csv`. + // The default windows delimiter is `\`, so the workplace path is `datafusion\datafusion`. + workspace_root + .replace(std::path::MAIN_SEPARATOR, object_store::path::DELIMITER) + } else { + workspace_root.to_string() + }; + + object_store::path::Path::parse(sanitized_workplace_root).unwrap() + }) +} + +/// Convert a single batch to a `Vec>` for comparison +fn convert_batch(batch: RecordBatch) -> Result>> { + (0..batch.num_rows()) + .map(|row| { + batch + .columns() + .iter() + .map(|col| cell_to_string(col, row)) + .collect::>>() + }) + .collect() +} + +macro_rules! get_row_value { + ($array_type:ty, $column: ident, $row: ident) => {{ + let array = $column.as_any().downcast_ref::<$array_type>().unwrap(); + + array.value($row) + }}; +} + +/// Normalizes the content of a single cell in RecordBatch prior to printing. +/// +/// This is to make the output comparable to the semi-standard .slt format +/// +/// Normalizations applied to [NULL Values and empty strings] +/// +/// [NULL Values and empty strings]: https://duckdb.org/dev/sqllogictest/result_verification#null-values-and-empty-strings +/// +/// Floating numbers are rounded to have a consistent representation with the Postgres runner. +/// +pub fn cell_to_string(col: &ArrayRef, row: usize) -> Result { + if !col.is_valid(row) { + // represent any null value with the string "NULL" + Ok(NULL_STR.to_string()) + } else { + match col.data_type() { + DataType::Null => Ok(NULL_STR.to_string()), + DataType::Boolean => { + Ok(bool_to_str(get_row_value!(array::BooleanArray, col, row))) + } + DataType::Float16 => { + Ok(f16_to_str(get_row_value!(array::Float16Array, col, row))) + } + DataType::Float32 => { + Ok(f32_to_str(get_row_value!(array::Float32Array, col, row))) + } + DataType::Float64 => { + Ok(f64_to_str(get_row_value!(array::Float64Array, col, row))) + } + DataType::Decimal128(precision, scale) => { + let value = get_row_value!(array::Decimal128Array, col, row); + Ok(i128_to_str(value, precision, scale)) + } + DataType::Decimal256(precision, scale) => { + let value = get_row_value!(array::Decimal256Array, col, row); + Ok(i256_to_str(value, precision, scale)) + } + DataType::LargeUtf8 => Ok(varchar_to_str(get_row_value!( + array::LargeStringArray, + col, + row + ))), + DataType::Utf8 => { + Ok(varchar_to_str(get_row_value!(array::StringArray, col, row))) + } + _ => { + let f = ArrayFormatter::try_new(col.as_ref(), &DEFAULT_FORMAT_OPTIONS); + Ok(f.unwrap().value(row).to_string()) + } + } + .map_err(DFSqlLogicTestError::Arrow) + } +} + +/// Converts columns to a result as expected by sqllogicteset. +pub(crate) fn convert_schema_to_types(columns: &Fields) -> Vec { + columns + .iter() + .map(|f| f.data_type()) + .map(|data_type| match data_type { + DataType::Boolean => DFColumnType::Boolean, + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 => DFColumnType::Integer, + DataType::Float16 + | DataType::Float32 + | DataType::Float64 + | DataType::Decimal128(_, _) + | DataType::Decimal256(_, _) => DFColumnType::Float, + DataType::Utf8 | DataType::LargeUtf8 => DFColumnType::Text, + DataType::Date32 + | DataType::Date64 + | DataType::Time32(_) + | DataType::Time64(_) => DFColumnType::DateTime, + DataType::Timestamp(_, _) => DFColumnType::Timestamp, + _ => DFColumnType::Another, + }) + .collect() +} diff --git a/wren-modeling-rs/sqllogictest/src/engine/output.rs b/wren-modeling-rs/sqllogictest/src/engine/output.rs new file mode 100644 index 000000000..24299856e --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/output.rs @@ -0,0 +1,57 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use sqllogictest::{ColumnType, DBOutput}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DFColumnType { + Boolean, + DateTime, + Integer, + Float, + Text, + Timestamp, + Another, +} + +impl ColumnType for DFColumnType { + fn from_char(value: char) -> Option { + match value { + 'B' => Some(Self::Boolean), + 'D' => Some(Self::DateTime), + 'I' => Some(Self::Integer), + 'P' => Some(Self::Timestamp), + 'R' => Some(Self::Float), + 'T' => Some(Self::Text), + _ => Some(Self::Another), + } + } + + fn to_char(&self) -> char { + match self { + Self::Boolean => 'B', + Self::DateTime => 'D', + Self::Integer => 'I', + Self::Timestamp => 'P', + Self::Float => 'R', + Self::Text => 'T', + Self::Another => '?', + } + } +} + +pub(crate) type DFOutput = DBOutput; diff --git a/wren-modeling-rs/sqllogictest/src/engine/runner.rs b/wren-modeling-rs/sqllogictest/src/engine/runner.rs new file mode 100644 index 000000000..c3470f755 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/runner.rs @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::{path::PathBuf, time::Duration}; + +use async_trait::async_trait; +use datafusion::arrow::record_batch::RecordBatch; +use datafusion::prelude::SessionContext; +use log::info; +use sqllogictest::DBOutput; + +use super::{ + error::Result, + normalize, + output::{DFColumnType, DFOutput}, + DFSqlLogicTestError, +}; + +pub struct DataFusion { + ctx: SessionContext, + relative_path: PathBuf, +} + +impl DataFusion { + pub fn new(ctx: SessionContext, relative_path: PathBuf) -> Self { + Self { ctx, relative_path } + } +} + +#[async_trait] +impl sqllogictest::AsyncDB for DataFusion { + type Error = DFSqlLogicTestError; + type ColumnType = DFColumnType; + + async fn run(&mut self, sql: &str) -> Result { + info!( + "[{}] Running query: \"{}\"", + self.relative_path.display(), + sql + ); + run_query(&self.ctx, sql).await + } + + /// Engine name of current database. + fn engine_name(&self) -> &str { + "DataFusion" + } + + /// [`DataFusion`] calls this function to perform sleep. + /// + /// The default implementation is `std::thread::sleep`, which is universal to any async runtime + /// but would block the current thread. If you are running in tokio runtime, you should override + /// this by `tokio::time::sleep`. + async fn sleep(dur: Duration) { + tokio::time::sleep(dur).await; + } +} + +async fn run_query(ctx: &SessionContext, sql: impl Into) -> Result { + let df = ctx.sql(sql.into().as_str()).await?; + + let types = normalize::convert_schema_to_types(df.schema().fields()); + let results: Vec = df.collect().await?; + let rows = normalize::convert_batches(results)?; + + if rows.is_empty() && types.is_empty() { + Ok(DBOutput::StatementComplete(0)) + } else { + Ok(DBOutput::Rows { types, rows }) + } +} diff --git a/wren-modeling-rs/sqllogictest/src/engine/utils.rs b/wren-modeling-rs/sqllogictest/src/engine/utils.rs new file mode 100644 index 000000000..471c9a86f --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/engine/utils.rs @@ -0,0 +1,34 @@ +use datafusion::common::exec_datafusion_err; +use std::path::{Path, PathBuf}; + +pub fn read_dir_recursive>( + path: P, +) -> datafusion::common::Result> { + let mut dst = vec![]; + read_dir_recursive_impl(&mut dst, path.as_ref())?; + Ok(dst) +} + +/// Append all paths recursively to dst +fn read_dir_recursive_impl( + dst: &mut Vec, + path: &Path, +) -> datafusion::common::Result<()> { + let entries = std::fs::read_dir(path) + .map_err(|e| exec_datafusion_err!("Error reading directory {path:?}: {e}"))?; + for entry in entries { + let path = entry + .map_err(|e| { + exec_datafusion_err!("Error reading entry in directory {path:?}: {e}") + })? + .path(); + + if path.is_dir() { + read_dir_recursive_impl(dst, &path)?; + } else { + dst.push(path); + } + } + + Ok(()) +} diff --git a/wren-modeling-rs/sqllogictest/src/lib.rs b/wren-modeling-rs/sqllogictest/src/lib.rs new file mode 100644 index 000000000..b9728a820 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/lib.rs @@ -0,0 +1,4 @@ +pub mod engine; + +mod test_context; +pub use test_context::TestContext; diff --git a/wren-modeling-rs/sqllogictest/src/test_context.rs b/wren-modeling-rs/sqllogictest/src/test_context.rs new file mode 100644 index 000000000..aae67f616 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/src/test_context.rs @@ -0,0 +1,223 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::any::Any; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use async_trait::async_trait; +use datafusion::arrow::datatypes::SchemaRef; +use datafusion::error::Result; +use datafusion::execution::context::SessionState; +use datafusion::logical_expr::Expr; +use datafusion::physical_plan::ExecutionPlan; +use datafusion::prelude::SessionConfig; +use datafusion::{ + catalog::{schema::MemorySchemaProvider, CatalogProvider, MemoryCatalogProvider}, + datasource::{TableProvider, TableType}, + prelude::{CsvReadOptions, SessionContext}, +}; +use log::info; +use tempfile::TempDir; + +use wren_core::logical_plan::rule::{ModelAnalyzeRule, ModelGenerationRule}; +use wren_core::logical_plan::utils::create_schema; +use wren_core::mdl::builder::{ColumnBuilder, ManifestBuilder, ModelBuilder}; +use wren_core::mdl::manifest::Model; +use wren_core::mdl::{AnalyzedWrenMDL, WrenMDL}; + +use crate::engine::utils::read_dir_recursive; + +const TEST_RESOURCES: &str = "tests/resources"; + +/// Context for running tests +pub struct TestContext { + /// Context for running queries + ctx: SessionContext, + /// Temporary directory created and cleared at the end of the test + test_dir: Option, +} + +impl TestContext { + pub fn new(ctx: SessionContext) -> Self { + Self { + ctx, + test_dir: None, + } + } + + /// Create a SessionContext, configured for the specific sqllogictest + /// test(.slt file) , if possible. + /// + /// If `None` is returned (e.g. because some needed feature is not + /// enabled), the file should be skipped + pub async fn try_new_for_test_file(relative_path: &Path) -> Option { + let config = SessionConfig::new() + // hardcode target partitions so plans are deterministic + .with_target_partitions(4); + + let ctx = SessionContext::new_with_config(config); + let test_ctx = TestContext::new(ctx); + + let file_name = relative_path.file_name().unwrap().to_str().unwrap(); + match file_name { + "model.slt" => { + info!("Registering local temporary table"); + register_ecommerce_table(&test_ctx).await; + } + _ => { + info!("Using default SessionContext"); + } + }; + Some(test_ctx) + } + + /// Enables the test directory feature. If not enabled, + /// calling `testdir_path` will result in a panic. + pub fn enable_testdir(&mut self) { + if self.test_dir.is_none() { + self.test_dir = Some(TempDir::new().expect("failed to create testdir")); + } + } + + /// Returns the path to the test directory. Panics if the test + /// directory feature is not enabled via `enable_testdir`. + pub fn testdir_path(&self) -> &Path { + self.test_dir.as_ref().expect("testdir not enabled").path() + } + + /// Returns a reference to the internal SessionContext + pub fn session_ctx(&self) -> &SessionContext { + &self.ctx + } +} + +pub async fn register_ecommerce_table(test_ctx: &TestContext) { + let path = PathBuf::from(TEST_RESOURCES).join("ecommerce"); + let data = read_dir_recursive(&path).unwrap(); + + // register csv file with the execution context + for file in data.iter() { + let table_name = file.file_stem().unwrap().to_str().unwrap(); + test_ctx + .ctx + .register_csv(table_name, file.to_str().unwrap(), CsvReadOptions::new()) + .await + .unwrap(); + } + register_ecommerce_mdl(&test_ctx.ctx).await; +} + +async fn register_ecommerce_mdl(ctx: &SessionContext) { + let manifest = ManifestBuilder::new() + .model( + ModelBuilder::new("customers") + .column(ColumnBuilder::new("city", "varchar").build()) + .column(ColumnBuilder::new("id", "varchar").build()) + .column(ColumnBuilder::new("state", "varchar").build()) + .primary_key("id") + .build(), + ) + .model( + ModelBuilder::new("order_items") + .column(ColumnBuilder::new("freight_value", "double").build()) + .column(ColumnBuilder::new("id", "varchar").build()) + .column(ColumnBuilder::new("item_number", "integer").build()) + .column(ColumnBuilder::new("order_id", "varchar").build()) + .column(ColumnBuilder::new("price", "double").build()) + .column(ColumnBuilder::new("product_id", "varchar").build()) + .column(ColumnBuilder::new("shipping_limit_date", "varchar").build()) + .primary_key("id") + .build(), + ) + .model( + ModelBuilder::new("orders") + .column(ColumnBuilder::new("approved_timestamp", "timestamp").build()) + .column(ColumnBuilder::new("customer_id", "varchar").build()) + .column(ColumnBuilder::new("delivered_carrier_date", "date").build()) + .column(ColumnBuilder::new("estimated_deliver_date", "date").build()) + .column(ColumnBuilder::new("order_id", "varchar").build()) + .column(ColumnBuilder::new("purchase_timestamp", "timestamp").build()) + .build(), + ) + .build(); + let analyzed_mdl = Arc::new(AnalyzedWrenMDL::analyze(manifest)); + ctx.state().with_analyzer_rules(vec![ + Arc::new(ModelAnalyzeRule::new(Arc::clone(&analyzed_mdl))), + Arc::new(ModelGenerationRule::new(Arc::clone(&analyzed_mdl))), + ]); + register_table_with_mdl(ctx, Arc::clone(&analyzed_mdl.wren_mdl)).await; +} + +pub async fn register_table_with_mdl(ctx: &SessionContext, wren_mdl: Arc) { + let catalog = MemoryCatalogProvider::new(); + let schema = MemorySchemaProvider::new(); + + catalog + .register_schema(&wren_mdl.manifest.schema, Arc::new(schema)) + .unwrap(); + ctx.register_catalog(&wren_mdl.manifest.catalog, Arc::new(catalog)); + + for model in wren_mdl.manifest.models.iter() { + let table = WrenDataSource::new(Arc::clone(model)); + ctx.register_table( + format!( + "{}.{}.{}", + &wren_mdl.manifest.catalog, &wren_mdl.manifest.schema, &model.name + ), + Arc::new(table), + ) + .unwrap(); + } +} + +struct WrenDataSource { + schema: SchemaRef, +} + +impl WrenDataSource { + pub fn new(model: Arc) -> Self { + let schema = create_schema(model.columns.clone()); + Self { schema } + } +} + +#[async_trait] +impl TableProvider for WrenDataSource { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + self.schema.clone() + } + + fn table_type(&self) -> TableType { + TableType::View + } + + async fn scan( + &self, + _state: &SessionState, + _projection: Option<&Vec>, + // filters and limit can be used here to inject some push-down operations if needed + _filters: &[Expr], + _limit: Option, + ) -> Result> { + unreachable!("WrenDataSource should be replaced before physical planning") + } +} diff --git a/wren-modeling-rs/sqllogictest/test_sql_files/model.slt b/wren-modeling-rs/sqllogictest/test_sql_files/model.slt new file mode 100644 index 000000000..91d8ebf81 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/test_sql_files/model.slt @@ -0,0 +1,2 @@ +statement ok +SELECT * from orders diff --git a/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/customers.csv b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/customers.csv new file mode 100644 index 000000000..34b077e0d --- /dev/null +++ b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/customers.csv @@ -0,0 +1,100 @@ +Id,City,State +06b8999e2fba1a1fbc88172c00ba8bc7,Los Angeles,CA +18955e83d337fd6b2def6b18a428ac77,San Francisco,CA +4e7b3e00288586ebd08712fdd0374a03,San Francisco,CA +b2b6027bc5c5109e529d4dc6358b12c3,Chula Vista,CA +4f2d8ab171c80ec8364f7c12e35b23ad,Long Beach,CA +879864dab9bc3047522c92c82e1212b8,Chula Vista,CA +fd826e7cf63160e536e0908c76c3f441,San Francisco,CA +5e274e7a0c3809e14aba7ad5aae0d407,Sacramento,CA +5adf08e34b2e993982a47070956c5c65,Oakland,CA +4b7139f34592b3a31687243a302fa75b,Santa Ana,CA +9fb35e4ed6f0a14a4977cd9aea4042bb,Chula Vista,CA +5aa9e4fdd4dfd20959cad2d772509598,San Francisco,CA +b2d1536598b73a9abd18e0d75d92f0a3,Chula Vista,CA +eabebad39a88bb6f5b52376faec28612,San Francisco,CA +1f1c7bf1c9b041b292af6c1c4470b753,Stockton,CA +206f3129c0e4d7d0b9550426023f0a08,San Jose,CA +a7c125a0a07b75146167b7f04a7f8e98,San Francisco,CA +c5c61596a3b6bd0cee5766992c48a9a1,Chula Vista,CA +9b8ce803689b3562defaad4613ef426f,San Francisco,CA +49d0ea0986edde72da777f15456a0ee0,Los Angeles,CA +154c4ded6991bdfa3cd249d11abf4130,San Diego,CA +690172ab319622688d3b4df42f676898,San Jose,CA +2938121a40a20953c43caa8c98787fcb,San Francisco,CA +237098a64674ae89babdc426746260fc,Oakland,CA +cb721d7b4f271fd87011c4c83462c076,San Francisco,CA +f681356046d9fde60e70c73a18d65ea2,San Francisco,CA +167bd30a409e3e4127df5a9408ebd394,Los Angeles,CA +6e359a57a91f84095cc64e1b351aef8c,San Francisco,CA +e0eea8f69a457b3f1fa246e44c9ebefd,Sacramento,CA +e3109970a3fe8021d5ff82c577ce5606,San Francisco,CA +261cb4f92498ca05d5bd1a327a261d9c,San Francisco,CA +6f92779347724b67e44e3224f3b4cffd,San Francisco,CA +2d5831cb2dff7cdefba62e950ae3dc7b,Santa Ana,CA +b2bed119388167a954382cca36c4777f,San Francisco,CA +469634941c27cd844170935a3cf60b95,Oakland,CA +df0aa5b8586495e0ddf6b601122e43a1,Sacramento,CA +41c8f4b570869791379a925899a6af8a,Chula Vista,CA +54f755c3fd2709231f9964a1430c5218,Chula Vista,CA +4c06b42fbf7b97ab10779cda5549cd1c,San Francisco,CA +b6368ca0f56d4632f44d58ca431487b2,San Francisco,CA +4a0e66fd30684aa1409cd1b66fec77cc,Chula Vista,CA +c168abb9077b7821adae01dc1f0886c5,San Francisco,CA +a3b0fda37bae14cf754877bed475e80c,San Francisco,CA +0ccd415657ae8a6cd1c71b00155a019e,Chula Vista,CA +c532a74a3ebf1bacce2e2bcce3783317,San Francisco,CA +19cecb194f54e614b70d971306a9931b,Santa Ana,CA +f34a6e874087ec1f0e3dab9fdf659c5d,Chula Vista,CA +c132855c926907970dcf6f2bf0b33a24,Sacramento,CA +df85b96ba2ce3e49bde101b1614f52ac,San Francisco,CA +4d27341acd30a36bca39008ee9bb9050,Chula Vista,CA +d3b6830d18c7de943d1e707d1f061d40,San Francisco,CA +79de53946db384e2d7a9bd131792ad17,Chula Vista,CA +a562ab1e728449e3461829dfe2e36f73,Long Beach,CA +b64ed91eab98972150bdaf77ca921934,San Francisco,CA +8247b5583327ab8be19f96e1fb82f77b,San Jose,CA +8fcaa9368903f3a9a28aeaff28c14638,Long Beach,CA +a9b0d1c26105279e1b8edc63d06bd668,San Francisco,CA +aa9f03ecd3728c9bd12e6d962c66c7cb,Sacramento,CA +230c0d740401730c7197d16376893525,Santa Ana,CA +a905baa530258422594f1b05615bd225,Long Beach,CA +4fa19f7da692e6bf9602aaad3c372eda,Oakland,CA +03f846ad03437d864a8d2a22976dcafe,San Francisco,CA +de4e13fd7d6469c5ada77d0843c55e42,San Jose,CA +8276de07ef25225d412b8462d73f8664,San Jose,CA +cc32707d2e2f7c92ab449f9b28154809,San Francisco,CA +a02f66c3af7b16eec19ddcd98b645fe3,San Francisco,CA +26acee41e2f75689a5615892f06ea0bd,Santa Ana,CA +f64cdee66599119324ce57a97e43700d,Chula Vista,CA +7ab7a537b678b6dd73d825ff6ee7be9d,Sacramento,CA +7300450cedf7e4c35c243c4a03c1e8a6,Santa Ana,CA +4c7241af24b5344cb01fe687643de4fe,San Diego,CA +97e126f19a6f04b3462619f36862bcd2,San Francisco,CA +6d27a9361e591da38c87a5e70253f3f2,Sacramento,CA +6810c3dc47f641181fcc7f73275c3d19,San Jose,CA +b514422efcf14bef34858a0829bef189,San Francisco,CA +0aae2862f8eac77f10a34f44860720ac,San Francisco,CA +6c9a5923526346cbc0bd7bbd92269c01,San Francisco,CA +1b2cb35b19b40b61f953d32ea157b337,Sacramento,CA +12d1b4294fef21016c9614eb31e55e15,San Francisco,CA +f6529ffebe6b3440d45d89604a4239ac,Chula Vista,CA +8264e3518163dd09211870b24a5d741d,San Francisco,CA +8392e3d4cfeec63f2a8bfea68bf1f91f,Santa Ana,CA +38d1cd89306128348ffdf4cc23f3a50a,San Francisco,CA +91ec76836092bba85d11761078ed7bb5,Oakland,CA +f9dfa0a2934ffbb22e66924952548be8,San Francisco,CA +5a3260cfde2a918b597dada7ddd247bb,San Francisco,CA +ee3a81b2771fec5f9e982cdb1b3a4804,Chula Vista,CA +784c407781aa34749a388c9283782b56,San Jose,CA +3f6ede29d4c69cd3316d2035b6cec1fb,San Francisco,CA +6bed27564bd99d78d09c1fac13da56fd,San Francisco,CA +670254dd2e886ffe621b3831afb47d7d,Santa Ana,CA +f7cb015ff73be957ee6a30e2577742c5,Sacramento,CA +ea2196dc456ba36fe4f6b81dca4867d4,Stockton,CA +09241c552e9fe2420997a6c535e9d408,San Francisco,CA +e50a30de3c32f9406a7185f40ce6874d,Santa Ana,CA +f89c1a6b9c966869e441e55bc14acddc,San Francisco,CA +23e96758fd640560e9b1fbcda90abfc4,San Francisco,CA +369708cabd9831ea6fde670a3b602a92,San Francisco,CA +5f8b4882b5a4ec7bf6d2107e6cd0cf29,Chula Vista,CA \ No newline at end of file diff --git a/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/order_items.csv b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/order_items.csv new file mode 100644 index 000000000..744929560 --- /dev/null +++ b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/order_items.csv @@ -0,0 +1,101 @@ +Id,OrderId,ItemNumber,ProductId,ShippingLimitDate,Price,FreightValue +1,03c83b31dbc387f83f1b5579b53182fb,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/15 7:26,119.8,14.68 +2,048e6e4623dbf118c43e0f5572016faa,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/5 18:09,119.8,11.73 +3,05d61bb749461f4805dbda51f767fa47,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/4 19:15,240,4 +4,08cbb1d4cd574b126569b208fd4b26ea,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/10/19 19:35,287.4,6.9 +5,08cc8c614786867bb0a845cd117d349b,1,d6160fb7873f184099d9bc95e30376af,2022/9/8 13:50,260,5.72 +6,0945dadba69dad599e42c31e78450de4,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/9/6 2:35,240,12.15 +7,09f8cd8a102b3f3fa2d5db0c6aee489a,1,e004a3b7f9524967fdb68bb70f850eb0,2022/9/21 16:50,146,16.03 +8,0a187dd7f4efe70e8c3b6e3779b3b133,1,d6160fb7873f184099d9bc95e30376af,2022/9/8 20:03,260,6.71 +9,0af28d87520565eb3b57c9b2abe1a2cc,1,34f99d82cfc355d08d8db780d14aa002,2022/10/11 20:28,600,25.3 +10,0af4a846cbf75b456c934b786d683618,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/9/6 2:44,240,8.17 +11,0b382db57a557ff7703eaba5e991e7fb,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/11/8 12:15,287.4,8.5 +12,0be31296e8456f2017743c363de5a1da,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/8 17:46,260,10.3 +13,0d91fd5b8475f8f0576a6e8f90c6497d,1,1f411e9a31196b71ed8438c8254b858e,2023/5/16 14:58,124,11.74 +14,0d91fd5b8475f8f0576a6e8f90c6497d,2,1f411e9a31196b71ed8438c8254b858e,2023/5/16 14:58,124,11.74 +15,0d91fd5b8475f8f0576a6e8f90c6497d,3,1f411e9a31196b71ed8438c8254b858e,2023/5/16 14:58,124,11.74 +16,0e6386e1cb625fe4fdbd3d101d2cb687,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/8 0:35,260,7.7 +17,106ff3ba3e84e22713bf2a10c582fd94,1,a04087ab6a96ffa041f8a2701a72b616,2023/4/12 1:35,158,7.46 +18,12c860cdab62139623897892199fea4f,1,caaa713799b547352795c831a14f1c3c,2023/4/23 16:35,170,6.84 +19,12f2f3174deba94de2aa43ad4adf6d3e,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/10/18 22:26,268,6.45 +20,13b4f8c4b570d25d95bfd3c915ed9c38,1,a04087ab6a96ffa041f8a2701a72b616,2023/2/5 4:16,158,13.8 +21,1407ed7d738d943469994266e9706b46,1,d6160fb7873f184099d9bc95e30376af,2022/9/5 11:05,240,12.15 +22,1457abef78cd241e85fd946503cb4e16,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/11/3 13:30,268,6.77 +23,17774450f243f686546b04633a04dbdb,1,1e5428c428e0f783acd6e3d94ba4ee2a,2022/12/21 1:17,119.8,13.54 +24,17affe817f6b2a24ec4a9b5bb67a1671,1,d6160fb7873f184099d9bc95e30376af,2022/10/11 18:25,320,5.57 +25,17e8356af05cf890ac4604347b3c17a6,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/10/17 20:56,287.4,8.5 +26,1b0bc119ea5ac37e73e2275afc7904ee,3,e56f102e763165e7e32d0f9955f8ee4a,2023/8/20 14:30,278,3.02 +27,1b2b354adeb38c1c308c915faf7d3288,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/9/5 9:45,220,7.42 +28,1badd176eb41888c1a20a7614fba59e1,1,d6160fb7873f184099d9bc95e30376af,2022/9/25 9:50,270,8.38 +29,1d6bda6e5595652f956770a32ae4ba0d,1,6e135c0c17d87c489e2f89efe42b948a,2023/5/28 2:18,146,5.48 +30,1e0b82a0da7f09ea9ba1b82dfaf4fa23,1,caaa713799b547352795c831a14f1c3c,2023/4/26 6:11,170,5.4 +31,1ea7acde8dacf11a098d0b85b13cdc0e,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/11/3 16:35,287.4,8.5 +32,2352f0c7b4fe903c071e032c9d6d6593,1,4e11734666acc305a4649de43f0bd181,2023/4/23 5:50,119.8,22.09 +33,2352f0c7b4fe903c071e032c9d6d6593,2,4e11734666acc305a4649de43f0bd181,2023/4/23 5:50,119.8,22.09 +34,24536ae68dcaea969641d76fc4cc5e4a,1,6e263657e75994ff623356f9cff692db,2023/6/25 4:36,131.8,5.92 +35,24536ae68dcaea969641d76fc4cc5e4a,2,6e263657e75994ff623356f9cff692db,2023/6/25 4:36,131.8,5.92 +36,24536ae68dcaea969641d76fc4cc5e4a,3,6e263657e75994ff623356f9cff692db,2023/6/25 4:36,131.8,5.92 +37,245b2a09d0f787df368d4a370a63eb58,1,7c8e2b381bb0fcba5b368961d7823cd2,2022/12/12 19:33,119.8,11.73 +38,256ccc96b2c931006d95ba7a05181037,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/19 19:35,134,6.59 +39,2582e88a3e8a129cc809ccaf9a6d35d6,1,e004a3b7f9524967fdb68bb70f850eb0,2022/10/22 22:05,146,16.03 +40,28e418b320f6b759b6aea79734f165e5,1,fb8c11da2e1b1634e879d1c3c6762dc2,2022/9/8 15:24,170,7.07 +41,28e418b320f6b759b6aea79734f165e5,2,fb8c11da2e1b1634e879d1c3c6762dc2,2022/9/8 15:24,170,7.07 +42,29dba01f92da927e611d340bc135f679,1,caaa713799b547352795c831a14f1c3c,2023/4/25 8:12,170,6.84 +43,2bcf1f79964f8b4f4f62f19441601b1a,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/9/8 2:55,240,10.34 +44,2ca1528ba222dcef1b182632376e53a0,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/15 1:34,119.8,18.34 +45,2ce81d16edb6568f5ff5b9ec2e5cb4b7,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/4 14:55,119.8,5.55 +46,2d472b4b2d08108565852ca0782507f2,1,d6160fb7873f184099d9bc95e30376af,2022/9/21 0:20,330,8.19 +47,2e9d8685e015feaba98d02e5544583f6,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/5 14:05,240,8.17 +48,338515ff7c1216299678e18a593799a5,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/10/26 17:49,287.4,8.5 +49,34aed6e4313d7573b0d3b459f3a805ea,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/11/8 7:06,287.4,10.49 +50,34d0587652282a8e4f05da5083e1f276,1,b0fd001716da5648244fe5c374d03df8,2022/9/6 13:50,170,11.66 +51,3644344c9ad371ecfc67c6cbeb93531c,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/9/8 20:03,240,6.57 +52,3689c2503baba1c0879d94cdbe6d6d60,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/4 5:24,240,10.16 +53,36db19001f33a6f23042adbc4a0e30a5,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/3 23:31,240,8.17 +54,3710f61b45bb40db596e352722b6937e,1,d6160fb7873f184099d9bc95e30376af,2022/9/13 18:55,310,10.65 +55,397cda88cf14fa47dd00f8ce20346f25,1,194e74feac1f84a8ee5a517d4b3e107a,2023/8/17 4:55,179.8,6.71 +56,3a4b013e014723cc38c9faa8ffdc6387,1,34f99d82cfc355d08d8db780d14aa002,2023/4/16 7:35,680,25.29 +57,3bd820484fb08cbd09f9d3f378fae1a4,1,b008888e5e01a5c9da36306228e900d1,2023/6/11 18:30,158,16.02 +58,3c140d933d3475ec823f9b748ca5c49f,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/12/18 11:36,268,8.37 +59,43f32db0dedd975e294e98ce23b24661,1,1f411e9a31196b71ed8438c8254b858e,2023/5/18 4:12,124,5.18 +60,43f32db0dedd975e294e98ce23b24661,2,1f411e9a31196b71ed8438c8254b858e,2023/5/18 4:12,124,5.18 +61,43f32db0dedd975e294e98ce23b24661,3,1f411e9a31196b71ed8438c8254b858e,2023/5/18 4:12,124,5.18 +62,442693e8e705ad1f9c91f762f0d297bc,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/3 21:25,240,10.16 +63,465c108e33e2b7e6953f020140a457ba,1,d6160fb7873f184099d9bc95e30376af,2022/9/12 20:55,259.98,8.31 +64,4733b3de4bf04376a9d53930899ef18c,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/12 14:28,119.8,11.08 +65,4bfda23ff0ed8665aa9113bfb4130bf3,1,a04087ab6a96ffa041f8a2701a72b616,2023/8/1 14:30,130,6.6 +66,4cfdd4464ee3dbf59c17a69045429af4,1,bc4cd4da98dd128c39bf0b8c2674032f,2022/9/6 2:50,240,7.56 +67,4f8fb49bd955f713c79333a0a2d9da57,1,7c8e2b381bb0fcba5b368961d7823cd2,2023/1/2 12:49,119.8,14.68 +68,5383435eb2b05b6f4d5be7720974fe67,1,1e5428c428e0f783acd6e3d94ba4ee2a,2022/12/26 9:59,119.8,14.68 +69,53d6fdda7e6cd60c16d5051f00ac2af3,1,d6160fb7873f184099d9bc95e30376af,2022/9/4 22:45,240,5.01 +70,561cdd2accb4b96049e2f85972d8bffe,1,b008888e5e01a5c9da36306228e900d1,2023/6/14 13:53,158,16.02 +71,564e414f20390e6417f5e6c6ad3f095e,1,a04087ab6a96ffa041f8a2701a72b616,2022/12/28 2:11,119.8,16.48 +72,59b115dfb1007eca4cb7cd1a4cf2c790,1,d6160fb7873f184099d9bc95e30376af,2022/9/8 15:23,260,10.3 +73,5a3483866ae86e48df2c0da23f2684f2,1,9aedf557945e816d48539ee56293f860,2022/9/4 20:44,220,7.42 +74,5a50ee4e7bf3b3bf306a1c3f19a1a885,1,d6160fb7873f184099d9bc95e30376af,2022/9/5 18:07,240,10.16 +75,5a902cc5a20f8cd076f116d16d5aef9d,1,d6160fb7873f184099d9bc95e30376af,2022/10/9 20:25,320,10.72 +76,5afaa7b618c8776fad63ca23a14a8035,1,1f411e9a31196b71ed8438c8254b858e,2023/5/15 17:15,124,4.88 +77,5afaa7b618c8776fad63ca23a14a8035,2,1f411e9a31196b71ed8438c8254b858e,2023/5/15 17:15,124,4.88 +78,5ca18035ac56a17dd903b0e8410aae06,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/18 2:31,119.8,22.24 +79,5e34ad6a26152d8d8743a70e214b1a98,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/3 17:11,119.8,5.55 +80,5f5a937b8e63ac95df2db115354de971,1,a04087ab6a96ffa041f8a2701a72b616,2023/1/3 4:14,119.8,16.48 +81,5f66067164a5ca7adf49a87bfe4cccc1,1,a04087ab6a96ffa041f8a2701a72b616,2023/8/6 19:24,130,20.83 +82,5fa37b0775f5467f987580b059bce54a,1,d6160fb7873f184099d9bc95e30376af,2022/9/6 13:45,240,5.58 +83,607911c4ac62f9038b4ec434673e486e,1,194e74feac1f84a8ee5a517d4b3e107a,2023/8/9 16:50,179.8,11.81 +84,607911c4ac62f9038b4ec434673e486e,2,194e74feac1f84a8ee5a517d4b3e107a,2023/8/9 16:50,179.8,11.81 +85,60dc36170c9cc8c0215fafa26627670f,1,d6160fb7873f184099d9bc95e30376af,2022/9/19 0:15,310,6.74 +86,60e233aaaa492c07368a9bac1c0ee2d4,1,e004a3b7f9524967fdb68bb70f850eb0,2022/10/16 20:42,146,7.16 +87,63a2ef605ed6a193d40b312ca1b014d5,1,1e5428c428e0f783acd6e3d94ba4ee2a,2022/12/19 21:10,119.8,11.73 +88,63b0abee9c82e79c3b6e860d070f2c62,1,d6160fb7873f184099d9bc95e30376af,2022/9/3 21:55,240,6.57 +89,63e700a4baf3602c94a8d6648e896d41,1,d6160fb7873f184099d9bc95e30376af,2022/10/6 10:07,320,10.72 +90,64582c699f99b4dd3c326b13c3bdc644,1,e004a3b7f9524967fdb68bb70f850eb0,2022/10/16 11:36,146,14.78 +91,65d1a82f9ce69290556d70fc555cba43,1,caaa713799b547352795c831a14f1c3c,2023/4/24 9:35,170,12.04 +92,694e3d121f16bcbb9b4595b6c6ab3baf,1,a5215a7a9f46c4185b12f38e9ddf2abc,2022/12/21 16:20,268,5.21 +93,694e3d121f16bcbb9b4595b6c6ab3baf,2,a5215a7a9f46c4185b12f38e9ddf2abc,2022/12/21 16:20,268,5.21 +94,6ae192daf1b6e9b99225e7300b79df48,1,d6160fb7873f184099d9bc95e30376af,2022/9/8 20:54,260,7.7 +95,6b24cf0591948a0a949ee51131cb78ad,1,d6160fb7873f184099d9bc95e30376af,2022/9/4 17:50,240,4.3 +96,6b8a03d40f75b75ec65ed5ba13d80f95,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/10/30 22:56,287.4,9.08 +97,6d9da1dcc8d05c588e4ea617d0fe9465,1,6e263657e75994ff623356f9cff692db,2023/8/14 14:26,146,6.02 +98,6fd59e3ae7e24c50131f6bc97c4c7776,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/11/8 20:45,287.4,3.95 +99,6fd59e3ae7e24c50131f6bc97c4c7776,2,588531f8ec37e7d5ff5b7b22ea0488f8,2022/11/8 20:45,287.4,3.95 +100,70623d7ff2e126f3688dba1042ec9531,1,588531f8ec37e7d5ff5b7b22ea0488f8,2022/10/27 16:38,287.4,6.9 diff --git a/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/orders.csv b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/orders.csv new file mode 100644 index 000000000..5ebd2d58d --- /dev/null +++ b/wren-modeling-rs/sqllogictest/tests/resources/ecommerce/orders.csv @@ -0,0 +1,100 @@ +OrderId,CustomerId,Status,PurchaseTimestamp,ApprovedTimestamp,DeliveredCarrierDate,DeliveredCustomerDate,EstimatedDeliveryDate +76754c0e642c8f99a8c3fcb8a14ac700,f6c39f83de772dd502809cee2fee4c41,delivered,2022/10/26 9:31:00,2022/10/26 9:49:00,2022/10/26 21:33,2022/11/1 21:17,2022/11/22 0:00 +607911c4ac62f9038b4ec434673e486e,7cec2ad3ecf1b10ce543161225a13a97,delivered,2023/8/6 16:41:00,2023/8/6 16:50:00,2023/8/8 11:09,2023/8/13 17:57,2023/8/22 0:00 +0af28d87520565eb3b57c9b2abe1a2cc,0a209a88c2e3dc2981c79ad85c558059,delivered,2022/10/5 19:23:00,2022/10/5 20:28:00,2022/10/6 17:15,2022/10/17 20:58,2022/11/6 0:00 +0d91fd5b8475f8f0576a6e8f90c6497d,9f34131462cd5edccdc9d4db9f7d2bbc,delivered,2023/5/10 14:49:00,2023/5/11 14:58:00,2023/5/15 7:43,2023/5/21 10:48,2023/5/29 0:00 +256ccc96b2c931006d95ba7a05181037,2a381888bc87c4fde2eda8bedb291234,delivered,2023/1/15 19:26:00,2023/1/15 19:35:00,2023/1/18 1:33,2023/1/19 13:32,2023/2/8 0:00 +e130d3f737127ae52681b9338aea9b64,b9da524b08224eaff1bf76ef4d4299ac,delivered,2023/5/25 14:41:00,2023/5/26 10:19:00,2023/5/28 13:48,2023/6/5 9:02,2023/6/19 0:00 +05d61bb749461f4805dbda51f767fa47,80f16375ce928674fe11f2e5130208e1,delivered,2022/8/29 19:04:00,2022/8/29 19:15:00,2022/8/31 19:37,2022/9/15 18:42,2022/10/10 0:00 +561cdd2accb4b96049e2f85972d8bffe,6ed3ba51865085a8de8bb730cefc8a32,delivered,2023/6/6 13:36:00,2023/6/6 13:53:00,2023/6/7 13:49,2023/6/25 19:29,2023/7/20 0:00 +63b0abee9c82e79c3b6e860d070f2c62,b51ecad2f840841fc7f24a4aca209f01,delivered,2022/8/28 21:45:00,2022/8/28 21:55:00,2022/8/31 19:26,2022/9/9 17:05,2022/9/19 0:00 +cf76d482b013f8af6dc046a3001c1c4f,04eafb40a16989307464f27f1fed8907,delivered,2023/7/4 13:08:00,2023/7/5 16:25:00,2023/7/6 9:09,2023/7/12 20:26,2023/8/6 0:00 +cc4687e4e102c1c8c606a5a0a8e475db,079e85f42f5af80c74240b97b7a4dee1,delivered,2022/10/31 20:13:00,2022/11/1 20:25:00,2022/11/3 22:25,2022/11/16 19:37,2022/12/6 0:00 +820349ed7a25bbfa196bb4f9a28442f2,687f63c64f1b99fbd97d3d1e940c781a,delivered,2022/11/4 9:35:00,2022/11/4 9:50:00,2022/11/16 21:58,2022/11/27 18:32,2022/11/29 0:00 +12f2f3174deba94de2aa43ad4adf6d3e,cc4fef4299b231a913af5205feed5e36,delivered,2022/10/11 9:50:00,2022/10/13 21:26:00,2022/10/17 20:14,2022/10/30 20:16,2022/11/16 0:00 +fb973be52c6622812266932dc2152017,652809adfb5055cf87dd32aca5a8b760,delivered,2022/8/29 16:07:00,2022/8/29 16:27:00,2022/8/31 19:37,2022/9/8 20:55,2022/9/20 0:00 +29dba01f92da927e611d340bc135f679,11eb7497f0981fd5b7cb235f4118c034,delivered,2023/4/18 18:08:00,2023/4/19 8:12:00,2023/4/19 19:06,2023/4/27 20:12,2023/5/15 0:00 +8a4f504a9826004fb1b0957a12aad33a,8d4fbbdcabdfc2f9b1f52cd523ea5865,delivered,2023/8/20 16:26:00,2023/8/21 16:35:00,2023/8/23 13:51,2023/8/29 21:38,2023/9/25 0:00 +a73de8635a0cb0aee3da3736197f76b2,d1f24011654af00df1a8326c6cb8aac1,delivered,2022/10/2 22:45:00,2022/10/2 22:56:00,2022/10/4 21:03,2022/10/9 17:20,2022/10/25 0:00 +cf6908e8b524d8fac0d70a5cc8bd749b,c85ab216eceb57e7059d16f16744b513,delivered,2022/8/29 13:58:00,2022/8/29 14:10:00,2022/8/31 19:37,2022/9/6 22:34,2022/9/20 0:00 +a23f4025d60af6199b5eb5631b070e7f,c69a08ef1c07bde2bbcab44dce1bd0b6,delivered,2022/12/7 20:48:00,2022/12/9 16:31:00,2022/12/13 22:10,2022/12/22 13:52,2023/1/5 0:00 +e72e22dce48aaee824371b8e02641f15,d1ea8164411774eb6b46718f894eaace,delivered,2022/10/16 10:31:00,2022/10/16 10:49:00,2022/10/19 20:37,2022/10/25 21:37,2022/11/3 0:00 +d858299446e4d2e4a686d1dcf9a93359,6ec162d7f7fafad0e98a7a8527f9a0fc,delivered,2023/1/7 10:18:00,2023/1/7 10:27:00,2023/1/9 23:39,2023/1/16 22:41,2023/2/2 0:00 +b5b103fe531168050d785d5b8de74174,c3ab6358d2556844d38ed5dbab0b41ce,delivered,2022/1/30 13:13:00,2022/1/31 13:33:00,2022/2/2 9:37,2022/2/21 12:01,2022/3/21 0:00 +245b2a09d0f787df368d4a370a63eb58,024dad8e71332c433bc9a494565b9c49,delivered,2022/12/6 19:26:00,2022/12/6 19:33:00,2022/12/11 22:55,2023/1/10 20:45,2023/1/11 0:00 +d4a07b48835f3637cdf8baeab13c8a9a,17b48c3c14659979aca9d9bb5e057dee,delivered,2022/10/7 14:59:00,2022/10/7 15:14:00,2022/10/11 22:46,2022/10/20 19:36,2022/11/7 0:00 +8193580d95dea97d43937711060b2846,4227cf6df4208d3ff1ce0aaa14e265a9,delivered,2022/11/9 11:20:00,2022/11/10 2:55:00,2022/11/14 21:22,2022/11/23 23:09,2022/12/5 0:00 +bb9e3fdfc33f7fe56fd38f6adacf760f,36d29f1c80a81a79fbf6185b00b5ce3c,delivered,2022/12/26 14:45:00,2022/12/26 14:53:00,2022/12/27 17:54,2023/1/20 0:18,2023/1/29 0:00 +2352f0c7b4fe903c071e032c9d6d6593,b115c9e03fe6ed4c0b0ad50733f1ae98,delivered,2023/4/13 8:15:00,2023/4/17 5:50:00,2023/4/17 22:46,2023/4/23 13:32,2023/5/18 0:00 +59b115dfb1007eca4cb7cd1a4cf2c790,c36218bce6a33d4814601c7744386d9e,delivered,2022/9/3 15:10:00,2022/9/3 15:23:00,2022/9/6 23:49,2022/9/12 22:15,2022/9/27 0:00 +08cbb1d4cd574b126569b208fd4b26ea,51478fa6f626871ba9023c80585e4952,delivered,2022/10/15 19:25:00,2022/10/15 19:35:00,2022/10/18 20:25,2022/10/26 21:14,2022/11/7 0:00 +70623d7ff2e126f3688dba1042ec9531,fe5113a38e3575c04f5a3413100d4e48,delivered,2022/10/23 16:09:00,2022/10/23 16:38:00,2022/10/25 21:43,2022/10/31 15:30,2022/11/14 0:00 +a8a52afd408a3023bafcd9ed3c0d2e15,bccacce2d37971951fd3e82965330b87,delivered,2022/8/30 22:43:00,2022/8/30 22:55:00,2022/9/1 20:07,2022/9/8 20:09,2022/9/19 0:00 +03c83b31dbc387f83f1b5579b53182fb,6d70d16ff8b3a0131e209b2bba542930,delivered,2023/1/6 16:00:00,2023/1/9 7:26:00,2023/1/9 23:38,2023/1/17 20:04,2023/2/6 0:00 +5a3483866ae86e48df2c0da23f2684f2,82b56e1af615d6fe0b24b8f706f172f9,delivered,2022/8/29 20:30:00,2022/8/29 20:44:00,2022/8/31 19:50,2022/9/4 14:51,2022/9/18 0:00 +f296152f7123f2d4455e82df8835f5a2,194e5864217a3571944250937dec0a41,delivered,2023/8/21 9:39:00,2023/8/22 20:30:00,2023/8/23 14:37,2023/8/28 21:04,2023/9/5 0:00 +106ff3ba3e84e22713bf2a10c582fd94,02d1b5b8831241174c6ef13efd35abbd,delivered,2023/4/8 1:21:00,2023/4/8 1:35:00,2023/4/10 18:06,2023/4/20 10:03,2023/4/25 0:00 +048e6e4623dbf118c43e0f5572016faa,7c43cc8f6953deea674d34207a646886,delivered,2022/12/31 17:57:00,2022/12/31 18:09:00,2023/1/3 23:22,2023/1/10 21:42,2023/2/8 0:00 +cdca52aa619c8e59bd64701d54571cc0,36da15e62a3ecf96427337aba9139f2e,delivered,2022/9/4 12:30:00,2022/9/5 4:24:00,2022/9/6 22:07,2022/9/12 17:10,2022/9/26 0:00 +a81957953164f65e49dd6af3972b9db5,607cac3b738f3e37909b517901df2e85,shipped,2023/5/31 12:58:00,2023/6/3 11:30:00,2023/6/4 14:55,,2023/7/23 0:00 +65d1a82f9ce69290556d70fc555cba43,e5f657faf489a605ece3d14855b8f2c4,delivered,2023/4/18 9:26:00,2023/4/18 9:35:00,2023/4/18 18:28,2023/4/26 18:19,2023/5/16 0:00 +4733b3de4bf04376a9d53930899ef18c,e8d32260f2ebace5f1b80c9b213601ff,shipped,2023/1/8 14:16:00,2023/1/8 14:28:00,2023/1/10 20:14,,2023/2/14 0:00 +3a4b013e014723cc38c9faa8ffdc6387,e7c905bf4bb13543e8df947af4f3d9e9,delivered,2023/4/9 11:32:00,2023/4/10 7:35:00,2023/4/11 17:51,2023/4/13 2:06,2023/4/24 0:00 +cd44196e80474114b859765d62f12ddf,38a63fe852918021685a1f656a4d5049,delivered,2023/4/15 21:41:00,2023/4/15 21:55:00,2023/4/16 20:07,2023/4/23 18:56,2023/5/8 0:00 +e3ef3902f4da2bf727f00602d912d034,897c8801e3fd285ce1196daed5ae2de7,delivered,2022/9/17 8:33:00,2022/9/17 8:45:00,2022/9/19 19:36,2022/10/2 17:19,2022/10/6 0:00 +63e700a4baf3602c94a8d6648e896d41,4c8f87ae1692e8e9cb90458557bcf4cb,delivered,2022/9/30 21:44:00,2022/10/2 10:07:00,2022/10/3 19:08,2022/10/13 17:42,2022/10/30 0:00 +564e414f20390e6417f5e6c6ad3f095e,3c8863310a791b2426da5c04de18e70c,delivered,2022/12/20 19:29:00,2022/12/21 2:11:00,2022/12/26 20:52,2023/1/12 12:28,2023/1/24 0:00 +3644344c9ad371ecfc67c6cbeb93531c,286cd699eec7a6faf30e808b715f1180,delivered,2022/9/1 16:54:00,2022/9/2 20:03:00,2022/9/7 0:04,2022/9/19 21:21,2022/9/25 0:00 +85ead861d8aad2f8734d85d187a42b7f,7c7165957bda66e88708766f34c334f3,delivered,2022/12/7 9:08:00,2022/12/11 18:15:00,2022/12/13 22:24,2022/12/19 21:42,2023/1/3 0:00 +34aed6e4313d7573b0d3b459f3a805ea,7afdb6b1737919a448daeb749ee49758,delivered,2022/10/31 18:50:00,2022/11/1 7:06:00,2022/11/3 20:52,2022/11/14 21:54,2022/11/27 0:00 +ca6e9fb5f1a78410c6e1a89640efa8f6,4d77bdf492f3c64a182a33b91618abae,delivered,2023/7/1 20:49:00,2023/7/1 21:10:00,2023/7/3 10:24,2023/7/4 0:38,2023/7/20 0:00 +28e418b320f6b759b6aea79734f165e5,97697c5f77b484cb0cec9eaa81c679d5,delivered,2022/9/2 15:09:00,2022/9/2 15:24:00,2022/9/7 0:04,2022/9/19 19:16,2022/9/27 0:00 +17affe817f6b2a24ec4a9b5bb67a1671,a2593be583c3bec897ec5664bb6e4770,delivered,2022/10/5 17:54:00,2022/10/5 18:25:00,2022/10/6 20:32,2022/10/10 20:37,2022/10/27 0:00 +fefacc66af859508bf1a7934eab1e97f,f48d464a0baaea338cb25f816991ab1f,delivered,2023/7/25 18:10:00,2023/7/27 4:05:00,2023/8/3 14:42,2023/8/15 14:57,2023/8/10 0:00 +e7bf5b3305b73ad31a0306f433c8dd84,192c6d78c5af56baf060b9b2792bdbb5,delivered,2022/9/6 13:14:00,2022/9/8 13:30:00,2022/9/15 17:38,2022/9/21 16:13,2022/10/4 0:00 +5e34ad6a26152d8d8743a70e214b1a98,a0e9c9c5a19366c5eec698ac38498a1e,delivered,2022/12/26 9:43:00,2022/12/27 17:11:00,2023/1/3 23:08,2023/1/8 16:03,2023/1/19 0:00 +90b3ce1268706ec7923743d5867f26a1,b77a36877fb9b74ab4dec5f4e939419a,delivered,2023/4/4 16:58:00,2023/4/5 16:55:00,2023/4/7 1:18,2023/4/11 1:10,2023/4/16 0:00 +1d6bda6e5595652f956770a32ae4ba0d,c903f7a5a846c6117e671107d202286c,delivered,2023/5/22 0:49:00,2023/5/24 2:18:00,2023/5/25 13:48,2023/6/4 21:54,2023/5/30 0:00 +706296e2ab240216ffef853d1095d04f,f0685eff82fe533447ab1a84c25704c1,delivered,2022/8/29 16:25:00,2022/8/30 16:25:00,2022/8/31 19:33,2022/9/4 19:27,2022/9/18 0:00 +b4869c2e4b11586a953130fa13632aa9,1f32801ff0c8f0aeaa8a6af5ca6ce862,delivered,2022/9/1 10:51:00,2022/9/1 11:04:00,2022/9/4 21:21,2022/9/11 20:03,2022/9/25 0:00 +b8b17051a70af754f5a15916490b578c,263f578b432f4c4a154e9c7002bc6c06,delivered,2023/1/4 12:32:00,2023/1/4 12:48:00,2023/1/9 23:52,2023/1/23 1:03,2023/2/2 0:00 +5383435eb2b05b6f4d5be7720974fe67,7fe98b2b2fa5e20a973f275bd5121c2a,delivered,2022/12/18 11:09:00,2022/12/19 9:59:00,2022/12/22 22:17,2023/1/4 23:09,2023/1/17 0:00 +afd5ca74e46f54e04d9104595814ccb1,3c12c66685930cfd507eb4201111b516,delivered,2022/10/1 19:27:00,2022/10/3 14:07:00,2022/10/6 22:35,2022/10/23 20:24,2022/10/30 0:00 +3bd820484fb08cbd09f9d3f378fae1a4,6260e99309fefdfaef12d0ef8fe82e77,delivered,2023/6/1 18:20:00,2023/6/1 18:33:00,2023/6/4 14:55,2023/8/10 17:52,2023/7/17 0:00 +1b2b354adeb38c1c308c915faf7d3288,b0962c45b3881a52d605fe965ca9a776,delivered,2022/8/30 9:33:00,2022/8/30 9:45:00,2022/8/31 19:32,2022/9/4 20:51,2022/9/19 0:00 +338515ff7c1216299678e18a593799a5,a5252e81424e5fb2c7ddf01256cf94e0,delivered,2022/10/22 17:32:00,2022/10/22 17:49:00,2022/10/24 22:07,2022/10/30 19:33,2022/11/14 0:00 +a806770e48c22cc0c2607982f3e87e30,ec690140f77f5ff4c884704c976e3489,delivered,2022/9/1 14:00:00,2022/9/1 14:15:00,2022/9/4 20:51,2022/9/15 18:53,2022/9/25 0:00 +2ce81d16edb6568f5ff5b9ec2e5cb4b7,a2a5fc7e8fc7e880cd68a9768b8caa22,delivered,2022/12/28 14:45:00,2022/12/28 14:55:00,2023/1/3 23:23,2023/1/8 16:28,2023/1/19 0:00 +ff2ac13b0f36dffb6af9699f59b18377,c7a68b2b2775db26644d8f4e28c03604,delivered,2022/9/4 14:26:00,2022/9/4 14:35:00,2022/9/6 18:08,2022/9/11 19:35,2022/9/26 0:00 +83e50e2e58cfc9839fb932948f1d7ce8,71c41989b581f59300b8dc475aa48c0b,delivered,2023/5/3 8:48:00,2023/5/3 9:12:00,2023/5/3 13:25,2023/5/8 15:48,2023/5/23 0:00 +1badd176eb41888c1a20a7614fba59e1,ed189d4d749b9877a2aa93d9d32f339b,delivered,2022/9/18 1:50:00,2022/9/19 9:50:00,2022/9/20 19:38,2022/10/2 19:25,2022/10/13 0:00 +694e3d121f16bcbb9b4595b6c6ab3baf,2e0339ff984d6f41f11dfdbc7c93dcda,delivered,2022/12/15 13:43:00,2022/12/16 16:20:00,2022/12/19 23:19,2022/12/22 1:07,2023/1/10 0:00 +397cda88cf14fa47dd00f8ce20346f25,b47024b93a109910a33dc796ea7593f9,delivered,2023/8/10 18:17:00,2023/8/14 4:55:00,2023/8/15 13:01,2023/8/16 18:28,2023/8/16 0:00 +ed35f59756edecbfcefb866146f00905,3a1e5902bb274b86dd980f371e207651,delivered,2022/12/16 19:41:00,2022/12/16 19:50:00,2022/12/22 21:58,2023/1/3 19:43,2023/1/16 0:00 +977649a07fb7be354cc39d9a79ee83e1,d7d1465f1de475b3d507ab913951fb3c,delivered,2023/7/11 10:10:00,2023/7/11 10:26:00,2023/7/12 12:19,2023/7/27 16:51,2023/7/30 0:00 +63a2ef605ed6a193d40b312ca1b014d5,a95ad7bcbf76f95fcb884dfdfa84ae68,delivered,2022/12/11 20:45:00,2022/12/13 21:10:00,2022/12/19 1:48,2023/1/6 14:05,2023/1/15 0:00 +8185ba302492c86291d7b1912c94ded3,f066138977b82fde31c0fa4c67c6600f,delivered,2023/4/18 22:10:00,2023/4/18 22:31:00,2023/4/19 22:37,2023/4/24 22:17,2023/5/14 0:00 +8dbc85d1447242f3b127dda390d56e19,3d979689f636322c62418b6346b1c6d2,delivered,2023/6/22 12:23:00,2023/6/22 12:36:00,2023/6/22 13:00,2023/7/6 1:08,2023/7/17 0:00 +8171523911786efd1d91c66d69051fcd,6152fbfc8a92ee25fd821740bd33b089,delivered,2023/6/11 21:17:00,2023/6/12 9:00:00,2023/6/14 13:27,2023/6/21 18:54,2023/7/4 0:00 +fdddc15fc7b9c37476820d6eb722f33b,66b84aaef46998841f07082b24004842,delivered,2022/8/29 10:55:00,2022/8/30 12:35:00,2022/8/31 19:50,2022/10/20 18:09,2022/9/19 0:00 +60dc36170c9cc8c0215fafa26627670f,7074bf81824d0b653d0f13bc3b846c18,delivered,2022/9/12 0:00:00,2022/9/13 0:15:00,2022/9/14 19:20,2022/9/21 15:58,2022/10/6 0:00 +2582e88a3e8a129cc809ccaf9a6d35d6,7d875beb796e3f0fb726c3854dbd3037,delivered,2022/10/16 21:33:00,2022/10/16 22:05:00,2022/10/19 20:28,2022/10/27 18:33,2022/11/8 0:00 +6b24cf0591948a0a949ee51131cb78ad,ac7c0515474bc3f9cf19474ee4d228eb,delivered,2022/8/29 17:42:00,2022/8/29 17:50:00,2022/8/31 19:36,2022/9/1 21:44,2022/9/13 0:00 +0945dadba69dad599e42c31e78450de4,be33968269bf8c51037b65cdf4edff1b,delivered,2022/8/29 15:20:00,2022/8/31 2:35:00,2022/9/1 20:07,2022/9/25 18:10,2022/10/10 0:00 +858d458b39082890e96fbb1fed8c2a52,f959b7bc834045511217e6410985963f,delivered,2022/9/6 16:10:00,2022/9/6 17:15:00,2022/9/11 19:48,2022/9/16 16:18,2022/9/26 0:00 +1407ed7d738d943469994266e9706b46,e85b1dcef5f92e0647dc214b455b83c8,delivered,2022/8/30 10:51:00,2022/8/30 11:05:00,2022/8/31 19:36,2022/9/13 20:10,2022/9/28 0:00 +a498326cbf752acac00b7d65561dcd98,efa2729fe65e29fa63ac050b9f9fe3df,delivered,2022/12/16 20:23:00,2022/12/16 20:32:00,2022/12/19 23:19,2022/12/27 20:24,2023/1/16 0:00 +5ca18035ac56a17dd903b0e8410aae06,4e5ca9b956b369f83366360cf7dbd9b6,delivered,2023/1/11 22:10:00,2023/1/13 4:55:00,2023/1/17 21:42,2023/1/25 19:23,2023/2/19 0:00 +5a50ee4e7bf3b3bf306a1c3f19a1a885,72680b3da61c269ad2e5d342356fec77,delivered,2022/8/30 17:50:00,2022/8/30 18:07:00,2022/8/31 19:36,2022/9/15 19:07,2022/10/10 0:00 +ccac1a9dee556f517bdb745dd29bcb68,1a46899c81a1f73c1338cffd1164751d,delivered,2022/9/5 18:45:00,2022/9/5 18:55:00,2022/9/8 19:27,2022/9/29 16:55,2022/10/2 0:00 +ceed0f54453282dec0cc7c591237d5b6,fa4de7819dda8ca15bf86e413ba6473f,delivered,2022/9/17 19:18:00,2022/9/17 19:30:00,2022/9/19 19:36,2022/10/3 19:22,2022/10/10 0:00 +c22f4ba5610aa110fb73444de7600a2e,ed7c9514731cc85c9cd18dbf8115e8b0,delivered,2022/9/28 15:52:00,2022/9/28 16:07:00,2022/10/2 21:32,2022/10/10 20:57,2022/10/25 0:00 +0a187dd7f4efe70e8c3b6e3779b3b133,23fafaadc0b11501176401ba0003c22d,delivered,2022/9/2 19:48:00,2022/9/2 20:03:00,2022/9/7 0:07,2022/9/13 22:02,2022/9/26 0:00 +f4b7ccd15f831ff995d4f307b0dc0f0b,223a807962e222b758aeee5610103f47,delivered,2022/8/29 22:38:00,2022/8/29 22:50:00,2022/8/31 19:41,2022/9/5 18:52,2022/9/15 0:00 +64582c699f99b4dd3c326b13c3bdc644,a095450e0630a3f9776ba4196dac1da1,delivered,2022/10/9 10:22:00,2022/10/9 10:36:00,2022/10/10 21:26,2022/10/30 14:54,2022/11/17 0:00 +d55b88c264e0cacf41c8f8ce846bc1ee,3e35e35a82de3fc574900adca713dff2,delivered,2023/6/11 23:05:00,2023/6/11 23:35:00,2023/6/12 13:08,2023/7/4 20:03,2023/7/4 0:00 +6d9da1dcc8d05c588e4ea617d0fe9465,ce9167d363251ec55e167d15816ea22e,delivered,2023/8/9 14:12:00,2023/8/9 14:26:00,2023/8/10 14:51,2023/8/14 16:04,2023/8/15 0:00 +82ee69216f11b23e2d21d2796e079366,7f8aa5f321fc28ae90ee6a38cd6e3150,delivered,2023/8/8 11:34:00,2023/8/8 11:45:00,2023/8/14 13:59,2023/8/17 12:28,2023/8/23 0:00 +5afaa7b618c8776fad63ca23a14a8035,bca5625f843661186a352a2c160843ee,delivered,2023/5/10 17:07:00,2023/5/10 17:15:00,2023/5/11 17:25,2023/5/14 16:31,2023/5/21 0:00 +d19772e154f0929b89a1dab65648767b,b95975b682b09a1890557a1ab4021873,delivered,2022/10/5 19:12:00,2022/10/6 19:07:00,2022/10/11 23:13,2022/10/23 20:17,2022/11/10 0:00 +bfb8512783ccd2271d2e96d3b856bf2b,4ee7fc32553f4f1cda85112b9a2719e3,invoiced,2022/9/16 12:37:00,2022/9/16 12:45:00,,,2022/10/6 0:00