Various improvements:
-Clippy suggestions (all levels enabled) - removing bad code, having const fns, adding must_use, etc -Upgraded all packages to their latest versions, including azure -Removing all unwrap()s, now they are replaced with something better, or except() for more info. -Replaced lazy_static with once_cell
This commit is contained in:
Родитель
0f81cc0cc4
Коммит
9e45fff548
|
@ -12,25 +12,24 @@ keywords = ["sdk", "azure", "kusto", "azure-data-explorer"]
|
||||||
categories = ["api-bindings"]
|
categories = ["api-bindings"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arrow = { version = "13", optional = true }
|
arrow = { version = "15.0.0", optional = true }
|
||||||
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust", rev = "66db4b485ce56b68be148708d9c810960a50be51", features = [
|
azure_core = { git = "https://github.com/Azure/azure-sdk-for-rust", rev = "8586a66b20fba463c39156f0390e583ec305ab2d", features = [
|
||||||
"enable_reqwest",
|
"enable_reqwest",
|
||||||
"enable_reqwest_gzip",
|
"enable_reqwest_gzip",
|
||||||
] }
|
] }
|
||||||
azure_identity = { git = "https://github.com/Azure/azure-sdk-for-rust", rev = "66db4b485ce56b68be148708d9c810960a50be51" }
|
azure_identity = { git = "https://github.com/Azure/azure-sdk-for-rust", rev = "8586a66b20fba463c39156f0390e583ec305ab2d" }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1.56"
|
||||||
async-convert = "1"
|
async-convert = "1.0.0"
|
||||||
bytes = "1"
|
bytes = "1.1.0"
|
||||||
futures = "0.3"
|
futures = "0.3.21"
|
||||||
http = "0.2"
|
http = "0.2.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1.0.81"
|
||||||
serde_with = { version = "1.12.0", features = ["json"] }
|
serde_with = { version = "1.12.1", features = ["json"] }
|
||||||
thiserror = "1"
|
thiserror = "1.0.31"
|
||||||
lazy_static = "1.4.0"
|
hashbrown = "0.12.1"
|
||||||
hashbrown = "0.12.0"
|
regex = "1.5.6"
|
||||||
regex = "1.5.5"
|
time = { version = "0.3.11", features = [
|
||||||
time = { version = "0.3.9", features = [
|
|
||||||
"serde",
|
"serde",
|
||||||
"parsing",
|
"parsing",
|
||||||
"formatting",
|
"formatting",
|
||||||
|
@ -38,14 +37,15 @@ time = { version = "0.3.9", features = [
|
||||||
"serde-well-known",
|
"serde-well-known",
|
||||||
] }
|
] }
|
||||||
derive_builder = "0.11.2"
|
derive_builder = "0.11.2"
|
||||||
|
once_cell = "1.12.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
arrow = { version = "13", features = ["prettyprint"] }
|
arrow = { version = "15.0.0", features = ["prettyprint"] }
|
||||||
dotenv = "*"
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
tokio = { version = "1", features = ["macros"] }
|
tokio = { version = "1.19.2", features = ["macros"] }
|
||||||
chrono = "*"
|
chrono = "0.4.19"
|
||||||
oauth2 = "*"
|
oauth2 = "4.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["arrow"]
|
default = ["arrow"]
|
||||||
|
|
|
@ -29,13 +29,13 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
&client_secret,
|
&client_secret,
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = KustoClient::try_from(kcsb).unwrap();
|
let client = KustoClient::try_from(kcsb).expect("Failed to create Kusto client");
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.execute_command(database, query)
|
.execute_command(database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to execute query");
|
||||||
|
|
||||||
println!("command response: {:?}", response);
|
println!("command response: {:?}", response);
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,13 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
&client_secret,
|
&client_secret,
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = KustoClient::try_from(kcsb).unwrap();
|
let client = KustoClient::try_from(kcsb).expect("Failed to create Kusto client");
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.execute_query(database, query)
|
.execute_query(database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to execute query");
|
||||||
|
|
||||||
for table in &response.tables {
|
for table in &response.tables {
|
||||||
match table {
|
match table {
|
||||||
|
|
|
@ -16,12 +16,12 @@ use azure_core::error::{ErrorKind, ResultExt};
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::ColumnType;
|
use crate::models::ColumnType;
|
||||||
use crate::models::*;
|
use crate::models::{Column, DataTable};
|
||||||
use crate::types::{KustoDateTime, KustoDuration};
|
use crate::types::{KustoDateTime, KustoDuration};
|
||||||
|
|
||||||
fn convert_array_string(values: Vec<serde_json::Value>) -> Result<ArrayRef> {
|
fn convert_array_string(values: Vec<serde_json::Value>) -> Result<ArrayRef> {
|
||||||
let strings: Vec<Option<String>> = serde_json::from_value(serde_json::Value::Array(values))?;
|
let strings: Vec<Option<String>> = serde_json::from_value(serde_json::Value::Array(values))?;
|
||||||
let strings: Vec<Option<&str>> = strings.iter().map(|opt| opt.as_deref()).collect();
|
let strings: Vec<Option<&str>> = strings.iter().map(Option::as_deref).collect();
|
||||||
Ok(Arc::new(StringArray::from(strings)))
|
Ok(Arc::new(StringArray::from(strings)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,42 +85,23 @@ fn convert_array_i64(values: Vec<serde_json::Value>) -> Result<ArrayRef> {
|
||||||
Ok(Arc::new(Int64Array::from(ints)))
|
Ok(Arc::new(Int64Array::from(ints)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_column(data: Vec<serde_json::Value>, column: Column) -> Result<(Field, ArrayRef)> {
|
pub fn convert_column(data: Vec<serde_json::Value>, column: &Column) -> Result<(Field, ArrayRef)> {
|
||||||
|
let column_name = &column.column_name;
|
||||||
match column.column_type {
|
match column.column_type {
|
||||||
ColumnType::String => convert_array_string(data).map(|data| {
|
ColumnType::String => convert_array_string(data)
|
||||||
(
|
.map(|data| (Field::new(column_name, DataType::Utf8, true), data)),
|
||||||
Field::new(column.column_name.as_str(), DataType::Utf8, true),
|
ColumnType::Bool | ColumnType::Boolean => convert_array_bool(data)
|
||||||
data,
|
.map(|data| (Field::new(column_name, DataType::Boolean, true), data)),
|
||||||
)
|
ColumnType::Int => convert_array_i32(data)
|
||||||
}),
|
.map(|data| (Field::new(column_name, DataType::Int32, true), data)),
|
||||||
ColumnType::Bool | ColumnType::Boolean => convert_array_bool(data).map(|data| {
|
ColumnType::Long => convert_array_i64(data)
|
||||||
(
|
.map(|data| (Field::new(column_name, DataType::Int64, true), data)),
|
||||||
Field::new(column.column_name.as_str(), DataType::Boolean, true),
|
ColumnType::Real => convert_array_float(data)
|
||||||
data,
|
.map(|data| (Field::new(column_name, DataType::Float64, true), data)),
|
||||||
)
|
|
||||||
}),
|
|
||||||
ColumnType::Int => convert_array_i32(data).map(|data| {
|
|
||||||
(
|
|
||||||
Field::new(column.column_name.as_str(), DataType::Int32, true),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
ColumnType::Long => convert_array_i64(data).map(|data| {
|
|
||||||
(
|
|
||||||
Field::new(column.column_name.as_str(), DataType::Int64, true),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
ColumnType::Real => convert_array_float(data).map(|data| {
|
|
||||||
(
|
|
||||||
Field::new(column.column_name.as_str(), DataType::Float64, true),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
ColumnType::Datetime => convert_array_datetime(data).map(|data| {
|
ColumnType::Datetime => convert_array_datetime(data).map(|data| {
|
||||||
(
|
(
|
||||||
Field::new(
|
Field::new(
|
||||||
column.column_name.as_str(),
|
column_name,
|
||||||
DataType::Timestamp(TimeUnit::Nanosecond, None),
|
DataType::Timestamp(TimeUnit::Nanosecond, None),
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
|
@ -129,11 +110,7 @@ pub fn convert_column(data: Vec<serde_json::Value>, column: Column) -> Result<(F
|
||||||
}),
|
}),
|
||||||
ColumnType::Timespan => convert_array_timespan(data).map(|data| {
|
ColumnType::Timespan => convert_array_timespan(data).map(|data| {
|
||||||
(
|
(
|
||||||
Field::new(
|
Field::new(column_name, DataType::Duration(TimeUnit::Nanosecond), true),
|
||||||
column.column_name.as_str(),
|
|
||||||
DataType::Duration(TimeUnit::Nanosecond),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
@ -158,7 +135,7 @@ pub fn convert_table(table: DataTable) -> Result<RecordBatch> {
|
||||||
buffer
|
buffer
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(table.columns.into_iter())
|
.zip(table.columns.into_iter())
|
||||||
.map(|(data, column)| convert_column(data, column))
|
.map(|(data, column)| convert_column(data, &column))
|
||||||
.try_for_each::<_, Result<()>>(|result| {
|
.try_for_each::<_, Result<()>>(|result| {
|
||||||
let (field, data) = result?;
|
let (field, data) = result?;
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
|
@ -173,6 +150,7 @@ pub fn convert_table(table: DataTable) -> Result<RecordBatch> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::models::TableKind;
|
||||||
use crate::operations::query::{KustoResponseDataSetV2, ResultTable};
|
use crate::operations::query::{KustoResponseDataSetV2, ResultTable};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -188,7 +166,7 @@ mod tests {
|
||||||
column_name: "int_col".to_string(),
|
column_name: "int_col".to_string(),
|
||||||
column_type: ColumnType::Int,
|
column_type: ColumnType::Int,
|
||||||
};
|
};
|
||||||
assert_eq!(c, ref_col)
|
assert_eq!(c, ref_col);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -218,7 +196,7 @@ mod tests {
|
||||||
}],
|
}],
|
||||||
rows: vec![],
|
rows: vec![],
|
||||||
};
|
};
|
||||||
assert_eq!(t, ref_tbl)
|
assert_eq!(t, ref_tbl);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -226,15 +204,16 @@ mod tests {
|
||||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
path.push("tests/inputs/dataframe.json");
|
path.push("tests/inputs/dataframe.json");
|
||||||
|
|
||||||
let data = std::fs::read_to_string(path).unwrap();
|
let data = std::fs::read_to_string(path).expect("Failed to read file");
|
||||||
let tables: Vec<ResultTable> = serde_json::from_str(&data).unwrap();
|
let tables: Vec<ResultTable> =
|
||||||
|
serde_json::from_str(&data).expect("Failed to deserialize result table");
|
||||||
let response = KustoResponseDataSetV2 { tables };
|
let response = KustoResponseDataSetV2 { tables };
|
||||||
let record_batches = response
|
let record_batches = response
|
||||||
.into_record_batches()
|
.into_record_batches()
|
||||||
.collect::<std::result::Result<Vec<_>, _>>()
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
.unwrap();
|
.expect("Failed to convert to record batches");
|
||||||
|
|
||||||
assert!(record_batches[0].num_columns() > 0);
|
assert!(record_batches[0].num_columns() > 0);
|
||||||
assert!(record_batches[0].num_rows() > 0)
|
assert!(record_batches[0].num_rows() > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
use azure_core::headers::{HeaderValue, AUTHORIZATION};
|
||||||
use azure_core::{auth::TokenCredential, Context, Policy, PolicyResult, Request};
|
use azure_core::{auth::TokenCredential, Context, Policy, PolicyResult, Request};
|
||||||
use http::header::AUTHORIZATION;
|
|
||||||
use http::HeaderValue;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -44,11 +43,9 @@ impl Policy for AuthorizationPolicy {
|
||||||
);
|
);
|
||||||
|
|
||||||
let token = self.credential.get_token(&self.resource).await?;
|
let token = self.credential.get_token(&self.resource).await?;
|
||||||
let auth_header_value = format!("Bearer {}", token.token.secret().clone());
|
let auth_header_value = format!("Bearer {}", token.token.secret());
|
||||||
|
|
||||||
request
|
request.insert_header(AUTHORIZATION, HeaderValue::from(auth_header_value));
|
||||||
.headers_mut()
|
|
||||||
.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header_value)?);
|
|
||||||
|
|
||||||
next[0].send(ctx, request, &next[1..]).await
|
next[0].send(ctx, request, &next[1..]).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,17 @@ use crate::connection_string::{ConnectionString, ConnectionStringBuilder};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::operations::query::{QueryRunner, QueryRunnerBuilder, V1QueryRunner, V2QueryRunner};
|
use crate::operations::query::{QueryRunner, QueryRunnerBuilder, V1QueryRunner, V2QueryRunner};
|
||||||
use azure_core::auth::TokenCredential;
|
use azure_core::auth::TokenCredential;
|
||||||
use azure_core::prelude::*;
|
|
||||||
use azure_core::{ClientOptions, Context, Pipeline, Request};
|
use azure_core::{ClientOptions, Context, Pipeline};
|
||||||
use azure_identity::token_credentials::{
|
use azure_identity::{
|
||||||
AzureCliCredential, ClientSecretCredential, DefaultAzureCredential,
|
AzureCliCredential, ClientSecretCredential, DefaultAzureCredential,
|
||||||
ImdsManagedIdentityCredential, TokenCredentialOptions,
|
ImdsManagedIdentityCredential, TokenCredentialOptions,
|
||||||
};
|
};
|
||||||
use http::Uri;
|
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const API_VERSION: &str = "2019-02-13";
|
|
||||||
|
|
||||||
/// Options for specifying how a Kusto client will behave
|
/// Options for specifying how a Kusto client will behave
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct KustoClientOptions {
|
pub struct KustoClientOptions {
|
||||||
|
@ -24,6 +22,7 @@ pub struct KustoClientOptions {
|
||||||
|
|
||||||
impl KustoClientOptions {
|
impl KustoClientOptions {
|
||||||
/// Create new options
|
/// Create new options
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
@ -57,7 +56,7 @@ fn new_pipeline_from_options(
|
||||||
|
|
||||||
/// Kusto client for Rust.
|
/// Kusto client for Rust.
|
||||||
/// The client is a wrapper around the Kusto REST API.
|
/// The client is a wrapper around the Kusto REST API.
|
||||||
/// To read more about it, go to https://docs.microsoft.com/en-us/azure/kusto/api/rest/
|
/// To read more about it, go to [https://docs.microsoft.com/en-us/azure/kusto/api/rest/](https://docs.microsoft.com/en-us/azure/kusto/api/rest/)
|
||||||
///
|
///
|
||||||
/// The primary methods are:
|
/// The primary methods are:
|
||||||
/// `execute_query`: executes a KQL query against the Kusto service.
|
/// `execute_query`: executes a KQL query against the Kusto service.
|
||||||
|
@ -116,11 +115,11 @@ impl KustoClient {
|
||||||
.with_query(query.into())
|
.with_query(query.into())
|
||||||
.with_context(Context::new())
|
.with_context(Context::new())
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.expect("Unexpected error when building query runner - please report this issue to the Kusto team")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a KQL query.
|
/// Execute a KQL query.
|
||||||
/// To learn more about KQL go to https://docs.microsoft.com/en-us/azure/kusto/query/
|
/// To learn more about KQL go to [https://docs.microsoft.com/en-us/azure/kusto/query/](https://docs.microsoft.com/en-us/azure/kusto/query)
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -142,20 +141,7 @@ impl KustoClient {
|
||||||
V1QueryRunner(self.execute(database, query, QueryKind::Management))
|
V1QueryRunner(self.execute(database, query, QueryKind::Management))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prepare_request(&self, uri: Uri, http_method: http::Method) -> Request {
|
pub(crate) const fn pipeline(&self) -> &Pipeline {
|
||||||
let mut request = Request::new(uri, http_method);
|
|
||||||
request.insert_headers(&Version::from(API_VERSION));
|
|
||||||
request.insert_headers(&Accept::from("application/json"));
|
|
||||||
request.insert_headers(&ContentType::new("application/json; charset=utf-8"));
|
|
||||||
request.insert_headers(&AcceptEncoding::from("gzip"));
|
|
||||||
request.insert_headers(&ClientVersion::from(format!(
|
|
||||||
"Kusto.Rust.Client:{}",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
)));
|
|
||||||
request
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pipeline(&self) -> &Pipeline {
|
|
||||||
&self.pipeline
|
&self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,7 +169,7 @@ impl<'a> TryFrom<ConnectionString<'a>> for KustoClient {
|
||||||
ConnectionString {
|
ConnectionString {
|
||||||
msi_auth: Some(true),
|
msi_auth: Some(true),
|
||||||
..
|
..
|
||||||
} => Arc::new(ImdsManagedIdentityCredential {}),
|
} => Arc::new(ImdsManagedIdentityCredential::default()),
|
||||||
ConnectionString {
|
ConnectionString {
|
||||||
az_cli: Some(true), ..
|
az_cli: Some(true), ..
|
||||||
} => Arc::new(AzureCliCredential {}),
|
} => Arc::new(AzureCliCredential {}),
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// Set of properties that can be use in a connection string provided to KustoConnectionStringBuilder.
|
// Set of properties that can be use in a connection string provided to KustoConnectionStringBuilder.
|
||||||
// For a complete list of properties go to https://docs.microsoft.com/en-us/azure/kusto/api/connection-strings/kusto
|
// For a complete list of properties go to https://docs.microsoft.com/en-us/azure/kusto/api/connection-strings/kusto
|
||||||
|
|
||||||
|
use crate::error::ConnectionStringError;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
use lazy_static::lazy_static;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
enum ConnectionStringKey {
|
enum ConnectionStringKey {
|
||||||
DataSource,
|
DataSource,
|
||||||
|
@ -22,7 +23,7 @@ enum ConnectionStringKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionStringKey {
|
impl ConnectionStringKey {
|
||||||
fn to_str(&self) -> &'static str {
|
const fn to_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ConnectionStringKey::DataSource => "Data Source",
|
ConnectionStringKey::DataSource => "Data Source",
|
||||||
ConnectionStringKey::FederatedSecurity => "AAD Federated Security",
|
ConnectionStringKey::FederatedSecurity => "AAD Federated Security",
|
||||||
|
@ -44,8 +45,7 @@ impl ConnectionStringKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
static ALIAS_MAP: Lazy<HashMap<&'static str, ConnectionStringKey>> = Lazy::new(|| {
|
||||||
static ref ALIAS_MAP: HashMap<&'static str, ConnectionStringKey> = {
|
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
m.insert("data source", ConnectionStringKey::DataSource);
|
m.insert("data source", ConnectionStringKey::DataSource);
|
||||||
m.insert("addr", ConnectionStringKey::DataSource);
|
m.insert("addr", ConnectionStringKey::DataSource);
|
||||||
|
@ -117,8 +117,7 @@ lazy_static! {
|
||||||
m.insert("az cli", ConnectionStringKey::AzCli);
|
m.insert("az cli", ConnectionStringKey::AzCli);
|
||||||
|
|
||||||
m
|
m
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: when available
|
// TODO: when available
|
||||||
// pub const PUBLIC_APPLICATION_CERTIFICATE_NAME: &str = "Public Application Certificate";
|
// pub const PUBLIC_APPLICATION_CERTIFICATE_NAME: &str = "Public Application Certificate";
|
||||||
|
@ -136,30 +135,21 @@ lazy_static! {
|
||||||
ConnectionStringKey::ApplicationCertificateX5C => "Application Certificate x5c",
|
ConnectionStringKey::ApplicationCertificateX5C => "Application Certificate x5c",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConnectionStringError {
|
|
||||||
#[error("Missing value for key '{}'", key)]
|
|
||||||
MissingValue { key: String },
|
|
||||||
#[error("Unexpected key '{}'", key)]
|
|
||||||
UnexpectedKey { key: String },
|
|
||||||
#[error("Parsing error: {}", msg)]
|
|
||||||
ParsingError { msg: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a connection string to connect to a Kusto service instance.
|
/// Build a connection string to connect to a Kusto service instance.
|
||||||
///
|
///
|
||||||
/// For more information on Kusto connection strings visit:
|
/// For more information on Kusto connection strings visit:
|
||||||
/// https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto
|
/// [https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto)
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ConnectionStringBuilder<'a>(ConnectionString<'a>);
|
pub struct ConnectionStringBuilder<'a>(ConnectionString<'a>);
|
||||||
|
|
||||||
impl<'a> ConnectionStringBuilder<'a> {
|
impl<'a> ConnectionStringBuilder<'a> {
|
||||||
/// Creates a ConnectionStringBuilder with no configuration options set
|
/// Creates a `ConnectionStringBuilder` with no configuration options set
|
||||||
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(ConnectionString::default())
|
Self(ConnectionString::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a ConnectionStringBuilder that will authenticate with AAD application and key.
|
/// Creates a `ConnectionStringBuilder` that will authenticate with AAD application and key.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -167,6 +157,7 @@ impl<'a> ConnectionStringBuilder<'a> {
|
||||||
/// * `authority_id` - Authority id (aka Tenant id) must be provided
|
/// * `authority_id` - Authority id (aka Tenant id) must be provided
|
||||||
/// * `client_id` - AAD application ID.
|
/// * `client_id` - AAD application ID.
|
||||||
/// * `client_secret` - Corresponding key of the AAD application.
|
/// * `client_secret` - Corresponding key of the AAD application.
|
||||||
|
#[must_use]
|
||||||
pub fn new_with_aad_application_key_authentication(
|
pub fn new_with_aad_application_key_authentication(
|
||||||
service_url: &'a str,
|
service_url: &'a str,
|
||||||
authority_id: &'a str,
|
authority_id: &'a str,
|
||||||
|
@ -183,6 +174,7 @@ impl<'a> ConnectionStringBuilder<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn build(&self) -> String {
|
pub fn build(&self) -> String {
|
||||||
let mut kv_pairs = Vec::new();
|
let mut kv_pairs = Vec::new();
|
||||||
|
|
||||||
|
@ -266,7 +258,7 @@ impl<'a> ConnectionStringBuilder<'a> {
|
||||||
/// A Kusto service connection string.
|
/// A Kusto service connection string.
|
||||||
///
|
///
|
||||||
/// For more information on Kusto connection strings visit:
|
/// For more information on Kusto connection strings visit:
|
||||||
/// https://docs.microsoft.com/en-us/azure/kusto/api/connection-strings/kusto
|
/// [https://docs.microsoft.com/en-us/azure/kusto/api/connection-strings/kusto](https://docs.microsoft.com/en-us/azure/data-explorer/kusto/api/connection-strings/kusto)
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ConnectionString<'a> {
|
pub struct ConnectionString<'a> {
|
||||||
/// The URI specifying the Kusto service endpoint.
|
/// The URI specifying the Kusto service endpoint.
|
||||||
|
@ -422,10 +414,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_parses_empty_connection_string() {
|
fn it_parses_empty_connection_string() {
|
||||||
assert_eq!(
|
assert_eq!(ConnectionString::new(""), Ok(ConnectionString::default()));
|
||||||
ConnectionString::new("").unwrap(),
|
|
||||||
ConnectionString::default()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Defines `KustoRsError` for representing failures in various operations.
|
//! Defines `KustoRsError` for representing failures in various operations.
|
||||||
use http::uri::InvalidUri;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::num::TryFromIntError;
|
||||||
use thiserror;
|
use thiserror;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -21,24 +21,34 @@ pub enum Error {
|
||||||
NotImplemented(String),
|
NotImplemented(String),
|
||||||
|
|
||||||
/// Error relating to (de-)serialization of JSON data
|
/// Error relating to (de-)serialization of JSON data
|
||||||
#[error(transparent)]
|
#[error("Error in JSON serialization/deserialization: {0}")]
|
||||||
JsonError(#[from] serde_json::Error),
|
JsonError(#[from] serde_json::Error),
|
||||||
|
|
||||||
/// Error occurring within core azure crates
|
/// Error occurring within core azure crates
|
||||||
#[error(transparent)]
|
#[error("Error in azure-core: {0}")]
|
||||||
AzureError(#[from] azure_core::error::Error),
|
AzureError(#[from] azure_core::error::Error),
|
||||||
|
|
||||||
/// Errors raised when parsing connection information
|
/// Errors raised when parsing connection information
|
||||||
#[error("Configuration error: {0}")]
|
#[error("Connection string error: {0}")]
|
||||||
ConfigurationError(#[from] crate::connection_string::ConnectionStringError),
|
ConnectionStringError(#[from] ConnectionStringError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum InvalidArgumentError {
|
pub enum InvalidArgumentError {
|
||||||
#[error(transparent)]
|
|
||||||
InvalidUri(#[from] InvalidUri),
|
|
||||||
#[error("{0} is not a valid duration")]
|
#[error("{0} is not a valid duration")]
|
||||||
InvalidDuration(String),
|
InvalidDuration(String),
|
||||||
|
#[error("{0} is too large to fit in a u32")]
|
||||||
|
PayloadTooLarge(#[from] TryFromIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ConnectionStringError {
|
||||||
|
#[error("Missing value for key '{}'", key)]
|
||||||
|
MissingValue { key: String },
|
||||||
|
#[error("Unexpected key '{}'", key)]
|
||||||
|
UnexpectedKey { key: String },
|
||||||
|
#[error("Parsing error: {}", msg)]
|
||||||
|
ParsingError { msg: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#[cfg(feature = "arrow")]
|
#[cfg(feature = "arrow")]
|
||||||
use crate::arrow::convert_table;
|
use crate::arrow::convert_table;
|
||||||
use crate::client::{KustoClient, QueryKind};
|
use crate::client::{KustoClient, QueryKind};
|
||||||
|
|
||||||
use crate::error::{Error, InvalidArgumentError};
|
use crate::error::{Error, InvalidArgumentError};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
DataSetCompletion, DataSetHeader, DataTable, QueryBody, RequestProperties, TableKind, TableV1,
|
DataSetCompletion, DataSetHeader, DataTable, QueryBody, RequestProperties, TableKind, TableV1,
|
||||||
|
@ -9,8 +10,9 @@ use crate::request_options::RequestOptions;
|
||||||
#[cfg(feature = "arrow")]
|
#[cfg(feature = "arrow")]
|
||||||
use arrow::record_batch::RecordBatch;
|
use arrow::record_batch::RecordBatch;
|
||||||
use async_convert::TryFrom;
|
use async_convert::TryFrom;
|
||||||
|
use azure_core::error::Error as CoreError;
|
||||||
use azure_core::prelude::*;
|
use azure_core::prelude::*;
|
||||||
use azure_core::{collect_pinned_stream, Response as HttpResponse};
|
use azure_core::{collect_pinned_stream, Request, Response as HttpResponse, Url};
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -45,15 +47,25 @@ pub struct V2QueryRunner(pub QueryRunner);
|
||||||
|
|
||||||
impl V1QueryRunner {
|
impl V1QueryRunner {
|
||||||
pub fn into_future(self) -> V1QueryRun {
|
pub fn into_future(self) -> V1QueryRun {
|
||||||
|
Box::pin(async {
|
||||||
let V1QueryRunner(query_runner) = self;
|
let V1QueryRunner(query_runner) = self;
|
||||||
Box::pin(query_runner.into_future().map_ok(|e| e.try_into().unwrap()))
|
let future = query_runner.into_future().await?;
|
||||||
|
Ok(
|
||||||
|
std::convert::TryInto::try_into(future).expect("Unexpected conversion error from KustoResponse to KustoResponseDataSetV1 - please report this issue to the Kusto team")
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl V2QueryRunner {
|
impl V2QueryRunner {
|
||||||
pub fn into_future(self) -> V2QueryRun {
|
pub fn into_future(self) -> V2QueryRun {
|
||||||
|
Box::pin(async {
|
||||||
let V2QueryRunner(query_runner) = self;
|
let V2QueryRunner(query_runner) = self;
|
||||||
Box::pin(query_runner.into_future().map_ok(|e| e.try_into().unwrap()))
|
let future = query_runner.into_future().await?;
|
||||||
|
Ok(
|
||||||
|
std::convert::TryInto::try_into(future).expect("Unexpected conversion error from KustoResponse to KustoResponseDataSetV2 - please report this issue to the Kusto team")
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,10 +79,8 @@ impl QueryRunner {
|
||||||
QueryKind::Management => this.client.management_url(),
|
QueryKind::Management => this.client.management_url(),
|
||||||
QueryKind::Query => this.client.query_url(),
|
QueryKind::Query => this.client.query_url(),
|
||||||
};
|
};
|
||||||
let mut request = this.client.prepare_request(
|
let mut request =
|
||||||
url.parse().map_err(InvalidArgumentError::InvalidUri)?,
|
prepare_request(url.parse().map_err(CoreError::from)?, http::Method::POST);
|
||||||
http::Method::POST,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(request_id) = &this.client_request_id {
|
if let Some(request_id) = &this.client_request_id {
|
||||||
request.insert_headers(request_id);
|
request.insert_headers(request_id);
|
||||||
|
@ -91,8 +101,10 @@ impl QueryRunner {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let bytes = bytes::Bytes::from(serde_json::to_string(&body)?);
|
let bytes = bytes::Bytes::from(serde_json::to_string(&body)?);
|
||||||
request.insert_headers(&ContentLength::new(bytes.len() as i32));
|
request.insert_headers(&ContentLength::new(
|
||||||
request.set_body(bytes.into());
|
std::convert::TryInto::try_into(bytes.len()).map_err(InvalidArgumentError::from)?,
|
||||||
|
));
|
||||||
|
request.set_body(bytes);
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
|
@ -159,11 +171,12 @@ impl std::convert::TryFrom<KustoResponse> for KustoResponseDataSetV1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KustoResponseDataSetV2 {
|
impl KustoResponseDataSetV2 {
|
||||||
|
#[must_use]
|
||||||
pub fn table_count(&self) -> usize {
|
pub fn table_count(&self) -> usize {
|
||||||
self.tables.len()
|
self.tables.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consumes the response into an iterator over all PrimaryResult tables within the response dataset
|
/// Consumes the response into an iterator over all `PrimaryResult` tables within the response dataset
|
||||||
pub fn into_primary_results(self) -> impl Iterator<Item = DataTable> {
|
pub fn into_primary_results(self) -> impl Iterator<Item = DataTable> {
|
||||||
self.tables.into_iter().filter_map(|table| match table {
|
self.tables.into_iter().filter_map(|table| match table {
|
||||||
ResultTable::DataTable(table) if table.table_kind == TableKind::PrimaryResult => {
|
ResultTable::DataTable(table) if table.table_kind == TableKind::PrimaryResult => {
|
||||||
|
@ -186,6 +199,7 @@ pub struct KustoResponseDataSetV1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KustoResponseDataSetV1 {
|
impl KustoResponseDataSetV1 {
|
||||||
|
#[must_use]
|
||||||
pub fn table_count(&self) -> usize {
|
pub fn table_count(&self) -> usize {
|
||||||
self.tables.len()
|
self.tables.len()
|
||||||
}
|
}
|
||||||
|
@ -224,6 +238,21 @@ impl TryFrom<HttpResponse> for KustoResponseDataSetV1 {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
pub fn prepare_request(uri: Url, http_method: http::Method) -> Request {
|
||||||
|
const API_VERSION: &str = "2019-02-13";
|
||||||
|
|
||||||
|
let mut request = Request::new(uri, http_method);
|
||||||
|
request.insert_headers(&Version::from(API_VERSION));
|
||||||
|
request.insert_headers(&Accept::from("application/json"));
|
||||||
|
request.insert_headers(&ContentType::new("application/json; charset=utf-8"));
|
||||||
|
request.insert_headers(&AcceptEncoding::from("gzip"));
|
||||||
|
request.insert_headers(&ClientVersion::from(format!(
|
||||||
|
"Kusto.Rust.Client:{}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
)));
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -244,7 +273,7 @@ mod tests {
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
let parsed = serde_json::from_str::<KustoResponseDataSetV1>(data);
|
let parsed = serde_json::from_str::<KustoResponseDataSetV1>(data);
|
||||||
assert!(parsed.is_ok())
|
assert!(parsed.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -252,9 +281,11 @@ mod tests {
|
||||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
path.push("tests/inputs/adminthenquery.json");
|
path.push("tests/inputs/adminthenquery.json");
|
||||||
|
|
||||||
let data = std::fs::read_to_string(path).unwrap();
|
let data = std::fs::read_to_string(&path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to read {}", path.display()));
|
||||||
|
|
||||||
let parsed = serde_json::from_str::<KustoResponseDataSetV1>(&data).unwrap();
|
let parsed = serde_json::from_str::<KustoResponseDataSetV1>(&data)
|
||||||
assert_eq!(parsed.table_count(), 4)
|
.expect("Failed to parse response");
|
||||||
|
assert_eq!(parsed.table_count(), 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ pub use crate::operations::query::{
|
||||||
KustoResponse, KustoResponseDataSetV1, KustoResponseDataSetV2, ResultTable,
|
KustoResponse, KustoResponseDataSetV1, KustoResponseDataSetV2, ResultTable,
|
||||||
};
|
};
|
||||||
// Token credentials are re-exported for user convenience
|
// Token credentials are re-exported for user convenience
|
||||||
pub use azure_identity::token_credentials::{
|
pub use azure_identity::{
|
||||||
AutoRefreshingTokenCredential, AzureCliCredential, ClientSecretCredential,
|
AutoRefreshingTokenCredential, AzureCliCredential, ClientSecretCredential,
|
||||||
DefaultAzureCredential, DefaultAzureCredentialBuilder, EnvironmentCredential,
|
DefaultAzureCredential, DefaultAzureCredentialBuilder, EnvironmentCredential,
|
||||||
ManagedIdentityCredentialError, TokenCredentialOptions,
|
TokenCredentialOptions,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use azure_core::error::{ErrorKind, ResultExt};
|
use azure_core::error::{ErrorKind, ResultExt};
|
||||||
use lazy_static::lazy_static;
|
use once_cell::sync::Lazy;
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
@ -42,7 +42,7 @@ impl Debug for KustoDateTime {
|
||||||
|
|
||||||
impl From<OffsetDateTime> for KustoDateTime {
|
impl From<OffsetDateTime> for KustoDateTime {
|
||||||
fn from(time: OffsetDateTime) -> Self {
|
fn from(time: OffsetDateTime) -> Self {
|
||||||
KustoDateTime(time)
|
Self(time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ pub struct KustoDuration(pub Duration);
|
||||||
|
|
||||||
impl From<Duration> for KustoDuration {
|
impl From<Duration> for KustoDuration {
|
||||||
fn from(duration: Duration) -> Self {
|
fn from(duration: Duration) -> Self {
|
||||||
KustoDuration(duration)
|
Self(duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,18 +74,20 @@ impl Deref for KustoDuration {
|
||||||
fn parse_regex_segment(captures: &Captures, name: &str) -> i64 {
|
fn parse_regex_segment(captures: &Captures, name: &str) -> i64 {
|
||||||
captures
|
captures
|
||||||
.name(name)
|
.name(name)
|
||||||
.map(|m| m.as_str().parse::<i64>().unwrap())
|
.map_or(0, |m| m.as_str().parse::<i64>().expect("Failed to parse regex segment as i64 - this is a bug - please report this issue to the Kusto team"))
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
static KUSTO_DURATION_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
Regex::new(r"^(?P<neg>-)?((?P<days>\d+)\.)?(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d+)(\.(?P<nanos>\d+))?$")
|
||||||
|
.expect("Failed to compile KustoDuration regex, this should never happen - please report this issue to the Kusto team")
|
||||||
|
});
|
||||||
|
|
||||||
impl FromStr for KustoDuration {
|
impl FromStr for KustoDuration {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
lazy_static! {
|
KUSTO_DURATION_REGEX
|
||||||
static ref RE: Regex = Regex::new(r"^(?P<neg>\-)?((?P<days>\d+)\.)?(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d+)(\.(?P<nanos>\d+))?$").unwrap();
|
.captures(s)
|
||||||
}
|
.map(|captures| {
|
||||||
if let Some(captures) = RE.captures(s) {
|
|
||||||
let neg = match captures.name("neg") {
|
let neg = match captures.name("neg") {
|
||||||
None => 1,
|
None => 1,
|
||||||
Some(_) => -1,
|
Some(_) => -1,
|
||||||
|
@ -101,10 +103,9 @@ impl FromStr for KustoDuration {
|
||||||
+ Duration::minutes(minutes)
|
+ Duration::minutes(minutes)
|
||||||
+ Duration::seconds(seconds)
|
+ Duration::seconds(seconds)
|
||||||
+ Duration::nanoseconds(nanos * 100)); // Ticks
|
+ Duration::nanoseconds(nanos * 100)); // Ticks
|
||||||
Ok(KustoDuration(duration))
|
Self(duration)
|
||||||
} else {
|
})
|
||||||
Err(InvalidArgumentError::InvalidDuration(s.to_string()).into())
|
.ok_or_else(|| InvalidArgumentError::InvalidDuration(s.to_string()).into())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +126,8 @@ impl Display for KustoDuration {
|
||||||
neg * (self.whole_hours() - self.whole_days() * 24),
|
neg * (self.whole_hours() - self.whole_days() * 24),
|
||||||
neg * (self.whole_minutes() - self.whole_hours() * 60),
|
neg * (self.whole_minutes() - self.whole_hours() * 60),
|
||||||
neg * (self.whole_seconds() - self.whole_minutes() * 60),
|
neg * (self.whole_seconds() - self.whole_minutes() * 60),
|
||||||
neg as i128 * (self.whole_nanoseconds() - self.whole_seconds() as i128 * 1_000_000_000)
|
i128::from(neg)
|
||||||
|
* (self.whole_nanoseconds() - i128::from(self.whole_seconds()) * 1_000_000_000)
|
||||||
/ 100 // Ticks
|
/ 100 // Ticks
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -146,20 +148,22 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn string_conversion() {
|
fn string_conversion() {
|
||||||
let refs: Vec<(&str, i64)> = vec![
|
let refs: Vec<(&str, i64)> = vec![
|
||||||
("1.00:00:00.0000000", 86400000000000),
|
("1.00:00:00.0000000", 86_400_000_000_000),
|
||||||
("01:00:00.0000000", 3600000000000),
|
("01:00:00.0000000", 3_600_000_000_000),
|
||||||
("01:00:00", 3600000000000),
|
("01:00:00", 3_600_000_000_000),
|
||||||
("00:05:00.0000000", 300000000000),
|
("00:05:00.0000000", 300_000_000_000),
|
||||||
("00:00:00.0000001", 100),
|
("00:00:00.0000001", 100),
|
||||||
("-01:00:00", -3600000000000),
|
("-01:00:00", -3_600_000_000_000),
|
||||||
("-1.00:00:00.0000000", -86400000000000),
|
("-1.00:00:00.0000000", -86_400_000_000_000),
|
||||||
("00:00:00.1234567", 123456700),
|
("00:00:00.1234567", 123_456_700),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (from, to) in refs {
|
for (from, to) in refs {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
KustoDuration::from_str(from).unwrap().whole_nanoseconds(),
|
KustoDuration::from_str(from)
|
||||||
to as i128
|
.unwrap_or_else(|_| panic!("Failed to parse duration {}", from))
|
||||||
|
.whole_nanoseconds(),
|
||||||
|
i128::from(to)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,7 +180,8 @@ mod tests {
|
||||||
];
|
];
|
||||||
|
|
||||||
for duration in refs {
|
for duration in refs {
|
||||||
let parsed = KustoDuration::from_str(duration).unwrap();
|
let parsed = KustoDuration::from_str(duration)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to parse duration {}", duration));
|
||||||
assert_eq!(format!("{:?}", parsed), duration);
|
assert_eq!(format!("{:?}", parsed), duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,7 @@ macro_rules! assert_batches_eq {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn arrow_roundtrip() {
|
async fn arrow_roundtrip() {
|
||||||
let (client, database) = setup::create_kusto_client("data_arrow_roundtrip")
|
let (client, database) = setup::create_kusto_client("data_arrow_roundtrip");
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let query = "
|
let query = "
|
||||||
datatable(
|
datatable(
|
||||||
|
@ -48,11 +46,11 @@ async fn arrow_roundtrip() {
|
||||||
.execute_query(&database, query)
|
.execute_query(&database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to run query");
|
||||||
let batches = response
|
let batches = response
|
||||||
.into_record_batches()
|
.into_record_batches()
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.unwrap();
|
.expect("Failed to collect batches");
|
||||||
|
|
||||||
let expected_schema = Arc::new(Schema::new(vec![
|
let expected_schema = Arc::new(Schema::new(vec![
|
||||||
Field::new("id", DataType::Int32, true),
|
Field::new("id", DataType::Int32, true),
|
||||||
|
@ -83,7 +81,9 @@ async fn arrow_roundtrip() {
|
||||||
assert_batches_eq!(
|
assert_batches_eq!(
|
||||||
expected,
|
expected,
|
||||||
// we have to de-select the duration column, since pretty printing is not supported in arrow
|
// we have to de-select the duration column, since pretty printing is not supported in arrow
|
||||||
&[batches[0].project(&[0, 1, 2, 3, 4, 5, 6]).unwrap()]
|
&[batches[0]
|
||||||
|
.project(&[0, 1, 2, 3, 4, 5, 6])
|
||||||
|
.expect("Failed to project numbers")]
|
||||||
);
|
);
|
||||||
assert_eq!(expected_schema, batches[0].schema())
|
assert_eq!(expected_schema, batches[0].schema());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,14 @@ mod setup;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_query_delete_table() {
|
async fn create_query_delete_table() {
|
||||||
let (client, database) = setup::create_kusto_client("data_create_query_delete_table")
|
let (client, database) = setup::create_kusto_client("data_create_query_delete_table");
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let query = ".set KustoRsTest <| let text=\"Hello, World!\"; print str=text";
|
let query = ".set KustoRsTest <| let text=\"Hello, World!\"; print str=text";
|
||||||
let response = client
|
let response = client
|
||||||
.execute_command(&database, query)
|
.execute_command(&database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to run query");
|
||||||
|
|
||||||
assert_eq!(response.table_count(), 1);
|
assert_eq!(response.table_count(), 1);
|
||||||
|
|
||||||
|
@ -21,7 +19,7 @@ async fn create_query_delete_table() {
|
||||||
.execute_command(&database, query)
|
.execute_command(&database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to run query");
|
||||||
|
|
||||||
assert_eq!(response.table_count(), 4);
|
assert_eq!(response.table_count(), 4);
|
||||||
|
|
||||||
|
@ -30,7 +28,7 @@ async fn create_query_delete_table() {
|
||||||
.execute_query(&database, query)
|
.execute_query(&database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to run query");
|
||||||
|
|
||||||
let results = response.into_primary_results().collect::<Vec<_>>();
|
let results = response.into_primary_results().collect::<Vec<_>>();
|
||||||
assert_eq!(results[0].rows.len(), 1);
|
assert_eq!(results[0].rows.len(), 1);
|
||||||
|
@ -40,7 +38,7 @@ async fn create_query_delete_table() {
|
||||||
.execute_command(&database, query)
|
.execute_command(&database, query)
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.expect("Failed to run query");
|
||||||
|
|
||||||
assert_eq!(response.tables[0].rows.len(), 0)
|
assert_eq!(response.tables[0].rows.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
#![cfg(feature = "mock_transport_framework")]
|
#![cfg(feature = "mock_transport_framework")]
|
||||||
use azure_core::auth::{TokenCredential, TokenResponse};
|
use azure_core::auth::{AccessToken, TokenCredential, TokenResponse};
|
||||||
use azure_core::Error as CoreError;
|
use azure_core::error::Error as CoreError;
|
||||||
use azure_kusto_data::prelude::*;
|
use azure_kusto_data::prelude::*;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use oauth2::AccessToken;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -21,12 +19,10 @@ impl TokenCredential for DummyCredential {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_kusto_client(
|
pub fn create_kusto_client(transaction_name: &str) -> (KustoClient, String) {
|
||||||
transaction_name: &str,
|
let transaction_path = Path::new(&workspace_root().expect("Failed to get workspace root"))
|
||||||
) -> Result<(KustoClient, String), Box<dyn Error + Send + Sync>> {
|
|
||||||
let transaction_path = Path::new(&workspace_root().unwrap())
|
|
||||||
.join(format!("test/transactions/{}", transaction_name));
|
.join(format!("test/transactions/{}", transaction_name));
|
||||||
std::fs::create_dir_all(&transaction_path).unwrap();
|
std::fs::create_dir_all(&transaction_path).expect("Failed to create transaction directory");
|
||||||
let db_path = transaction_path.join("_db");
|
let db_path = transaction_path.join("_db");
|
||||||
|
|
||||||
let (service_url, credential, database): (String, Arc<dyn TokenCredential>, String) =
|
let (service_url, credential, database): (String, Arc<dyn TokenCredential>, String) =
|
||||||
|
@ -48,27 +44,32 @@ pub async fn create_kusto_client(
|
||||||
|
|
||||||
// Wee need to persist the database name as well, since it may change per recording run depending on who
|
// Wee need to persist the database name as well, since it may change per recording run depending on who
|
||||||
// records it, is part of the request, and as such validated against.
|
// records it, is part of the request, and as such validated against.
|
||||||
std::fs::write(db_path, &database).unwrap();
|
std::fs::write(db_path, &database).expect("Failed to write database name to file");
|
||||||
|
|
||||||
let credential = Arc::new(ClientSecretCredential::new(
|
let credential = Arc::new(ClientSecretCredential::new(
|
||||||
tenant_id.to_string(),
|
tenant_id,
|
||||||
client_id.to_string(),
|
client_id,
|
||||||
client_secret.to_string(),
|
client_secret,
|
||||||
TokenCredentialOptions::default(),
|
TokenCredentialOptions::default(),
|
||||||
));
|
));
|
||||||
(service_url, credential, database)
|
(service_url, credential, database)
|
||||||
} else {
|
} else {
|
||||||
let credential = Arc::new(DummyCredential {});
|
let credential = Arc::new(DummyCredential {});
|
||||||
let database = String::from_utf8_lossy(&std::fs::read(db_path).unwrap()).to_string();
|
let database = String::from_utf8_lossy(
|
||||||
|
&std::fs::read(&db_path)
|
||||||
|
.expect(&format!("Could not read db path {}", db_path.display())),
|
||||||
|
)
|
||||||
|
.to_string();
|
||||||
(String::new(), credential, database)
|
(String::new(), credential, database)
|
||||||
};
|
};
|
||||||
|
|
||||||
let options = KustoClientOptions::new_with_transaction_name(transaction_name.to_string());
|
let options = KustoClientOptions::new_with_transaction_name(transaction_name.to_string());
|
||||||
|
|
||||||
Ok((
|
(
|
||||||
KustoClient::new_with_options(service_url, credential, options).unwrap(),
|
KustoClient::new_with_options(service_url, credential, options)
|
||||||
|
.expect("Failed to create KustoClient"),
|
||||||
database,
|
database,
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run cargo to get the root of the workspace
|
/// Run cargo to get the root of the workspace
|
||||||
|
@ -82,10 +83,10 @@ fn workspace_root() -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let key = "workspace_root\":\"";
|
let key = "workspace_root\":\"";
|
||||||
let index = output
|
let index = output
|
||||||
.find(key)
|
.find(key)
|
||||||
.ok_or_else(|| format!("workspace_root key not found in metadata"))?;
|
.ok_or_else(|| "workspace_root key not found in metadata".to_string())?;
|
||||||
let value = &output[index + key.len()..];
|
let value = &output[index + key.len()..];
|
||||||
let end = value
|
let end = value
|
||||||
.find("\"")
|
.find('\"')
|
||||||
.ok_or_else(|| format!("workspace_root value was malformed"))?;
|
.ok_or_else(|| "workspace_root value was malformed".to_string())?;
|
||||||
Ok(value[..end].into())
|
Ok(value[..end].into())
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче