Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
473 changes: 252 additions & 221 deletions Cargo.lock

Large diffs are not rendered by default.

62 changes: 29 additions & 33 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories = ["command-line-utilities", "database"]

[dependencies]
# SQL query engine (Apache Arrow DataFusion)
datafusion = { version = "51", default-features = false, features = [
datafusion = { version = "52.0.0", default-features = false, features = [
"nested_expressions",
"datetime_expressions",
"regex_expressions",
Expand All @@ -21,80 +21,76 @@ datafusion = { version = "51", default-features = false, features = [
"sql",
"recursive_protection",
] }
datafusion-functions-json = "0.51"
datafusion-functions-json = "0.52.0"

# PRQL compiler (alternative query language)
prqlc = "0.13"

# Kubernetes client
kube = { version = "2", features = ["runtime", "client", "derive", "rustls-tls", "http-proxy"] }
k8s-openapi = { version = "0.26", features = ["v1_32"] }
k8s-metrics = "0.26"
kube = { version = "3.0.0", features = ["runtime", "client", "derive", "rustls-tls", "http-proxy"] }
k8s-openapi = { version = "0.27.0", features = ["v1_32"] }
k8s-metrics = "0.27.0"

# TLS provider for rustls (aws-lc-rs preferred for performance)
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs", "std", "tls12"] }
rustls = { version = "0.23.36", default-features = false, features = ["aws-lc-rs", "std", "tls12"] }

# PostgreSQL wire protocol (for daemon mode)
datafusion-postgres = "0.13"
datafusion-postgres = "0.14.0"

# REPL / CLI
rustyline = { version = "17", features = ["derive"] }
indicatif = "0.18"
console = "0.16"
comfy-table = "7"
dirs = "6"
rustyline = { version = "17.0.2", features = ["derive"] }
indicatif = "0.18.3"
console = "0.16.2"
comfy-table = "7.2.2"
dirs = "6.0.0"

# Async runtime
tokio = { version = "1", features = ["full"] }
tokio = { version = "1.49.0", features = ["full"] }

# Random number generation (for retry jitter)
fastrand = "2"
fastrand = "2.3.0"

# CLI argument parsing
clap = { version = "4", features = ["derive"] }
clap = { version = "4.5.54", features = ["derive"] }

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"

# Date/time
chrono = "0.4"
chrono = "0.4.43"

# Error handling
anyhow = "1"
anyhow = "1.0.100"

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-rolling-file = { version = "0.1", features = ["non-blocking"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-rolling-file = { version = "0.1.3", features = ["non-blocking"] }

# Futures
futures = "0.3"
futures = "0.3.31"

# Async streaming
async-stream = "0.3"
async-stream = "0.3.6"

# Async trait support
async-trait = "0.1"
async-trait = "0.1.89"

# YAML output
serde_yaml = "0.9"
serde_yaml = "0.9.34"

# Regex for LIKE pattern matching
regex = "1"
regex = "1.12.2"

# Pin lazy-regex to 3.4.2 (3.5.0 has a bug with regex::bytes)
lazy-regex = "=3.5.1"
lazy-regex = "3.5.1"

# Atomic file operations
tempfile = "3"

[dev-dependencies]
tempfile = "3.24.0"

[profile.release]
strip = true
lto = "thin"
codegen-units = 1
opt-level = "z" # Optimize for size


168 changes: 168 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,171 @@ pub enum OutputFormat {
Csv,
Yaml,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_default_output_format() {
let args = Args::parse_from(["k8sql"]);
assert!(matches!(args.output, OutputFormat::Table));
}

#[test]
fn test_output_format_json() {
let args = Args::parse_from(["k8sql", "-o", "json"]);
assert!(matches!(args.output, OutputFormat::Json));
}

#[test]
fn test_output_format_csv() {
let args = Args::parse_from(["k8sql", "-o", "csv"]);
assert!(matches!(args.output, OutputFormat::Csv));
}

#[test]
fn test_output_format_yaml() {
let args = Args::parse_from(["k8sql", "-o", "yaml"]);
assert!(matches!(args.output, OutputFormat::Yaml));
}

#[test]
fn test_context_single() {
let args = Args::parse_from(["k8sql", "-c", "prod"]);
assert_eq!(args.context, Some("prod".to_string()));
}

#[test]
fn test_context_comma_separated() {
let args = Args::parse_from(["k8sql", "-c", "prod,staging,dev"]);
assert_eq!(args.context, Some("prod,staging,dev".to_string()));
}

#[test]
fn test_context_glob_pattern() {
let args = Args::parse_from(["k8sql", "-c", "prod-*"]);
assert_eq!(args.context, Some("prod-*".to_string()));
}

#[test]
fn test_context_wildcard() {
let args = Args::parse_from(["k8sql", "-c", "*"]);
assert_eq!(args.context, Some("*".to_string()));
}

#[test]
fn test_query_simple() {
let args = Args::parse_from(["k8sql", "-q", "SELECT * FROM pods"]);
assert_eq!(args.query, Some("SELECT * FROM pods".to_string()));
}

#[test]
fn test_query_with_special_characters() {
let args = Args::parse_from([
"k8sql",
"-q",
"SELECT * FROM pods WHERE labels->>'app' = 'nginx'",
]);
assert!(args.query.as_ref().unwrap().contains("->>"));
}

#[test]
fn test_file_flag() {
let args = Args::parse_from(["k8sql", "-f", "/path/to/query.sql"]);
assert_eq!(args.file, Some("/path/to/query.sql".to_string()));
}

#[test]
fn test_no_headers_flag() {
let args = Args::parse_from(["k8sql", "--no-headers"]);
assert!(args.no_headers);
}

#[test]
fn test_verbose_flag() {
let args = Args::parse_from(["k8sql", "-v"]);
assert!(args.verbose);
}

#[test]
fn test_refresh_crds_flag() {
let args = Args::parse_from(["k8sql", "--refresh-crds"]);
assert!(args.refresh_crds);
}

#[test]
fn test_daemon_subcommand_defaults() {
let args = Args::parse_from(["k8sql", "daemon"]);
match args.command {
Some(Command::Daemon { port, bind }) => {
assert_eq!(port, 15432);
assert_eq!(bind, "127.0.0.1");
}
_ => panic!("Expected Daemon command"),
}
}

#[test]
fn test_daemon_custom_port() {
let args = Args::parse_from(["k8sql", "daemon", "--port", "5432"]);
match args.command {
Some(Command::Daemon { port, .. }) => assert_eq!(port, 5432),
_ => panic!("Expected Daemon command"),
}
}

#[test]
fn test_daemon_custom_bind() {
let args = Args::parse_from(["k8sql", "daemon", "--bind", "0.0.0.0"]);
match args.command {
Some(Command::Daemon { bind, .. }) => assert_eq!(bind, "0.0.0.0"),
_ => panic!("Expected Daemon command"),
}
}

#[test]
fn test_daemon_both_options() {
let args = Args::parse_from(["k8sql", "daemon", "-p", "5433", "-b", "192.168.1.1"]);
match args.command {
Some(Command::Daemon { port, bind }) => {
assert_eq!(port, 5433);
assert_eq!(bind, "192.168.1.1");
}
_ => panic!("Expected Daemon command"),
}
}

#[test]
fn test_interactive_subcommand() {
let args = Args::parse_from(["k8sql", "interactive"]);
assert!(matches!(args.command, Some(Command::Interactive)));
}

#[test]
fn test_combined_flags() {
let args = Args::parse_from([
"k8sql",
"-c",
"prod",
"-q",
"SELECT name FROM pods",
"-o",
"json",
"-v",
]);
assert_eq!(args.context, Some("prod".to_string()));
assert_eq!(args.query, Some("SELECT name FROM pods".to_string()));
assert!(matches!(args.output, OutputFormat::Json));
assert!(args.verbose);
}

#[test]
fn test_no_command_no_query() {
// Just k8sql with no arguments (interactive mode by default)
let args = Args::parse_from(["k8sql"]);
assert!(args.command.is_none());
assert!(args.query.is_none());
assert!(args.context.is_none());
}
}
6 changes: 1 addition & 5 deletions src/daemon/pgwire_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

use std::sync::Arc;

use datafusion_postgres::auth::AuthManager;
use datafusion_postgres::datafusion_pg_catalog::pg_catalog::context::EmptyContextProvider;
use datafusion_postgres::datafusion_pg_catalog::setup_pg_catalog;
use datafusion_postgres::{QueryHook, ServerOptions, serve_with_hooks};
Expand Down Expand Up @@ -46,9 +45,6 @@ impl PgWireServer {

let ctx = Arc::new(ctx);

// Create default auth manager (accepts "postgres" user with empty password)
let auth_manager = Arc::new(AuthManager::new());

// Create custom hooks for k8sql-specific commands
let hooks: Vec<Arc<dyn QueryHook>> = vec![
Arc::new(SetConfigHook::new()), // Handle SET commands from PostgreSQL clients
Expand All @@ -75,7 +71,7 @@ impl PgWireServer {
);

// Use datafusion-postgres to serve queries with our custom hooks
serve_with_hooks(ctx, &server_options, auth_manager, hooks)
serve_with_hooks(ctx, &server_options, hooks)
.await
.map_err(|e| anyhow::anyhow!("{}", e))
}
Expand Down
22 changes: 10 additions & 12 deletions src/datafusion_integration/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,25 +512,23 @@ mod tests {

#[test]
fn test_is_not_found_error_with_404() {
let api_err = kube::Error::Api(kube::error::ErrorResponse {
status: "Failure".to_string(),
message: "namespaces \"missing\" not found".to_string(),
reason: "NotFound".to_string(),
code: 404,
});
let api_err = kube::Error::Api(
kube::core::Status::failure("namespaces \"missing\" not found", "NotFound")
.with_code(404)
.boxed(),
);
let err = anyhow::Error::new(api_err);

assert!(is_not_found_error(&err));
}

#[test]
fn test_is_not_found_error_with_other_code() {
let api_err = kube::Error::Api(kube::error::ErrorResponse {
status: "Failure".to_string(),
message: "Forbidden".to_string(),
reason: "Forbidden".to_string(),
code: 403,
});
let api_err = kube::Error::Api(
kube::core::Status::failure("Forbidden", "Forbidden")
.with_code(403)
.boxed(),
);
let err = anyhow::Error::new(api_err);

assert!(!is_not_found_error(&err));
Expand Down
Loading