Skip to content

Commit 95c61e7

Browse files
authored
fix(plugins): list Oracle-maintained schemas as system and stop hiding Dameng SYSDBA (#2839)
Claude-Session: https://claude.ai/code/session_018swsRkBkYjm7Y8WFfgRg7T Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 42e2a4b commit 95c61e7

15 files changed

Lines changed: 267 additions & 17 deletions

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4949
- Data grid jumping to the top on a later reload after a page change failed.
5050
- System databases such as `mysql` missing from the database switcher, the tab database picker and Open Quickly. (#2832)
5151
- SQL Server and ClickHouse system databases listed as user databases once the database switcher finished loading.
52+
- Oracle system schemas such as `SYS` and `XDB` listed with user schemas.
53+
- Dameng `SYSDBA` schema hidden from the sidebar and listed under System.
5254
- TiDB's `INFORMATION_SCHEMA` and `PERFORMANCE_SCHEMA` listed as user databases on a MySQL or MariaDB connection.
5355
- SQL Server database size and table count showing the current database's numbers, and no size at 2 GB or more.
5456
- ClickHouse databases with no tables missing from the database switcher and database statistics.

‎Plugins/DamengDriverPlugin/DamengPlugin.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class DamengPlugin: NSObject, TableProPlugin, DriverPlugin {
2929
]
3030
static let databaseGroupingStrategy: GroupingStrategy = .hierarchicalSchema
3131
static let pathFieldRole: PathFieldRole = .database
32-
static let systemSchemaNames = ["SYS", "SYSDBA", "SYSAUDITOR", "SYSSSO", "CTISYS"]
32+
static let systemSchemaNames = DamengSystemSchemas.listed
3333
static let supportsCascadeDrop = true
3434
static let supportsDropSchema = true
3535
static let supportsForeignKeyDisable = false
@@ -332,8 +332,8 @@ final class DamengPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
332332
}
333333

334334
func dropSchema(name: String) async throws {
335-
guard !DamengPlugin.systemSchemaNames.contains(name.uppercased()) else {
336-
throw DamengError(message: String(localized: "Dameng system schemas cannot be dropped."))
335+
guard !DamengSystemSchemas.isProtectedFromDrop(name) else {
336+
throw DamengError(message: String(localized: "Dameng's built-in schemas cannot be dropped."))
337337
}
338338
guard name.caseInsensitiveCompare(activeSchema ?? "") != .orderedSame else {
339339
throw DamengError(message: String(localized: "Switch away from a schema before dropping it."))

‎Plugins/DamengDriverPlugin/DamengPluginDriver+Schema.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ extension DamengPluginDriver {
270270
return PluginDatabaseMetadata(
271271
name: database,
272272
tableCount: result.rows.first?.first?.asText.flatMap(Int.init),
273-
isSystemDatabase: DamengPlugin.systemSchemaNames.contains(database.uppercased())
273+
isSystemDatabase: DamengSystemSchemas.listed.contains(database)
274274
)
275275
}
276276

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// DamengSystemSchemas.swift
3+
// DamengDriverPlugin
4+
//
5+
6+
import Foundation
7+
8+
/// Two lists, because showing a schema and letting it be dropped are different questions. `listed` is what the
9+
/// sidebar and the pickers treat as system: schemas that hold the engine's own objects. `SYSDBA` is not on it,
10+
/// because it is the administrator login's own default schema and where that login's tables land. The drop guard is
11+
/// wider and matches any spelling: `SYSDBA` also holds a few DM-supplied procedures and `SYSDBO` is the other preset
12+
/// administrator, so a `DROP SCHEMA ... CASCADE` on either is refused even though they list with the user's schemas.
13+
enum DamengSystemSchemas {
14+
static let listed: [String] = ["SYS", "SYSAUDITOR", "SYSSSO", "CTISYS", "SYSJOB", "SYSGEO2"]
15+
16+
static let protectedFromDrop: Set<String> = [
17+
"SYS", "SYSDBA", "SYSDBO", "SYSAUDITOR", "SYSSSO", "CTISYS", "SYSJOB", "SYSGEO2"
18+
]
19+
20+
static func isProtectedFromDrop(_ name: String) -> Bool {
21+
protectedFromDrop.contains(name.uppercased())
22+
}
23+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// PluginMetadataRegistry+OracleSystemSchemas.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
extension PluginMetadataRegistry {
9+
/// The schemas Oracle creates and maintains itself. The core is the `ORACLE_MAINTAINED = 'Y'` set measured from
10+
/// `ALL_USERS` on Oracle AI Database 26ai Free; the rest are the predefined accounts the 19c and 11.2 Security
11+
/// Guides list for options that image leaves out, since `ORACLE_MAINTAINED` only exists from 12.1.0.2. Schemas
12+
/// named with a release number, such as `APEX_240200`, cannot be listed by name.
13+
static let oracleSystemSchemaNames: [String] = [
14+
"ANONYMOUS", "APEX_PUBLIC_USER", "APPQOSSYS", "ASMSNMP", "AUDSYS", "BAASSYS", "CTXSYS",
15+
"DBSFWUSER", "DBSNMP", "DGPDB_INT", "DIP", "DVF", "DVSYS", "EXFSYS", "FLOWS_FILES",
16+
"GGSHAREDCAP", "GGSYS", "GSMADMIN_INTERNAL", "GSMCATUSER", "GSMROOTUSER", "GSMUSER", "LBACSYS",
17+
"MDDATA", "MDSYS", "MGMT_VIEW", "OJVMSYS", "OLAPSYS", "ORACLE_OCM", "ORDDATA", "ORDPLUGINS",
18+
"ORDSYS", "OUTLN", "OWBSYS", "REMOTE_SCHEDULER_AGENT", "SI_INFORMTN_SCHEMA", "SPATIAL_CSW_ADMIN_USR",
19+
"SPATIAL_WFS_ADMIN_USR", "SYS", "SYS$UMF", "SYSBACKUP", "SYSDG", "SYSKM", "SYSMAN", "SYSRAC",
20+
"SYSTEM", "VECSYS", "WKPROXY", "WKSYS", "WK_TEST", "WMSYS", "XDB", "XS$NULL"
21+
]
22+
}

‎TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -551,10 +551,8 @@ extension PluginMetadataRegistry {
551551
containerEntityName: "Schema",
552552
defaultPrimaryKeyColumn: nil,
553553
immutableColumns: [],
554-
systemDatabaseNames: [
555-
"SYS", "SYSTEM", "OUTLN", "DBSNMP", "APPQOSSYS", "WMSYS", "XDB"
556-
],
557-
systemSchemaNames: [],
554+
systemDatabaseNames: PluginMetadataRegistry.oracleSystemSchemaNames,
555+
systemSchemaNames: PluginMetadataRegistry.oracleSystemSchemaNames,
558556
fileExtensions: [],
559557
databaseGroupingStrategy: .hierarchicalSchema,
560558
structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment]
@@ -612,7 +610,7 @@ extension PluginMetadataRegistry {
612610
defaultPrimaryKeyColumn: nil,
613611
immutableColumns: [],
614612
systemDatabaseNames: [],
615-
systemSchemaNames: ["SYS", "SYSDBA", "SYSAUDITOR", "SYSSSO", "CTISYS"],
613+
systemSchemaNames: ["SYS", "SYSAUDITOR", "SYSSSO", "CTISYS", "SYSJOB", "SYSGEO2"],
616614
fileExtensions: [],
617615
databaseGroupingStrategy: .hierarchicalSchema,
618616
structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment]

‎TablePro/Core/Plugins/PluginMetadataRegistry+SnapshotAdoption.swift‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ extension PluginMetadataRegistry {
7272
)
7373
}
7474

75+
/// A name is a system database or schema when either the plugin or the app's curated entry lists it. An installed
76+
/// plugin can predate the app's list or report none at all: every published Oracle plugin lists no system
77+
/// schemas, which left `SYS` and `XDB` among the user schemas whichever plugin version was installed.
78+
static func adoptCuratedSystemNames(
79+
_ snapshot: inout PluginMetadataSnapshot,
80+
registryDefault: PluginMetadataSnapshot
81+
) {
82+
let databases = mergedSystemNames(
83+
reported: snapshot.schema.systemDatabaseNames,
84+
curated: registryDefault.schema.systemDatabaseNames
85+
)
86+
let schemas = mergedSystemNames(
87+
reported: snapshot.schema.systemSchemaNames,
88+
curated: registryDefault.schema.systemSchemaNames
89+
)
90+
guard databases != snapshot.schema.systemDatabaseNames
91+
|| schemas != snapshot.schema.systemSchemaNames else { return }
92+
snapshot = snapshot.withSystemNames(databases: databases, schemas: schemas)
93+
}
94+
95+
private static func mergedSystemNames(reported: [String], curated: [String]) -> [String] {
96+
var seen = Set(reported)
97+
return reported + curated.filter { seen.insert($0).inserted }
98+
}
99+
75100
/// A plugin built before its engine moved to schema-only switching still
76101
/// declares database switching with bySchema grouping. The app's registry
77102
/// default is the ground truth for routing, so its switch fields win.

‎TablePro/Core/Plugins/PluginMetadataRegistry.swift‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,43 @@ struct PluginMetadataSnapshot: Sendable {
300300
)
301301
}
302302

303+
func withSystemNames(databases: [String], schemas: [String]) -> PluginMetadataSnapshot {
304+
PluginMetadataSnapshot(
305+
displayName: displayName, iconName: iconName, defaultPort: defaultPort,
306+
requiresAuthentication: requiresAuthentication, supportsForeignKeys: supportsForeignKeys,
307+
supportsSchemaEditing: supportsSchemaEditing, isDownloadable: isDownloadable,
308+
primaryUrlScheme: primaryUrlScheme, parameterStyle: parameterStyle,
309+
navigationModel: navigationModel, explainVariants: explainVariants,
310+
pathFieldRole: pathFieldRole, supportsHealthMonitor: supportsHealthMonitor,
311+
urlSchemes: urlSchemes, postConnectActions: postConnectActions,
312+
brandColorHex: brandColorHex, queryLanguageName: queryLanguageName,
313+
editorLanguage: editorLanguage, connectionMode: connectionMode,
314+
supportsDatabaseSwitching: supportsDatabaseSwitching,
315+
structureEditing: structureEditing,
316+
capabilities: capabilities,
317+
schema: SchemaInfo(
318+
defaultSchemaName: schema.defaultSchemaName,
319+
defaultGroupName: schema.defaultGroupName,
320+
tableEntityName: schema.tableEntityName,
321+
containerEntityName: schema.containerEntityName,
322+
schemaEntityName: schema.schemaEntityName,
323+
defaultPrimaryKeyColumn: schema.defaultPrimaryKeyColumn,
324+
immutableColumns: schema.immutableColumns,
325+
systemDatabaseNames: databases,
326+
systemSchemaNames: schemas,
327+
fileExtensions: schema.fileExtensions,
328+
fileSignatures: schema.fileSignatures,
329+
databaseGroupingStrategy: schema.databaseGroupingStrategy,
330+
structureColumnFields: schema.structureColumnFields,
331+
implicitSchemaName: schema.implicitSchemaName,
332+
rowMatchExcludedTypePrefixes: schema.rowMatchExcludedTypePrefixes,
333+
rowMatchTextTypePrefixes: schema.rowMatchTextTypePrefixes
334+
),
335+
editor: editor,
336+
connection: connection
337+
)
338+
}
339+
303340
func withBranding(from source: PluginMetadataSnapshot) -> PluginMetadataSnapshot {
304341
PluginMetadataSnapshot(
305342
displayName: source.displayName, iconName: source.iconName, defaultPort: defaultPort,
@@ -455,6 +492,7 @@ final class PluginMetadataRegistry: @unchecked Sendable {
455492
if let registryDefault = defaultSnapshots[typeId] {
456493
resolved = resolved.withIsDownloadable(registryDefault.isDownloadable)
457494
Self.adoptCuratedCaseSensitivity(&resolved, registryDefault: registryDefault)
495+
Self.adoptCuratedSystemNames(&resolved, registryDefault: registryDefault)
458496
if Self.declaresLegacySchemaOnlyRouting(resolved, registryDefault: registryDefault) {
459497
Logger(subsystem: "com.TablePro", category: "PluginMetadataRegistry").notice(
460498
"Plugin '\(typeId, privacy: .public)' declares legacy two-tier switching for a schema-only engine; applying the app's switch routing"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// PluginMetadataRegistrySystemNameAdoptionTests.swift
3+
// TableProTests
4+
//
5+
// A plugin's own snapshot replaces the curated entry when it loads, so a published plugin that lists fewer system
6+
// names than the app, or none, took them away. No plugin loads under XCTest, so these build the plugin's snapshot
7+
// from the curated one the way a published plugin would report it.
8+
//
9+
10+
import Foundation
11+
@testable import TablePro
12+
import TableProPluginKit
13+
import Testing
14+
15+
@Suite("PluginMetadataRegistry system name adoption")
16+
struct PluginMetadataRegistrySystemNameAdoptionTests {
17+
private func curated(_ typeId: String) -> PluginMetadataSnapshot? {
18+
PluginMetadataRegistry.shared.snapshot(forRegisteredTypeId: typeId)
19+
}
20+
21+
@Test("A published Oracle plugin that lists no system schemas keeps the app's")
22+
func emptyPluginSchemaListKeepsCuratedSchemas() {
23+
guard let oracle = curated("Oracle") else {
24+
Issue.record("Registry default for Oracle missing")
25+
return
26+
}
27+
var published = oracle.withSystemNames(
28+
databases: ["SYS", "SYSTEM", "OUTLN", "DBSNMP", "APPQOSSYS", "WMSYS", "XDB"],
29+
schemas: []
30+
)
31+
32+
PluginMetadataRegistry.adoptCuratedSystemNames(&published, registryDefault: oracle)
33+
34+
#expect(published.schema.systemSchemaNames == oracle.schema.systemSchemaNames)
35+
#expect(Set(published.schema.systemDatabaseNames) == Set(oracle.schema.systemDatabaseNames))
36+
}
37+
38+
@Test("A name either side lists counts, the plugin's names first and none twice")
39+
func namesAreUnitedInOrder() {
40+
guard let base = curated("Dameng") else {
41+
Issue.record("Registry default for Dameng missing")
42+
return
43+
}
44+
var plugin = base.withSystemNames(databases: ["b", "a"], schemas: ["S1"])
45+
let registryDefault = base.withSystemNames(databases: ["a", "c"], schemas: ["S2", "S1"])
46+
47+
PluginMetadataRegistry.adoptCuratedSystemNames(&plugin, registryDefault: registryDefault)
48+
49+
#expect(plugin.schema.systemDatabaseNames == ["b", "a", "c"])
50+
#expect(plugin.schema.systemSchemaNames == ["S1", "S2"])
51+
}
52+
53+
@Test("A plugin that already lists every curated name is left as it reported")
54+
func completePluginListIsUnchanged() {
55+
guard let base = curated("Dameng") else {
56+
Issue.record("Registry default for Dameng missing")
57+
return
58+
}
59+
var plugin = base.withSystemNames(databases: [], schemas: ["CTISYS", "SYS", "SYSAUDITOR", "SYSSSO", "SYSJOB", "SYSGEO2"])
60+
61+
PluginMetadataRegistry.adoptCuratedSystemNames(&plugin, registryDefault: base)
62+
63+
#expect(plugin.schema.systemSchemaNames == ["CTISYS", "SYS", "SYSAUDITOR", "SYSSSO", "SYSJOB", "SYSGEO2"])
64+
}
65+
66+
/// Measured on Oracle AI Database 26ai Free: `ALL_USERS` reports these with `ORACLE_MAINTAINED = 'Y'`, and the
67+
/// administrator account a PDB is created with, `PDBADMIN`, with `N`.
68+
@Test("Oracle's own schemas are system schemas, and the accounts users create are not")
69+
func oracleMaintainedSchemasAreSystem() {
70+
let schemas = PluginMetadataRegistry.shared.snapshot(for: .oracle)?.schema.systemSchemaNames ?? []
71+
for name in ["SYS", "SYSTEM", "XDB", "AUDSYS", "MDSYS", "CTXSYS", "GSMADMIN_INTERNAL", "XS$NULL"] {
72+
#expect(schemas.contains(name), "\(name) is Oracle-maintained")
73+
}
74+
for name in ["PDBADMIN", "OPS$ORACLE", "HR", "SCOTT"] {
75+
#expect(!schemas.contains(name), "\(name) is not Oracle-maintained")
76+
}
77+
}
78+
79+
@Test("Oracle names the same schemas whether it is asked for databases or schemas")
80+
func oracleListsAgree() {
81+
let schema = PluginMetadataRegistry.shared.snapshot(for: .oracle)?.schema
82+
#expect(schema?.systemDatabaseNames == schema?.systemSchemaNames)
83+
}
84+
}

‎TableProTests/Core/Plugins/PluginMetadataSnapshotCopyTests.swift‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ struct PluginMetadataSnapshotCopyTests {
4242
("withExplainVariants", original.withExplainVariants([])),
4343
("withBranding", original.withBranding(from: original)),
4444
("withIsDownloadable", original.withIsDownloadable(!original.isDownloadable)),
45-
("withSwitchRouting", original.withSwitchRouting(from: original))
45+
("withSwitchRouting", original.withSwitchRouting(from: original)),
46+
("withSystemNames", original.withSystemNames(databases: [], schemas: []))
4647
]
4748

4849
for (name, copy) in copies {
@@ -59,14 +60,32 @@ struct PluginMetadataSnapshotCopyTests {
5960
("withExplainVariants", original.withExplainVariants([])),
6061
("withBranding", original.withBranding(from: original)),
6162
("withIsDownloadable", original.withIsDownloadable(!original.isDownloadable)),
62-
("withSwitchRouting", original.withSwitchRouting(from: original))
63+
("withSwitchRouting", original.withSwitchRouting(from: original)),
64+
("withSystemNames", original.withSystemNames(databases: [], schemas: []))
6365
]
6466

6567
for (name, copy) in copies {
6668
#expect(copy.schema.implicitSchemaName == "(default)", "\(name) reset the implicit schema")
6769
}
6870
}
6971

72+
@Test("withSystemNames replaces only the two system name lists")
73+
func withSystemNamesKeepsEveryOtherSchemaField() throws {
74+
let original = try #require(PluginMetadataRegistry.shared.snapshot(for: .mysql))
75+
let copy = original.withSystemNames(databases: ["a"], schemas: ["b"])
76+
77+
#expect(copy.schema.systemDatabaseNames == ["a"])
78+
#expect(copy.schema.systemSchemaNames == ["b"])
79+
#expect(copy.schema.defaultSchemaName == original.schema.defaultSchemaName)
80+
#expect(copy.schema.containerEntityName == original.schema.containerEntityName)
81+
#expect(copy.schema.databaseGroupingStrategy == original.schema.databaseGroupingStrategy)
82+
#expect(copy.schema.structureColumnFields == original.schema.structureColumnFields)
83+
#expect(copy.schema.rowMatchTextTypePrefixes == original.schema.rowMatchTextTypePrefixes)
84+
#expect(copy.schema.fileSignatures.count == original.schema.fileSignatures.count)
85+
#expect(copy.editor.columnTypesByCategory == original.editor.columnTypesByCategory)
86+
#expect(copy.capabilities.supportsSSH == original.capabilities.supportsSSH)
87+
}
88+
7089
/// An engine whose `ALTER TABLE` can add a constraint says so, and is not pushed through a
7190
/// rebuild it does not need.
7291
@Test("An engine with the statements is curated as altering, not rebuilding")

0 commit comments

Comments
 (0)