Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Generated and Expression fields on the column row, with a stored or virtual choice. (#2478)
- `check_constraints` and `generation_expression` in the MCP `describe_table` response.
- Remote File pane for SQLite, opening a read-only copy of a database that lives on an SSH server. (#2474)
- Rename on a table's right-click menu, editing the row's label in place. (#2482)
- Rename Database and Rename Schema on the sidebar's container rows, where the engine has them. (#2482)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions Plugins/BigQueryDriverPlugin/BigQueryPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ final class BigQueryPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "BigQuery"

static let supportsRenameTable = true
static let databaseDisplayName = "Google BigQuery"
static let iconName = "bigquery-icon"
static let defaultPort = 0
Expand Down
18 changes: 18 additions & 0 deletions Plugins/BigQueryDriverPlugin/BigQueryPluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BigQueryPluginDriver+Rename.swift
// BigQueryDriverPlugin
//

import Foundation
import TableProPluginKit

extension BigQueryPluginDriver {
/// The new name is bare and the table stays in its dataset. BigQuery refuses the statement
/// while a streaming buffer is active, which is roughly five hours after the last row streamed
/// in, and for an external table; both come back as the server's own message.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let quoted = quoteIdentifier(name)
let target = schema.map { "\(quoteIdentifier($0)).\(quoted)" } ?? quoted
_ = try await execute(query: "ALTER \(objectType) \(target) RENAME TO \(quoteIdentifier(newName))")
}
}
2 changes: 2 additions & 0 deletions Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ final class ClickHousePlugin: NSObject, TableProPlugin, DriverPlugin {
static let structureColumnFields: [StructureColumnField] = [.name, .type, .nullable, .defaultValue, .comment]
static let supportsQueryProgress = true
static let supportsDropDatabase = true
static let supportsRenameTable = true
static let supportsRenameDatabase = true

static let sqlDialect: SQLDialectDescriptor? = SQLDialectDescriptor(
identifierQuote: "`",
Expand Down
22 changes: 22 additions & 0 deletions Plugins/ClickHouseDriverPlugin/ClickHousePluginDriver+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,28 @@ extension ClickHousePluginDriver {
_ = try await execute(query: "DROP DATABASE `\(escapedName)`")
}

/// Both sides are qualified with the same database, so this renames in place. Qualifying them
/// differently is how ClickHouse moves a table, which is a different verb to the user.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let database = schema ?? lock.withLock { _currentDatabase }
let old = qualified(database: database, name: name)
let new = qualified(database: database, name: newName)
_ = try await execute(query: "RENAME TABLE \(old) TO \(new)")
}

/// Needs the Atomic database engine, the default since 20.10. An Ordinary database refuses,
/// and the server's own message says so.
func renameDatabase(name: String, to newName: String) async throws {
_ = try await execute(
query: "RENAME DATABASE \(quoteIdentifier(name)) TO \(quoteIdentifier(newName))"
)
}

private func qualified(database: String?, name: String) -> String {
guard let database, !database.isEmpty else { return quoteIdentifier(name) }
return "\(quoteIdentifier(database)).\(quoteIdentifier(name))"
}

// MARK: - All Tables Metadata

func allTablesMetadataSQL(schema: String?) -> String? {
Expand Down
3 changes: 3 additions & 0 deletions Plugins/CloudflareD1DriverPlugin/CloudflareD1Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ final class CloudflareD1Plugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "Cloudflare D1"

static let supportsRenameTable = true
static let supportsRenameView = false
static let databaseDisplayName = "Cloudflare D1"
static let iconName = "cloudflare-d1-icon"
static let defaultPort = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// CloudflareD1PluginDriver+Rename.swift
// CloudflareD1DriverPlugin
//

import Foundation
import TableProPluginKit

extension CloudflareD1PluginDriver {
/// SQLite's rules, and SQLite's one rename: `ALTER TABLE` refuses a view. A D1 database is an
/// API object whose edit endpoint accepts only read replication, so its name cannot change.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
guard objectType.uppercased() == "TABLE" else {
throw PluginDriverUnsupportedOperation.renameTable
}
_ = try await execute(
query: "ALTER TABLE \(quoteIdentifier(name)) RENAME TO \(quoteIdentifier(newName))"
)
}
}
2 changes: 2 additions & 0 deletions Plugins/DamengDriverPlugin/DamengPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ final class DamengPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "Dameng"

static let supportsRenameTable = true
static let databaseDisplayName = "Dameng DM8"
static let iconName = "cylinder"
static let defaultPort = 5_236
Expand Down
17 changes: 17 additions & 0 deletions Plugins/DamengDriverPlugin/DamengPluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// DamengPluginDriver+Rename.swift
// DamengDriverPlugin
//

import Foundation
import TableProPluginKit

extension DamengPluginDriver {
/// Oracle-compatible, so the new name stays bare. Dameng also ships `sp_rename`, which is not
/// used here: the ALTER form is the one its own documentation leads with.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let quoted = quoteIdentifier(name)
let target = schema.map { "\(quoteIdentifier($0)).\(quoted)" } ?? quoted
_ = try await execute(query: "ALTER \(objectType) \(target) RENAME TO \(quoteIdentifier(newName))")
}
}
2 changes: 2 additions & 0 deletions Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ final class DuckDBPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "DuckDB"

static let supportsRenameTable = true
static let databaseDisplayName = "DuckDB"
static let iconName = "duckdb-icon"
static let defaultPort = 9_494
Expand Down
17 changes: 17 additions & 0 deletions Plugins/DuckDBDriverPlugin/DuckDBPluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// DuckDBPluginDriver+Rename.swift
// DuckDBDriverPlugin
//

import Foundation
import TableProPluginKit

extension DuckDBPluginDriver {
/// The new name is bare and the object stays in its schema. DuckDB has no `ALTER SCHEMA
/// RENAME` and no database rename at all, so those stay unimplemented.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let quoted = quoteIdentifier(name)
let target = schema.map { "\(quoteIdentifier($0)).\(quoted)" } ?? quoted
_ = try await execute(query: "ALTER \(objectType) \(target) RENAME TO \(quoteIdentifier(newName))")
}
}
3 changes: 3 additions & 0 deletions Plugins/LibSQLDriverPlugin/LibSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ final class LibSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "libSQL"

static let supportsRenameTable = true
static let supportsRenameView = false
static let additionalDatabaseTypeIds = ["Turso"]
static let databaseDisplayName = "libSQL / Turso"
static let iconName = "libsql-icon"
Expand Down
20 changes: 20 additions & 0 deletions Plugins/LibSQLDriverPlugin/LibSQLPluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// LibSQLPluginDriver+Rename.swift
// LibSQLDriverPlugin
//

import Foundation
import TableProPluginKit

extension LibSQLPluginDriver {
/// SQLite's rules, and SQLite's one rename: `ALTER TABLE` refuses a view. A Turso database
/// name has no libSQL wire operation, which is why `dropDatabase` already refuses too.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
guard objectType.uppercased() == "TABLE" else {
throw PluginDriverUnsupportedOperation.renameTable
}
_ = try await execute(
query: "ALTER TABLE \(quoteIdentifier(name)) RENAME TO \(quoteIdentifier(newName))"
)
}
}
2 changes: 2 additions & 0 deletions Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "SQL Server"

