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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-multiplatform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: '1.3.10'

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: '1.3.10'

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: '1.3.10'

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down
31 changes: 1 addition & 30 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: '1.3.10'

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down Expand Up @@ -392,32 +392,3 @@ jobs:
merged/latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

upgradelink-upload:
needs: release
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- name: Resolve release tag
id: release_meta
shell: bash
env:
TAG_NAME: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
echo "release_tag=${TAG_NAME}" >> "$GITHUB_OUTPUT"

- name: Upload latest.json to UpgradeLink
uses: toolsetlink/upgradelink-action@3.0.2
with:
access_key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
access_secret: ${{ secrets.UPGRADE_LINK_ACCESS_SECRET }}
config: |
{
"app_type": "tauri",
"request": {
"app_key": "${{ secrets.UPGRADE_LINK_TAURI_KEY }}",
"latest_json_url": "https://github.com/${{ github.repository }}/releases/download/${{ steps.release_meta.outputs.release_tag }}/latest.json"
}
}
29 changes: 29 additions & 0 deletions .github/workflows/upgradelink-upload.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: UpgradeLink Upload

on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to upload (e.g. v1.2.3)"
required: true
type: string

jobs:
upgradelink-upload:
permissions:
contents: read
runs-on: ubuntu-22.04
steps:
- name: Upload latest.json to UpgradeLink
uses: toolsetlink/upgradelink-action@3.0.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Resolve the peeled commit behind tag 3.0.2
git ls-remote https://github.com/toolsetlink/upgradelink-action refs/tags/3.0.2^{} | awk '{print $1}'

Repository: codeErrorSleep/dbpaw

Length of output: 104


Pin third-party GitHub Action to an immutable commit SHA.

Using @3.0.2 is mutable and can be retargeted. For release workflows, pin to a full commit SHA instead.

🔒 Suggested change
-        uses: toolsetlink/upgradelink-action@3.0.2
+        uses: toolsetlink/upgradelink-action@5d1d2fdafb4372bd0b527909639f3ea786e8856d
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/upgradelink-upload.yml at line 18, The workflow currently
references the third-party action by a mutable tag "uses:
toolsetlink/upgradelink-action@3.0.2"; replace that tag with the action's
immutable full commit SHA (e.g., "uses:
toolsetlink/upgradelink-action@<full-commit-sha>") to prevent retargeting—locate
the action repo, copy the commit SHA for the desired release, and update the
workflow entry that mentions toolsetlink/upgradelink-action accordingly.

with:
access_key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
access_secret: ${{ secrets.UPGRADE_LINK_ACCESS_SECRET }}
config: |
{
"app_type": "tauri",
"request": {
"app_key": "${{ secrets.UPGRADE_LINK_TAURI_KEY }}",
"latest_json_url": "https://github.com/${{ github.repository }}/releases/download/${{ inputs.tag }}/latest.json"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "git+https://github.com/codeErrorSleep/dbpaw.git"
},
"private": true,
"version": "0.3.2",
"version": "0.3.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
6 changes: 5 additions & 1 deletion scripts/test-integration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ case "${it_db}" in
run_integration_test "mysql_command_integration"
run_integration_test "mysql_stateful_command_integration"
;;
starrocks)
run_integration_test "starrocks_integration"
run_integration_test "starrocks_command_integration"
;;
Comment on lines +42 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add StarRocks to the all integration matrix.

Line 42 adds targeted StarRocks execution, but IT_DB=all still omits StarRocks, so CI full runs won’t cover the new driver path.

✅ Proposed patch
   all)
+    run_integration_test "starrocks_integration"
+    run_integration_test "starrocks_command_integration"
     run_integration_test "mysql_integration"
     run_integration_test "mysql_command_integration"
     run_integration_test "mysql_stateful_command_integration"

