mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
Closes #447. Each SQL console gets an active-schema dropdown (PostgreSQL / MySQL / Oracle schemas; SQL Server selects the database, since T-SQL has no per-session default schema), remembered per console file; hidden for SQLite and ClickHouse. Every statement run through the pool first asserts the console's context, or the connection's default when none is selected (skipped when the tracked context already matches), and manual-commit sessions restore the default before the connection returns to the pool, so one console's schema never leaks into another. Co-authored-by: Inf1mum <[email protected]>
137 lines
6.8 KiB
Rust
137 lines
6.8 KiB
Rust
use ide_core::{CancellationToken, IdeError, IdeResult};
|
|
use ide_db::{CellValue, DataSource, DbDriver, DbManager, DbRowEdit, DbSslMode, SortKey, sql};
|
|
use std::sync::Arc;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn check(condition: bool, message: &str) -> IdeResult<()> {
|
|
if condition { Ok(()) } else { Err(IdeError::invalid(message)) }
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires CLICKHOUSE_TEST_HOST, CLICKHOUSE_TEST_USER, and CLICKHOUSE_TEST_PASSWORD"]
|
|
async fn clickhouse_cloud_manager_catalogs_and_session_table() -> IdeResult<()> {
|
|
let tmp = tempfile::tempdir()?;
|
|
let root = tmp.path().to_path_buf();
|
|
let ssh = Arc::new(ide_ssh::SshManager::new(root.join("ssh-passwords.json")));
|
|
let db = DbManager::new(root.clone(), root.join("passwords.json"), root.join("history.json"), ssh);
|
|
let mut source = DataSource {
|
|
id: "clickhouse".into(),
|
|
name: "clickhouse".into(),
|
|
driver: DbDriver::Clickhouse,
|
|
host: std::env::var("CLICKHOUSE_TEST_HOST").expect("set CLICKHOUSE_TEST_HOST"),
|
|
port: Some(
|
|
std::env::var("CLICKHOUSE_TEST_PORT")
|
|
.map(|value| value.parse().expect("numeric CLICKHOUSE_TEST_PORT"))
|
|
.unwrap_or(8443),
|
|
),
|
|
user: std::env::var("CLICKHOUSE_TEST_USER").expect("set CLICKHOUSE_TEST_USER"),
|
|
database: "default".into(),
|
|
ssl: DbSslMode::VerifyFull,
|
|
..Default::default()
|
|
};
|
|
source.introspection.show_system = true;
|
|
let password = std::env::var("CLICKHOUSE_TEST_PASSWORD").expect("set CLICKHOUSE_TEST_PASSWORD");
|
|
check(!db.test(&source, Some(password.clone()), None).await?.is_empty(), "missing server version")?;
|
|
db.save_sources(vec![source]).await?;
|
|
db.set_password("clickhouse", Some(password), false).await?;
|
|
let cancel = CancellationToken::new();
|
|
let suffix = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
|
|
let name = format!("manager_test_{}_{suffix}", std::process::id());
|
|
let table = sql::quote_ident(DbDriver::Clickhouse, &name);
|
|
db.execute(
|
|
"clickhouse",
|
|
&format!(
|
|
"CREATE TEMPORARY TABLE {table} (id UInt64, label Nullable(String) DEFAULT NULL, \
|
|
category LowCardinality(Nullable(String)) DEFAULT NULL, items Array(Nullable(UInt8)), \
|
|
label_size UInt64 ALIAS length(ifNull(label, ''))) ENGINE = Memory"
|
|
),
|
|
0,
|
|
&cancel,
|
|
)
|
|
.await?;
|
|
let checked: IdeResult<()> = async {
|
|
db.execute(
|
|
"clickhouse",
|
|
&format!("INSERT INTO {table} (id, label, items) VALUES (1, 'first', [1, NULL]), (1, NULL, [])"),
|
|
0,
|
|
&cancel,
|
|
)
|
|
.await?;
|
|
let schemas = db.schemas("clickhouse").await?;
|
|
check(schemas.iter().any(|schema| schema == "default"), "default database missing")?;
|
|
check(schemas.iter().any(|schema| schema == "system"), "system database missing")?;
|
|
let tables = db.tables("clickhouse", "system").await?;
|
|
check(tables.iter().any(|table| table.name == "one"), "system table listing missing one")?;
|
|
let details = db.table_details("clickhouse", "", &name).await?;
|
|
check(details.columns.len() == 5, "temporary table columns missing")?;
|
|
for (name, data_type, nullable) in [
|
|
("id", "UInt64", false),
|
|
("label", "Nullable(String)", true),
|
|
("category", "LowCardinality(Nullable(String))", true),
|
|
("items", "Array(Nullable(UInt8))", false),
|
|
] {
|
|
check(
|
|
details
|
|
.columns
|
|
.iter()
|
|
.any(|column| column.name == name && column.data_type == data_type && column.nullable == nullable),
|
|
"column type or nullability mismatch",
|
|
)?;
|
|
}
|
|
check(
|
|
details.columns.iter().any(|column| column.name == "label" && column.default.as_deref() == Some("NULL")),
|
|
"explicit default expression missing",
|
|
)?;
|
|
check(details.indexes.iter().all(|index| !index.unique), "ClickHouse index reported unique")?;
|
|
check(details.foreign_keys.is_empty() && details.triggers.is_empty(), "unsupported table objects returned")?;
|
|
let sort = [SortKey { column: "id".into(), descending: false }];
|
|
let first = db.table_page("clickhouse", "", &name, None, &sort, 0, 1, &cancel).await?;
|
|
check(first.result.rows.len() == 1 && first.result.has_more, "first page limit failed")?;
|
|
let second = db.table_page("clickhouse", "", &name, None, &sort, 1, 1, &cancel).await?;
|
|
check(second.result.rows.len() == 1 && !second.result.has_more, "page offset failed")?;
|
|
let ddl = db.ddl("clickhouse", "", &name).await?;
|
|
check(ddl.contains(&name) && ddl.contains("Memory"), "temporary table DDL missing")?;
|
|
let completion = db.completion("clickhouse").await?;
|
|
check(completion.default_schema == "default", "completion default database mismatch")?;
|
|
check(completion.foreign_keys.is_empty(), "completion returned unsupported foreign keys")?;
|
|
if let Some(table) = completion.tables.first() {
|
|
let details = db.table_details("clickhouse", &table.schema, &table.name).await?;
|
|
check(
|
|
table.columns.iter().all(|column| {
|
|
details.columns.iter().any(|field| field.name == column.name && field.data_type == column.data_type)
|
|
}),
|
|
"completion columns differ from catalog",
|
|
)?;
|
|
}
|
|
let plan = db.explain("clickhouse", &format!("SELECT id FROM {table}"), &cancel, None).await?;
|
|
check(!plan.is_empty(), "query plan missing")?;
|
|
let value = r"path\O'Brien";
|
|
let result = db
|
|
.execute("clickhouse", &format!("SELECT {}", sql::quote_literal(DbDriver::Clickhouse, value)), 1, &cancel)
|
|
.await?;
|
|
check(result.rows == vec![vec![Some(value.to_owned())]], "quoted literal changed value")?;
|
|
let edits = [DbRowEdit::Update {
|
|
key: vec![CellValue { column: "id".into(), value: Some("1".into()) }],
|
|
values: vec![CellValue { column: "label".into(), value: Some("changed".into()) }],
|
|
}];
|
|
check(
|
|
db.apply_edits("clickhouse", "", &name, &edits)
|
|
.await
|
|
.err()
|
|
.is_some_and(|error| error.message.contains("table editing is unsupported")),
|
|
"supplied row key authorized table editing",
|
|
)?;
|
|
check(db.set_manual_commit("clickhouse", true).await.is_err(), "manual transactions were accepted")?;
|
|
for commit in [false, true] {
|
|
check(db.finish_transaction("clickhouse", commit).await.is_err(), "transaction control was accepted")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
.await;
|
|
let cleanup = db.execute("clickhouse", &format!("DROP TEMPORARY TABLE {table}"), 0, &cancel).await;
|
|
db.disconnect("clickhouse").await;
|
|
checked?;
|
|
cleanup?;
|
|
Ok(())
|
|
}
|