static let supportsRenameTable = true
static let databaseDisplayName = "SQL Server"
static let iconName = "mssql-icon"
static let defaultPort = 1433
Expand Down
27 changes: 27 additions & 0 deletions Plugins/MSSQLDriverPlugin/MSSQLPluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// MSSQLPluginDriver+Rename.swift
// MSSQLDriverPlugin
//

import Foundation
import TableProPluginKit

extension MSSQLPluginDriver {
/// `sp_rename` takes names as string literals rather than identifiers, and the new one must be
/// a single part: passing `schema.new` renames the object to something literally called
/// "schema.new". Its object type argument is what tells the procedure this is not a column.
///
/// Each half of the old name is bracketed before the two are joined, because `@objname` is
/// parsed as a multipart name: a table legitimately called `quarter.1` would otherwise be read
/// as the object `1` in the schema `quarter`.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let qualified = [schema, name].compactMap { $0 }.map(quoteIdentifier).joined(separator: ".")
_ = try await execute(
query: "EXEC sp_rename \(literal(qualified)), \(literal(newName)), 'OBJECT'"
)
}

private func literal(_ value: String) -> String {
"N'\(value.replacingOccurrences(of: "'", with: "''"))'"
}
}
2 changes: 2 additions & 0 deletions Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "MongoDB"