Based on learnings: For new database driver additions, follow the step-by-step checklist in ADD_NEW_DB.md for exact file paths, line numbers, and implementation details; update scripts/test-integration.sh to include the new driver.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-integration.sh` around lines 42 - 45, The integration matrix
omits StarRocks when IT_DB=all; update the script so the "all" case triggers the
StarRocks tests by adding the same run_integration_test calls used in the
starrocks) case. Locate the case or switch that handles IT_DB="all" in
scripts/test-integration.sh and add run_integration_test "starrocks_integration"
and run_integration_test "starrocks_command_integration" alongside the other DB
entries so full CI runs include the new driver path; use the existing
run_integration_test invocation pattern to ensure consistent behavior.

mariadb)
run_integration_test "mariadb_integration"
run_integration_test "mariadb_command_integration"
Expand Down Expand Up @@ -89,7 +93,7 @@ case "${it_db}" in
run_integration_test "oracle_command_integration"
;;
*)
echo "[error] Invalid IT_DB='${it_db}'. Expected one of: mysql|mariadb|postgres|clickhouse|mssql|duckdb|sqlite|oracle|all"
echo "[error] Invalid IT_DB='${it_db}'. Expected one of: mysql|starrocks|mariadb|postgres|clickhouse|mssql|duckdb|sqlite|oracle|all"
exit 1
;;
esac
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["devtools"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check Rust/Tauri code paths for devtools usage and debug gating ==="
rg -n -C2 --type=rust --type=toml 'devtools|open_devtools|close_devtools|debug_toggle_devtools|cfg\(debug_assertions\)'

echo
echo "=== Check workflows/scripts for release builds enabling custom features ==="
rg -n -C2 'tauri build|cargo tauri build|--features'

Repository: codeErrorSleep/dbpaw

Length of output: 3369


🏁 Script executed:

#!/bin/bash
# Check Cargo.toml for build profile overrides
echo "=== Check Cargo.toml for build profile configuration ==="
cat -n src-tauri/Cargo.toml | tail -30

echo
echo "=== Check if menu creation has any conditional guards ==="
rg -n -B5 -A5 'developer_menu' src-tauri/src/lib.rs

Repository: codeErrorSleep/dbpaw

Length of output: 2958


🏁 Script executed:

#!/bin/bash
# Check for any release profile configuration in full Cargo.toml
echo "=== Full Cargo.toml to check for [profile.release] section ==="
wc -l src-tauri/Cargo.toml
cat -n src-tauri/Cargo.toml | grep -A10 '\[profile'

Repository: codeErrorSleep/dbpaw

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Show entire Cargo.toml
echo "=== Full Cargo.toml content ==="
cat -n src-tauri/Cargo.toml

Repository: codeErrorSleep/dbpaw

Length of output: 2254


Disable Tauri devtools in production by using a feature flag.

The devtools feature is globally enabled and unconditionally added to the Developer menu—enabling end-users to access devtools in production. This weakens security posture.

🔧 Suggested approach
 [dependencies]
-tauri = { version = "2", features = ["devtools"] }
+tauri = { version = "2" }
+
+[features]
+default = []
+devtools = ["tauri/devtools"]

Enable --features devtools only in local/dev builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/Cargo.toml` at line 21, Remove the unconditional "devtools"
dependency feature from the tauri dependency entry in Cargo.toml and instead
declare a Cargo feature named "devtools" that enables the tauri/devtools
dependency feature; specifically, stop listing features = ["devtools"] on the
tauri dependency and add a [features] section where devtools =
["tauri/devtools"], so devtools is only activated when building with --features
devtools (and update your dev/local build commands to pass --features devtools
as needed).

tauri-plugin-opener = "2"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
Expand Down
172 changes: 168 additions & 4 deletions src-tauri/src/commands/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ pub async fn create_database_by_id(
}

