diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts index 1746ae5..4445dac 100644 --- a/frontend/wailsjs/runtime/runtime.d.ts +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -246,85 +246,4 @@ export function OnFileDropOff() :void export function CanResolveFilePaths(): boolean; // Resolves file paths for an array of files -export function ResolveFilePaths(files: File[]): void - -// Notification types -export interface NotificationOptions { - id: string; - title: string; - subtitle?: string; // macOS and Linux only - body?: string; - categoryId?: string; - data?: { [key: string]: any }; -} - -export interface NotificationAction { - id?: string; - title?: string; - destructive?: boolean; // macOS-specific -} - -export interface NotificationCategory { - id?: string; - actions?: NotificationAction[]; - hasReplyField?: boolean; - replyPlaceholder?: string; - replyButtonTitle?: string; -} - -// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) -// Initializes the notification service for the application. -// This must be called before sending any notifications. -export function InitializeNotifications(): Promise; - -// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) -// Cleans up notification resources and releases any held connections. -export function CleanupNotifications(): Promise; - -// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) -// Checks if notifications are available on the current platform. -export function IsNotificationAvailable(): Promise; - -// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) -// Requests notification authorization from the user (macOS only). -export function RequestNotificationAuthorization(): Promise; - -// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) -// Checks the current notification authorization status (macOS only). -export function CheckNotificationAuthorization(): Promise; - -// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) -// Sends a basic notification with the given options. -export function SendNotification(options: NotificationOptions): Promise; - -// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) -// Sends a notification with action buttons. Requires a registered category. -export function SendNotificationWithActions(options: NotificationOptions): Promise; - -// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) -// Registers a notification category that can be used with SendNotificationWithActions. -export function RegisterNotificationCategory(category: NotificationCategory): Promise; - -// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) -// Removes a previously registered notification category. -export function RemoveNotificationCategory(categoryId: string): Promise; - -// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) -// Removes all pending notifications from the notification center. -export function RemoveAllPendingNotifications(): Promise; - -// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) -// Removes a specific pending notification by its identifier. -export function RemovePendingNotification(identifier: string): Promise; - -// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) -// Removes all delivered notifications from the notification center. -export function RemoveAllDeliveredNotifications(): Promise; - -// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) -// Removes a specific delivered notification by its identifier. -export function RemoveDeliveredNotification(identifier: string): Promise; - -// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) -// Removes a notification by its identifier (cross-platform convenience function). -export function RemoveNotification(identifier: string): Promise; +export function ResolveFilePaths(files: File[]): void \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js index f4db9e7..7cb89d7 100644 --- a/frontend/wailsjs/runtime/runtime.js +++ b/frontend/wailsjs/runtime/runtime.js @@ -239,60 +239,4 @@ export function CanResolveFilePaths() { export function ResolveFilePaths(files) { return window.runtime.ResolveFilePaths(files); -} - -export function InitializeNotifications() { - return window.runtime.InitializeNotifications(); -} - -export function CleanupNotifications() { - return window.runtime.CleanupNotifications(); -} - -export function IsNotificationAvailable() { - return window.runtime.IsNotificationAvailable(); -} - -export function RequestNotificationAuthorization() { - return window.runtime.RequestNotificationAuthorization(); -} - -export function CheckNotificationAuthorization() { - return window.runtime.CheckNotificationAuthorization(); -} - -export function SendNotification(options) { - return window.runtime.SendNotification(options); -} - -export function SendNotificationWithActions(options) { - return window.runtime.SendNotificationWithActions(options); -} - -export function RegisterNotificationCategory(category) { - return window.runtime.RegisterNotificationCategory(category); -} - -export function RemoveNotificationCategory(categoryId) { - return window.runtime.RemoveNotificationCategory(categoryId); -} - -export function RemoveAllPendingNotifications() { - return window.runtime.RemoveAllPendingNotifications(); -} - -export function RemovePendingNotification(identifier) { - return window.runtime.RemovePendingNotification(identifier); -} - -export function RemoveAllDeliveredNotifications() { - return window.runtime.RemoveAllDeliveredNotifications(); -} - -export function RemoveDeliveredNotification(identifier) { - return window.runtime.RemoveDeliveredNotification(identifier); -} - -export function RemoveNotification(identifier) { - return window.runtime.RemoveNotification(identifier); -} +} \ No newline at end of file diff --git a/internal/db/dameng_impl.go b/internal/db/dameng_impl.go index ec51b2c..92b1ab7 100644 --- a/internal/db/dameng_impl.go +++ b/internal/db/dameng_impl.go @@ -211,9 +211,13 @@ func (d *DamengDB) GetDatabases() ([]string, error) { } func (d *DamengDB) GetTables(dbName string) ([]string, error) { - query := fmt.Sprintf("SELECT owner, table_name FROM all_tables WHERE owner = '%s' ORDER BY table_name", strings.ToUpper(dbName)) - if dbName == "" { - query = "SELECT table_name FROM user_tables" + // 始终返回 OWNER.TABLE_NAME,与 Oracle 实现对齐,避免下游 SQL 缺少 schema 前缀(refs issue #445) + // 列别名用双引号包裹强制大写,避免不同驱动版本返回不一致 case 导致 row map 取值失败 + var query string + if dbName != "" { + query = fmt.Sprintf(`SELECT owner AS "OWNER", table_name AS "TABLE_NAME" FROM all_tables WHERE owner = '%s' ORDER BY table_name`, strings.ToUpper(dbName)) + } else { + query = `SELECT USER AS "OWNER", table_name AS "TABLE_NAME" FROM user_tables ORDER BY table_name` } data, _, err := d.Query(query) @@ -223,16 +227,14 @@ func (d *DamengDB) GetTables(dbName string) ([]string, error) { var tables []string for _, row := range data { - if dbName != "" { - if owner, okOwner := row["OWNER"]; okOwner { - if name, okName := row["TABLE_NAME"]; okName { - tables = append(tables, fmt.Sprintf("%v.%v", owner, name)) - continue - } - } + owner, okOwner := row["OWNER"] + name, okName := row["TABLE_NAME"] + if okOwner && okName && name != nil { + tables = append(tables, fmt.Sprintf("%v.%v", owner, name)) + continue } - if val, ok := row["TABLE_NAME"]; ok { - tables = append(tables, fmt.Sprintf("%v", val)) + if okName && name != nil { + tables = append(tables, fmt.Sprintf("%v", name)) } } return tables, nil diff --git a/internal/db/oracle_applychanges_test.go b/internal/db/oracle_applychanges_test.go index fcc801a..6abc160 100644 --- a/internal/db/oracle_applychanges_test.go +++ b/internal/db/oracle_applychanges_test.go @@ -28,6 +28,12 @@ type oracleRecordingState struct { execQueries []string execArgs [][]driver.NamedValue rowsAffected int64 + queryResults map[string]oracleRecordingQueryResult +} + +type oracleRecordingQueryResult struct { + columns []string + rows [][]driver.Value } func (s *oracleRecordingState) snapshotExecQueries() []string { @@ -80,6 +86,16 @@ func (c *oracleRecordingConn) ExecContext(_ context.Context, query string, args } func (c *oracleRecordingConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) { + c.state.mu.Lock() + if result, ok := c.state.queryResults[query]; ok { + c.state.mu.Unlock() + return &oracleRecordingRows{ + columns: append([]string(nil), result.columns...), + rows: cloneOracleRecordingRows(result.rows), + }, nil + } + c.state.mu.Unlock() + if strings.Contains(strings.ToLower(query), "tab_columns") { return &oracleRecordingRows{ columns: []string{"COLUMN_NAME", "DATA_TYPE", "NULLABLE", "DATA_DEFAULT"}, @@ -92,6 +108,14 @@ func (c *oracleRecordingConn) QueryContext(_ context.Context, query string, _ [] return &oracleRecordingRows{}, nil } +func cloneOracleRecordingRows(src [][]driver.Value) [][]driver.Value { + dst := make([][]driver.Value, len(src)) + for i, row := range src { + dst[i] = append([]driver.Value(nil), row...) + } + return dst +} + var _ driver.ExecerContext = (*oracleRecordingConn)(nil) var _ driver.QueryerContext = (*oracleRecordingConn)(nil) @@ -135,7 +159,7 @@ func openOracleRecordingDB(t *testing.T) (*sql.DB, *oracleRecordingState) { oracleRecordingDriverMu.Lock() oracleRecordingDriverSeq++ dsn := fmt.Sprintf("oracle-recording-%d", oracleRecordingDriverSeq) - state := &oracleRecordingState{rowsAffected: 1} + state := &oracleRecordingState{rowsAffected: 1, queryResults: map[string]oracleRecordingQueryResult{}} oracleRecordingDriverStates[dsn] = state oracleRecordingDriverMu.Unlock() diff --git a/internal/db/oracle_get_tables_test.go b/internal/db/oracle_get_tables_test.go new file mode 100644 index 0000000..c971bd0 --- /dev/null +++ b/internal/db/oracle_get_tables_test.go @@ -0,0 +1,84 @@ +package db + +import ( + "database/sql/driver" + "reflect" + "testing" +) + +func TestOracleGetTablesPrefixesOwnerForAllTablesQuery(t *testing.T) { + t.Parallel() + + dbConn, state := openOracleRecordingDB(t) + state.mu.Lock() + state.queryResults[`SELECT owner AS "OWNER", table_name AS "TABLE_NAME" FROM all_tables WHERE owner = 'MYCIMLED' ORDER BY table_name`] = oracleRecordingQueryResult{ + columns: []string{"OWNER", "TABLE_NAME"}, + rows: [][]driver.Value{ + {"MYCIMLED", "T_ADS"}, + {"MYCIMLED", "T_USERS"}, + }, + } + state.mu.Unlock() + + oracleDB := &OracleDB{conn: dbConn} + tables, err := oracleDB.GetTables("MYCIMLED") + if err != nil { + t.Fatalf("GetTables 返回错误: %v", err) + } + + want := []string{"MYCIMLED.T_ADS", "MYCIMLED.T_USERS"} + if !reflect.DeepEqual(tables, want) { + t.Fatalf("期望返回带 OWNER 前缀的表名 %v,实际 %v", want, tables) + } +} + +func TestOracleGetTablesPrefixesCurrentUserForUserTablesQuery(t *testing.T) { + t.Parallel() + + dbConn, state := openOracleRecordingDB(t) + state.mu.Lock() + state.queryResults[`SELECT USER AS "OWNER", table_name AS "TABLE_NAME" FROM user_tables ORDER BY table_name`] = oracleRecordingQueryResult{ + columns: []string{"OWNER", "TABLE_NAME"}, + rows: [][]driver.Value{ + {"LOGIN_USER", "T_ADS"}, + }, + } + state.mu.Unlock() + + oracleDB := &OracleDB{conn: dbConn} + tables, err := oracleDB.GetTables("") + if err != nil { + t.Fatalf("GetTables 返回错误: %v", err) + } + + want := []string{"LOGIN_USER.T_ADS"} + if !reflect.DeepEqual(tables, want) { + t.Fatalf("空 dbName 也应带 OWNER 前缀,期望 %v,实际 %v", want, tables) + } +} + +func TestOracleGetTablesSkipsRowsWithNullTableName(t *testing.T) { + t.Parallel() + + dbConn, state := openOracleRecordingDB(t) + state.mu.Lock() + state.queryResults[`SELECT owner AS "OWNER", table_name AS "TABLE_NAME" FROM all_tables WHERE owner = 'MYCIMLED' ORDER BY table_name`] = oracleRecordingQueryResult{ + columns: []string{"OWNER", "TABLE_NAME"}, + rows: [][]driver.Value{ + {"MYCIMLED", nil}, + {"MYCIMLED", "T_ADS"}, + }, + } + state.mu.Unlock() + + oracleDB := &OracleDB{conn: dbConn} + tables, err := oracleDB.GetTables("MYCIMLED") + if err != nil { + t.Fatalf("GetTables 返回错误: %v", err) + } + + want := []string{"MYCIMLED.T_ADS"} + if !reflect.DeepEqual(tables, want) { + t.Fatalf("NULL TABLE_NAME 应被跳过,期望 %v,实际 %v", want, tables) + } +} diff --git a/internal/db/oracle_impl.go b/internal/db/oracle_impl.go index d31bc7a..0571ba9 100644 --- a/internal/db/oracle_impl.go +++ b/internal/db/oracle_impl.go @@ -217,9 +217,13 @@ func (o *OracleDB) GetDatabases() ([]string, error) { func (o *OracleDB) GetTables(dbName string) ([]string, error) { // dbName is Schema/Owner - query := "SELECT table_name FROM user_tables" + // 始终返回 OWNER.TABLE_NAME,避免下游 SQL 缺少 schema 前缀导致 ORA-00942(refs issue #445) + // 列别名用双引号包裹强制大写,避免不同驱动版本返回不一致 case 导致 row map 取值失败 + var query string if dbName != "" { - query = fmt.Sprintf("SELECT owner, table_name FROM all_tables WHERE owner = '%s' ORDER BY table_name", strings.ToUpper(dbName)) + query = fmt.Sprintf(`SELECT owner AS "OWNER", table_name AS "TABLE_NAME" FROM all_tables WHERE owner = '%s' ORDER BY table_name`, strings.ToUpper(dbName)) + } else { + query = `SELECT USER AS "OWNER", table_name AS "TABLE_NAME" FROM user_tables ORDER BY table_name` } data, _, err := o.Query(query) @@ -229,16 +233,14 @@ func (o *OracleDB) GetTables(dbName string) ([]string, error) { var tables []string for _, row := range data { - if dbName != "" { - if owner, okOwner := row["OWNER"]; okOwner { - if name, okName := row["TABLE_NAME"]; okName { - tables = append(tables, fmt.Sprintf("%v.%v", owner, name)) - continue - } - } + owner, okOwner := row["OWNER"] + name, okName := row["TABLE_NAME"] + if okOwner && okName && name != nil { + tables = append(tables, fmt.Sprintf("%v.%v", owner, name)) + continue } - if val, ok := row["TABLE_NAME"]; ok { - tables = append(tables, fmt.Sprintf("%v", val)) + if okName && name != nil { + tables = append(tables, fmt.Sprintf("%v", name)) } } return tables, nil