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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Triggers as a sidebar section, listed per database and schema alongside Procedures and Functions. (#2383)
- Read-only source viewer for procedures, functions and triggers, with Copy, Export and Open in Editor. (#2383)
- Procedures, functions and triggers on MSSQL, Oracle, SQLite, ClickHouse, DuckDB, Snowflake, BigQuery, Cassandra, LibSQL, Cloudflare D1, Teradata and Dameng. (#2383)
- Procedures, functions and triggers in the quick switcher.
- Argument signatures on routine rows, shown when two routines in a section share a name.
- Schema-wide `list_triggers` for MCP clients, and `return_type` and `language` on `list_routines`.

### Changed

- The data grid draws its cells instead of building a view for each one, so a result with hundreds of columns opens at once and holds a fraction of the memory. (#2381)
Expand All @@ -22,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Tab out of a row's last cell and Shift+Tab out of its first doing nothing.
- Size All Columns to Fit leaving the far columns of a wide result unreachable.
- A table with 500 columns pinning a core for 20 seconds and taking a gigabyte to open. (#2381)
- One of two PostgreSQL function overloads missing from the sidebar, and Show DDL opening an arbitrary one. (#2383)
- MySQL Show DDL reading the session database instead of the one being browsed. (#2383)
- Routine tooltips, VoiceOver labels and Copy with Signature showing a return type in place of the argument list. (#2383)
- Duplicate routine rows in the flat sidebar taking the selection back to the first of them. (#2383)
- MySQL triggers losing their definer, `WHEN` clause and ordering in the Structure tab.
- Oracle triggers showing a header with no body in the Structure tab.

## [0.67.1] - 2026-08-22

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// TeradataObjectQueries.swift
// TableProTeradataCore
//
// Catalog SQL for routines and triggers. Pure, so it is testable without a server.
//

import Foundation

public enum TeradataObjectQueries {
/// DBC.TablesV.TableKind is a single character. These are the ones that are routines or
/// triggers rather than tables, views or indexes.
public enum TableKind {
public static let storedProcedure = "P"
public static let externalProcedure = "E"
public static let standardFunction = "F"
public static let aggregateFunction = "A"
public static let combinedFunction = "B"
public static let tableFunction = "R"
public static let orderedAnalyticFunction = "S"
public static let macro = "M"
public static let trigger = "G"

public static let procedures = [storedProcedure, externalProcedure]
public static let functions = [
standardFunction, aggregateFunction, combinedFunction,
tableFunction, orderedAnalyticFunction, macro,
]
}

public static func routineList(database: String) -> String {
let kinds = (TableKind.procedures + TableKind.functions)
.map { TeradataSchemaQueries.quoteLiteral($0) }
.joined(separator: ", ")
return """
SELECT TableName, TableKind, DatabaseName, RequestText, CreatorName
FROM DBC.TablesV
WHERE DatabaseName = \(TeradataSchemaQueries.quoteLiteral(database))
AND TableKind IN (\(kinds))
ORDER BY TableKind, TableName
"""
}

public static func triggerList(database: String, table: String?) -> String {
var query = """
SELECT TriggerName, SubjectTableDataBaseName, TableName, ActionTime, Event, Kind, \
EnabledFlag, RequestText, OrderNumber
FROM DBC.TriggersV
WHERE DatabaseName = \(TeradataSchemaQueries.quoteLiteral(database))
"""
if let table {
query += "\nAND TableName = \(TeradataSchemaQueries.quoteLiteral(table))"
}
return query + "\nORDER BY TableName, TriggerName"
}

/// SHOW PROCEDURE only returns text when the procedure was created with SPL retention on, so
/// DBC.TablesV.RequestText is tried first and this is the fallback.
public static func routineDefinition(kind: String, database: String, name: String) -> String {
let qualified = TeradataSchemaQueries.qualifiedName(database: database, table: name)
return TableKind.procedures.contains(kind)
? "SHOW PROCEDURE \(qualified)"
: "SHOW FUNCTION \(qualified)"
}

public static func isProcedure(kind: String?) -> Bool {
guard let kind = kind?.trimmingCharacters(in: .whitespaces) else { return false }
return TableKind.procedures.contains(kind)
}

public static func timing(fromActionTime actionTime: String?) -> String {
switch actionTime?.trimmingCharacters(in: .whitespaces).uppercased() {
case "B": return "BEFORE"
case "A": return "AFTER"
case "I": return "INSTEAD OF"
default: return actionTime?.trimmingCharacters(in: .whitespaces) ?? ""
}
}

public static func event(fromEventCode event: String?) -> String {
switch event?.trimmingCharacters(in: .whitespaces).uppercased() {
case "I": return "INSERT"
case "U": return "UPDATE"
case "D": return "DELETE"
default: return event?.trimmingCharacters(in: .whitespaces) ?? ""
}
}

public static func orientation(fromKind kind: String?) -> String {
kind?.trimmingCharacters(in: .whitespaces).uppercased() == "R" ? "ROW" : "STATEMENT"
}
}
1 change: 1 addition & 0 deletions Plugins/BigQueryDriverPlugin/BigQueryPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ final class BigQueryPlugin: NSObject, TableProPlugin, DriverPlugin {
static let queryLanguageName = "SQL"
static let editorLanguage: EditorLanguage = .sql
static let supportsForeignKeys = false
static let supportsRoutines = true
static let supportsSchemaEditing = false
static let supportsDatabaseSwitching = false
static let supportsSchemaSwitching = true
Expand Down
64 changes: 64 additions & 0 deletions Plugins/BigQueryDriverPlugin/BigQueryPluginDriver+Routines.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// BigQueryPluginDriver+Routines.swift
// BigQueryDriverPlugin
//

import Foundation
import TableProPluginKit

/// BigQuery has procedures and functions and no triggers.
public enum BigQueryObjectQueries {
public static func escapeLiteral(_ value: String) -> String {
value
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
}

/// The ddl column holds the whole CREATE statement, so the list and the source are one read.
public static func routineList(project: String, dataset: String) -> String {
"""
SELECT routine_name, routine_schema, routine_type, data_type, language, ddl
FROM `\(project).\(dataset).INFORMATION_SCHEMA.ROUTINES`
ORDER BY routine_type, routine_name
"""
}
}

extension BigQueryPluginDriver {
func fetchRoutines(schema: String?) async throws -> [PluginRoutineInfo] {
guard let conn = connection else { throw BigQueryError.notConnected }
let dataset = schema ?? currentSchema ?? ""
guard !dataset.isEmpty else { return [] }
let sql = BigQueryObjectQueries.routineList(project: conn.projectId, dataset: dataset)
let result = try await conn.executeQuery(sql, defaultDataset: dataset)
return (result.queryResponse.rows ?? []).compactMap { row -> PluginRoutineInfo? in
let cells = row.f ?? []
func text(_ index: Int) -> String? {
guard index < cells.count, case .string(let value) = cells[index].v else { return nil }
return value
}
guard let name = text(0) else { return nil }
let isProcedure = (text(2) ?? "").uppercased() == "PROCEDURE"
return PluginRoutineInfo(
name: name,
kind: isProcedure ? .procedure : .function,
schema: text(1) ?? dataset,
returnType: text(3),
language: text(4),
argumentSignature: nil,
identity: nil,
definition: text(5),
attributes: []
)
}
}

func fetchRoutineDDL(_ routine: PluginRoutineInfo) async throws -> String {
if let ddl = routine.definition, !ddl.isEmpty { return ddl }
let listed = try await fetchRoutines(schema: routine.schema)
guard let ddl = listed.first(where: { $0.name == routine.name })?.definition, !ddl.isEmpty else {
throw PluginObjectSourceError.notFound(routine.name)
}
return ddl
}
}
2 changes: 1 addition & 1 deletion Plugins/BigQueryDriverPlugin/BigQueryPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal final class BigQueryPluginDriver: PluginDatabaseDriver, @unchecked Send
private var _columnTypeCache: [String: [String]] = [:]
private var _queryTimeoutSeconds: Int = 300

private var connection: BigQueryConnection? {
var connection: BigQueryConnection? {
lock.withLock { _connection }
}

Expand Down
4 changes: 3 additions & 1 deletion Plugins/CassandraDriverPlugin/CassandraPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ internal final class CassandraPlugin: NSObject, TableProPlugin, DriverPlugin {
static let urlSchemes: [String] = ["cassandra", "cql", "scylladb", "scylla"]
static let requiresAuthentication = false
static let supportsForeignKeys = false
static let supportsRoutines = true
static let supportsDatabaseTriggerBrowse = true
static let brandColorHex = "#26A0D8"
static let queryLanguageName = "CQL"
static let supportsDatabaseSwitching = true
Expand Down Expand Up @@ -564,7 +566,7 @@ internal final class CassandraPluginDriver: PluginDatabaseDriver, @unchecked Sen

// MARK: - Private Helpers

private func resolveKeyspace(_ schema: String?) -> String {
func resolveKeyspace(_ schema: String?) -> String {
if let schema, !schema.isEmpty { return schema }
stateLock.lock()
defer { stateLock.unlock() }
Expand Down
Loading
Loading