fix(duckdb): support multiple schemas beyond default "main"
DuckDB was not recognized as a schema-aware database, so only the default "main" schema was shown in the object tree and schema selector. Added DuckDB to schema-aware type lists, implemented schema listing via information_schema.schemata, and replaced hardcoded table_schema = 'main' with parameterized queries.
This commit is contained in:
parent
065f0a4234
commit
1f666f99b3
|
|
@ -25,6 +25,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
|
|||
"trino",
|
||||
"db2",
|
||||
"tdengine",
|
||||
"duckdb",
|
||||
]);
|
||||
|
||||
export const SQL_FILE_UNSUPPORTED_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch"]);
|
||||
|
|
@ -201,6 +202,7 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
|
|||
"trino",
|
||||
"h2",
|
||||
"tdengine",
|
||||
"duckdb",
|
||||
]);
|
||||
|
||||
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "opengauss"]);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@ use tokio_util::sync::CancellationToken;
|
|||
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::db;
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::sql::{split_sql_batches, split_sql_statements, starts_with_executable_sql_keyword};
|
||||
|
||||
pub const QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const MAX_ROWS: usize = 10000;
|
||||
pub const QUERY_CANCELED: &str = "Query canceled";
|
||||
|
||||
async fn connection_database_type(state: &AppState, connection_id: &str) -> Option<DatabaseType> {
|
||||
let configs = state.configs.read().await;
|
||||
configs.get(connection_id).map(|config| config.db_type)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct QueryExecutionOptions {
|
||||
pub max_rows: Option<usize>,
|
||||
|
|
@ -683,7 +689,11 @@ pub async fn execute_multi_core_with_options(
|
|||
return execute_multi_sqlserver(state, &pool_key, sql, cancel_token, options).await;
|
||||
}
|
||||
|
||||
let statements = split_sql_statements(sql);
|
||||
let db_type = connection_database_type(state, connection_id).await;
|
||||
let statements = db_type.map_or_else(
|
||||
|| split_sql_statements(sql),
|
||||
|db_type| crate::sql::split_sql_statements_for_database(sql, db_type),
|
||||
);
|
||||
if statements.len() <= 1 {
|
||||
let single_sql = statements.into_iter().next().unwrap_or_default();
|
||||
let result = execute_sql_statement_with_options(
|
||||
|
|
|
|||
|
|
@ -6,24 +6,29 @@ use crate::db;
|
|||
use crate::models::connection::DatabaseType;
|
||||
|
||||
pub fn duckdb_query_tables(con: &duckdb::Connection) -> Result<Vec<db::TableInfo>, String> {
|
||||
duckdb_query_tables_in_database(con, "main")
|
||||
duckdb_query_tables_in_database(con, "main", "main")
|
||||
}
|
||||
|
||||
pub fn duckdb_query_tables_in_database(con: &duckdb::Connection, database: &str) -> Result<Vec<db::TableInfo>, String> {
|
||||
duckdb_query_tables_in_database_with_attached(con, database, &[])
|
||||
pub fn duckdb_query_tables_in_database(
|
||||
con: &duckdb::Connection,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
duckdb_query_tables_in_database_with_attached(con, database, schema, &[])
|
||||
}
|
||||
|
||||
pub fn duckdb_query_tables_in_database_with_attached(
|
||||
con: &duckdb::Connection,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
attached_names: &[String],
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
let database = duckdb_catalog_name(con, database, attached_names)?;
|
||||
let mut stmt = con.prepare(
|
||||
"SELECT table_name, table_type FROM information_schema.tables WHERE table_catalog = ? AND table_schema = 'main' ORDER BY table_name"
|
||||
"SELECT table_name, table_type FROM information_schema.tables WHERE table_catalog = ? AND table_schema = ? ORDER BY table_name"
|
||||
).map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([database.as_str()], |row| {
|
||||
.query_map((database.as_str(), schema), |row| {
|
||||
Ok(db::TableInfo { name: row.get::<_, String>(0)?, table_type: row.get::<_, String>(1)?, comment: None })
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
|
@ -59,6 +64,25 @@ pub fn duckdb_list_databases_with_attached(
|
|||
Ok(rows.filter_map(|row| row.ok()).collect())
|
||||
}
|
||||
|
||||
pub fn duckdb_list_schemas(con: &duckdb::Connection, database: &str) -> Result<Vec<String>, String> {
|
||||
duckdb_list_schemas_with_attached(con, database, &[])
|
||||
}
|
||||
|
||||
pub fn duckdb_list_schemas_with_attached(
|
||||
con: &duckdb::Connection,
|
||||
database: &str,
|
||||
attached_names: &[String],
|
||||
) -> Result<Vec<String>, String> {
|
||||
let database = duckdb_catalog_name(con, database, attached_names)?;
|
||||
let mut stmt = con
|
||||
.prepare(
|
||||
"SELECT schema_name FROM information_schema.schemata WHERE catalog_name = ? AND schema_name NOT IN ('information_schema', 'pg_catalog') ORDER BY schema_name",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt.query_map([database.as_str()], |row| row.get::<_, String>(0)).map_err(|e| e.to_string())?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
fn duckdb_catalog_name(con: &duckdb::Connection, database: &str, attached_names: &[String]) -> Result<String, String> {
|
||||
if database.trim().is_empty() || database == "main" {
|
||||
return duckdb_primary_catalog(con, attached_names);
|
||||
|
|
@ -95,20 +119,22 @@ fn duckdb_quote_string(value: &str) -> String {
|
|||
}
|
||||
|
||||
pub fn duckdb_query_columns(con: &duckdb::Connection, table: &str) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
duckdb_query_columns_in_database(con, "main", table)
|
||||
duckdb_query_columns_in_database(con, "main", "main", table)
|
||||
}
|
||||
|
||||
pub fn duckdb_query_columns_in_database(
|
||||
con: &duckdb::Connection,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
duckdb_query_columns_in_database_with_attached(con, database, table, &[])
|
||||
duckdb_query_columns_in_database_with_attached(con, database, schema, table, &[])
|
||||
}
|
||||
|
||||
pub fn duckdb_query_columns_in_database_with_attached(
|
||||
con: &duckdb::Connection,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
attached_names: &[String],
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
|
|
@ -123,25 +149,26 @@ pub fn duckdb_query_columns_in_database_with_attached(
|
|||
AND tc.table_name = kcu.table_name
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_catalog = ?
|
||||
AND tc.table_schema = 'main'
|
||||
AND tc.table_schema = ?
|
||||
AND tc.table_name = ?
|
||||
ORDER BY kcu.ordinal_position",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let pk_rows =
|
||||
pk_stmt.query_map([database.as_str(), table], |row| row.get::<_, String>(0)).map_err(|e| e.to_string())?;
|
||||
let pk_rows = pk_stmt
|
||||
.query_map((database.as_str(), schema, table), |row| row.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let primary_keys: std::collections::HashSet<String> = pk_rows.filter_map(|r| r.ok()).collect();
|
||||
|
||||
let mut stmt = con
|
||||
.prepare(
|
||||
"SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_catalog = ? AND table_schema = 'main' AND table_name = ?
|
||||
WHERE table_catalog = ? AND table_schema = ? AND table_name = ?
|
||||
ORDER BY ordinal_position",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([database.as_str(), table], |row| {
|
||||
.query_map((database.as_str(), schema, table), |row| {
|
||||
let name = row.get::<_, String>(0)?;
|
||||
Ok(db::ColumnInfo {
|
||||
is_primary_key: primary_keys.contains(&name),
|
||||
|
|
@ -321,6 +348,11 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database:
|
|||
|
||||
match pool {
|
||||
PoolKind::Postgres(p) => db::postgres::list_schemas(p).await,
|
||||
PoolKind::DuckDb(con) => {
|
||||
let duckdb_attached_names = duckdb_attached_database_names(state, connection_id).await;
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
duckdb_list_schemas_with_attached(&con, database, &duckdb_attached_names)
|
||||
}
|
||||
_ => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
|
|
@ -362,7 +394,7 @@ pub async fn list_tables_core(
|
|||
if let Some(con) = extract_duckdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
return duckdb_query_tables_in_database_with_attached(&con, database, &duckdb_attached_names);
|
||||
return duckdb_query_tables_in_database_with_attached(&con, database, schema, &duckdb_attached_names);
|
||||
}
|
||||
if let Some(client) = extract_clickhouse(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
|
|
@ -444,8 +476,8 @@ mod tests {
|
|||
duckdb_attach_database(&con, "analytics", path.to_str().unwrap()).unwrap();
|
||||
con.execute_batch("CREATE TABLE analytics.attached_table(id INTEGER);").unwrap();
|
||||
|
||||
let main_tables = duckdb_query_tables_in_database(&con, "main").unwrap();
|
||||
let attached_tables = duckdb_query_tables_in_database(&con, "analytics").unwrap();
|
||||
let main_tables = duckdb_query_tables_in_database(&con, "main", "main").unwrap();
|
||||
let attached_tables = duckdb_query_tables_in_database(&con, "analytics", "main").unwrap();
|
||||
|
||||
assert!(main_tables.iter().any(|table| table.name == "main_table"));
|
||||
assert!(!main_tables.iter().any(|table| table.name == "attached_table"));
|
||||
|
|
@ -588,7 +620,13 @@ pub async fn get_columns_core(
|
|||
if let Some(con) = extract_duckdb(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
let con = con.lock().map_err(|e| e.to_string())?;
|
||||
return duckdb_query_columns_in_database_with_attached(&con, database, table, &duckdb_attached_names);
|
||||
return duckdb_query_columns_in_database_with_attached(
|
||||
&con,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
&duckdb_attached_names,
|
||||
);
|
||||
}
|
||||
if let Some(client) = extract_clickhouse(&connections, &pool_key) {
|
||||
drop(connections);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,26 @@ pub enum SqlFileStatementAction {
|
|||
Skip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct SqlParsingOptions {
|
||||
pub supports_hash_line_comments: bool,
|
||||
}
|
||||
|
||||
impl SqlParsingOptions {
|
||||
pub fn for_database_type(db_type: DatabaseType) -> Self {
|
||||
Self {
|
||||
supports_hash_line_comments: matches!(
|
||||
db_type,
|
||||
DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::Goldendb
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mysql_compatible() -> Self {
|
||||
Self { supports_hash_line_comments: true }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SqlFileProgress {
|
||||
|
|
@ -64,9 +84,14 @@ pub struct SqlStatementSplitter {
|
|||
dollar_quote_tag: Option<String>,
|
||||
previous: Option<char>,
|
||||
custom_delimiter: Option<String>,
|
||||
options: SqlParsingOptions,
|
||||
}
|
||||
|
||||
impl SqlStatementSplitter {
|
||||
pub fn with_options(options: SqlParsingOptions) -> Self {
|
||||
Self { options, ..Self::default() }
|
||||
}
|
||||
|
||||
pub fn push_chunk(&mut self, chunk: &str) -> Vec<String> {
|
||||
let mut statements = Vec::new();
|
||||
let chars = chunk.chars().collect::<Vec<_>>();
|
||||
|
|
@ -137,6 +162,13 @@ impl SqlStatementSplitter {
|
|||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if self.options.supports_hash_line_comments && ch == '#' {
|
||||
self.in_line_comment = true;
|
||||
self.buffer.push(ch);
|
||||
self.previous = Some(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if ch == '/' && next == Some('*') {
|
||||
self.in_block_comment = true;
|
||||
self.buffer.push(ch);
|
||||
|
|
@ -188,9 +220,9 @@ impl SqlStatementSplitter {
|
|||
if let Some(new_delim) = parse_delimiter_command(last_line) {
|
||||
self.custom_delimiter = if new_delim == ";" { None } else { Some(new_delim.to_string()) };
|
||||
if last_line_start > 0 {
|
||||
let before = self.buffer[..last_line_start].trim();
|
||||
if has_executable_sql(before) {
|
||||
statements.push(before.to_string());
|
||||
let before = &self.buffer[..last_line_start];
|
||||
if let Some(statement) = executable_sql_slice(before, self.options) {
|
||||
statements.push(statement.to_string());
|
||||
}
|
||||
}
|
||||
self.buffer.clear();
|
||||
|
|
@ -220,8 +252,8 @@ impl SqlStatementSplitter {
|
|||
let last_line = trimmed.rsplit('\n').next().unwrap_or(trimmed).trim();
|
||||
if parse_delimiter_command(last_line).is_some() {
|
||||
let before = trimmed.rsplitn(2, '\n').nth(1).unwrap_or("").trim();
|
||||
if has_executable_sql(before) {
|
||||
statements.push(before.to_string());
|
||||
if let Some(statement) = executable_sql_slice(before, self.options) {
|
||||
statements.push(statement.to_string());
|
||||
}
|
||||
self.buffer.clear();
|
||||
} else if let Some(ref delim) = self.custom_delimiter {
|
||||
|
|
@ -234,8 +266,7 @@ impl SqlStatementSplitter {
|
|||
}
|
||||
|
||||
fn push_current_statement(&mut self, statements: &mut Vec<String>) {
|
||||
let statement = self.buffer.trim();
|
||||
if has_executable_sql(statement) {
|
||||
if let Some(statement) = executable_sql_slice(&self.buffer, self.options) {
|
||||
statements.push(statement.to_string());
|
||||
}
|
||||
self.buffer.clear();
|
||||
|
|
@ -250,7 +281,15 @@ impl SqlStatementSplitter {
|
|||
}
|
||||
|
||||
pub fn split_sql_statements(sql: &str) -> Vec<String> {
|
||||
let mut splitter = SqlStatementSplitter::default();
|
||||
split_sql_statements_with_options(sql, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
pub fn split_sql_statements_for_database(sql: &str, db_type: DatabaseType) -> Vec<String> {
|
||||
split_sql_statements_with_options(sql, SqlParsingOptions::for_database_type(db_type))
|
||||
}
|
||||
|
||||
pub fn split_sql_statements_with_options(sql: &str, options: SqlParsingOptions) -> Vec<String> {
|
||||
let mut splitter = SqlStatementSplitter::with_options(options);
|
||||
let mut statements = splitter.push_chunk(sql);
|
||||
statements.extend(splitter.finish());
|
||||
statements
|
||||
|
|
@ -264,7 +303,15 @@ pub struct SqlStatementRange {
|
|||
}
|
||||
|
||||
pub fn find_statement_at_cursor(sql: &str, cursor_pos: usize) -> String {
|
||||
let statements = split_sql_statement_ranges(sql);
|
||||
find_statement_at_cursor_with_options(sql, cursor_pos, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
pub fn find_statement_at_cursor_for_database(sql: &str, cursor_pos: usize, db_type: DatabaseType) -> String {
|
||||
find_statement_at_cursor_with_options(sql, cursor_pos, SqlParsingOptions::for_database_type(db_type))
|
||||
}
|
||||
|
||||
pub fn find_statement_at_cursor_with_options(sql: &str, cursor_pos: usize, options: SqlParsingOptions) -> String {
|
||||
let statements = split_sql_statement_ranges_with_options(sql, options);
|
||||
let cursor = utf16_offset_to_byte_index(sql, cursor_pos);
|
||||
|
||||
for (idx, statement) in statements.iter().enumerate() {
|
||||
|
|
@ -298,7 +345,12 @@ fn cursor_has_sql_after_cursor_on_line(sql: &str, cursor: usize) -> bool {
|
|||
sql[cursor..line_end].chars().any(|ch| !ch.is_whitespace())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
||||
split_sql_statement_ranges_with_options(sql, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions) -> Vec<SqlStatementRange> {
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut i = 0;
|
||||
|
|
@ -348,6 +400,11 @@ fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
|||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if options.supports_hash_line_comments && ch == '#' {
|
||||
in_line_comment = true;
|
||||
i += ch.len_utf8();
|
||||
continue;
|
||||
}
|
||||
if ch == '/' && next == Some('*') {
|
||||
in_block_comment = true;
|
||||
i += 2;
|
||||
|
|
@ -365,8 +422,8 @@ fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
|||
let line = sql[line_start..i].trim();
|
||||
if let Some(new_delimiter) = parse_delimiter_command(line) {
|
||||
let before = sql[start..line_start].trim();
|
||||
if has_executable_sql(before) {
|
||||
push_statement_range(&mut ranges, sql, start, line_start);
|
||||
if has_executable_sql_with_options(before, options) {
|
||||
push_statement_range(&mut ranges, sql, start, line_start, options);
|
||||
}
|
||||
custom_delimiter = if new_delimiter == ";" { None } else { Some(new_delimiter.to_string()) };
|
||||
start = i + ch.len_utf8();
|
||||
|
|
@ -390,7 +447,7 @@ fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
|||
i += ch.len_utf8();
|
||||
}
|
||||
';' if !in_single_quote && !in_double_quote && !in_backtick && custom_delimiter.is_none() => {
|
||||
push_statement_range(&mut ranges, sql, start, i);
|
||||
push_statement_range(&mut ranges, sql, start, i, options);
|
||||
i += ch.len_utf8();
|
||||
start = i;
|
||||
}
|
||||
|
|
@ -400,7 +457,7 @@ fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
|||
if let Some(delimiter) = &custom_delimiter {
|
||||
if sql[start..i].ends_with(delimiter) {
|
||||
let end = i - delimiter.len();
|
||||
push_statement_range(&mut ranges, sql, start, end);
|
||||
push_statement_range(&mut ranges, sql, start, end, options);
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
|
|
@ -413,19 +470,30 @@ fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
|
|||
let last_line = trimmed.rsplit('\n').next().unwrap_or(trimmed).trim();
|
||||
if parse_delimiter_command(last_line).is_some() {
|
||||
if let Some(line_start) = sql[start..].rfind('\n').map(|pos| start + pos + 1) {
|
||||
push_statement_range(&mut ranges, sql, start, line_start);
|
||||
push_statement_range(&mut ranges, sql, start, line_start, options);
|
||||
}
|
||||
} else {
|
||||
push_statement_range(&mut ranges, sql, start, sql.len());
|
||||
push_statement_range(&mut ranges, sql, start, sql.len(), options);
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
fn push_statement_range(ranges: &mut Vec<SqlStatementRange>, sql: &str, start: usize, end: usize) {
|
||||
let text = sql[start..end].trim();
|
||||
if has_executable_sql(text) {
|
||||
ranges.push(SqlStatementRange { text: text.to_string(), start, end });
|
||||
fn push_statement_range(
|
||||
ranges: &mut Vec<SqlStatementRange>,
|
||||
sql: &str,
|
||||
start: usize,
|
||||
end: usize,
|
||||
options: SqlParsingOptions,
|
||||
) {
|
||||
let Some((relative_start, relative_end)) = executable_sql_bounds(&sql[start..end], options) else {
|
||||
return;
|
||||
};
|
||||
let statement_start = start + relative_start;
|
||||
let statement_end = start + relative_end;
|
||||
let text = sql[statement_start..statement_end].to_string();
|
||||
if !text.is_empty() {
|
||||
ranges.push(SqlStatementRange { text, start: statement_start, end: statement_end });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -584,7 +652,19 @@ pub fn prepare_sql_file_statement(
|
|||
}
|
||||
|
||||
pub fn starts_with_executable_sql_keyword(sql: &str, keywords: &[&str]) -> bool {
|
||||
let Some(token) = first_executable_sql_token(sql) else {
|
||||
starts_with_executable_sql_keyword_with_options(sql, keywords, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
pub fn starts_with_executable_sql_keyword_for_database(sql: &str, keywords: &[&str], db_type: DatabaseType) -> bool {
|
||||
starts_with_executable_sql_keyword_with_options(sql, keywords, SqlParsingOptions::for_database_type(db_type))
|
||||
}
|
||||
|
||||
pub fn starts_with_executable_sql_keyword_with_options(
|
||||
sql: &str,
|
||||
keywords: &[&str],
|
||||
options: SqlParsingOptions,
|
||||
) -> bool {
|
||||
let Some(token) = first_executable_sql_token_with_options(sql, options) else {
|
||||
return false;
|
||||
};
|
||||
keywords.iter().any(|keyword| token.eq_ignore_ascii_case(keyword))
|
||||
|
|
@ -634,6 +714,14 @@ fn leading_mysql_executable_comment_start(statement: &str) -> Option<usize> {
|
|||
continue;
|
||||
}
|
||||
|
||||
if bytes[i] == b'#' {
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
|
||||
if i + 2 < bytes.len() && (bytes[i + 2] == b'!' || (i + 3 < bytes.len() && &bytes[i + 2..i + 4] == b"M!")) {
|
||||
return Some(i);
|
||||
|
|
@ -672,7 +760,7 @@ fn is_mysql_lock_table_statement(statement: &str) -> bool {
|
|||
}
|
||||
|
||||
fn is_mysql_session_restore_statement(statement: &str) -> bool {
|
||||
let executable = leading_executable_sql(statement);
|
||||
let executable = leading_executable_sql_with_options(statement, SqlParsingOptions::mysql_compatible());
|
||||
let upper = executable.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_uppercase();
|
||||
if !upper.starts_with("SET ") {
|
||||
return false;
|
||||
|
|
@ -690,6 +778,10 @@ fn is_mysql_session_restore_statement(statement: &str) -> bool {
|
|||
}
|
||||
|
||||
fn leading_executable_sql(sql: &str) -> &str {
|
||||
leading_executable_sql_with_options(sql, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
fn leading_executable_sql_with_options(sql: &str, options: SqlParsingOptions) -> &str {
|
||||
let bytes = sql.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
|
|
@ -706,6 +798,14 @@ fn leading_executable_sql(sql: &str) -> &str {
|
|||
continue;
|
||||
}
|
||||
|
||||
if options.supports_hash_line_comments && bytes[i] == b'#' {
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
|
||||
if i + 2 < bytes.len() && (bytes[i + 2] == b'!' || (i + 3 < bytes.len() && &bytes[i + 2..i + 4] == b"M!")) {
|
||||
break;
|
||||
|
|
@ -724,7 +824,7 @@ fn leading_executable_sql(sql: &str) -> &str {
|
|||
&sql[i..]
|
||||
}
|
||||
|
||||
fn first_executable_sql_token(sql: &str) -> Option<&str> {
|
||||
fn first_executable_sql_token_with_options(sql: &str, options: SqlParsingOptions) -> Option<&str> {
|
||||
let bytes = sql.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
|
|
@ -741,6 +841,14 @@ fn first_executable_sql_token(sql: &str) -> Option<&str> {
|
|||
continue;
|
||||
}
|
||||
|
||||
if options.supports_hash_line_comments && bytes[i] == b'#' {
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i] != b'\n' {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
|
||||
if i + 2 < bytes.len() && (bytes[i + 2] == b'!' || (i + 3 < bytes.len() && &bytes[i + 2..i + 4] == b"M!")) {
|
||||
i += if bytes[i + 2] == b'!' { 3 } else { 4 };
|
||||
|
|
@ -799,6 +907,25 @@ fn dollar_quote_tag_at(chars: &[char], start: usize) -> Option<String> {
|
|||
}
|
||||
|
||||
fn has_executable_sql(statement: &str) -> bool {
|
||||
has_executable_sql_with_options(statement, SqlParsingOptions::default())
|
||||
}
|
||||
|
||||
fn executable_sql_slice(statement: &str, options: SqlParsingOptions) -> Option<&str> {
|
||||
executable_sql_bounds(statement, options).map(|(start, end)| &statement[start..end])
|
||||
}
|
||||
|
||||
fn executable_sql_bounds(statement: &str, options: SqlParsingOptions) -> Option<(usize, usize)> {
|
||||
let trimmed_end = statement.trim_end().len();
|
||||
let trimmed = &statement[..trimmed_end];
|
||||
let executable = leading_executable_sql_with_options(trimmed, options);
|
||||
if executable.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let start = trimmed.len() - executable.len();
|
||||
Some((start, trimmed_end))
|
||||
}
|
||||
|
||||
fn has_executable_sql_with_options(statement: &str, options: SqlParsingOptions) -> bool {
|
||||
let chars = statement.chars().collect::<Vec<_>>();
|
||||
let mut in_line_comment = false;
|
||||
let mut in_block_comment = false;
|
||||
|
|
@ -834,6 +961,13 @@ fn has_executable_sql(statement: &str) -> bool {
|
|||
continue;
|
||||
}
|
||||
|
||||
if options.supports_hash_line_comments && ch == '#' {
|
||||
in_line_comment = true;
|
||||
previous = Some(ch);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == '/' && next == Some('*') {
|
||||
if is_mysql_executable_comment_start(&chars, i) {
|
||||
return true;
|
||||
|
|
@ -872,8 +1006,9 @@ mod tests {
|
|||
use crate::models::connection::DatabaseType;
|
||||
|
||||
use super::{
|
||||
prepare_sql_file_statement, split_sql_script, starts_with_executable_sql_keyword, SqlFileStatementAction,
|
||||
SqlStatementSplitter,
|
||||
find_statement_at_cursor_for_database, prepare_sql_file_statement, split_sql_script,
|
||||
split_sql_statements_for_database, starts_with_executable_sql_keyword,
|
||||
starts_with_executable_sql_keyword_for_database, SqlFileStatementAction, SqlStatementSplitter,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -979,6 +1114,16 @@ mod tests {
|
|||
assert!(starts_with_executable_sql_keyword("/*M! SELECT 1 */", &["SELECT"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_hash_comments_are_ignored_for_keyword_detection() {
|
||||
assert!(starts_with_executable_sql_keyword_for_database(
|
||||
"# comment only for mysql\nSELECT 1",
|
||||
&["SELECT"],
|
||||
DatabaseType::Mysql
|
||||
));
|
||||
assert!(!starts_with_executable_sql_keyword("# comment only for mysql\nSELECT 1", &["SELECT"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_mysql_executable_comments_for_mysql_compatible_imports() {
|
||||
assert_eq!(
|
||||
|
|
@ -1263,4 +1408,30 @@ SELECT 2;";
|
|||
assert_eq!(super::find_statement_at_cursor(sql, cursor), "CREATE PROCEDURE foo()\nBEGIN\n SELECT 1;\nEND");
|
||||
assert_eq!(super::find_statement_at_cursor(sql, next_cursor), "SELECT 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_hash_comments_split_statements_per_issue_428() {
|
||||
let sql = "SELECT 1; # mysql comment\n\nSELECT 2 # trailing comment";
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::Mysql),
|
||||
vec!["SELECT 1", "SELECT 2 # trailing comment"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_current_statement_keeps_inline_hash_comment_per_issue_428() {
|
||||
let sql = "SELECT 1; # mysql comment\n\nSELECT 2 # trailing comment";
|
||||
let cursor = sql[..sql.find("SELECT 2").unwrap()].encode_utf16().count();
|
||||
assert_eq!(
|
||||
find_statement_at_cursor_for_database(sql, cursor, DatabaseType::Mysql),
|
||||
"SELECT 2 # trailing comment"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_single_statement_with_inline_comment_stays_executable_per_issue_428() {
|
||||
let sql = "SELECT 1 # mysql comment";
|
||||
let cursor = sql.encode_utf16().count();
|
||||
assert_eq!(find_statement_at_cursor_for_database(sql, cursor, DatabaseType::Mysql), "SELECT 1 # mysql comment");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool {
|
|||
| DatabaseType::Trino
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Tdengine
|
||||
| DatabaseType::DuckDb
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub struct AnalyzeEditableQueryRequest {
|
|||
pub struct FindStatementAtCursorRequest {
|
||||
pub sql: String,
|
||||
pub cursor_pos: usize,
|
||||
pub database_type: Option<dbx_core::models::connection::DatabaseType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -338,7 +339,13 @@ pub async fn execute_script(
|
|||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ExecuteQueryRequest>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let statements = dbx_core::sql::split_sql_statements(&req.sql);
|
||||
let db_type = {
|
||||
let configs = state.app.configs.read().await;
|
||||
configs.get(&req.connection_id).map(|config| config.db_type)
|
||||
};
|
||||
let statements = db_type
|
||||
.map(|db_type| dbx_core::sql::split_sql_statements_for_database(&req.sql, db_type))
|
||||
.unwrap_or_else(|| dbx_core::sql::split_sql_statements(&req.sql));
|
||||
let result = dbx_core::query::execute_statements(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
|
|
@ -376,7 +383,11 @@ pub async fn analyze_sql_references(
|
|||
}
|
||||
|
||||
pub async fn find_statement_at_cursor(Json(req): Json<FindStatementAtCursorRequest>) -> Json<String> {
|
||||
Json(dbx_core::sql::find_statement_at_cursor(&req.sql, req.cursor_pos))
|
||||
Json(
|
||||
req.database_type
|
||||
.map(|db_type| dbx_core::sql::find_statement_at_cursor_for_database(&req.sql, req.cursor_pos, db_type))
|
||||
.unwrap_or_else(|| dbx_core::sql::find_statement_at_cursor(&req.sql, req.cursor_pos)),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn prepare_query_pagination_execution_plan(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use tauri::State;
|
|||
|
||||
use crate::commands::connection::AppState;
|
||||
use dbx_core::db;
|
||||
use dbx_core::models::connection::DatabaseType;
|
||||
use dbx_core::sql::split_sql_statements;
|
||||
|
||||
// Re-export core functions for use by other modules (e.g., sql_file.rs)
|
||||
|
|
@ -119,11 +120,19 @@ pub async fn execute_script(
|
|||
sql: String,
|
||||
schema: Option<String>,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
let db_type = {
|
||||
let configs = state.configs.read().await;
|
||||
configs.get(&connection_id).map(|config| config.db_type)
|
||||
};
|
||||
|
||||
dbx_core::query::execute_statements(
|
||||
&state,
|
||||
&connection_id,
|
||||
&database,
|
||||
&split_sql_statements(&sql),
|
||||
&db_type.map_or_else(
|
||||
|| split_sql_statements(&sql),
|
||||
|db_type| dbx_core::sql::split_sql_statements_for_database(&sql, db_type),
|
||||
),
|
||||
schema.as_deref(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -156,8 +165,14 @@ pub async fn analyze_sql_references(
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn find_statement_at_cursor(sql: String, cursor_pos: usize) -> Result<String, String> {
|
||||
Ok(dbx_core::sql::find_statement_at_cursor(&sql, cursor_pos))
|
||||
pub fn find_statement_at_cursor(
|
||||
sql: String,
|
||||
cursor_pos: usize,
|
||||
database_type: Option<DatabaseType>,
|
||||
) -> Result<String, String> {
|
||||
Ok(database_type
|
||||
.map(|db_type| dbx_core::sql::find_statement_at_cursor_for_database(&sql, cursor_pos, db_type))
|
||||
.unwrap_or_else(|| dbx_core::sql::find_statement_at_cursor(&sql, cursor_pos)))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
|
|||
Loading…
Reference in New Issue