diff --git a/frontend/src/store.test.ts b/frontend/src/store.test.ts index a7ad2505..8e0a3739 100644 --- a/frontend/src/store.test.ts +++ b/frontend/src/store.test.ts @@ -314,6 +314,23 @@ describe('store appearance persistence', () => { expect(reloaded.useStore.getState().queryOptions.wordWrap).toBe(true); }); + it('persists zero as the unlimited SQL query row limit', async () => { + const { useStore } = await importStore(); + + useStore.getState().setQueryOptions({ maxRows: 0 }); + expect(useStore.getState().queryOptions.maxRows).toBe(0); + + const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}'); + expect(persisted.state.queryOptions.maxRows).toBe(0); + + vi.resetModules(); + const reloaded = await importStore(); + expect(reloaded.useStore.getState().queryOptions.maxRows).toBe(0); + + reloaded.useStore.getState().setQueryOptions({ maxRows: -1 }); + expect(reloaded.useStore.getState().queryOptions.maxRows).toBe(5000); + }); + it('persists the table overview view mode across store reloads', async () => { const { useStore } = await importStore(); expect(useStore.getState().queryOptions.tableOverviewViewMode).toBeUndefined(); diff --git a/frontend/src/store.ts b/frontend/src/store.ts index ec104fab..4954cb63 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -2794,7 +2794,7 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => { const queryEditorEditorHeightRatio = sanitizeQueryEditorEditorHeightRatio( raw.queryEditorEditorHeightRatio, ); - if (!Number.isFinite(maxRows) || maxRows <= 0) { + if (!Number.isFinite(maxRows) || maxRows < 0) { return { maxRows: 5000, wordWrap, diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index a7962e5a..32716696 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "net/url" "sort" "strconv" "strings" @@ -1865,8 +1866,33 @@ func ensureNonNilSlice[T any](items []T) []T { return items } +func resolveConfiguredMongoDatabase(config connection.ConnectionConfig) string { + if !strings.EqualFold(strings.TrimSpace(config.Type), "mongodb") { + return "" + } + if database := strings.TrimSpace(config.Database); database != "" { + return database + } + + rawURI := strings.TrimSpace(config.URI) + lowerURI := strings.ToLower(rawURI) + if !strings.HasPrefix(lowerURI, "mongodb://") && !strings.HasPrefix(lowerURI, "mongodb+srv://") { + return "" + } + parsed, err := url.Parse(rawURI) + if err != nil { + return "" + } + database := strings.Trim(strings.TrimSpace(parsed.Path), "/") + if database == "" || strings.Contains(database, "/") { + return "" + } + return database +} + func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.QueryResult { runConfig := normalizeRunConfig(config, "") + configuredMongoDatabase := resolveConfiguredMongoDatabase(runConfig) if strings.EqualFold(strings.TrimSpace(runConfig.Type), "redis") { runConfig.Type = "redis" client, err := a.getRedisClient(runConfig) @@ -1890,6 +1916,12 @@ func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.Quer logger.Error(err, "DBGetDatabases 获取连接失败:%s", formatConnSummary(runConfig)) return connection.QueryResult{Success: false, Message: err.Error()} } + if configuredMongoDatabase != "" { + return connection.QueryResult{ + Success: true, + Data: []map[string]string{{"Database": configuredMongoDatabase}}, + } + } dbs, err := dbInst.GetDatabases() if err != nil && shouldRefreshCachedConnection(err) { diff --git a/internal/app/methods_db_conn_test.go b/internal/app/methods_db_conn_test.go index da21ae2d..5c4a3c51 100644 --- a/internal/app/methods_db_conn_test.go +++ b/internal/app/methods_db_conn_test.go @@ -10,9 +10,12 @@ import ( ) type releaseRecordingDB struct { - closed int - connect func(config connection.ConnectionConfig) error - closeErr error + closed int + connect func(config connection.ConnectionConfig) error + closeErr error + pingCalls int + getDatabasesCalls int + databases []string } func (f *releaseRecordingDB) Connect(config connection.ConnectionConfig) error { @@ -25,12 +28,18 @@ func (f *releaseRecordingDB) Close() error { f.closed++ return f.closeErr } -func (f *releaseRecordingDB) Ping() error { return nil } +func (f *releaseRecordingDB) Ping() error { + f.pingCalls++ + return nil +} func (f *releaseRecordingDB) Query(query string) ([]map[string]interface{}, []string, error) { return nil, nil, nil } -func (f *releaseRecordingDB) Exec(query string) (int64, error) { return 0, nil } -func (f *releaseRecordingDB) GetDatabases() ([]string, error) { return nil, nil } +func (f *releaseRecordingDB) Exec(query string) (int64, error) { return 0, nil } +func (f *releaseRecordingDB) GetDatabases() ([]string, error) { + f.getDatabasesCalls++ + return f.databases, nil +} func (f *releaseRecordingDB) GetTables(dbName string) ([]string, error) { return nil, nil } func (f *releaseRecordingDB) GetCreateStatement(dbName, tableName string) (string, error) { return "", nil @@ -75,6 +84,86 @@ func TestNormalizeTestConnectionConfig_ZeroTimeout(t *testing.T) { } } +func TestDBGetDatabases_MongoConfiguredDatabaseSkipsEnumeration(t *testing.T) { + installFakeOptionalDriverRuntime(t) + + tests := []struct { + name string + config connection.ConnectionConfig + want string + }{ + { + name: "database field", + config: connection.ConnectionConfig{ + Type: "mongodb", + Database: " application ", + }, + want: "application", + }, + { + name: "database in URI", + config: connection.ConnectionConfig{ + Type: "mongodb", + URI: "mongodb://user:password@localhost:27017/reporting?authSource=admin", + }, + want: "reporting", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + app := NewApp() + database := &releaseRecordingDB{databases: []string{"admin", "other"}} + app.dbCache[getCacheKey(test.config)] = cachedDatabase{ + inst: database, + config: normalizeCacheKeyConfig(test.config), + } + + result := app.DBGetDatabases(test.config) + if !result.Success { + t.Fatalf("expected scoped MongoDB database lookup to succeed, got %q", result.Message) + } + if database.pingCalls != 1 { + t.Fatalf("expected scoped MongoDB lookup to validate the connection, got %d pings", database.pingCalls) + } + if database.getDatabasesCalls != 0 { + t.Fatalf("expected scoped MongoDB lookup to skip database enumeration, got %d calls", database.getDatabasesCalls) + } + rows, ok := result.Data.([]map[string]string) + if !ok { + t.Fatalf("expected database rows, got %#v", result.Data) + } + if len(rows) != 1 || rows[0]["Database"] != test.want { + t.Fatalf("expected only configured database %q, got %#v", test.want, rows) + } + }) + } +} + +func TestDBGetDatabases_MongoWithoutConfiguredDatabaseStillEnumerates(t *testing.T) { + installFakeOptionalDriverRuntime(t) + + config := connection.ConnectionConfig{Type: "mongodb"} + app := NewApp() + database := &releaseRecordingDB{databases: []string{"admin", "application"}} + app.dbCache[getCacheKey(config)] = cachedDatabase{ + inst: database, + config: normalizeCacheKeyConfig(config), + } + + result := app.DBGetDatabases(config) + if !result.Success { + t.Fatalf("expected unscoped MongoDB database lookup to succeed, got %q", result.Message) + } + if database.getDatabasesCalls != 1 { + t.Fatalf("expected unscoped MongoDB lookup to enumerate databases once, got %d calls", database.getDatabasesCalls) + } + rows, ok := result.Data.([]map[string]string) + if !ok || len(rows) != 2 || rows[0]["Database"] != "admin" || rows[1]["Database"] != "application" { + t.Fatalf("expected enumerated MongoDB databases, got %#v", result.Data) + } +} + func TestValidateTestConnectionInput_ClickHouseRequiresTarget(t *testing.T) { err := validateTestConnectionInput(connection.ConnectionConfig{Type: "clickhouse"}) if err == nil {