дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Project & files - Project view modes (Project Files / Open Files / All Changed Files / Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go, Maven/Gradle jars; read-only jar/zip browsing - ⌘N New popup with type search and more file kinds; New Project, Rename Project, Reload All from Disk, Export/Import Settings, Power Save Mode - File type associations, extra content roots and exclude patterns (indexed and watched without reopening the project) Editor & navigation - Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline, history; Find in Files: Module scope, custom pattern scopes, multiline - Search Everywhere filters, Assign Shortcut, settings results; named bookmark lists, file/folder bookmarks; Structure filters and Show Inherited; Recent Locations timestamps and breadcrumbs; Code Vision author - Completion options, editor tab placement/layout, inline completion (LSP 3.18) - Spell checking (Typo inspection, ide-spell) and Inspections settings with custom profiles; Code Style schemes, formatter markers, Do not format Run, VCS, services, database - Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker, Compound), Modify options, Before launch, env dialog, Current File, Emulate terminal (PTY) - Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees - Generic Services host (Docker connections/registries, run sessions); TODO scope/changes tabs with changelists; Coverage report import - Data sources: wire-compatible vendors, URL import, SSL certs, Options, Schemas and Advanced tabs, color and comment Workbench - Run Anything, Customize Main Toolbar, named tool window layouts, menu completeness, Code Cleanup, macros, "Don't Show Again", editor banners - Chinese translations for the new UI Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
269 рядки
12 KiB
Rust
269 рядки
12 KiB
Rust
//! Introspection, queries and table edits against a real SQLite file.
|
|
|
|
use ide_core::{CancellationToken, IdeResult};
|
|
use ide_db::{CellValue, DataSource, DbDriver, DbManager, DbOptions, DbProperty, DbRowEdit, DbTableKind, SortKey};
|
|
|
|
async fn setup() -> (tempfile::TempDir, DbManager) {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path().to_path_buf();
|
|
std::fs::File::create(root.join("app.db")).unwrap();
|
|
let db = DbManager::new(root.clone(), root.join("passwords.json"));
|
|
db.save_sources(vec![DataSource {
|
|
id: "s1".into(),
|
|
name: "app".into(),
|
|
driver: DbDriver::Sqlite,
|
|
file: Some("app.db".into()),
|
|
..Default::default()
|
|
}])
|
|
.await
|
|
.unwrap();
|
|
let c = CancellationToken::new();
|
|
for stmt in [
|
|
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, note TEXT, avatar BLOB)",
|
|
"CREATE UNIQUE INDEX users_name ON users (name)",
|
|
"CREATE VIEW named AS SELECT name FROM users",
|
|
"INSERT INTO users (name, note, avatar) VALUES ('ann', NULL, x'CAFE'), ('bob', 'hi', NULL), ('cy', 'x', NULL)",
|
|
] {
|
|
db.execute("s1", stmt, 100, &c).await.unwrap();
|
|
}
|
|
(tmp, db)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn introspects_schemas_tables_columns_and_indexes() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
assert_eq!(db.schemas("s1").await?, vec!["main"]);
|
|
let tables = db.tables("s1", "main").await?;
|
|
let names: Vec<_> = tables.iter().map(|t| (t.name.as_str(), t.kind)).collect();
|
|
assert_eq!(names, vec![("named", DbTableKind::View), ("users", DbTableKind::Table)]);
|
|
let d = db.table_details("s1", "main", "users").await?;
|
|
let cols: Vec<_> = d.columns.iter().map(|c| (c.name.as_str(), c.primary_key, c.nullable)).collect();
|
|
assert_eq!(cols, vec![("id", true, true), ("name", false, false), ("note", false, true), ("avatar", false, true)]);
|
|
assert_eq!(d.indexes.len(), 1);
|
|
assert_eq!(d.indexes[0].name, "users_name");
|
|
assert!(d.indexes[0].unique);
|
|
assert_eq!(d.indexes[0].columns, vec!["name"]);
|
|
let completion = db.completion("s1").await?;
|
|
assert_eq!(completion.default_schema, "main");
|
|
let users = completion.tables.iter().find(|t| t.name == "users").unwrap();
|
|
let cols: Vec<_> = users.columns.iter().map(|c| (c.name.as_str(), c.data_type.as_str(), c.primary_key)).collect();
|
|
assert_eq!(
|
|
cols,
|
|
vec![("id", "INTEGER", true), ("name", "TEXT", false), ("note", "TEXT", false), ("avatar", "BLOB", false)]
|
|
);
|
|
assert!(!users.columns[1].nullable);
|
|
// `INTEGER PRIMARY KEY` is the rowid: SQLite assigns it.
|
|
let defaults: Vec<_> = users.columns.iter().map(|c| c.has_default).collect();
|
|
assert_eq!(defaults, vec![true, false, false, false]);
|
|
let named = completion.tables.iter().find(|t| t.name == "named").unwrap();
|
|
assert_eq!(named.kind, DbTableKind::View);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn completion_model_has_foreign_keys() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
let c = CancellationToken::new();
|
|
for stmt in [
|
|
"CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id), status TEXT DEFAULT 'new')",
|
|
// Implicit reference to the primary key; composite key.
|
|
"CREATE TABLE notes (user_id INTEGER REFERENCES users, a INT, b INT, \
|
|
FOREIGN KEY (a, b) REFERENCES pairs (x, y))",
|
|
"CREATE TABLE pairs (x INT, y INT, PRIMARY KEY (x, y))",
|
|
] {
|
|
db.execute("s1", stmt, 1, &c).await?;
|
|
}
|
|
let m = db.completion("s1").await?;
|
|
let orders = m.tables.iter().find(|t| t.name == "orders").unwrap();
|
|
let defaults: Vec<_> = orders.columns.iter().map(|c| (c.name.as_str(), c.has_default)).collect();
|
|
assert_eq!(defaults, vec![("id", true), ("user_id", false), ("status", true)]);
|
|
let pairs = m.tables.iter().find(|t| t.name == "pairs").unwrap();
|
|
assert!(pairs.columns.iter().all(|c| !c.has_default));
|
|
let keys: Vec<_> = m
|
|
.foreign_keys
|
|
.iter()
|
|
.map(|k| (k.table.as_str(), k.columns.join(","), k.ref_table.as_str(), k.ref_columns.join(",")))
|
|
.collect();
|
|
assert_eq!(
|
|
keys,
|
|
vec![
|
|
("notes", "a,b".into(), "pairs", "x,y".into()),
|
|
("notes", "user_id".into(), "users", "id".into()),
|
|
("orders", "user_id".into(), "users", "id".into()),
|
|
]
|
|
);
|
|
let d = db.table_details("s1", "main", "notes").await?;
|
|
let keys: Vec<_> =
|
|
d.foreign_keys.iter().map(|k| (k.columns.join(","), k.ref_table.as_str(), k.ref_columns.join(","))).collect();
|
|
assert_eq!(keys, vec![("a,b".into(), "pairs", "x,y".into()), ("user_id".into(), "users", "id".into())]);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn queries_with_limits_nulls_and_blobs() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
let c = CancellationToken::new();
|
|
let r = db.execute("s1", "SELECT id, name, note, avatar FROM users ORDER BY id", 2, &c).await?;
|
|
assert_eq!(r.columns.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), vec!["id", "name", "note", "avatar"]);
|
|
assert_eq!(r.rows.len(), 2);
|
|
assert!(r.has_more);
|
|
assert_eq!(r.rows[0], vec![Some("1".into()), Some("ann".into()), None, Some("0xCAFE".into())]);
|
|
assert_eq!(r.affected, None);
|
|
|
|
let empty = db.execute("s1", "SELECT name FROM users WHERE id < 0", 10, &c).await?;
|
|
assert!(empty.rows.is_empty());
|
|
assert_eq!(empty.columns.len(), 1, "columns of an empty result come from the prepared statement");
|
|
|
|
let upd = db.execute("s1", "UPDATE users SET note = 'n' WHERE id > 1", 10, &c).await?;
|
|
assert_eq!(upd.affected, Some(2.0));
|
|
|
|
let err = db.execute("s1", "SELECT nope FROM users", 10, &c).await.unwrap_err();
|
|
assert!(err.message.contains("nope"), "{}", err.message);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pages_sorts_filters_and_edits_in_a_transaction() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
let c = CancellationToken::new();
|
|
let sort = [SortKey { column: "name".into(), descending: true }];
|
|
let page = db.table_page("s1", "main", "users", Some("id > 0"), &sort, 0, 2, &c).await?;
|
|
assert_eq!(page.result.rows.len(), 2);
|
|
assert!(page.result.has_more);
|
|
assert_eq!(page.result.rows[0][1].as_deref(), Some("cy"));
|
|
let last = db.table_page("s1", "main", "users", None, &sort, 2, 2, &c).await?;
|
|
assert_eq!(last.result.rows.len(), 1);
|
|
assert!(!last.result.has_more);
|
|
|
|
let key = |id: &str| vec![CellValue { column: "id".into(), value: Some(id.into()) }];
|
|
let n = db
|
|
.apply_edits(
|
|
"s1",
|
|
"main",
|
|
"users",
|
|
&[
|
|
DbRowEdit::Update {
|
|
key: key("1"),
|
|
values: vec![CellValue { column: "note".into(), value: Some("it's".into()) }],
|
|
},
|
|
DbRowEdit::Delete { key: key("2") },
|
|
DbRowEdit::Insert { values: vec![CellValue { column: "name".into(), value: Some("dee".into()) }] },
|
|
],
|
|
)
|
|
.await?;
|
|
assert_eq!(n, 3);
|
|
let r = db.execute("s1", "SELECT name, note FROM users ORDER BY id", 10, &c).await?;
|
|
let rows: Vec<_> = r.rows.iter().map(|r| (r[0].clone().unwrap(), r[1].clone())).collect();
|
|
assert_eq!(rows, vec![("ann".into(), Some("it's".into())), ("cy".into(), Some("x".into())), ("dee".into(), None)]);
|
|
|
|
// A failing edit rolls back the whole batch.
|
|
let err = db
|
|
.apply_edits(
|
|
"s1",
|
|
"main",
|
|
"users",
|
|
&[
|
|
DbRowEdit::Delete { key: key("1") },
|
|
DbRowEdit::Insert { values: vec![CellValue { column: "name".into(), value: Some("cy".into()) }] },
|
|
],
|
|
)
|
|
.await;
|
|
assert!(err.is_err());
|
|
let count = db.execute("s1", "SELECT count(*) FROM users", 10, &c).await?;
|
|
assert_eq!(count.rows[0][0].as_deref(), Some("3"));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_connection_and_disconnect() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
let source = db.source("s1")?;
|
|
assert!(db.test(&source, None, None).await?.starts_with("SQLite "));
|
|
assert!(db.is_connected("s1"));
|
|
db.disconnect("s1").await;
|
|
assert!(!db.is_connected("s1"));
|
|
let missing = DataSource { file: Some("missing/nope.db".into()), ..source };
|
|
assert!(db.test(&missing, None, None).await.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
async fn count(db: &DbManager) -> String {
|
|
let r = db.execute("s1", "SELECT count(*) FROM users", 1, &CancellationToken::new()).await.unwrap();
|
|
r.rows[0][0].clone().unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manual_commit_keeps_a_transaction_until_commit_or_rollback() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
let c = CancellationToken::new();
|
|
db.set_manual_commit("s1", true).await;
|
|
db.execute("s1", "INSERT INTO users (name) VALUES ('dee')", 1, &c).await?;
|
|
// The session sees its own change.
|
|
assert_eq!(count(&db).await, "4");
|
|
db.finish_transaction("s1", false).await?;
|
|
assert_eq!(count(&db).await, "3");
|
|
|
|
db.execute("s1", "INSERT INTO users (name) VALUES ('eve')", 1, &c).await?;
|
|
db.finish_transaction("s1", true).await?;
|
|
// Leaving manual mode rolls back what is still open.
|
|
db.execute("s1", "DELETE FROM users", 1, &c).await?;
|
|
db.set_manual_commit("s1", false).await;
|
|
assert_eq!(count(&db).await, "4");
|
|
// Disconnecting with an open transaction does not hang.
|
|
db.set_manual_commit("s1", true).await;
|
|
db.execute("s1", "DELETE FROM users", 1, &c).await?;
|
|
db.disconnect("s1").await;
|
|
assert_eq!(count(&db).await, "4");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn targets_name_a_database_of_a_data_source() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
assert_eq!(ide_db::split_target("s1"), ("s1", None));
|
|
assert_eq!(ide_db::split_target("s1:app_dev"), ("s1", Some("app_dev")));
|
|
assert_eq!(ide_db::split_target("s1:a:b"), ("s1", Some("a:b")));
|
|
assert_eq!(ide_db::split_target("s1:"), ("s1", None));
|
|
assert_eq!(db.source("s1:other")?.database, "other");
|
|
assert_eq!(db.source("s1:other")?.id, "s1");
|
|
assert!(db.source("nope:other").is_err());
|
|
// Only PostgreSQL lists databases.
|
|
assert!(db.databases("s1").await.is_err());
|
|
db.disconnect_source("s1").await;
|
|
assert!(db.connected().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ddl_of_tables_and_views() -> IdeResult<()> {
|
|
let (_tmp, db) = setup().await;
|
|
assert_eq!(
|
|
db.ddl("s1", "main", "users").await?,
|
|
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, note TEXT, avatar BLOB);\n\n\
|
|
CREATE UNIQUE INDEX users_name ON users (name);\n"
|
|
);
|
|
assert_eq!(db.ddl("s1", "main", "named").await?, "CREATE VIEW named AS SELECT name FROM users;\n");
|
|
assert!(db.ddl("s1", "main", "nope").await.is_err());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn applies_driver_properties_and_the_startup_script_on_connect() -> IdeResult<()> {
|
|
let (tmp, db) = setup().await;
|
|
let mut s = db.sources()?.remove(0);
|
|
s.properties = vec![DbProperty { name: "cache_size".into(), value: "-1234".into() }];
|
|
s.options = DbOptions {
|
|
startup_script: "CREATE TEMP TABLE boot (x); INSERT INTO boot VALUES (7);".into(),
|
|
..Default::default()
|
|
};
|
|
db.save_sources(vec![s.clone()]).await?;
|
|
let c = CancellationToken::new();
|
|
let r = db.execute("s1", "SELECT (SELECT x FROM boot), (SELECT cache_size FROM pragma_cache_size)", 10, &c).await?;
|
|
assert_eq!(r.rows, vec![vec![Some("7".into()), Some("-1234".into())]]);
|
|
// URL only: the file comes from the URL.
|
|
let url = format!("jdbc:sqlite:{}", tmp.path().join("app.db").display());
|
|
db.save_sources(vec![DataSource { url_only: true, url, file: None, ..s }]).await?;
|
|
assert_eq!(db.schemas("s1").await?, vec!["main"]);
|
|
Ok(())
|
|
}
|