let exec_res = match driver.as_str() {
"mysql" | "mariadb" | "tidb" => {
driver if crate::db::drivers::is_mysql_family_driver(driver) => {
let sql = build_mysql_create_database_sql(&payload, &db_name)?;
super::execute_with_retry(&state, id, None, |driver| {
let sql_clone = sql.clone();
Expand Down Expand Up @@ -348,7 +348,7 @@ pub async fn create_database_by_id_direct(
}

let exec_res = match driver.as_str() {
"mysql" | "mariadb" | "tidb" => {
driver if crate::db::drivers::is_mysql_family_driver(driver) => {
let sql = build_mysql_create_database_sql(&payload, &db_name)?;
super::execute_with_retry_from_app_state(state, id, None, |driver| {
let sql_clone = sql.clone();
Expand Down Expand Up @@ -419,6 +419,122 @@ pub async fn test_connection_ephemeral(
})
}

#[tauri::command]
pub async fn get_mysql_charsets_by_id(
state: State<'_, AppState>,
id: i64,
) -> Result<Vec<String>, String> {
super::execute_with_retry(&state, id, None, |driver| async move {
let result = driver
.execute_query("SHOW CHARACTER SET".to_string())
.await?;
let mut charsets: Vec<String> = result
.data
.iter()
.filter_map(|row| {
row.get("Charset")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.collect();
charsets.sort();
Ok(charsets)
})
.await
}

#[tauri::command]
pub async fn get_mysql_collations_by_id(
state: State<'_, AppState>,
id: i64,
charset: Option<String>,
) -> Result<Vec<String>, String> {
let sql = match &charset {
Some(cs) if is_safe_option_token(cs) => {
format!("SHOW COLLATION WHERE Charset = '{}'", cs)
}
Some(cs) => {
return Err(format!("[VALIDATION_ERROR] Invalid charset: {}", cs));
}
None => "SHOW COLLATION".to_string(),
};
super::execute_with_retry(&state, id, None, |driver| {
let sql = sql.clone();
async move {
let result = driver.execute_query(sql).await?;
let mut collations: Vec<String> = result
.data
.iter()
.filter_map(|row| {
row.get("Collation")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.collect();
collations.sort();
Ok(collations)
}
})
.await
}
Comment on lines +422 to +479

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject non-MySQL connections before running these queries.

These handlers currently execute SHOW CHARACTER SET / SHOW COLLATION for any saved connection id. If a non-MySQL connection reaches them, callers get a raw SQL error instead of a predictable [UNSUPPORTED] response. Mirroring the driver-family guard used in create_database_by_id* would make this API much safer to consume.

Also applies to: 481-536

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/commands/connection.rs` around lines 422 - 479, Both
get_mysql_charsets_by_id and get_mysql_collations_by_id must reject non-MySQL
drivers up-front like the create_database_by_id* handlers do: inside the
execute_with_retry closure (or before calling it) check the driver's family and
return Err("[UNSUPPORTED] This operation is only supported for MySQL
connections") when it is not DriverFamily::Mysql. Reference the functions
get_mysql_charsets_by_id and get_mysql_collations_by_id, the existing
execute_with_retry helper, and mirror the driver-family guard pattern used in
create_database_by_id* so non-MySQL connection ids produce the predictable
[UNSUPPORTED] error instead of raw SQL errors.


pub async fn get_mysql_charsets_by_id_direct(
state: &AppState,
id: i64,
) -> Result<Vec<String>, String> {
super::execute_with_retry_from_app_state(state, id, None, |driver| async move {
let result = driver
.execute_query("SHOW CHARACTER SET".to_string())
.await?;
let mut charsets: Vec<String> = result
.data
.iter()
.filter_map(|row| {
row.get("Charset")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.collect();
charsets.sort();
Ok(charsets)
})
.await
}

pub async fn get_mysql_collations_by_id_direct(
state: &AppState,
id: i64,
charset: Option<String>,
) -> Result<Vec<String>, String> {
let sql = match &charset {
Some(cs) if is_safe_option_token(cs) => {
format!("SHOW COLLATION WHERE Charset = '{}'", cs)
}
Some(cs) => {
return Err(format!("[VALIDATION_ERROR] Invalid charset: {}", cs));
}
None => "SHOW COLLATION".to_string(),
};
super::execute_with_retry_from_app_state(state, id, None, |driver| {
let sql = sql.clone();
async move {
let result = driver.execute_query(sql).await?;
let mut collations: Vec<String> = result
.data
.iter()
.filter_map(|row| {
row.get("Collation")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.collect();
collations.sort();
Ok(collations)
}
})
.await
}

#[tauri::command]
pub async fn get_connections(state: State<'_, AppState>) -> Result<Vec<Connection>, String> {
let local_db = {
Expand Down Expand Up @@ -553,8 +669,8 @@ mod tests {
validate_database_name, CreateDatabasePayload,
};
use super::{
normalize_create_database_error, normalize_option_token, quote_clickhouse_ident,
quote_mssql_ident, quote_mysql_ident, quote_pg_ident,
is_safe_option_token, normalize_create_database_error, normalize_option_token,
quote_clickhouse_ident, quote_mssql_ident, quote_mysql_ident, quote_pg_ident,
};
use crate::connection_input::normalize_connection_form;
use crate::models::ConnectionForm;
Expand Down Expand Up @@ -734,4 +850,52 @@ mod tests {
.unwrap_err();
assert!(err.contains("does not support charset option"));
}

#[test]
fn get_mysql_collations_charset_validation_rejects_unsafe_tokens() {
// Verify the validation logic used by get_mysql_collations_by_id/_direct.
// A charset with spaces or semicolons must be rejected.
assert!(!is_safe_option_token("utf8 mb4"));
assert!(!is_safe_option_token("utf8;drop"));
assert!(!is_safe_option_token(""));
}

#[test]
fn get_mysql_collations_charset_validation_accepts_valid_charsets() {
// All standard MySQL charset names must pass the token check.
let valid = [
"utf8mb4",
"utf8",
"latin1",
"gbk",
"gb18030",
"ascii",
"binary",
"utf8mb4_0900_ai_ci",
];
for cs in valid {
assert!(is_safe_option_token(cs), "expected '{}' to be accepted", cs);
}
}

#[test]
fn mysql_create_database_sql_is_reusable_for_starrocks_connections() {
assert!(crate::db::drivers::is_mysql_family_driver("starrocks"));

let sql = build_mysql_create_database_sql(
&CreateDatabasePayload {
name: "analytics".to_string(),
if_not_exists: Some(true),
charset: None,
collation: None,
encoding: None,
lc_collate: None,
lc_ctype: None,
},
"analytics",
)
.unwrap();

assert_eq!(sql, "CREATE DATABASE IF NOT EXISTS `analytics`");
}
}
Loading
Loading