use std::{
fs::File,
io::{ErrorKind, Write},
path::{Path, PathBuf},
str,
sync::{Arc, Once},
};
use assert_cmd::{assert::Assert, cargo::cargo_bin_cmd, Command};
use bytes::Bytes;
use odbc_api::{
buffers::TextRowSet, environment, sys::AttrConnectionPooling, BindParamDesc, Connection,
ConnectionOptions, Cursor, Environment, IntoParameter,
};
use parquet::{
column::writer::ColumnWriter,
data_type::{ByteArray, FixedLenByteArray},
file::{
properties::WriterProperties, reader::FileReader, serialized_reader::SerializedFileReader,
writer::SerializedFileWriter,
},
schema::parser::parse_message_type,
};
use predicates::{ord::eq, str::contains};
use tempfile::{tempdir, NamedTempFile, TempDir};
const MSSQL: &str = "Driver={ODBC Driver 18 for SQL Server};\
Server=localhost;\
UID=SA;\
PWD=My@Test@Password1;\
TrustServerCertificate=yes;";
const POSTGRES: &str = "Driver={PostgreSQL UNICODE};\
Server=localhost;\
Port=5432;\
Database=test;\
Uid=test;\
Pwd=test;";
fn env() -> &'static Environment {
Once::new().call_once(|| {
unsafe {
Environment::set_connection_pooling(AttrConnectionPooling::OnePerDriver)
.expect("ODBC manager must be able to initialize connection pools");
}
});
environment().unwrap()
}
fn parquet_read_out(file: &str) -> Assert {
let mut cmd = Command::new("parquet-read");
cmd.args([file]).assert().success()
}
fn parquet_schema_out(file: &str) -> Assert {
let mut cmd = Command::new("parquet-schema");
cmd.args([file]).assert().success()
}
#[test]
fn append_user_and_password_to_connection_string() {
let table_name = "AppendUserAndPasswordToConnectionString";
TableMssql::new(table_name, &["VARCHAR(10)"]);
let connection_string = "Driver={ODBC Driver 18 for SQL Server};Server=localhost;\
TrustServerCertificate=yes;";
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
connection_string,
"--user",
"SA",
"--password",
"My@Test@Password1",
out_str,
&query,
])
.assert()
.success();
}
#[test]
fn insert() {
roundtrip("insert.par", "odbc2parquet_insert").success();
}
#[test]
fn insert_empty_document() {
roundtrip("empty_document.par", "odbc2parquet_empty_document").success();
}
#[test]
fn nullable_parquet_buffers() {
let table_name = "NullableParquetBuffers";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[[Some("Hello")], [None], [Some("World")], [None]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected = "\
{a: \"Hello\"}\n\
{a: null}\n\
{a: \"World\"}\n\
{a: null}\n\
";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn foobar_connection_string() {
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Tempfile path must be utf8");
let mut cmd = cargo_bin_cmd!();
cmd.args([
"-vvvv",
"query",
"-c",
"foobar",
out_str,
"SELECT * FROM [uk-500$]",
])
.assert()
.failure()
.code(1);
}
#[test]
fn should_give_good_error_if_specifying_directory_for_output() {
let out_dir = tempdir().unwrap();
let out_path = out_dir.path();
let out_str = out_path.to_str().expect("Tempfile path must be utf8");
let mut cmd = cargo_bin_cmd!();
cmd.args(["query", "-c", MSSQL, out_str, "SELECT 42 AS A"])
.assert()
.stderr(contains("Could not create output file '"))
.failure()
.code(1);
}
#[test]
fn parameters_in_query() {
let table_name = "ParametersInQuery";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)", "INTEGER"]);
table.insert_rows_as_text(&[["Wrong", "5"], ["Right", "42"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a,b FROM {table_name} WHERE b=?");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
"42",
])
.assert()
.success();
let expected = "\
{a: \"Right\", b: 42}\n\
";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn should_error_on_truncation_utf_8() {
let table_name = "ErrorOnTruncationUtf8";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[["0123456789"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
let assertion = cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--encoding",
"system",
"--connection-string",
MSSQL,
"--column-length-limit",
"5",
&query,
])
.assert();
let expectation = "A field exceeds the maximum element length of a column buffer. You can use \
the `--column-length-limit` option to increase the maximum element size of columns. The \
driver indicated an actual length of 10. The error occurred for column a.";
assertion.failure().stderr(contains(expectation));
}
#[test]
fn should_error_on_truncation_utf_16() {
let table_name = "ErrorOnTruncationUtf16";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[["0123456789"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
let assertion = cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--encoding",
"Utf16",
"--connection-string",
MSSQL,
"--column-length-limit",
"5",
&query,
])
.assert();
let expectation = "A field exceeds the maximum element length of a column buffer. You can use \
the `--column-length-limit` option to increase the maximum element size of columns. Sadly \
the driver did not return a length indicator for the value, so you will have to guess its \
actual length. The error occurred for column a.";
assertion.failure().stderr(contains(expectation));
}
#[test]
fn should_error_on_truncation_for_sequential_fetch() {
let table_name = "ErrorOnTruncationForSequentialFetch";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[["0123456789"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
let assertion = cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--sequential-fetching",
"--column-length-limit",
"5",
&query,
])
.assert();
#[cfg(target_os = "windows")]
let expectation = "A field exceeds the maximum element length of a column buffer. You can \
use the `--column-length-limit` option to increase the maximum element size of columns. \
Sadly the driver did not return a length indicator for the value, so you will have to \
guess its actual length. The error occurred for column a.";
#[cfg(not(target_os = "windows"))]
let expectation = "A field exceeds the maximum element length of a column buffer. You can \
use the `--column-length-limit` option to increase the maximum element size of columns. \
The driver indicated an actual length of 10. The error occurred for column a.";
assertion.failure().stderr(contains(expectation));
}
#[test]
fn should_allow_specifying_explicit_compression_level() {
let table_name = "ShouldAllowSpecifyingExplicitCompressionLevel";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[["Hello"], ["World"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
"--column-compression-default",
"gzip",
"--column-compression-level-default",
"9",
out_str,
&query,
"42",
])
.assert()
.success();
}
#[test]
fn query_sales() {
let table_name = "QuerySales";
let mut table = TableMssql::new(table_name, &["DATE", "TIME(7)", "INT", "DECIMAL(10,2)"]);
table.insert_rows_as_text(&[
["2020-09-09", "00:05:34", "54", "9.99"],
["2020-09-10", "12:05:32", "54", "9.99"],
["2020-09-10", "14:05:32", "34", "2.00"],
["2020-09-11", "06:05:12", "12", "-1.50"],
]);
let expected_values = "\
{a: 2020-09-09, b: 334000000000, c: 54, d: 9.99}\n\
{a: 2020-09-10, b: 43532000000000, c: 54, d: 9.99}\n\
{a: 2020-09-10, b: 50732000000000, c: 34, d: 2.00}\n\
{a: 2020-09-11, b: 21912000000000, c: 12, d: -1.50}\n\
";
let query = format!("SELECT a,b,c,d FROM {table_name} ORDER BY id");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Tempfile path must be utf8");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_sales_sequentially() {
let table_name = "QuerySalesSequentially";
let mut table = TableMssql::new(table_name, &["DATE", "TIME(7)", "INT", "DECIMAL(10,2)"]);
table.insert_rows_as_text(&[
["2020-09-09", "00:05:34", "54", "9.99"],
["2020-09-10", "12:05:32", "54", "9.99"],
["2020-09-10", "14:05:32", "34", "2.00"],
["2020-09-11", "06:05:12", "12", "-1.50"],
]);
let expected_values = "\
{a: 2020-09-09, b: 334000000000, c: 54, d: 9.99}\n\
{a: 2020-09-10, b: 43532000000000, c: 54, d: 9.99}\n\
{a: 2020-09-10, b: 50732000000000, c: 34, d: 2.00}\n\
{a: 2020-09-11, b: 21912000000000, c: 12, d: -1.50}\n\
";
let query = format!("SELECT a,b,c,d FROM {table_name} ORDER BY id");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Tempfile path must be utf8");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--sequential-fetching",
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_decimals() {
let table_name = "QueryDecimals";
let mut table = TableMssql::new(
table_name,
&[
"NUMERIC(3,2) NOT NULL",
"DECIMAL(3,2) NOT NULL",
"DECIMAL(3,0) NOT NULL",
"DECIMAL(10,0) NOT NULL",
],
);
table.insert_rows_as_text(&[["1.23", "1.23", "3", "1234567890"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a,b,c,d FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 1.23, b: 1.23, c: 3., d: 1234567890.}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains(
"message schema {\n REQUIRED INT32 a (DECIMAL(3,2));\n \
REQUIRED INT32 b (DECIMAL(3,2));\n \
REQUIRED INT32 c (DECIMAL(3,0));\n \
REQUIRED INT64 d (DECIMAL(10,0));\n\
}",
));
}
#[test]
fn query_decimals_avoid_decimal() {
let table_name = "QueryDecimalsAvoidDecimal";
let mut table = TableMssql::new(
table_name,
&[
"NUMERIC(3,2) NOT NULL",
"DECIMAL(3,2) NOT NULL",
"DECIMAL(3,0) NOT NULL",
"DECIMAL(10,0) NOT NULL",
],
);
table.insert_rows_as_text(&[["1.23", "1.23", "3", "1234567890"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a,b,c,d FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--avoid-decimal",
&query,
])
.assert()
.success();
let expected_values = "{a: \"1.23\", b: \"1.23\", c: 3, d: 1234567890}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains(
"message schema {\n \
REQUIRED BYTE_ARRAY a (UTF8);\n \
REQUIRED BYTE_ARRAY b (UTF8);\n \
REQUIRED INT32 c (INTEGER(32,true));\n \
REQUIRED INT64 d (INTEGER(64,true));\n\
}",
));
}
#[test]
fn query_decimals_avoid_decimal_int64_not_supported_by_driver() {
let table_name = "QueryDecimalsAvoidDecimalInt64NotSupportedByDriver";
let mut table = TableMssql::new(table_name, &["DECIMAL(10,0) NOT NULL"]);
table.insert_rows_as_text(&[["1234567890"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--avoid-decimal",
"--driver-does-not-support-64bit-integers",
&query,
])
.assert()
.success();
let expected_values = "{a: 1234567890}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains(
"message schema {\n \
REQUIRED INT64 a (INTEGER(64,true));\n\
}",
));
}
#[test]
fn query_decimals_optional() {
let table_name = "QueryDecimalsOptional";
let mut table = TableMssql::new(
table_name,
&["NUMERIC(3,2)", "DECIMAL(3,2)", "DECIMAL(3,0)"],
);
table.insert_rows_as_text(&[
[Some("1.23"), Some("1.23"), Some("123")],
[None, None, None],
[Some("4.56"), Some("4.56"), Some("456")],
]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a,b,c FROM {table_name} ORDER BY id;");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values =
"{a: 1.23, b: 1.23, c: 123.}\n{a: null, b: null, c: null}\n{a: 4.56, b: 4.56, c: 456.}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_large_numeric_as_text() {
let table_name = "QueryLargeNumericAsText";
let mut table = TableMssql::new(table_name, &["NUMERIC(10,0) NOT NULL"]);
table.insert_rows_as_text(&[["1234567890"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--driver-does-not-support-64bit-integers",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq("{a: 1234567890.}\n"));
parquet_schema_out(out_str).stdout(contains("{\n REQUIRED INT64 a (DECIMAL(10,0));\n}"));
}
#[test]
fn query_numeric_13_3() {
let table_name = "QueryNumeric13_3";
let mut table = TableMssql::new(table_name, &["NUMERIC(13,3) NOT NULL"]);
table.insert_rows_as_text(&[["-1234567890.123"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: -1234567890.123}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("{\n REQUIRED INT64 a (DECIMAL(13,3));\n}"));
}
#[test]
fn query_numeric_33_3() {
let table_name = "QueryNumeric33_3";
let mut table = TableMssql::new(table_name, &["NUMERIC(33,3) NOT NULL"]);
table.insert_rows_as_text(&[["-123456789012345678901234567890.123"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: -123456789012345678901234567890.123}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains(
"{\n REQUIRED FIXED_LEN_BYTE_ARRAY (14) a (DECIMAL(33,3));\n}",
));
}
#[test]
fn query_timestamp_with_timezone_mssql() {
let table_name = "QueryTimestampWithTimezone";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIMEOFFSET"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('2022-09-07 16:04:12.1234567 +02:00');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 1662559452123456700}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIMESTAMP(NANOS,true));"));
}
#[test]
fn query_timestamp_mssql_precision_7() {
let table_name = "QueryTimestampMssqlPrecision7";
let mut table = TableMssql::new(table_name, &["DATETIME2(7)"]);
table.insert_rows_as_text(&[["2022-09-07 16:04:12.1234567"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 1662566652123456700}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIMESTAMP(NANOS,false));"));
}
#[test]
fn query_unsigned_tinyint() {
let table_name = "QueryUnsignedTinyInt";
let mut table = TableMssql::new(table_name, &["TINYINT"]);
table.insert_rows_as_text(&[["255"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 255}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT32 a (INTEGER(8,false));"));
}
#[test]
fn should_error_if_timestamp_is_out_of_range() {
let table_name = "ShouldErrorIfTimestampIsOutOfRange";
let mut table = TableMssql::new(table_name, &["DATETIME2(7)"]);
table.insert_rows_as_text(&[["2700-01-01 00:00:00"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.failure()
.stderr(contains(
"Invalid timestamp: 2700-01-01 00:00:00. The valid range for timestamps with \
nano seconds precision is between 1677-09-21 00:12:44 and 2262-04-11 \
23:47:16.854775807. Other timestamps can not be represented in parquet. To mitigate \
this you could downcast the precision in the query or convert the column to text.",
));
}
#[test]
fn should_correctly_fetch_upper_bound_timestamp() {
let table_name = "ShouldCorrectlyFetchUpperBoundTimestamp";
let mut table = TableMssql::new(table_name, &["DATETIME2(7)"]);
table.insert_rows_as_text(&[["2262-04-11 23:47:16.854775807"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 9223372036854775800}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn should_correctly_fetch_lower_bound_timestamp() {
let table_name = "ShouldCorrectlyFetchLowerBound";
let mut table = TableMssql::new(table_name, &["DATETIME2(7)"]);
table.insert_rows_as_text(&[["1677-09-21 00:12:44"]]);
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: -9223372036000000000}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_timestamp_ms_with_timezone_mssql() {
let table_name = "QueryTimestampMsWithTimezone";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIMEOFFSET(3)"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('2022-09-07 16:04:12 +02:00');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 2022-09-07 14:04:12.000 +00:00}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIMESTAMP(MILLIS,true));"));
}
#[test]
fn query_time_mssql() {
let table_name = "QueryTime";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('16:04:12');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected_values = "{a: 57852000000000}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIME(NANOS,false));"));
}
#[test]
fn query_time_0_mssql() {
let table_name = "QueryTime0";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME(0)"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('16:04:12');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT32 a (TIME(MILLIS,false));"));
}
#[test]
fn query_timestamp_with_timezone_postgres() {
let table_name = "QueryTimestampWithTimezone";
let conn = env()
.connect_with_connection_string(POSTGRES, ConnectionOptions::default())
.unwrap();
setup_empty_table_pg(&conn, table_name, &["TIMESTAMPTZ"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('2022-09-07 16:04:12 +02:00');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
POSTGRES,
&query,
])
.assert()
.success();
let expected_values = "{a: 2022-09-07 14:04:12.000000 +00:00}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIMESTAMP(MICROS,false));"));
}
#[test]
fn query_timestamp_postgres() {
let table_name = "QueryTimestamp";
let conn = env()
.connect_with_connection_string(POSTGRES, ConnectionOptions::default())
.unwrap();
setup_empty_table_pg(&conn, table_name, &["TIMESTAMP"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
('2022-09-07 16:04:12');"
);
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
POSTGRES,
&query,
])
.assert()
.success();
let expected_values = "{a: 2022-09-07 16:04:12.000000 +00:00}\n";
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains("OPTIONAL INT64 a (TIMESTAMP(MICROS,false));"));
}
#[test]
fn query_all_the_types() {
let table_name = "AllTheTypes";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(
&conn,
table_name,
&[
"CHAR(5) NOT NULL",
"NUMERIC(3,2) NOT NULL",
"DECIMAL(3,2) NOT NULL",
"INTEGER NOT NULL",
"SMALLINT NOT NULL",
"FLOAT(3) NOT NULL",
"REAL NOT NULL",
"DOUBLE PRECISION NOT NULL",
"VARCHAR(100) NOT NULL",
"DATE NOT NULL",
"TIME NOT NULL",
"DATETIME NOT NULL",
],
)
.unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a,b,c,d,e,f,g,h,i,j,k,l)
VALUES
('abcde', 1.23, 1.23, 42, 42, 1.23, 1.23, 1.23, 'Hello, World!', '2020-09-16', '03:54:12', '2020-09-16 03:54:12');"
);
conn.execute(&insert, (), None).unwrap();
let expected_values = "{\
a: \"abcde\", \
b: 1.23, \
c: 1.23, \
d: 42, \
e: 42, \
f: 1.23, \
g: 1.23, \
h: 1.23, \
i: \"Hello, World!\", \
j: 2020-09-16, \
k: 14052000000000, \
l: 2020-09-16 03:54:12.000 +00:00\
}\n";
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a,b,c,d,e,f,g,h,i,j,k,l FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_bits() {
let table_name = "QueryBits";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BIT"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
(0), (1), (NULL), (1), (0);"
);
conn.execute(&insert, (), None).unwrap();
let expected_values = "\
{a: false}\n\
{a: true}\n\
{a: null}\n\
{a: true}\n\
{a: false}\n\
";
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn query_doubles() {
let table_name = "QueryDoubles";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DOUBLE PRECISION NOT NULL"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
(0.1), (2.3);"
);
conn.execute(&insert, (), None).unwrap();
let expected_values = "\
{a: 0.1}\n\
{a: 2.3}\n\
";
let expected_schema = "REQUIRED DOUBLE a;";
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
parquet_schema_out(out_str).stdout(contains(expected_schema));
}
#[test]
fn query_comes_back_with_no_rows() {
let table_name = "QueryComesBackWithNoRows";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DOUBLE PRECISION NOT NULL"]).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
"--no-empty-file",
out_str,
&query,
])
.assert()
.success();
assert_eq!(
ErrorKind::NotFound,
File::open(out_str).err().unwrap().kind()
);
}
#[test]
fn no_empty_file_works_with_split_files() {
let table_name = "NoEmptyFileWorksWithSplitFiles";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DOUBLE PRECISION NOT NULL"]).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
"--row-groups-per-file",
"1",
"--no-empty-file",
out_str,
&query,
])
.assert()
.success();
assert_eq!(
ErrorKind::NotFound,
File::open(out_str).err().unwrap().kind()
);
}
#[test]
fn emit_file_despite_no_empty_file_set() {
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = "SELECT 42 as a";
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
"--no-empty-file",
out_str,
query,
])
.assert()
.success();
let expected = "{a: 42}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn read_query_from_stdin() {
let table_name = "ReadQueryFromStdin";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INT"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
(54),
(54),
(34),
(12);"
);
conn.execute(&insert, (), None).unwrap();
let expected_values = "\
{a: 54}\n\
{a: 54}\n\
{a: 34}\n\
{a: 12}\n\
";
let query = format!("SELECT a FROM {table_name} ORDER BY id");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Tempfile path must be utf8");
cargo_bin_cmd!()
.args(["-vvvv", "query", out_str, "--connection-string", MSSQL, "-"])
.write_stdin(query)
.assert()
.success();
parquet_read_out(out_str).stdout(eq(expected_values));
}
#[test]
fn split_files_on_num_row_groups() {
let table_name = "SplitFilesOnNumRowGroups";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let insert = format!("INSERT INTO {table_name} (A) VALUES(1),(2),(3),(4),(5)");
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--batch-size-row",
"1",
"--row-groups-per-file",
"2",
&query,
])
.assert()
.success();
let num_row_groups_in = |file_name| {
let path = out_dir.path().join(file_name);
let file = File::open(path).unwrap();
SerializedFileReader::new(file)
.unwrap()
.metadata()
.num_row_groups()
};
assert_eq!(2, num_row_groups_in("out_01.par"));
assert_eq!(2, num_row_groups_in("out_02.par"));
assert_eq!(1, num_row_groups_in("out_03.par"));
}
#[test]
fn file_name_no_empty_file_and_split_files() {
let table_name = "FileNameNoEmptyFileAndSplitFiles";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let insert = format!("INSERT INTO {table_name} (A) VALUES(1),(2),(3)");
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--no-empty-file",
"--batch-size-row",
"1",
"--row-groups-per-file",
"1",
&query,
])
.assert()
.success();
parquet_read_out(out_dir.path().join("out_01.par").to_str().unwrap());
parquet_read_out(out_dir.path().join("out_02.par").to_str().unwrap());
parquet_read_out(out_dir.path().join("out_03.par").to_str().unwrap());
}
#[test]
fn split_files_on_size_limit() {
let table_name = "SplitFilesOnSizeLimit";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let insert = format!("INSERT INTO {table_name} (A) VALUES(1),(2),(3)");
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--batch-size-row",
"1",
"--file-size-threshold",
"1B",
&query,
])
.assert()
.success();
parquet_read_out(out_dir.path().join("out_01.par").to_str().unwrap());
parquet_read_out(out_dir.path().join("out_02.par").to_str().unwrap());
parquet_read_out(out_dir.path().join("out_03.par").to_str().unwrap());
}
#[test]
fn configurable_suffix_length() {
let table_name = "ConfigurableSuffixLength";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let insert = format!("INSERT INTO {table_name} (A) VALUES(1)");
conn.execute(&insert, (), None).unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name}");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
"--batch-size-row",
"1",
"--file-size-threshold",
"1B",
"--suffix-length",
"4",
&query,
])
.assert()
.success();
parquet_read_out(out_dir.path().join("out_0001.par").to_str().unwrap());
}
#[test]
fn varbinary_column() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, "VarbinaryColumn", &["VARBINARY(10)"]).unwrap();
conn.execute(
"INSERT INTO VarbinaryColumn (a) Values \
(CONVERT(Binary(5), 'Hello')),\
(CONVERT(Binary(5), 'World')),\
(NULL)",
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = "SELECT a FROM VarbinaryColumn;";
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
query,
])
.assert()
.success();
let expected = "{a: [72, 101, 108, 108, 111]}\n{a: [87, 111, 114, 108, 100]}\n{a: null}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn query_varchar_max() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "QueryVarcharMax";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(MAX)"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) Values ('Hello, World!');"),
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
out_str,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq("{a: \"Hello, World!\"}\n"));
}
#[test]
fn query_varchar_max_utf16() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "QueryVarcharMaxUtf16";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(MAX)"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) Values ('Hello, World!');"),
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--encoding",
"Utf16",
"--connection-string",
MSSQL,
out_str,
&query,
])
.assert()
.success();
parquet_read_out(out_str).stdout(eq("{a: \"Hello, World!\"}\n"));
}
#[test]
fn binary_column() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, "BinaryColumn", &["BINARY(5)"]).unwrap();
conn.execute(
"INSERT INTO BinaryColumn (a) Values \
(CONVERT(Binary(5), 'Hello')),\
(CONVERT(Binary(5), 'World')),\
(NULL)",
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = "SELECT a FROM BinaryColumn;";
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
query,
])
.assert()
.success();
let expected = "{a: [72, 101, 108, 108, 111]}\n{a: [87, 111, 114, 108, 100]}\n{a: null}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn prefer_varbinary() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "PreferVarbinary";
setup_empty_table_mssql(&conn, table_name, &["BINARY(5)"]).unwrap();
conn.execute(
&format!(
"INSERT INTO {table_name} (a) Values \
(CONVERT(Binary(5), 'Hello')),\
(CONVERT(Binary(5), 'World')),\
(NULL)"
),
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--prefer-varbinary",
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
let expected = "{a: [72, 101, 108, 108, 111]}\n{a: [87, 111, 114, 108, 100]}\n{a: null}\n";
parquet_read_out(out_str).stdout(eq(expected));
parquet_schema_out(out_str).stdout(contains("OPTIONAL BYTE_ARRAY a;"));
}
#[test]
fn interior_nul_in_varchar() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, "InteriorNul", &["VARCHAR(10)"]).unwrap();
conn.execute(
"INSERT INTO InteriorNul (a) VALUES (?);",
&"a\0b".into_parameter(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = "SELECT a FROM InteriorNul;";
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
query,
])
.assert()
.success();
let expected = "{a: \"a\0b\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
#[cfg(not(target_os = "windows"))] fn nchar_not_truncated() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "NCharNotTruncated";
setup_empty_table_mssql(&conn, table_name, &["NCHAR(1)"]).unwrap();
conn.execute(
&format!("INSERT INTO {} (a) VALUES (?);", table_name),
&"Ü".into_parameter(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = &format!("SELECT a FROM {};", table_name);
cargo_bin_cmd!()
.args(&[
"-vvvv",
"query",
"--encoding",
"system",
"--connection-string",
MSSQL,
out_str,
query,
])
.assert()
.success();
let expected = "{a: \"Ü\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
#[cfg(not(target_os = "windows"))] fn system_encoding() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "SystemEncoding";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(10)"]).unwrap();
conn.execute(
&format!("INSERT INTO {} (a) VALUES (?);", table_name),
&"Ü".into_parameter(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = &format!("SELECT a FROM {};", table_name);
cargo_bin_cmd!()
.args(&[
"-vvvv",
"query",
"--encoding",
"system",
"--connection-string",
MSSQL,
out_str,
query,
])
.assert()
.success();
let expected = "{a: \"Ü\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn utf_16_encoding() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "Utf16Encoding";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(10)"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) VALUES (?);"),
&"Ü".into_parameter(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = &format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--encoding",
"utf16",
"--connection-string",
MSSQL,
out_str,
query,
])
.assert()
.success();
let expected = "{a: \"Ü\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn issue_862() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "Issue862";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(50)"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) VALUES (?),(?);"),
(
&"Colt Telecom España S.A.".into_parameter(),
&"123456".into_parameter(),
),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = &format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--encoding",
"utf16",
"--connection-string",
MSSQL,
out_str,
query,
])
.assert()
.success();
let expected = "{a: \"Colt Telecom España S.A.\"}\n{a: \"123456\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
fn auto_encoding() {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
let table_name = "AutoEncoding";
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(1)"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) VALUES (?);"),
&"Ü".into_parameter(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = &format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--encoding",
"auto",
"--connection-string",
MSSQL,
out_str,
query,
])
.assert()
.success();
let expected = "{a: \"Ü\"}\n";
parquet_read_out(out_str).stdout(eq(expected));
}
#[test]
pub fn insert_32_bit_integer() {
let table_name = "Insert32BitInteger";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let message_type = "
message schema {
REQUIRED INT32 a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(42i32), Some(5), Some(1)]);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("42\n5\n1", actual);
}
#[test]
pub fn insert_optional_32_bit_integer() {
let table_name = "InsertOptional32BitInteger";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(42i32), None, Some(1)]);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("42\nNULL\n1", actual);
}
#[test]
pub fn insert_64_bit_integer() {
let table_name = "Insert64BitInteger";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let message_type = "
message schema {
REQUIRED INT64 a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(-42i64), Some(5), Some(1)]);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("-42\n5\n1", actual);
}
#[test]
pub fn insert_file_with_last_row_group_of_less_size() {
let table_name = "InsertFileWithLastRowGroupOfLessSize";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
let tmp_dir = tempdir().unwrap();
let input_path = tmp_dir.path().join("input.par");
let message_type = "
message schema {
REQUIRED INT64 a;
}
";
let schema = Arc::new(parse_message_type(message_type).unwrap());
let props = Arc::new(WriterProperties::builder().build());
let file = File::create(&input_path).unwrap();
let mut writer = SerializedFileWriter::new(file, schema, props).unwrap();
let mut row_group_writer = writer.next_row_group().unwrap();
let mut col_writer = row_group_writer.next_column().unwrap().unwrap();
i64::write_batch(col_writer.untyped(), &[1, 2], None);
col_writer.close().unwrap();
row_group_writer.close().unwrap();
let mut row_group_writer = writer.next_row_group().unwrap();
let mut col_writer = row_group_writer.next_column().unwrap().unwrap();
i64::write_batch(col_writer.untyped(), &[1], None);
col_writer.close().unwrap();
row_group_writer.close().unwrap();
writer.close().unwrap();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1\n2\n1", actual);
}
#[test]
pub fn insert_optional_64_bit_integer() {
let table_name = "InsertOptional64BitInteger";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BIGINT"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT64 a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(-42i64), None, Some(1)]);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("-42\nNULL\n1", actual);
}
#[test]
pub fn insert_utf8() {
let table_name = "InsertUtf8";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(50)"]).unwrap();
let message_type = "
message schema {
REQUIRED BYTE_ARRAY a (UTF8);
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(text),
Some("Hallo, Welt!".into()),
Some("Bonjour, Monde!".into()),
],
);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("Hello, World!\nHallo, Welt!\nBonjour, Monde!", actual);
}
#[test]
pub fn insert_optional_utf8() {
let table_name = "InsertOptionalUtf8";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(50)"]).unwrap();
let message_type = "
message schema {
OPTIONAL BYTE_ARRAY a (UTF8);
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[Some(text), None, Some("Hallo, Welt!".into())],
);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("Hello, World!\nNULL\nHallo, Welt!", actual);
}
#[test]
pub fn insert_utf16() {
let table_name = "InsertUtf16";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(50)"]).unwrap();
let message_type = "
message schema {
REQUIRED BYTE_ARRAY a (UTF8);
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(text),
Some("Hallo, Welt!".into()),
Some("Bonjour, Monde!".into()),
],
);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
"--encoding",
"Utf16",
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("Hello, World!\nHallo, Welt!\nBonjour, Monde!", actual);
}
#[test]
pub fn insert_optional_utf16() {
let table_name = "InsertOptionalUtf16";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARCHAR(50)"]).unwrap();
let message_type = "
message schema {
OPTIONAL BYTE_ARRAY a (UTF8);
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[Some(text), None, Some("Hallo, Welt!".into())],
);
let input_path = input.path_as_str();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
"--encoding",
"Utf16",
input_path,
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("Hello, World!\nNULL\nHallo, Welt!", actual);
}
#[test]
pub fn insert_bool() {
let table_name = "InsertBool";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BIT"]).unwrap();
let message_type = "
message schema {
REQUIRED BOOLEAN a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(true), Some(false), Some(false)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1\n0\n0", actual);
}
#[test]
pub fn insert_optional_bool() {
let table_name = "InsertOptionalBool";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BIT"]).unwrap();
let message_type = "
message schema {
OPTIONAL BOOLEAN a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(true), None, Some(false)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1\nNULL\n0", actual);
}
#[test]
pub fn insert_f32() {
let table_name = "InsertF32";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["FLOAT"]).unwrap();
let message_type = "
message schema {
REQUIRED FLOAT a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(1.2f32), Some(3.4), Some(5.6)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"1.2000000476837158\n3.4000000953674316\n5.5999999046325684",
actual
);
}
#[test]
pub fn insert_optional_f32() {
let table_name = "InsertOptionalF32";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["FLOAT"]).unwrap();
let message_type = "
message schema {
OPTIONAL FLOAT a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(1.2f32), None, Some(3.4)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1.2000000476837158\nNULL\n3.4000000953674316", actual);
}
#[test]
pub fn insert_f64() {
let table_name = "InsertF64";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["FLOAT(53)"]).unwrap();
let message_type = "
message schema {
REQUIRED DOUBLE a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(1.2f64), Some(3.4), Some(5.6)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1.2\n3.3999999999999999\n5.5999999999999996", actual);
}
#[test]
pub fn insert_optional_f64() {
let table_name = "InsertOptionalF64";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["FLOAT(53)"]).unwrap();
let message_type = "
message schema {
OPTIONAL DOUBLE a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(1.2f64), None, Some(3.4)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1.2\nNULL\n3.3999999999999999", actual);
}
#[test]
pub fn insert_date() {
let table_name = "InsertDate";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATE"]).unwrap();
let message_type = "
message schema {
REQUIRED INT32 a (DATE);
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i32), Some(365), Some(18695)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1970-01-01\n1971-01-01\n2021-03-09", actual);
}
#[test]
pub fn insert_optional_date() {
let table_name = "InsertOptionalDate";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["Date"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a (DATE);
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i32), None, Some(18695)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1970-01-01\nNULL\n2021-03-09", actual);
}
#[test]
pub fn insert_time_ms() {
let table_name = "InsertTimeMs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME(3)"]).unwrap();
let message_type = "
message schema {
REQUIRED INT32 a (TIME_MILLIS);
}
";
let input = TmpParquetFile::with_1_dim(
message_type,
&[Some(0i32), Some(3_600_000), Some(82_800_000)],
);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("00:00:00.000\n01:00:00.000\n23:00:00.000", actual);
}
#[test]
pub fn insert_optional_time_ms() {
let table_name = "InsertOptionalTimeMs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME(3)"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a (TIME_MILLIS);
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i32), None, Some(82_800_000)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("00:00:00.000\nNULL\n23:00:00.000", actual);
}
#[test]
pub fn insert_time_us() {
let table_name = "InsertTimeUs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME(6)"]).unwrap();
let message_type = "
message schema {
REQUIRED INT64 a (TIME_MICROS);
}
";
let input = TmpParquetFile::with_1_dim(
message_type,
&[Some(0i64), Some(3_600_000_000), Some(82_800_000_000)],
);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("00:00:00.000000\n01:00:00.000000\n23:00:00.000000", actual);
}
#[test]
pub fn insert_optional_time_us() {
let table_name = "InsertOptionalTimeUs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["TIME(6)"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT64 a (TIME_MICROS);
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i64), None, Some(82_800_000_000)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("00:00:00.000000\nNULL\n23:00:00.000000", actual);
}
#[test]
pub fn insert_decimal_from_i32() {
let table_name = "InsertDecimalFromI32";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
REQUIRED INT32 a (DECIMAL(9,2));
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i32), Some(123456789), Some(-42)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\n1234567.89\n-.42", actual);
}
#[test]
pub fn insert_decimal_from_i32_optional() {
let table_name = "InsertDecimalFromI32Optional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a (DECIMAL(9,2));
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i32), None, Some(-42)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\nNULL\n-.42", actual);
}
#[test]
pub fn insert_decimal_from_i64() {
let table_name = "InsertDecimalFromI64";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
REQUIRED INT64 a (DECIMAL(9,2));
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i64), Some(123456789), Some(-42)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\n1234567.89\n-.42", actual);
}
#[test]
pub fn insert_decimal_from_i64_optional() {
let table_name = "InsertDecimalFromI64Optional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT64 a (DECIMAL(9,2));
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i64), None, Some(-42)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\nNULL\n-.42", actual);
}
#[test]
pub fn insert_timestamp_ms() {
let table_name = "InsertTimestampMs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIME2"]).unwrap();
let message_type = "
message schema {
REQUIRED INT64 a (TIMESTAMP_MILLIS);
}
";
let input =
TmpParquetFile::with_1_dim(message_type, &[Some(0i64), Some(1), Some(1616367053000)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"1970-01-01 00:00:00.0000000\n1970-01-01 00:00:00.0010000\n2021-03-21 22:50:53.0000000",
actual
);
}
#[test]
pub fn insert_timestamp_ms_optional() {
let table_name = "InsertTimestampMsOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIME2"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT64 a (TIMESTAMP_MILLIS);
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(0i64), None, Some(1616367053000)]);
let input_path = input.path.clone();
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input_path.to_str().unwrap(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"1970-01-01 00:00:00.0000000\nNULL\n2021-03-21 22:50:53.0000000",
actual
);
}
#[test]
pub fn insert_timestamp_us() {
let table_name = "InsertTimestampUs";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIME2"]).unwrap();
let message_type = "
message schema {
REQUIRED INT64 a (TIMESTAMP_MICROS);
}
";
let input =
TmpParquetFile::with_1_dim(message_type, &[Some(0i64), Some(1), Some(1616367053000000)]);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"1970-01-01 00:00:00.0000000\n1970-01-01 00:00:00.0000010\n2021-03-21 22:50:53.0000000",
actual
);
}
#[test]
pub fn insert_timestamp_us_optional() {
let table_name = "InsertTimestampUsOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DATETIME2"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT64 a (TIMESTAMP_MICROS);
}
";
let input =
TmpParquetFile::with_1_dim(message_type, &[Some(0i64), None, Some(1616367053000000)]);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"1970-01-01 00:00:00.0000000\nNULL\n2021-03-21 22:50:53.0000000",
actual
);
}
#[test]
pub fn insert_binary() {
let table_name = "InsertBinary";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARBINARY(50)"]).unwrap();
let message_type = "
message schema {
REQUIRED BYTE_ARRAY a;
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(text),
Some("Hallo, Welt!".into()),
Some("Bonjour, Monde!".into()),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"48656C6C6F2C20576F726C6421\n48616C6C6F2C2057656C7421\n426F6E6A6F75722C204D6F6E646521",
actual
);
}
#[test]
pub fn insert_binary_optional() {
let table_name = "InsertBinaryOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["VARBINARY(50)"]).unwrap();
let message_type = "
message schema {
OPTIONAL BYTE_ARRAY a;
}
";
let text: ByteArray = "Hello, World!".into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[Some(text), None, Some("Hallo, Welt!".into())],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"48656C6C6F2C20576F726C6421\nNULL\n48616C6C6F2C2057656C7421",
actual
);
}
#[test]
pub fn insert_fixed_len_binary() {
let table_name = "InsertFixedLenBinary";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BINARY(13)"]).unwrap();
let message_type = "
message schema {
REQUIRED FIXED_LEN_BYTE_ARRAY(13) a;
}
";
let to_fixed_len_byte_array = |input: &str| -> FixedLenByteArray {
let ba: ByteArray = input.into();
ba.into()
};
let text: FixedLenByteArray = to_fixed_len_byte_array("Hello, World!");
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(text),
Some(to_fixed_len_byte_array("Hallo, Welt!!")),
Some(to_fixed_len_byte_array("0123456789012")),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"48656C6C6F2C20576F726C6421\n48616C6C6F2C2057656C742121\n30313233343536373839303132",
actual
);
}
#[test]
pub fn insert_fixed_len_binary_optional() {
let table_name = "InsertFixedLenBinaryOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BINARY(13)"]).unwrap();
let message_type = "
message schema {
OPTIONAL FIXED_LEN_BYTE_ARRAY(13) a;
}
";
let to_fixed_len_byte_array = |input: &str| -> FixedLenByteArray {
let ba: ByteArray = input.into();
ba.into()
};
let text: FixedLenByteArray = to_fixed_len_byte_array("Hello, World!");
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(text),
None,
Some(to_fixed_len_byte_array("0123456789012")),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(
"48656C6C6F2C20576F726C6421\nNULL\n30313233343536373839303132",
actual
);
}
#[test]
pub fn insert_decimal_from_binary() {
let table_name = "InsertDecimalFromBinary";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
REQUIRED BYTE_ARRAY a (DECIMAL(9,2));
}
";
let zero: ByteArray = vec![0u8].into();
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(zero),
Some(vec![1].into()),
Some(vec![1, 0, 0].into()),
Some(vec![255, 255, 255].into()),
Some(vec![255].into()),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\n.01\n655.36\n-.01\n-.01", actual);
}
#[test]
pub fn insert_decimal_from_binary_optional() {
let table_name = "InsertDecimalFromBinaryOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(9,2)"]).unwrap();
let message_type = "
message schema {
OPTIONAL BYTE_ARRAY a (DECIMAL(9,2));
}
";
let zero: ByteArray = vec![0u8].into();
let input = TmpParquetFile::with_1_dim(message_type, &[Some(zero), None, Some(vec![1].into())]);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\nNULL\n.01", actual);
}
#[test]
pub fn insert_decimal_from_fixed_binary() {
let table_name = "InsertDecimalFromFixedBinary";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(5,2)"]).unwrap();
let message_type = "
message schema {
REQUIRED FIXED_LEN_BYTE_ARRAY(3) a (DECIMAL(5,2));
}
";
let to_fixed_len_byte_array = |input: Vec<u8>| -> FixedLenByteArray {
let ba: ByteArray = input.into();
ba.into()
};
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(to_fixed_len_byte_array(vec![0, 0, 0])),
Some(to_fixed_len_byte_array(vec![0, 0, 1])),
Some(to_fixed_len_byte_array(vec![1, 0, 0])),
Some(to_fixed_len_byte_array(vec![255, 255, 255])),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".00\n.01\n655.36\n-.01", actual);
}
#[test]
pub fn insert_decimal_from_fixed_binary_optional() {
let table_name = "InsertDecimalFromFixedBinaryOptional";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["DECIMAL(5,2)"]).unwrap();
let message_type = "
message schema {
OPTIONAL FIXED_LEN_BYTE_ARRAY(3) a (DECIMAL(5,2));
}
";
let to_fixed_len_byte_array = |input: Vec<u8>| -> FixedLenByteArray {
let ba: ByteArray = input.into();
ba.into()
};
let input = TmpParquetFile::with_1_dim(
message_type,
&[
Some(to_fixed_len_byte_array(vec![0, 0, 1])),
None,
Some(to_fixed_len_byte_array(vec![255, 255, 255])),
],
);
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
input.path_as_str(),
table_name,
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!(".01\nNULL\n-.01", actual);
}
#[test]
fn basic_use_exec_for_insert() {
let table_name = "InsertUsingExec";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER", "INTEGER"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a;
}
";
let input = TmpParquetFile::with_1_dim(message_type, &[Some(1i32), None, Some(2)]);
cargo_bin_cmd!()
.args([
"-vvvv",
"exec",
"--connection-string",
MSSQL,
input.path_as_str(),
&format!("INSERT INTO {table_name} (a) VALUES (?a?)"),
])
.assert()
.success();
let query = format!("SELECT a FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1\nNULL\n2", actual);
}
#[test]
fn exec_with_switched_order() {
let table_name = "ExecWithSwitchedOrder";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER", "INTEGER"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a;
OPTIONAL INT32 b;
}
";
let input = TmpParquetFile::with_2_dim(message_type, &[Some(1i32)], &[Some(2i32)]);
cargo_bin_cmd!()
.args([
"-vvvv",
"exec",
"--connection-string",
MSSQL,
input.path_as_str(),
&format!("INSERT INTO {table_name} (b, a) VALUES (?b?, ?a?)"),
])
.assert()
.success();
let query = format!("SELECT a, b FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1,2", actual);
}
#[test]
fn exec_with_multiple_placeholder_mentions() {
let table_name = "ExecWithMultiplePlaceholderMentions";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER", "VARCHAR(50)", "INTEGER"]).unwrap();
let message_type = "
message schema {
OPTIONAL INT32 a;
OPTIONAL BYTE_ARRAY b (UTF8);
}
";
let text: ByteArray = "Hello".into();
let input = TmpParquetFile::with_2_dim(message_type, &[Some(1i32)], &[Some(text)]);
cargo_bin_cmd!()
.args([
"-vvvv",
"exec",
"--connection-string",
MSSQL,
input.path_as_str(),
&format!("INSERT INTO {table_name} (a, b, c) VALUES (?a?, ?b?, ?a?)"),
])
.assert()
.success();
let query = format!("SELECT a, b, c FROM {table_name} ORDER BY Id");
let cursor = conn.execute(&query, (), None).unwrap().unwrap();
let actual = cursor_to_string(cursor);
assert_eq!("1,Hello,1", actual);
}
#[test]
pub fn write_query_result_to_stdout() {
let table_name = "WriteQueryResultToStdout";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["INTEGER"]).unwrap();
conn.execute(
&format!("INSERT INTO {table_name} (a) VALUES (?)"),
[42i32, 5, 64].as_slice(),
None,
)
.unwrap();
let query = format!("SELECT a FROM {table_name} ORDER BY id");
let command = cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
"-", &query,
])
.assert()
.success();
let output = &command.get_output().stdout;
assert!(!output.is_empty());
let mut output_file = NamedTempFile::new().unwrap();
output_file.write_all(output).unwrap();
let expected = "{a: 42}\n";
let output_path = output_file.path().to_str().unwrap();
parquet_read_out(output_path).stdout(eq(expected));
}
#[test]
pub fn reject_writing_to_stdout_and_file_size_limit() {
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
"FakeConnectionString",
"--file-size-threshold",
"1GiB",
"-", "SELECT a FROM FakeTableName ORDER BY id",
])
.assert()
.failure()
.stderr(contains(
"file-size-threshold conflicts with specifying stdout ('-') as output.",
));
}
#[test]
fn write_statistics_for_text_columns() {
let table_name = "WriteStatisticsForTextColumns";
let mut table = TableMssql::new(table_name, &["VARCHAR(10)"]);
table.insert_rows_as_text(&[["aaa"], ["zzz"]]);
let query = format!("SELECT a FROM {table_name}");
let command = cargo_bin_cmd!()
.args([
"query",
"--connection-string",
MSSQL,
"-", &query,
])
.assert()
.success();
let bytes = Bytes::from(command.get_output().stdout.clone());
let reader = SerializedFileReader::new(bytes).unwrap();
let stats = reader
.metadata()
.row_group(0)
.column(0)
.statistics()
.unwrap();
assert_eq!(
"aaa",
str::from_utf8(stats.min_bytes_opt().unwrap()).unwrap()
);
assert_eq!(
"zzz",
str::from_utf8(stats.max_bytes_opt().unwrap()).unwrap()
);
}
#[test]
#[ignore = "Takes too long to run"]
fn query_4097_bits() {
let num_bits = 4097;
let table_name = "Query4097Bits";
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
setup_empty_table_mssql(&conn, table_name, &["BIT"]).unwrap();
let insert = format!(
"INSERT INTO {table_name}
(a)
VALUES
(?);"
);
let desc = BindParamDesc::bit(false);
let mut parameter_buffer = conn
.prepare(&insert)
.unwrap()
.into_column_inserter(num_bits, [desc])
.unwrap();
parameter_buffer.set_num_rows(num_bits);
parameter_buffer.execute().unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let query = format!("SELECT a FROM {table_name};");
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
out_str,
"--connection-string",
MSSQL,
&query,
])
.assert()
.success();
}
struct TmpParquetFile {
_tmp_dir: TempDir,
path: PathBuf,
}
impl TmpParquetFile {
pub fn with_1_dim<T>(message_type: &str, values: &[Option<T>]) -> Self
where
T: WriteToCw + Clone,
{
let values = ColumnDataImpl::new(values);
Self::new(message_type, &[&values])
}
pub fn with_2_dim<T, U>(
message_type: &str,
values_a: &[Option<T>],
values_b: &[Option<U>],
) -> Self
where
T: WriteToCw + Clone,
U: WriteToCw + Clone,
{
let column_a = ColumnDataImpl::new(values_a);
let column_b = ColumnDataImpl::new(values_b);
Self::new(message_type, &[&column_a, &column_b])
}
pub fn new(message_type: &str, values: &[&dyn ColumnData]) -> Self {
let tmp_dir = tempdir().expect("Must be able to create temporary directory");
let path = tmp_dir.path().join("input.par");
write_values_to_file(message_type, &path, values);
TmpParquetFile {
_tmp_dir: tmp_dir,
path,
}
}
pub fn path_as_str(&self) -> &str {
self.path
.to_str()
.expect("Temporary file path must be utf8")
}
}
fn write_values_to_file(message_type: &str, input_path: &Path, columns: &[&dyn ColumnData]) {
let schema = Arc::new(parse_message_type(message_type).unwrap());
let props = Arc::new(WriterProperties::builder().build());
let file = File::create(input_path).unwrap();
let mut writer = SerializedFileWriter::new(file, schema, props).unwrap();
let mut row_group_writer = writer.next_row_group().unwrap();
for col in columns {
let mut col_writer = row_group_writer.next_column().unwrap().unwrap();
col.write(&mut col_writer.untyped());
col_writer.close().unwrap();
}
row_group_writer.close().unwrap();
writer.close().unwrap();
}
const COLUMN_NAMES: &[&str] = &["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"];
pub trait ColumnData {
fn write(&self, column_writer: &mut ColumnWriter);
}
struct ColumnDataImpl<T> {
values: Vec<T>,
def_levels: Vec<i16>,
}
impl<T> ColumnDataImpl<T> {
pub fn new(input: &[Option<T>]) -> Self
where
T: Clone,
{
let def_levels = input
.iter()
.map(|opt| if opt.is_some() { 1i16 } else { 0 })
.collect::<Vec<_>>();
let values = input
.into_iter()
.cloned()
.filter_map(|opt| opt)
.collect::<Vec<_>>();
ColumnDataImpl { values, def_levels }
}
}
impl<T> ColumnData for ColumnDataImpl<T>
where
T: WriteToCw,
{
fn write(&self, column_writer: &mut ColumnWriter) {
T::write_batch(column_writer, &self.values, Some(&self.def_levels));
}
}
pub struct TableMssql<'a, const NUM_COLUMNS: usize> {
pub name: &'a str,
pub conn: Connection<'a>,
}
impl<'a, const NUM_COLUMNS: usize> TableMssql<'a, NUM_COLUMNS> {
pub fn new(name: &'a str, column_types: &'a [&'a str; NUM_COLUMNS]) -> Self {
let conn = env()
.connect_with_connection_string(
MSSQL,
ConnectionOptions {
login_timeout_sec: Some(5),
..Default::default()
},
)
.expect("Must be able to connect to MSSQL database.");
setup_empty_table_mssql(&conn, name, column_types)
.expect("Must be able to setup empty table.");
TableMssql { name, conn }
}
pub fn insert_rows_as_text<'b, C>(&mut self, content: &[[C; NUM_COLUMNS]])
where
C: Into<Option<&'b str>> + Copy,
{
let statement = self.insert_statement(NUM_COLUMNS);
let capacity = content.len();
let max_str_len = (0..NUM_COLUMNS)
.map(|col_index| {
(0..content.len())
.map(|row_index| {
let opt: Option<&str> = content[row_index][col_index].into();
opt.map(|s| s.len()).unwrap_or(0)
})
.max()
.unwrap_or(0)
})
.collect::<Vec<_>>();
let mut inserter = self
.conn
.prepare(&statement)
.unwrap()
.into_text_inserter(capacity, max_str_len)
.unwrap();
for (row_index, row) in content.iter().enumerate() {
for (column_index, element) in row.iter().enumerate() {
let element: Option<&str> = (*element).into();
let element = element.map(|s| s.as_bytes());
inserter
.column_mut(column_index)
.set_cell(row_index, element);
}
}
inserter.set_num_rows(content.len());
inserter.execute().unwrap();
}
fn insert_statement(&self, number_of_columns: usize) -> String {
let columns = COLUMN_NAMES[..number_of_columns].join(",");
let placeholders = vec!["?"; number_of_columns].join(",");
format!(
"INSERT INTO {} ({}) VALUES ({})",
self.name, columns, placeholders
)
}
}
pub fn setup_empty_table_mssql(
conn: &Connection,
table_name: &str,
column_types: &[&str],
) -> Result<(), odbc_api::Error> {
let identity = "int IDENTITY(1,1)";
setup_empty_table(table_name, column_types, conn, identity)
}
fn setup_empty_table(
table_name: &str,
column_types: &[&str],
conn: &Connection,
identity: &str,
) -> Result<(), odbc_api::Error> {
let drop_table = &format!("DROP TABLE IF EXISTS {table_name}");
let cols = column_types
.iter()
.zip(COLUMN_NAMES)
.map(|(ty, name)| format!("{name} {ty}"))
.collect::<Vec<_>>()
.join(", ");
let create_table = format!("CREATE TABLE {table_name} (id {identity},{cols});");
conn.execute(drop_table, (), None)?;
conn.execute(&create_table, (), None)?;
Ok(())
}
pub fn setup_empty_table_pg(
conn: &Connection,
table_name: &str,
column_types: &[&str],
) -> Result<(), odbc_api::Error> {
let identity = "SERIAL PRIMARY KEY";
setup_empty_table(table_name, column_types, conn, identity)
}
fn roundtrip(file: &'static str, table_name: &str) -> Assert {
let conn = env()
.connect_with_connection_string(MSSQL, ConnectionOptions::default())
.unwrap();
conn.execute(&format!("DROP TABLE IF EXISTS {table_name}"), (), None)
.unwrap();
conn.execute(
&format!("CREATE TABLE {table_name} (country VARCHAR(255), population BIGINT);"),
(),
None,
)
.unwrap();
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().join("out.par");
let out_str = out_path.to_str().expect("Temporary file path must be utf8");
let in_path = format!("tests/{file}");
cargo_bin_cmd!()
.args([
"-vvvv",
"insert",
"--connection-string",
MSSQL,
&in_path,
table_name,
])
.assert()
.success();
cargo_bin_cmd!()
.args([
"-vvvv",
"query",
"--connection-string",
MSSQL,
out_str,
&format!("SELECT country, population FROM {table_name} ORDER BY population;"),
])
.assert()
.success();
let expectation = String::from_utf8(
std::process::Command::new("parquet-read")
.args(&[&in_path][..])
.output()
.unwrap()
.stdout,
)
.unwrap();
parquet_read_out(out_str).stdout(expectation)
}
fn cursor_to_string(mut cursor: impl Cursor) -> String {
let batch_size = 20;
let mut buffer = TextRowSet::for_cursor(batch_size, &mut cursor, None).unwrap();
let mut row_set_cursor = cursor.bind_buffer(&mut buffer).unwrap();
let mut text = String::new();
while let Some(row_set) = row_set_cursor.fetch().unwrap() {
for row_index in 0..row_set.num_rows() {
if row_index != 0 {
text.push('\n');
}
for col_index in 0..row_set.num_cols() {
if col_index != 0 {
text.push(',');
}
text.push_str(
row_set
.at_as_str(col_index, row_index)
.unwrap()
.unwrap_or("NULL"),
);
}
}
}
text
}
trait WriteToCw: Sized {
fn write_batch(col_writer: &mut ColumnWriter, values: &[Self], def_levels: Option<&[i16]>);
}
macro_rules! impl_write_to_cw {
($type:ty , $variant:ident) => {
impl WriteToCw for $type {
fn write_batch(
col_writer: &mut ColumnWriter,
values: &[Self],
def_levels: Option<&[i16]>,
) {
match col_writer {
ColumnWriter::$variant(ref mut cw) => {
cw.write_batch(values, def_levels, None).unwrap();
}
_ => panic!("Unexpected Column Writer type"),
}
}
}
};
}
impl_write_to_cw!(bool, BoolColumnWriter);
impl_write_to_cw!(i32, Int32ColumnWriter);
impl_write_to_cw!(i64, Int64ColumnWriter);
impl_write_to_cw!(f32, FloatColumnWriter);
impl_write_to_cw!(f64, DoubleColumnWriter);
impl_write_to_cw!(ByteArray, ByteArrayColumnWriter);
impl_write_to_cw!(FixedLenByteArray, FixedLenByteArrayColumnWriter);