fix(ci): split checks and enforce warning-free Rust builds
This commit is contained in:
parent
dcbb1c69c5
commit
083ffe7a35
|
|
@ -18,11 +18,43 @@ jobs:
|
|||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
# The workspace intentionally contains platform-specific CLI/MCP packages for every release target.
|
||||
NPM_CONFIG_LOGLEVEL: error
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10.27.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22.13.0
|
||||
cache: pnpm
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm --filter dbx... install --frozen-lockfile
|
||||
|
||||
- name: Frontend check
|
||||
run: pnpm check
|
||||
|
||||
packages:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.packages == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
# Unsupported-platform package warnings are expected while validating cross-platform package metadata.
|
||||
NPM_CONFIG_LOGLEVEL: error
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10.27.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
|
@ -38,8 +70,8 @@ jobs:
|
|||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Frontend check
|
||||
run: pnpm check
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@1.97.1
|
||||
|
||||
- name: Node package tests
|
||||
run: pnpm test:packages
|
||||
|
|
@ -55,6 +87,9 @@ jobs:
|
|||
# sccache cannot reuse Cargo incremental artifacts, so avoid generating them in CI.
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
# The fast lane skips only bundled DuckDB while retaining the other default capabilities.
|
||||
RUST_FEATURE_MODE: ${{ github.event_name == 'pull_request' && needs.changes.outputs.rust_full != 'true' && 'fast' || 'full' }}
|
||||
RUST_FAST_FEATURES: dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
# Fork PRs cannot read repository secrets, so retain the GHA backend for them.
|
||||
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
|
||||
steps:
|
||||
|
|
@ -103,12 +138,19 @@ jobs:
|
|||
shared-key: ci-rust-fmt-clippy-x86_64-unknown-linux-gnu
|
||||
# Preserve completed dependency builds when a later lint step fails.
|
||||
cache-on-failure: true
|
||||
# PR caches are large and branch-scoped; restore them from main without saving per-PR copies.
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Cargo fmt check
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Cargo clippy
|
||||
run: cargo clippy --workspace --locked --all-targets
|
||||
run: |
|
||||
if [ "$RUST_FEATURE_MODE" = "fast" ]; then
|
||||
cargo clippy --workspace --locked --all-targets --no-default-features --features "$RUST_FAST_FEATURES" -- -D warnings
|
||||
else
|
||||
cargo clippy --workspace --locked --all-targets -- -D warnings
|
||||
fi
|
||||
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
|
|
@ -123,6 +165,9 @@ jobs:
|
|||
# sccache cannot reuse Cargo incremental artifacts, so avoid generating them in CI.
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
# The fast lane skips only bundled DuckDB while retaining the other default capabilities.
|
||||
RUST_FEATURE_MODE: ${{ github.event_name == 'pull_request' && needs.changes.outputs.rust_full != 'true' && 'fast' || 'full' }}
|
||||
RUST_FAST_FEATURES: dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
# Fork PRs cannot read repository secrets, so retain the GHA backend for them.
|
||||
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
|
||||
steps:
|
||||
|
|
@ -171,9 +216,16 @@ jobs:
|
|||
cache-workspace-crates: true
|
||||
# Preserve completed dependency builds when a later test step fails.
|
||||
cache-on-failure: true
|
||||
# PR caches are large and branch-scoped; restore them from main without saving per-PR copies.
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Cargo test
|
||||
run: cargo test --workspace --locked
|
||||
run: |
|
||||
if [ "$RUST_FEATURE_MODE" = "fast" ]; then
|
||||
cargo test --workspace --locked --no-default-features --features "$RUST_FAST_FEATURES"
|
||||
else
|
||||
cargo test --workspace --locked
|
||||
fi
|
||||
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
|
|
@ -228,7 +280,9 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
packages: ${{ steps.filter.outputs.packages }}
|
||||
rust: ${{ steps.filter.outputs.rust }}
|
||||
rust_full: ${{ steps.rust-mode.outputs.full }}
|
||||
jdbc: ${{ steps.filter.outputs.jdbc }}
|
||||
agents: ${{ steps.filter.outputs.agents }}
|
||||
steps:
|
||||
|
|
@ -237,7 +291,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed areas
|
||||
uses: dorny/paths-filter@v3
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
|
@ -249,6 +303,17 @@ jobs:
|
|||
- '.oxfmtrc.json'
|
||||
- 'scripts/run-check.mjs'
|
||||
- '.github/workflows/ci.yml'
|
||||
packages:
|
||||
- 'packages/cli/**'
|
||||
- 'packages/mcp-server/**'
|
||||
- 'crates/dbx-cli/**'
|
||||
- 'crates/dbx-mcp/**'
|
||||
- 'scripts/verify-package-install.mjs'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.github/workflows/ci.yml'
|
||||
rust:
|
||||
- 'crates/**'
|
||||
- 'src-tauri/**'
|
||||
|
|
@ -261,6 +326,42 @@ jobs:
|
|||
agents:
|
||||
- 'agents/**'
|
||||
|
||||
- name: Select Rust feature coverage
|
||||
id: rust-mode
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||
RUST_CHANGED: ${{ steps.filter.outputs.rust }}
|
||||
run: |
|
||||
full=false
|
||||
|
||||
if [ "$RUST_CHANGED" = "true" ] && [ "${{ github.event_name }}" = "push" ]; then
|
||||
full=true
|
||||
elif [ "$RUST_CHANGED" = "true" ]; then
|
||||
if [ -z "$BASE_SHA" ] || echo "$BASE_SHA" | grep -Eq '^0+$'; then
|
||||
BASE_SHA="HEAD~1"
|
||||
fi
|
||||
|
||||
while IFS= read -r file; do
|
||||
case "$file" in
|
||||
Cargo.toml|Cargo.lock|rust-toolchain*|.github/workflows/ci.yml|*/Cargo.toml)
|
||||
full=true
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
# Feature-gated DuckDB code must keep the full default-feature checks on its PR.
|
||||
if echo "$file" | grep -Eqi 'duckdb' \
|
||||
|| { [ -f "$file" ] && grep -Eqi 'duckdb-bundled|cfg[^[:cntrl:]]*duckdb|feature[^[:cntrl:]]*duckdb' "$file"; } \
|
||||
|| git diff "$BASE_SHA" HEAD -- "$file" | grep -Eqi 'duckdb-bundled|cfg[^[:cntrl:]]*duckdb|feature[^[:cntrl:]]*duckdb'; then
|
||||
full=true
|
||||
break
|
||||
fi
|
||||
done < <(git diff --name-only "$BASE_SHA" HEAD -- Cargo.toml Cargo.lock 'rust-toolchain*' crates src-tauri .github/workflows/ci.yml)
|
||||
fi
|
||||
|
||||
echo "full=$full" >> "$GITHUB_OUTPUT"
|
||||
|
||||
agents:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.agents == 'true'
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
too-many-arguments-threshold = 10
|
||||
|
|
@ -3139,9 +3139,9 @@ fn clone_pool_kind(pool: &PoolKind) -> PoolKind {
|
|||
#[cfg(feature = "duckdb-bundled")]
|
||||
PoolKind::DuckDbWorker(client) => PoolKind::DuckDbWorker(client.clone()),
|
||||
#[cfg(not(feature = "duckdb-bundled"))]
|
||||
PoolKind::DuckDb(con) => PoolKind::DuckDb(con.clone()),
|
||||
PoolKind::DuckDb(_) => PoolKind::DuckDb(()),
|
||||
#[cfg(not(feature = "duckdb-bundled"))]
|
||||
PoolKind::DuckDbWorker(client) => PoolKind::DuckDbWorker(client.clone()),
|
||||
PoolKind::DuckDbWorker(_) => PoolKind::DuckDbWorker(()),
|
||||
PoolKind::MongoDb(client) => PoolKind::MongoDb(client.clone()),
|
||||
PoolKind::ClickHouse(client) => PoolKind::ClickHouse(client.clone()),
|
||||
PoolKind::SqlServer(client) => PoolKind::SqlServer(client.clone()),
|
||||
|
|
@ -3149,7 +3149,10 @@ fn clone_pool_kind(pool: &PoolKind) -> PoolKind {
|
|||
PoolKind::VectorDb(client) => PoolKind::VectorDb(client.clone()),
|
||||
PoolKind::InfluxDb(client) => PoolKind::InfluxDb(client.clone()),
|
||||
PoolKind::Agent(client) => PoolKind::Agent(client.clone()),
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
PoolKind::ExternalTabular(ext) => PoolKind::ExternalTabular(ext.clone()),
|
||||
#[cfg(not(feature = "duckdb-bundled"))]
|
||||
PoolKind::ExternalTabular(_) => PoolKind::ExternalTabular(()),
|
||||
PoolKind::ExternalDriver { driver_id, config, session } => {
|
||||
PoolKind::ExternalDriver { driver_id: driver_id.clone(), config: config.clone(), session: session.clone() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ impl DuckDbConnection {
|
|||
self.draining.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
// Preserve PoisonError ownership so close_connection can still release the contained DuckDB handle.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn into_inner(self) -> std::sync::LockResult<duckdb::Connection> {
|
||||
self.connection.into_inner()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,13 +183,15 @@ fn find_as_keyword_outside_quotes(sql: &str) -> Option<usize> {
|
|||
}
|
||||
in_double = !in_double;
|
||||
}
|
||||
b'a' | b'A' if !in_single && !in_double && i + 1 < bytes.len() => {
|
||||
if (bytes[i + 1] == b's' || bytes[i + 1] == b'S')
|
||||
b'a' | b'A'
|
||||
if !in_single
|
||||
&& !in_double
|
||||
&& i + 1 < bytes.len()
|
||||
&& (bytes[i + 1] == b's' || bytes[i + 1] == b'S')
|
||||
&& is_sql_word_boundary(bytes.get(i.wrapping_sub(1)).copied())
|
||||
&& is_sql_word_boundary(bytes.get(i + 2).copied())
|
||||
{
|
||||
return Some(i);
|
||||
}
|
||||
&& is_sql_word_boundary(bytes.get(i + 2).copied()) =>
|
||||
{
|
||||
return Some(i);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,7 +513,8 @@ impl DuckDbWorkerClient {
|
|||
let mut pending = self.inner.pending.lock().await;
|
||||
let ids = pending
|
||||
.iter()
|
||||
.filter_map(|(id, request)| (request.generation == generation).then(|| id.clone()))
|
||||
.filter(|&(_id, request)| request.generation == generation)
|
||||
.map(|(id, _request)| id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
ids.into_iter().filter_map(|id| pending.remove(&id).map(|request| (id, request.sender))).collect::<Vec<_>>()
|
||||
};
|
||||
|
|
@ -631,7 +632,8 @@ fn spawn_stdout_reader(
|
|||
let mut pending = pending.lock().await;
|
||||
let ids = pending
|
||||
.iter()
|
||||
.filter_map(|(id, request)| (request.generation == generation).then(|| id.clone()))
|
||||
.filter(|&(_id, request)| request.generation == generation)
|
||||
.map(|(id, _request)| id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
ids.into_iter().filter_map(|id| pending.remove(&id).map(|request| (id, request.sender))).collect::<Vec<_>>()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ fn build_query_url(client: &InfluxdbClient, database: Option<&str>, sql: &str) -
|
|||
}
|
||||
let encoded_sql = utf8_percent_encode(sql, NON_ALPHANUMERIC);
|
||||
params.push(format!("q={encoded_sql}"));
|
||||
format!("{}/query?{}", &client.base_url, params.join("&").as_str())
|
||||
format!("{}/query?{}", client.base_url, params.join("&").as_str())
|
||||
}
|
||||
|
||||
fn encode_url_param(value: &str) -> String {
|
||||
|
|
@ -231,12 +231,12 @@ fn encode_url_param(value: &str) -> String {
|
|||
|
||||
fn build_v2_buckets_url(client: &InfluxdbClient, offset: usize) -> Result<String, String> {
|
||||
let org = client.org.as_deref().ok_or_else(|| "InfluxDB 2.x organization is required".to_string())?;
|
||||
Ok(format!("{}/api/v2/buckets?org={}&limit=100&offset={offset}", &client.base_url, encode_url_param(org)))
|
||||
Ok(format!("{}/api/v2/buckets?org={}&limit=100&offset={offset}", client.base_url, encode_url_param(org)))
|
||||
}
|
||||
|
||||
fn build_v2_query_url(client: &InfluxdbClient) -> Result<String, String> {
|
||||
let org = client.org.as_deref().ok_or_else(|| "InfluxDB 2.x organization is required".to_string())?;
|
||||
Ok(format!("{}/api/v2/query?org={}", &client.base_url, encode_url_param(org)))
|
||||
Ok(format!("{}/api/v2/query?org={}", client.base_url, encode_url_param(org)))
|
||||
}
|
||||
|
||||
fn build_request(client: &InfluxdbClient, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
|
|
|
|||
|
|
@ -1692,7 +1692,7 @@ fn parse_legacy_mongo_date_display(value: &str) -> Option<DateTime> {
|
|||
if !millis.chars().all(|ch| ch.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
DateTime::parse_rfc3339_str(format!("{date}T{seconds}.{}Z", format!("{millis:0<3}"))).ok()
|
||||
DateTime::parse_rfc3339_str(format!("{date}T{seconds}.{millis:0<3}Z")).ok()
|
||||
}
|
||||
|
||||
fn parse_extended_json_date(obj: &serde_json::Map<String, serde_json::Value>) -> Option<DateTime> {
|
||||
|
|
@ -2003,7 +2003,7 @@ mod tests {
|
|||
#[test]
|
||||
fn document_id_filters_try_object_id_then_string_for_hex_ids() {
|
||||
let id = "507f1f77bcf86cd799439011";
|
||||
let filters = document_id_filters(&id);
|
||||
let filters = document_id_filters(id);
|
||||
|
||||
assert_eq!(filters.len(), 2);
|
||||
assert!(matches!(filters[0].get("_id"), Some(Bson::ObjectId(_))));
|
||||
|
|
@ -2013,7 +2013,7 @@ mod tests {
|
|||
#[test]
|
||||
fn document_id_filters_use_string_only_for_non_hex_ids() {
|
||||
let id = "customer-42";
|
||||
let filters = document_id_filters(&id);
|
||||
let filters = document_id_filters(id);
|
||||
|
||||
assert_eq!(filters.len(), 1);
|
||||
assert!(matches!(filters[0].get("_id"), Some(Bson::String(value)) if value == id));
|
||||
|
|
|
|||
|
|
@ -4547,7 +4547,6 @@ UNIQUE KEY(`tenant_id`, `name``part`)
|
|||
#[test]
|
||||
fn mysql_tcp_keepalive_uses_milliseconds_not_seconds() {
|
||||
assert_eq!(MYSQL_TCP_KEEPALIVE_MS, 30_000);
|
||||
assert!(MYSQL_TCP_KEEPALIVE_MS >= 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1400,7 +1400,7 @@ fn sqlite_completion_tables(
|
|||
if type_filters.is_empty() {
|
||||
type_filters.extend(["table", "view"]);
|
||||
}
|
||||
let placeholders = std::iter::repeat("?").take(type_filters.len()).collect::<Vec<_>>().join(", ");
|
||||
let placeholders = std::iter::repeat_n("?", type_filters.len()).collect::<Vec<_>>().join(", ");
|
||||
let sql = format!(
|
||||
"SELECT name, type FROM {}.sqlite_master WHERE type IN ({}) AND name NOT LIKE 'sqlite_%' AND {} ORDER BY name LIMIT ?",
|
||||
sqlite_quote_ident(&schema),
|
||||
|
|
|
|||
|
|
@ -1315,7 +1315,7 @@ fn sqlserver_completion_assistant_sql(request: &crate::types::CompletionAssistan
|
|||
}
|
||||
|
||||
if queries.is_empty() {
|
||||
format!("SELECT TOP (0) CAST('' AS NVARCHAR(128)) AS name, CAST('' AS NVARCHAR(128)) AS schema_name, CAST('' AS NVARCHAR(60)) AS object_type, CAST(NULL AS NVARCHAR(128)) AS parent_schema, CAST(NULL AS NVARCHAR(128)) AS parent_name, CAST(NULL AS NVARCHAR(MAX)) AS object_comment, CAST(NULL AS NVARCHAR(128)) AS data_type")
|
||||
"SELECT TOP (0) CAST('' AS NVARCHAR(128)) AS name, CAST('' AS NVARCHAR(128)) AS schema_name, CAST('' AS NVARCHAR(60)) AS object_type, CAST(NULL AS NVARCHAR(128)) AS parent_schema, CAST(NULL AS NVARCHAR(128)) AS parent_name, CAST(NULL AS NVARCHAR(MAX)) AS object_comment, CAST(NULL AS NVARCHAR(128)) AS data_type".to_string()
|
||||
} else if queries.len() == 1 {
|
||||
format!("SELECT * FROM ({}) AS dbx_completion ORDER BY name", queries.remove(0))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ async fn list_milvus_databases(client: &VectorClient) -> Result<Vec<String>, Str
|
|||
if !names.iter().any(|name| name == "default") {
|
||||
names.push("default".to_string());
|
||||
}
|
||||
names.sort_by(|a, b| a.cmp(b));
|
||||
names.sort();
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
|
|||
Err(error) => Err(error),
|
||||
},
|
||||
PoolKind::Elasticsearch(_) => Ok(vec!["default".to_string()]),
|
||||
PoolKind::VectorDb(client) => vector_driver::list_databases(&client).await,
|
||||
PoolKind::VectorDb(client) => vector_driver::list_databases(client).await,
|
||||
PoolKind::Agent(client) => {
|
||||
let mut client = client.lock().await;
|
||||
match client.mongo_list_databases::<Vec<serde_json::Value>>().await {
|
||||
|
|
@ -216,7 +216,7 @@ pub async fn list_collections_core(
|
|||
.map(|n| CollectionInfo { name: n.clone(), id: n, dimension: None, kind: None, bucket_name: None })
|
||||
.collect())
|
||||
}
|
||||
PoolKind::VectorDb(client) => vector_driver::list_collections_with_db(&client, database).await,
|
||||
PoolKind::VectorDb(client) => vector_driver::list_collections_with_db(client, database).await,
|
||||
PoolKind::Agent(client) => {
|
||||
let mut client = client.lock().await;
|
||||
let names = sort_names(client.mongo_list_collections(database).await?);
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ impl MessageQueueAdmin for PulsarAdmin {
|
|||
|
||||
async fn list_tenants(&self) -> Result<Vec<TenantInfo>, String> {
|
||||
let names: Vec<String> = self.get_json(&self.profile.tenants_path()).await?;
|
||||
stream::iter(names.into_iter())
|
||||
stream::iter(names)
|
||||
.map(|name| async move {
|
||||
match self.get_tenant(&name).await {
|
||||
Ok(tenant) => Ok(tenant),
|
||||
|
|
@ -214,7 +214,7 @@ impl MessageQueueAdmin for PulsarAdmin {
|
|||
async fn list_namespaces(&self, tenant: &str) -> Result<Vec<NamespaceInfo>, String> {
|
||||
// Returns fully-qualified `tenant/namespace` strings.
|
||||
let names: Vec<String> = self.get_json(&self.profile.namespaces_path(tenant)).await?;
|
||||
stream::iter(names.into_iter())
|
||||
stream::iter(names)
|
||||
.map(|full| async move {
|
||||
let namespace = full.rsplit('/').next().unwrap_or(&full).to_string();
|
||||
let admin_roles = match self.namespace_admin_roles(tenant, &namespace).await {
|
||||
|
|
@ -282,7 +282,7 @@ impl MessageQueueAdmin for PulsarAdmin {
|
|||
};
|
||||
let partitioned_set: std::collections::HashSet<String> = partitioned.iter().cloned().collect();
|
||||
let domain = if persistent { "persistent" } else { "non-persistent" };
|
||||
let partitioned_topics = stream::iter(partitioned.into_iter())
|
||||
let partitioned_topics = stream::iter(partitioned)
|
||||
.map(|full| async move {
|
||||
let partitions = self.partition_count_for_topic(domain, &full).await?;
|
||||
Ok::<_, String>(TopicInfo {
|
||||
|
|
|
|||
|
|
@ -16,22 +16,16 @@ use super::util::truncate;
|
|||
/// project's encrypted connection-secrets / keychain path and are never logged.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
#[derive(Default)]
|
||||
pub enum MqAuth {
|
||||
#[default]
|
||||
None,
|
||||
/// Pulsar JWT bearer token.
|
||||
Token {
|
||||
token: String,
|
||||
},
|
||||
Token { token: String },
|
||||
/// HTTP Basic auth.
|
||||
Basic {
|
||||
username: String,
|
||||
password: String,
|
||||
},
|
||||
Basic { username: String, password: String },
|
||||
/// Arbitrary API key header, e.g. `Authorization: <value>` or a custom header.
|
||||
ApiKey {
|
||||
header: String,
|
||||
value: String,
|
||||
},
|
||||
ApiKey { header: String, value: String },
|
||||
/// OAuth2 client-credentials flow (Pulsar's `oauth2` auth plugin).
|
||||
OAuth2 {
|
||||
issuer_url: String,
|
||||
|
|
@ -44,12 +38,6 @@ pub enum MqAuth {
|
|||
},
|
||||
}
|
||||
|
||||
impl Default for MqAuth {
|
||||
fn default() -> Self {
|
||||
MqAuth::None
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches an OAuth2 access token with its expiry so repeated requests reuse it.
|
||||
/// Also tracks in-flight token requests to prevent concurrent fetches.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ impl MqSystemKind {
|
|||
/// features are hidden rather than failing at call time.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Default)]
|
||||
pub struct MqCapabilities {
|
||||
pub supports_tenants: bool,
|
||||
pub supports_namespaces: bool,
|
||||
|
|
@ -54,31 +55,6 @@ pub struct MqCapabilities {
|
|||
pub supports_send_message: bool,
|
||||
}
|
||||
|
||||
impl Default for MqCapabilities {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
supports_tenants: false,
|
||||
supports_namespaces: false,
|
||||
supports_partitioned_topics: false,
|
||||
supports_subscriptions: false,
|
||||
supports_create_subscription: false,
|
||||
supports_reset_cursor: false,
|
||||
supports_skip_messages: false,
|
||||
supports_clear_backlog: false,
|
||||
supports_peek_messages: false,
|
||||
supports_expire_messages: false,
|
||||
supports_rate_limits: false,
|
||||
supports_backlog_quota: false,
|
||||
supports_retention: false,
|
||||
supports_permissions: false,
|
||||
supports_geo_replication: false,
|
||||
supports_token_management: false,
|
||||
supports_raw_admin_api: false,
|
||||
supports_send_message: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a connectivity test, including the detected server version and how
|
||||
/// it was determined (probe vs. fallback) so the UI can warn appropriately.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ use crate::models::connection::ConnectionConfig;
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||
#[derive(Default)]
|
||||
pub enum NacosAuthConfig {
|
||||
#[default]
|
||||
None,
|
||||
UsernamePassword { username: String, password: String },
|
||||
}
|
||||
|
||||
impl Default for NacosAuthConfig {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
UsernamePassword {
|
||||
username: String,
|
||||
password: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -1063,7 +1063,9 @@ fn push_optional(params: &mut Vec<(String, String)>, key: &str, value: Option<St
|
|||
}
|
||||
}
|
||||
|
||||
fn build_publish_forms(req: NacosConfigUpsert, namespace: String) -> (Vec<(String, String)>, Vec<(String, String)>) {
|
||||
type NacosForm = Vec<(String, String)>;
|
||||
|
||||
fn build_publish_forms(req: NacosConfigUpsert, namespace: String) -> (NacosForm, NacosForm) {
|
||||
let mut v3_form = vec![
|
||||
("dataId".to_string(), req.data_id.clone()),
|
||||
("groupName".to_string(), req.group.clone()),
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ use crate::nacos::port::NacosAdmin;
|
|||
pub use crate::nacos::config::{NacosAdminConfig as NacosConfig, NacosAuthConfig};
|
||||
pub use crate::nacos::types::*;
|
||||
|
||||
type NacosAdminEntry = (NacosAdminConfig, Arc<dyn NacosAdmin>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NacosAdminRegistry {
|
||||
instances: RwLock<HashMap<String, (NacosAdminConfig, Arc<dyn NacosAdmin>)>>,
|
||||
instances: RwLock<HashMap<String, NacosAdminEntry>>,
|
||||
build_locks: RwLock<HashMap<String, Arc<Mutex<()>>>>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ pub async fn nacos_raw_request_core(
|
|||
req: NacosRawRequest,
|
||||
) -> Result<NacosRawResponse, String> {
|
||||
crate::nacos::http::validate_raw_api_path(&req.path)?;
|
||||
if req.method.to_ascii_uppercase() != "GET" {
|
||||
if !req.method.eq_ignore_ascii_case("GET") {
|
||||
ensure_connection_writable(state, conn_id, "Run mutating Nacos raw request").await?;
|
||||
}
|
||||
let admin = get_admin(state, conn_id).await?;
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ fn informix_view_definition(schema: Option<&str>, name: &str, source: &str) -> (
|
|||
let view_name = captures.get(1).unwrap();
|
||||
let target_name = strip_informix_owner_qualifiers(view_name.as_str(), schema);
|
||||
let body = strip_informix_owner_qualifiers(&trimmed[view_name.end()..], schema);
|
||||
return (target_name.trim().to_string(), body);
|
||||
(target_name.trim().to_string(), body)
|
||||
} else {
|
||||
let body = strip_informix_owner_qualifiers(trimmed, schema);
|
||||
(informix_identifier(name), format!(" AS\n{body}"))
|
||||
|
|
@ -579,7 +579,7 @@ fn informix_owner_qualifier_replacement<'a>(source: &'a str, start: usize, schem
|
|||
return None;
|
||||
}
|
||||
let ident_start = skip_sql_whitespace(source, dot + 1);
|
||||
read_informix_identifier_text(source, ident_start).map(|(ident_end, ident_text)| (ident_end, ident_text))
|
||||
read_informix_identifier_text(source, ident_start)
|
||||
}
|
||||
|
||||
fn sql_single_quoted_literal_end(source: &str, start: usize) -> Option<usize> {
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ fn append_sql_target_safety_text(chars: &[char], result: &mut SqlTargetSafetyTex
|
|||
continue;
|
||||
}
|
||||
if ch == '/' && next == Some('*') {
|
||||
if let Some((body, close_index)) = mysql_executable_comment_body(&chars, index) {
|
||||
if let Some((body, close_index)) = mysql_executable_comment_body(chars, index) {
|
||||
result.text.push(' ');
|
||||
let body_chars: Vec<char> = body.chars().collect();
|
||||
append_sql_target_safety_text(&body_chars, result);
|
||||
|
|
@ -423,7 +423,7 @@ fn append_sql_target_safety_text(chars: &[char], result: &mut SqlTargetSafetyTex
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if let Some((tag, tag_len)) = dollar_quote_tag_at(&chars, index) {
|
||||
if let Some((tag, tag_len)) = dollar_quote_tag_at(chars, index) {
|
||||
index += tag_len;
|
||||
while index + tag_len <= chars.len() && !chars[index..index + tag_len].iter().collect::<String>().eq(&tag) {
|
||||
index += 1;
|
||||
|
|
@ -433,7 +433,7 @@ fn append_sql_target_safety_text(chars: &[char], result: &mut SqlTargetSafetyTex
|
|||
continue;
|
||||
}
|
||||
if ch == '\'' {
|
||||
index = skip_string_literal(&chars, index, '\'', '\'');
|
||||
index = skip_string_literal(chars, index, '\'', '\'');
|
||||
result.text.push(' ');
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,21 +12,16 @@ pub struct RunningQueryDiagnostics {
|
|||
|
||||
type InterruptFn = Box<dyn Fn() + Send + 'static>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum RunningTaskKind {
|
||||
Query,
|
||||
Count,
|
||||
Explain,
|
||||
Export,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Default for RunningTaskKind {
|
||||
fn default() -> Self {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RunningTaskMetadata {
|
||||
pub kind: RunningTaskKind,
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ fn effective_row_limit(format: &str, request: &QueryResultExportRequest) -> Opti
|
|||
}
|
||||
|
||||
fn xlsx_hard_limit_active(format: &str, request: &QueryResultExportRequest) -> bool {
|
||||
format == "xlsx" && request.row_limit.map_or(true, |limit| limit > XLSX_MAX_DATA_ROWS)
|
||||
format == "xlsx" && request.row_limit.is_none_or(|limit| limit > XLSX_MAX_DATA_ROWS)
|
||||
}
|
||||
|
||||
fn format_text_export_header(format: &str, columns: &[String]) -> String {
|
||||
|
|
|
|||
|
|
@ -963,7 +963,7 @@ fn add_standard_limit(
|
|||
offset: usize,
|
||||
dedup_projection_count: Option<usize>,
|
||||
) -> String {
|
||||
let order_sql = dedup_projection_count.map_or(String::new(), |count| format_positional_order_by(count));
|
||||
let order_sql = dedup_projection_count.map_or(String::new(), format_positional_order_by);
|
||||
|
||||
if has_top_level_limit(statement) {
|
||||
if !order_sql.is_empty() {
|
||||
|
|
|
|||
|
|
@ -774,12 +774,10 @@ fn command_may_target_first_key(argv: &[String]) -> bool {
|
|||
if argv.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
match argv[0].to_ascii_uppercase().as_str() {
|
||||
"PING" | "INFO" | "DBSIZE" | "TIME" | "ROLE" | "CLUSTER" | "CLIENT" | "COMMAND" | "HELLO" | "AUTH" | "QUIT" => {
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
!matches!(
|
||||
argv[0].to_ascii_uppercase().as_str(),
|
||||
"PING" | "INFO" | "DBSIZE" | "TIME" | "ROLE" | "CLUSTER" | "CLIENT" | "COMMAND" | "HELLO" | "AUTH" | "QUIT"
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn redis_load_more_in_db_core(
|
||||
|
|
|
|||
|
|
@ -1611,7 +1611,7 @@ fn oracle_object_info_can_have_table_comment(object: &db::ObjectInfo) -> bool {
|
|||
}
|
||||
|
||||
fn oracle_type_is_table_or_view(value: &str) -> bool {
|
||||
let normalized = value.to_ascii_uppercase().replace(' ', "_").replace('-', "_");
|
||||
let normalized = value.to_ascii_uppercase().replace([' ', '-'], "_");
|
||||
matches!(normalized.as_str(), "TABLE" | "BASE_TABLE" | "VIEW")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -395,9 +395,9 @@ impl SqlStatementSplitter {
|
|||
self.buffer.push(ch);
|
||||
}
|
||||
';' if !self.in_single_quote && !self.in_double_quote && !self.in_backtick => {
|
||||
if self.options.profile.supports_custom_delimiter_commands && self.on_delimiter_line() {
|
||||
self.buffer.push(ch);
|
||||
} else if self.custom_delimiter.is_some() {
|
||||
if (self.options.profile.supports_custom_delimiter_commands && self.on_delimiter_line())
|
||||
|| self.custom_delimiter.is_some()
|
||||
{
|
||||
self.buffer.push(ch);
|
||||
} else if self.options.profile.supports_mysql_routine_blocks
|
||||
&& starts_with_mysql_routine_block(&self.buffer)
|
||||
|
|
@ -1567,6 +1567,7 @@ fn parse_insert_values_tail(tail: &str) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SqlScanner {
|
||||
profile: SqlDialectProfile,
|
||||
in_single_quote: bool,
|
||||
|
|
@ -1609,9 +1610,7 @@ impl SqlScanner {
|
|||
}
|
||||
|
||||
if !self.in_single_quote && !self.in_double_quote && !self.in_backtick {
|
||||
if ch == '-' && next == Some('-') {
|
||||
self.in_line_comment = true;
|
||||
} else if self.profile.supports_hash_line_comments && ch == '#' {
|
||||
if (ch == '-' && next == Some('-')) || (self.profile.supports_hash_line_comments && ch == '#') {
|
||||
self.in_line_comment = true;
|
||||
} else if ch == '/' && next == Some('*') {
|
||||
self.in_block_comment = true;
|
||||
|
|
@ -1647,21 +1646,6 @@ impl SqlScanner {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for SqlScanner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
profile: SqlDialectProfile::default(),
|
||||
in_single_quote: false,
|
||||
in_double_quote: false,
|
||||
in_backtick: false,
|
||||
in_line_comment: false,
|
||||
in_block_comment: false,
|
||||
dollar_quote_tag: None,
|
||||
previous: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keyword_at(sql: &str, idx: usize, keyword: &str) -> bool {
|
||||
let end = idx + keyword.len();
|
||||
sql.get(idx..end).is_some_and(|candidate| candidate.eq_ignore_ascii_case(keyword))
|
||||
|
|
|
|||
|
|
@ -158,25 +158,21 @@ fn redact_sensitive_assignments(sql: &str) -> String {
|
|||
while j < chars.len() && chars[j].is_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
if j < chars.len() && (chars[j] == '=' || chars[j] == ':') {
|
||||
if is_sensitive_key(&key) {
|
||||
out.push_str(&key);
|
||||
for k in i..j {
|
||||
out.push(chars[k]);
|
||||
}
|
||||
if j < chars.len() && (chars[j] == '=' || chars[j] == ':') && is_sensitive_key(&key) {
|
||||
out.push_str(&key);
|
||||
out.extend(chars[i..j].iter().copied());
|
||||
out.push(chars[j]);
|
||||
j += 1;
|
||||
while j < chars.len() && chars[j].is_whitespace() {
|
||||
out.push(chars[j]);
|
||||
j += 1;
|
||||
while j < chars.len() && chars[j].is_whitespace() {
|
||||
out.push(chars[j]);
|
||||
j += 1;
|
||||
}
|
||||
while j < chars.len() && !chars[j].is_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
out.push_str("[REDACTED]");
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
while j < chars.len() && !chars[j].is_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
out.push_str("[REDACTED]");
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
out.push_str(&key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1063,10 +1063,10 @@ fn merge_missing_tunnel_profile_secrets(profile: &mut TransportLayerConfig, prev
|
|||
current.password = previous.password.clone();
|
||||
}
|
||||
}
|
||||
(TransportLayerConfig::HttpTunnel(current), TransportLayerConfig::HttpTunnel(previous)) => {
|
||||
if current.token.is_empty() {
|
||||
current.token = previous.token.clone();
|
||||
}
|
||||
(TransportLayerConfig::HttpTunnel(current), TransportLayerConfig::HttpTunnel(previous))
|
||||
if current.token.is_empty() =>
|
||||
{
|
||||
current.token = previous.token.clone();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -3246,7 +3246,7 @@ mod tests {
|
|||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
let original = mq_connection("pulsar", "existing-token");
|
||||
storage.save_connections(&[original.clone()]).await.unwrap();
|
||||
storage.save_connections(std::slice::from_ref(&original)).await.unwrap();
|
||||
|
||||
let mut metadata = original;
|
||||
metadata.name = "Pulsar renamed".to_string();
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ async fn try_export_native_table_stream(
|
|||
let row_csv = format_csv_rows(&[formatted]);
|
||||
write!(file, "\n{row_csv}").map_err(|e| format!("Failed to write CSV rows: {e}"))?;
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -591,7 +591,7 @@ async fn try_export_native_table_stream(
|
|||
let row_tsv = format_tsv_rows(&[formatted]);
|
||||
write!(file, "\n{row_tsv}").map_err(|e| format!("Failed to write TXT rows: {e}"))?;
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -636,7 +636,7 @@ async fn try_export_native_table_stream(
|
|||
);
|
||||
writer.write_row(&formatted).map_err(|e| format!("Failed to write XLSX row: {e}"))?;
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -691,7 +691,7 @@ async fn try_export_native_table_stream(
|
|||
write_json_row_object(&mut file, col_names, &formatted)?;
|
||||
is_first_row = false;
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -742,7 +742,7 @@ async fn try_export_native_table_stream(
|
|||
wrote_rows = true;
|
||||
}
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -808,7 +808,7 @@ async fn try_export_native_table_stream(
|
|||
flush_pending(&mut file, &mut pending_rows)?;
|
||||
}
|
||||
rows_exported += 1;
|
||||
if rows_exported % progress_interval == 0 {
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
@ -839,7 +839,7 @@ async fn try_export_native_table_stream(
|
|||
match stream_result {
|
||||
Ok(false) => Ok(false),
|
||||
Ok(true) => {
|
||||
if rows_exported % progress_interval != 0 {
|
||||
if !rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
export_id: request.export_id.clone(),
|
||||
table_name: request.table_name.clone(),
|
||||
|
|
|
|||
|
|
@ -2085,9 +2085,7 @@ where
|
|||
pending_rows.push(delimited_record_to_row(&record, columns.len(), config));
|
||||
}
|
||||
|
||||
let mut source_row_number = config.row_range.data_start_row;
|
||||
for record in reader.records() {
|
||||
source_row_number += 1;
|
||||
for (source_row_number, record) in (config.row_range.data_start_row.saturating_add(1)..).zip(reader.records()) {
|
||||
if config.row_range.last_data_row.is_some_and(|last| source_row_number > last) {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,12 +344,7 @@ pub(crate) fn wrap_dameng_identity_insert_sql(insert_sql: &str, table: &str, sch
|
|||
|
||||
pub(crate) fn wrap_dameng_identity_insert_sql_for_table(insert_sql: &str, full_table: &str) -> String {
|
||||
let trimmed = insert_sql.trim().trim_end_matches(';').trim();
|
||||
format!(
|
||||
"{};\n{};\n{};",
|
||||
format!("SET IDENTITY_INSERT {full_table} ON"),
|
||||
trimmed,
|
||||
format!("SET IDENTITY_INSERT {full_table} OFF")
|
||||
)
|
||||
format!("SET IDENTITY_INSERT {full_table} ON;\n{trimmed};\nSET IDENTITY_INSERT {full_table} OFF;")
|
||||
}
|
||||
|
||||
async fn execute_transfer_write_statement(
|
||||
|
|
@ -4139,7 +4134,7 @@ where
|
|||
can_reuse_source_table_ddl(source_db_type, target_db_type, preserves_target_table_name);
|
||||
let ddl = if can_reuse_source_ddl {
|
||||
let source_ddl = crate::schema::get_table_ddl_core(
|
||||
&state,
|
||||
state,
|
||||
&request.source_connection_id,
|
||||
&request.source_database,
|
||||
&request.source_schema,
|
||||
|
|
@ -5222,19 +5217,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn transfer_create_table_result_treats_existing_table_as_preexisting() {
|
||||
assert_eq!(
|
||||
transfer_create_table_created(
|
||||
Err("ERROR: relation \"items\" already exists (SQLSTATE 42P07)".to_string()),
|
||||
"create"
|
||||
)
|
||||
.unwrap(),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
transfer_create_table_created(Err("错误: 关系 \"items\" 已经存在".to_string()), "create").unwrap(),
|
||||
false
|
||||
);
|
||||
assert_eq!(transfer_create_table_created(Ok(()), "create").unwrap(), true);
|
||||
assert!(!transfer_create_table_created(
|
||||
Err("ERROR: relation \"items\" already exists (SQLSTATE 42P07)".to_string()),
|
||||
"create"
|
||||
)
|
||||
.unwrap());
|
||||
assert!(!transfer_create_table_created(Err("错误: 关系 \"items\" 已经存在".to_string()), "create").unwrap());
|
||||
assert!(transfer_create_table_created(Ok(()), "create").unwrap());
|
||||
assert_eq!(
|
||||
transfer_create_table_created(Err("permission denied for schema public".to_string()), "create")
|
||||
.unwrap_err(),
|
||||
|
|
|
|||
|
|
@ -630,6 +630,8 @@ fn default_permissions() -> dbx_core::agent_tools::AgentSqlPermissions {
|
|||
}
|
||||
}
|
||||
|
||||
// CallToolResult is the transport-native error payload; boxing it would complicate every MCP call site.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn validate_mongo_command(
|
||||
connection: &dbx_core::models::connection::ConnectionConfig,
|
||||
database: &str,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ name = "dbx-web"
|
|||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["mq-admin"]
|
||||
default = ["duckdb-bundled", "mq-admin", "sqlite-sqlcipher"]
|
||||
duckdb-bundled = ["dbx-core/duckdb-bundled"]
|
||||
mq-admin = ["dbx-core/mq-admin"]
|
||||
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
|
||||
|
||||
[dependencies]
|
||||
dbx-core = { path = "../dbx-core" }
|
||||
dbx-core = { path = "../dbx-core", default-features = false }
|
||||
redis = { version = "0.32", features = ["tokio-comp"] }
|
||||
axum = { version = "0.8", features = ["multipart", "ws"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "fs", "compression-gzip", "trace"] }
|
||||
|
|
|
|||
|
|
@ -66,63 +66,6 @@ fn normalize_public_base_path(value: Option<String>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_public_base_path, web_agent_dir_from_env, web_compression_predicate, XLSX_CONTENT_TYPE};
|
||||
use axum::body::Body;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::Response;
|
||||
use tower_http::compression::predicate::Predicate;
|
||||
|
||||
fn compression_response(content_type: &str) -> Response<Body> {
|
||||
Response::builder().header(CONTENT_TYPE, content_type).body(Body::from(vec![b'x'; 64])).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_compression_skips_streams_and_precompressed_exports() {
|
||||
let predicate = web_compression_predicate();
|
||||
|
||||
assert!(predicate.should_compress(&compression_response("application/json")));
|
||||
assert!(!predicate.should_compress(&compression_response("text/event-stream")));
|
||||
assert!(!predicate.should_compress(&compression_response(XLSX_CONTENT_TYPE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_public_base_path_defaults_to_root() {
|
||||
assert_eq!(normalize_public_base_path(None), "/");
|
||||
assert_eq!(normalize_public_base_path(Some("".to_string())), "/");
|
||||
assert_eq!(normalize_public_base_path(Some("/".to_string())), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_public_base_path_trims_and_preserves_segments() {
|
||||
assert_eq!(normalize_public_base_path(Some("dbx".to_string())), "/dbx");
|
||||
assert_eq!(normalize_public_base_path(Some("/dbx/".to_string())), "/dbx");
|
||||
assert_eq!(normalize_public_base_path(Some("/tools/dbx/?v=1".to_string())), "/tools/dbx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "DBX_PUBLIC_BASE_PATH contains invalid characters")]
|
||||
fn normalize_public_base_path_rejects_invalid_characters() {
|
||||
normalize_public_base_path(Some("/dbx admin".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_agent_dir_defaults_under_data_dir() {
|
||||
let data_dir = std::path::PathBuf::from("/app/data");
|
||||
assert_eq!(web_agent_dir_from_env(&data_dir, None), data_dir.join("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_agent_dir_uses_explicit_env_override() {
|
||||
let data_dir = std::path::PathBuf::from("/app/data");
|
||||
assert_eq!(
|
||||
web_agent_dir_from_env(&data_dir, Some("/custom/agents".to_string())),
|
||||
std::path::PathBuf::from("/custom/agents")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mq-admin")]
|
||||
fn add_mq_routes(router: Router<Arc<WebState>>) -> Router<Arc<WebState>> {
|
||||
router
|
||||
|
|
@ -363,7 +306,6 @@ async fn main() {
|
|||
.route("/query/build-search-result-where", post(routes::query::build_search_result_where))
|
||||
.route("/query/build-rename-object-sql", post(routes::query::build_rename_object_sql))
|
||||
.route("/query/build-create-database-sql", post(routes::query::build_create_database_sql))
|
||||
.route("/query/build-duckdb-attach-database-sql", post(routes::query::build_duckdb_attach_database_sql))
|
||||
.route("/query/build-sqlite-attach-database-sql", post(routes::query::build_sqlite_attach_database_sql))
|
||||
.route("/query/build-drop-object-sql", post(routes::query::build_drop_object_sql))
|
||||
.route("/query/build-drop-table-sql", post(routes::query::build_drop_table_sql))
|
||||
|
|
@ -632,6 +574,11 @@ async fn main() {
|
|||
.route("/cloud-sync/snippet/upload", post(routes::cloud_sync::snippet_sync_upload))
|
||||
.route("/cloud-sync/snippet/download", post(routes::cloud_sync::snippet_sync_download));
|
||||
|
||||
// Do not expose DuckDB-only handlers from builds that intentionally omit bundled DuckDB.
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
let api =
|
||||
api.route("/query/build-duckdb-attach-database-sql", post(routes::query::build_duckdb_attach_database_sql));
|
||||
|
||||
let api = add_mq_routes(api)
|
||||
.layer(middleware::from_fn_with_state(web_state.clone(), auth::auth_middleware))
|
||||
.with_state(web_state.clone());
|
||||
|
|
@ -681,3 +628,60 @@ async fn main() {
|
|||
.expect("Server error");
|
||||
shutdown_state.shutdown_background_tasks(std::time::Duration::from_secs(3)).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_public_base_path, web_agent_dir_from_env, web_compression_predicate, XLSX_CONTENT_TYPE};
|
||||
use axum::body::Body;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::http::Response;
|
||||
use tower_http::compression::predicate::Predicate;
|
||||
|
||||
fn compression_response(content_type: &str) -> Response<Body> {
|
||||
Response::builder().header(CONTENT_TYPE, content_type).body(Body::from(vec![b'x'; 64])).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_compression_skips_streams_and_precompressed_exports() {
|
||||
let predicate = web_compression_predicate();
|
||||
|
||||
assert!(predicate.should_compress(&compression_response("application/json")));
|
||||
assert!(!predicate.should_compress(&compression_response("text/event-stream")));
|
||||
assert!(!predicate.should_compress(&compression_response(XLSX_CONTENT_TYPE)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_public_base_path_defaults_to_root() {
|
||||
assert_eq!(normalize_public_base_path(None), "/");
|
||||
assert_eq!(normalize_public_base_path(Some("".to_string())), "/");
|
||||
assert_eq!(normalize_public_base_path(Some("/".to_string())), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_public_base_path_trims_and_preserves_segments() {
|
||||
assert_eq!(normalize_public_base_path(Some("dbx".to_string())), "/dbx");
|
||||
assert_eq!(normalize_public_base_path(Some("/dbx/".to_string())), "/dbx");
|
||||
assert_eq!(normalize_public_base_path(Some("/tools/dbx/?v=1".to_string())), "/tools/dbx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "DBX_PUBLIC_BASE_PATH contains invalid characters")]
|
||||
fn normalize_public_base_path_rejects_invalid_characters() {
|
||||
normalize_public_base_path(Some("/dbx admin".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_agent_dir_defaults_under_data_dir() {
|
||||
let data_dir = std::path::PathBuf::from("/app/data");
|
||||
assert_eq!(web_agent_dir_from_env(&data_dir, None), data_dir.join("agents"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_agent_dir_uses_explicit_env_override() {
|
||||
let data_dir = std::path::PathBuf::from("/app/data");
|
||||
assert_eq!(
|
||||
web_agent_dir_from_env(&data_dir, Some("/custom/agents".to_string())),
|
||||
std::path::PathBuf::from("/custom/agents")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@ mod tests {
|
|||
let (state, dir) = test_web_state().await;
|
||||
let initial = mq_config("mq-conn", "http://127.0.0.1:8080");
|
||||
let updated = mq_config("mq-conn", "http://127.0.0.1:8081");
|
||||
state.app.storage.save_connections(&[updated.clone()]).await.unwrap();
|
||||
state.app.storage.save_connections(std::slice::from_ref(&updated)).await.unwrap();
|
||||
state.app.configs.write().await.insert(initial.id.clone(), initial.clone());
|
||||
state.app.connections.write().await.insert(initial.id.clone(), PoolKind::MessageQueue);
|
||||
|
||||
|
|
|
|||
|
|
@ -172,6 +172,23 @@ pub async fn list_system_fonts() -> Result<Json<Vec<String>>, AppError> {
|
|||
Ok(Json(jdbc::list_system_fonts()))
|
||||
}
|
||||
|
||||
async fn progress_sender(state: &WebState, operation_id: &str) -> broadcast::Sender<String> {
|
||||
let mut channels = state.sse_channels.write().await;
|
||||
channels
|
||||
.entry(format!("agent-install-progress:{operation_id}"))
|
||||
.or_insert_with(|| {
|
||||
let (tx, _) = broadcast::channel::<String>(256);
|
||||
tx
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn send_progress_event(tx: &broadcast::Sender<String>, event: AgentProgressEvent) {
|
||||
if let Ok(payload) = serde_json::to_string(&event) {
|
||||
let _ = tx.send(payload);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::safe_upload_file_name;
|
||||
|
|
@ -191,20 +208,3 @@ mod tests {
|
|||
assert_eq!(file_name, "postgresql-42.7.jar");
|
||||
}
|
||||
}
|
||||
|
||||
async fn progress_sender(state: &WebState, operation_id: &str) -> broadcast::Sender<String> {
|
||||
let mut channels = state.sse_channels.write().await;
|
||||
channels
|
||||
.entry(format!("agent-install-progress:{operation_id}"))
|
||||
.or_insert_with(|| {
|
||||
let (tx, _) = broadcast::channel::<String>(256);
|
||||
tx
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn send_progress_event(tx: &broadcast::Sender<String>, event: AgentProgressEvent) {
|
||||
if let Ok(payload) = serde_json::to_string(&event) {
|
||||
let _ = tx.send(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ pub struct BuildCreateDatabaseSqlRequest {
|
|||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
pub struct BuildDuckDbAttachDatabaseSqlRequest {
|
||||
pub options: dbx_core::db_admin_sql::DuckDbAttachDatabaseSqlOptions,
|
||||
}
|
||||
|
|
@ -599,6 +600,7 @@ pub async fn build_create_database_sql(
|
|||
dbx_core::db_admin_sql::build_create_database_sql(req.options).map(Json).map_err(AppError)
|
||||
}
|
||||
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
pub async fn build_duckdb_attach_database_sql(Json(req): Json<BuildDuckDbAttachDatabaseSqlRequest>) -> Json<String> {
|
||||
Json(dbx_core::db_admin_sql::build_duckdb_attach_database_sql(req.options))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,7 +436,7 @@ mod tests {
|
|||
let first = state.mq_registry.get_or_build(&initial).await.unwrap();
|
||||
|
||||
let updated = mq_config("mq-conn", "http://127.0.0.1:8081");
|
||||
save_connection_configs(&state, &[updated.clone()]).await.unwrap();
|
||||
save_connection_configs(&state, std::slice::from_ref(&updated)).await.unwrap();
|
||||
|
||||
let cached_admin_url = state
|
||||
.configs
|
||||
|
|
@ -465,7 +465,7 @@ mod tests {
|
|||
let state = AppState::new_with_plugin_dir(storage, dir.join("plugins"));
|
||||
let initial = mq_config("mq-conn", "http://127.0.0.1:8080");
|
||||
let updated = mq_config("mq-conn", "http://127.0.0.1:8081");
|
||||
state.storage.save_connections(&[updated.clone()]).await.unwrap();
|
||||
state.storage.save_connections(std::slice::from_ref(&updated)).await.unwrap();
|
||||
state.configs.write().await.insert(initial.id.clone(), initial.clone());
|
||||
state.connections.write().await.insert(initial.id.clone(), PoolKind::MessageQueue);
|
||||
|
||||
|
|
@ -503,7 +503,7 @@ mod tests {
|
|||
}
|
||||
let stale = state.mq_registry.get_or_build(&removed).await.unwrap();
|
||||
|
||||
save_connection_configs(&state, &[kept.clone()]).await.unwrap();
|
||||
save_connection_configs(&state, std::slice::from_ref(&kept)).await.unwrap();
|
||||
|
||||
let configs = state.configs.read().await;
|
||||
assert!(configs.contains_key(&kept.id));
|
||||
|
|
@ -532,7 +532,7 @@ mod tests {
|
|||
}
|
||||
state.connections.write().await.insert(removed.id.clone(), PoolKind::MessageQueue);
|
||||
|
||||
save_connection_configs(&state, &[kept.clone()]).await.unwrap();
|
||||
save_connection_configs(&state, std::slice::from_ref(&kept)).await.unwrap();
|
||||
|
||||
assert!(!state.connections.read().await.contains_key(&removed.id));
|
||||
|
||||
|
|
@ -985,7 +985,7 @@ async fn test_connection_with_info_inner(
|
|||
#[cfg(feature = "mq-admin")]
|
||||
DatabaseType::MessageQueue => {
|
||||
let mqc = state.mq_admin_config_for_connection(connection_id, &config).await?;
|
||||
let kafka_launch = dbx_core::mq::service::resolve_kafka_launch_spec(&mqc, &state);
|
||||
let kafka_launch = dbx_core::mq::service::resolve_kafka_launch_spec(&mqc, state);
|
||||
let adapter = match state.mq_registry.get_or_build_config(connection_id, mqc, kafka_launch).await {
|
||||
Ok(adapter) => adapter,
|
||||
Err(err) => {
|
||||
|
|
|
|||
|
|
@ -290,57 +290,6 @@ fn find_config_by_name<'a>(
|
|||
configs.iter().find(|c| c.name.eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_mongo_database, resolve_mongo_target_values, write_port_file};
|
||||
|
||||
#[test]
|
||||
fn writes_bridge_port_file_to_resolved_data_dir() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"dbx-mcp-bridge-port-test-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let default_data_dir = root.join("default-app-data");
|
||||
let resolved_data_dir = root.join("resolved-data");
|
||||
std::fs::create_dir_all(&default_data_dir).unwrap();
|
||||
|
||||
let port_file = write_port_file(&resolved_data_dir, 49152).unwrap();
|
||||
|
||||
assert_eq!(port_file, resolved_data_dir.join("mcp-bridge-port"));
|
||||
assert_eq!(std::fs::read_to_string(port_file).unwrap(), "49152");
|
||||
assert!(!default_data_dir.join("mcp-bridge-port").exists());
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_database_uses_configured_default_for_missing_or_blank_request() {
|
||||
let configured = Some("sample_db".to_string());
|
||||
|
||||
assert_eq!(resolve_mongo_database(None, configured.clone()), "sample_db");
|
||||
assert_eq!(resolve_mongo_database(Some(String::new()), configured.clone()), "sample_db");
|
||||
assert_eq!(resolve_mongo_database(Some(" ".to_string()), configured), "sample_db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_database_preserves_explicit_target() {
|
||||
assert_eq!(resolve_mongo_database(Some("admin".to_string()), Some("sample_db".to_string())), "admin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_target_keeps_connection_id_separate_from_database() {
|
||||
assert_eq!(
|
||||
resolve_mongo_target_values(
|
||||
"connection-id".to_string(),
|
||||
Some("sample_db".to_string()),
|
||||
Some("default_db".to_string()),
|
||||
),
|
||||
("connection-id".to_string(), "sample_db".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn respond(stream: &mut tokio::net::TcpStream, status: &str, body: &str) {
|
||||
let resp = format!("HTTP/1.1 {status}\r\nContent-Length: {}\r\n\r\n{body}", body.len());
|
||||
let _ = stream.write_all(resp.as_bytes()).await;
|
||||
|
|
@ -969,3 +918,54 @@ async fn handle_execute_query_data(state: &Arc<AppState>, body: &str, stream: &m
|
|||
Err(e) => respond_error(stream, "500 Internal Server Error", &e).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{resolve_mongo_database, resolve_mongo_target_values, write_port_file};
|
||||
|
||||
#[test]
|
||||
fn writes_bridge_port_file_to_resolved_data_dir() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"dbx-mcp-bridge-port-test-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
let default_data_dir = root.join("default-app-data");
|
||||
let resolved_data_dir = root.join("resolved-data");
|
||||
std::fs::create_dir_all(&default_data_dir).unwrap();
|
||||
|
||||
let port_file = write_port_file(&resolved_data_dir, 49152).unwrap();
|
||||
|
||||
assert_eq!(port_file, resolved_data_dir.join("mcp-bridge-port"));
|
||||
assert_eq!(std::fs::read_to_string(port_file).unwrap(), "49152");
|
||||
assert!(!default_data_dir.join("mcp-bridge-port").exists());
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_database_uses_configured_default_for_missing_or_blank_request() {
|
||||
let configured = Some("sample_db".to_string());
|
||||
|
||||
assert_eq!(resolve_mongo_database(None, configured.clone()), "sample_db");
|
||||
assert_eq!(resolve_mongo_database(Some(String::new()), configured.clone()), "sample_db");
|
||||
assert_eq!(resolve_mongo_database(Some(" ".to_string()), configured), "sample_db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_database_preserves_explicit_target() {
|
||||
assert_eq!(resolve_mongo_database(Some("admin".to_string()), Some("sample_db".to_string())), "admin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongo_target_keeps_connection_id_separate_from_database() {
|
||||
assert_eq!(
|
||||
resolve_mongo_target_values(
|
||||
"connection-id".to_string(),
|
||||
Some("sample_db".to_string()),
|
||||
Some("default_db".to_string()),
|
||||
),
|
||||
("connection-id".to_string(), "sample_db".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,6 @@ mod execution_tests {
|
|||
assert_eq!(summary.status, SqlFileStatus::Cancelled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[test]
|
||||
fn duplicate_execution_id_is_rejected_without_replacing_token() {
|
||||
let mut executions = HashMap::new();
|
||||
|
|
|
|||
|
|
@ -334,9 +334,7 @@ fn linux_appimage_system_gtk_immodules_cache(
|
|||
let Some(gtk_im_module_file) = gtk_im_module_file else {
|
||||
return Some(system_cache_path);
|
||||
};
|
||||
let Some(appdir) = appdir else {
|
||||
return None;
|
||||
};
|
||||
let appdir = appdir?;
|
||||
|
||||
if std::path::Path::new(gtk_im_module_file).starts_with(std::path::Path::new(appdir)) {
|
||||
Some(system_cache_path)
|
||||
|
|
@ -526,7 +524,7 @@ fn apply_macos_app_icon_theme(app: &tauri::AppHandle, icon_theme: DesktopIconThe
|
|||
fn apply_desktop_icon_theme(app: &tauri::AppHandle, icon_theme: DesktopIconTheme) -> tauri::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
return apply_macos_app_icon_theme(app, icon_theme);
|
||||
apply_macos_app_icon_theme(app, icon_theme)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
|
@ -1010,11 +1008,11 @@ pub fn run() {
|
|||
let app = window.app_handle();
|
||||
if app.try_state::<CloseBehaviorState>().is_none() {
|
||||
api.prevent_close();
|
||||
hide_main_window_for_close(&app, window);
|
||||
hide_main_window_for_close(app, window);
|
||||
return;
|
||||
}
|
||||
api.prevent_close();
|
||||
request_app_close(&app, "settings");
|
||||
request_app_close(app, "settings");
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
use std::{
|
||||
ffi::CStr,
|
||||
sync::{Once, OnceLock},
|
||||
};
|
||||
use std::sync::{Once, OnceLock};
|
||||
|
||||
use objc2::{
|
||||
ffi,
|
||||
|
|
@ -19,9 +16,7 @@ static INSTALL_DOCK_QUIT_HANDLER: Once = Once::new();
|
|||
pub(crate) fn install_dock_quit_handler(app: &AppHandle) {
|
||||
let _ = APP_HANDLE.set(app.clone());
|
||||
INSTALL_DOCK_QUIT_HANDLER.call_once(|| {
|
||||
let Some(delegate_class) =
|
||||
AnyClass::get(CStr::from_bytes_with_nul(b"TaoAppDelegateParent\0").expect("valid class name"))
|
||||
else {
|
||||
let Some(delegate_class) = AnyClass::get(c"TaoAppDelegateParent") else {
|
||||
eprintln!("[WARN] failed to install macOS Dock quit handler: TaoAppDelegateParent not found");
|
||||
return;
|
||||
};
|
||||
|
|
@ -32,7 +27,7 @@ pub(crate) fn install_dock_quit_handler(app: &AppHandle) {
|
|||
as extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> NSApplicationTerminateReply,
|
||||
)
|
||||
};
|
||||
let method_types = CStr::from_bytes_with_nul(b"Q@:@\0").expect("valid Objective-C method type encoding");
|
||||
let method_types = c"Q@:@";
|
||||
|
||||
// Tao's app delegate does not implement applicationShouldTerminate:, so Dock Quit can
|
||||
// bypass Tauri's ExitRequested event. Add the method to Tao's registered delegate class
|
||||
|
|
|
|||
Loading…
Reference in New Issue