static let supportsRenameTable = true
static let databaseDisplayName = "MongoDB"
static let iconName = "mongodb-icon"
static let defaultPort = 27017
Expand Down
16 changes: 16 additions & 0 deletions Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,22 @@ final class MongoDBPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
)
}

/// `renameCollection` runs against `admin` and nowhere else, and it names both sides with the
/// full `database.collection`, so the two halves cannot be quoted or qualified the way a SQL
/// driver's would be. Atlas grants only the same-database form, which is all this offers.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
guard let conn = mongoConnection else {
throw MongoDBPluginError.notConnected
}
let database = schema ?? currentDb
let from = "\"\(escapeJsonString("\(database).\(name)"))\""
let to = "\"\(escapeJsonString("\(database).\(newName)"))\""
_ = try await conn.runCommand(
"{\"renameCollection\": \(from), \"to\": \(to)}",
database: "admin"
)
}

func dropDatabase(name: String) async throws {
guard let conn = mongoConnection else {
throw MongoDBPluginError.notConnected
Expand Down
1 change: 1 addition & 0 deletions Plugins/MySQLDriverPlugin/MySQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin {
)

static let supportsDropDatabase = true
static let supportsRenameTable = true
static let supportsTriggers = true
static let supportsRoutines = true
static let supportsDatabaseTriggerBrowse = true
Expand Down
9 changes: 9 additions & 0 deletions Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,15 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
_ = try await execute(query: "DROP DATABASE `\(escapedName)`")
}

/// `RENAME TABLE` rather than `ALTER TABLE ... RENAME TO`, because it is the only form that
/// takes a view, and both sides are qualified with the same schema so the statement cannot
/// move the object anywhere.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let old = MySQLObjectQueries.qualifiedIdentifier(schema: schema, name: name)
let new = MySQLObjectQueries.qualifiedIdentifier(schema: schema, name: newName)
_ = try await execute(query: "RENAME TABLE \(old) TO \(new)")
}

// MARK: - Database Switching

func switchDatabase(to database: String) async throws {
Expand Down
2 changes: 2 additions & 0 deletions Plugins/OracleDriverPlugin/OraclePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "Oracle"

static let supportsRenameTable = true
static let databaseDisplayName = "Oracle"
static let iconName = "oracle-icon"
static let defaultPort = 1_521
Expand Down
19 changes: 19 additions & 0 deletions Plugins/OracleDriverPlugin/OraclePluginDriver+Rename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// OraclePluginDriver+Rename.swift
// OracleDriverPlugin
//

import Foundation
import TableProPluginKit

extension OraclePluginDriver {
/// The new name must be bare. A qualified one raises ORA-14047, because Oracle renames in
/// place and has no statement that moves an object between schemas.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let quoted = OracleObjectQueries.quoteIdentifier(name)
let target = schema.map { "\(OracleObjectQueries.quoteIdentifier($0)).\(quoted)" } ?? quoted
_ = try await execute(
query: "ALTER \(objectType) \(target) RENAME TO \(OracleObjectQueries.quoteIdentifier(newName))"
)
}
}
25 changes: 25 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/LibPQDriverCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,31 @@ protocol LibPQBackedDriver: PluginDatabaseDriver {
}

extension LibPQBackedDriver {
/// The new name must be bare. Every libpq engine here rejects a qualified one, because this
/// statement renames in place and never moves the object; `SET SCHEMA` is the separate verb.
///
/// It lives on the protocol rather than on `PostgreSQLPluginDriver`, because Redshift and
/// CockroachDB are siblings of that class rather than subclasses: an implementation there
/// leaves both of them declaring the capability with nothing behind it.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
let target = "\(quoteIdentifier(schema ?? core.currentSchema)).\(quoteIdentifier(name))"
_ = try await execute(query: "ALTER \(objectType) \(target) RENAME TO \(quoteIdentifier(newName))")
}

/// Not the database the connection is on: PostgreSQL, Redshift and CockroachDB all answer that
/// with a refusal, so the app keeps the item off a row it is browsing.
func renameDatabase(name: String, to newName: String) async throws {
_ = try await execute(
query: "ALTER DATABASE \(quoteIdentifier(name)) RENAME TO \(quoteIdentifier(newName))"
)
}

func renameSchema(name: String, to newName: String) async throws {
_ = try await execute(
query: "ALTER SCHEMA \(quoteIdentifier(name)) RENAME TO \(quoteIdentifier(newName))"
)
}

func connect() async throws {
try await core.connect()
}
Expand Down
3 changes: 3 additions & 0 deletions Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
static let requiresReconnectForDatabaseSwitch = true
static let parameterStyle: ParameterStyle = .dollar
static let supportsDropDatabase = true
static let supportsRenameTable = true
static let supportsRenameDatabase = true
static let supportsRenameSchema = true
static let supportsDropSchema = true
static let supportsTriggers = true
static let supportsRoutines = true
Expand Down
15 changes: 15 additions & 0 deletions Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin {
static let fileExtensions: [String] = ["db", "db3", "s3db", "sl3", "sqlite", "sqlite3", "sqlitedb"]
static let brandColorHex = "#003B57"
static let supportsDatabaseSwitching = false
static let supportsRenameTable = true
static let supportsRenameView = false
static let supportsTriggers = true
static let supportsDatabaseTriggerBrowse = true
static let supportsTriggerEditing = true
Expand Down Expand Up @@ -1166,6 +1168,19 @@ final class SQLitePluginDriver: PluginDatabaseDriver, @unchecked Sendable {

// MARK: - ALTER TABLE DDL

/// `ALTER TABLE` is the only rename SQLite has and it refuses a view, so a view is turned
/// away here rather than by a message from the engine. From 3.25 the statement rewrites the
/// references to the table in every trigger and view, and from 3.26 in every foreign key,
/// unless `PRAGMA legacy_alter_table` is on.
func renameTable(name: String, schema: String?, to newName: String, objectType: String) async throws {
guard objectType.uppercased() == "TABLE" else {
throw PluginDriverUnsupportedOperation.renameTable
}
_ = try await execute(
query: "ALTER TABLE \(quoteIdentifier(name)) RENAME TO \(quoteIdentifier(newName))"
)
}

func generateAddColumnSQL(table: String, column: PluginColumnDefinition) -> String? {
let colDef = sqliteColumnDefinition(addableColumn(column), inlinePK: false)
return "ALTER TABLE \(quoteIdentifier(table)) ADD COLUMN \(colDef)"
Expand Down
9 changes: 9 additions & 0 deletions Plugins/SnowflakeDriverPlugin/SnowflakePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ final class SnowflakePlugin: NSObject, TableProPlugin, DriverPlugin {
static let capabilities: [PluginCapability] = [.databaseDriver]

static let databaseTypeId = "Snowflake"

static let supportsRenameTable = true

/// Off, and not because Snowflake refuses: `ALTER DATABASE ... RENAME TO` works and the driver
/// implements it. This tree hangs tables off schemas and draws no database rows at all, so
/// there is nowhere to raise the command from. Turn it back on with the row that reaches it.
static let supportsRenameDatabase = false

static let supportsRenameSchema = true
static let databaseDisplayName = "Snowflake"
static let iconName = "snowflake-icon"
static let defaultPort = 443
Expand Down
Loading
Loading