Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fe10086
feat: add Create Table foundation (plugin protocol + tab infrastructure)
datlechin Mar 30, 2026
e195aa9
feat: implement generateCreateTableSQL for all remaining plugins
datlechin Mar 30, 2026
08f3bc2
feat: add CreateTableView with full DataGridView integration
datlechin Mar 30, 2026
ee8b636
fix: remove references to non-existent schema/comment fields on Plugi…
datlechin Mar 30, 2026
46d9eed
fix: add static engines/charsets/collations to CreateTableOptions
datlechin Mar 30, 2026
065813b
fix: unwrap optional table options in SQL preview
datlechin Mar 30, 2026
bb00cbd
fix: auto-add PRIMARY KEY for AUTO_INCREMENT columns
datlechin Mar 30, 2026
ba4a5ca
feat: add Primary Key column to Create Table grid
datlechin Mar 30, 2026
adf53d3
fix: convert create-table tab to table tab after successful creation
datlechin Mar 30, 2026
7124249
fix: open create table inline when no tabs exist
datlechin Mar 30, 2026
b2f8060
fix: use DDLTextView for syntax-highlighted SQL preview
datlechin Mar 30, 2026
619bef8
fix: address code review issues for Create Table
datlechin Mar 30, 2026
36ed700
refactor: align CreateTableView with native macOS tab layout
datlechin Mar 30, 2026
59a0e40
fix: address CreateTableView UI/UX issues
datlechin Mar 30, 2026
162e60f
fix: equalize +/- button sizes with fixed frame
datlechin Mar 30, 2026
377d312
fix: remove redundant Copy button row from SQL Preview tab
datlechin Mar 30, 2026
b1d1675
fix: use native Picker labels instead of separate Text + labelsHidden
datlechin Mar 30, 2026
8bfce27
fix: address final review issues for Create Table
datlechin Mar 30, 2026
d6bb027
merge: resolve CHANGELOG conflict with main
datlechin Mar 30, 2026
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Visual Create Table UI with column, index, and foreign key editors (sidebar → "Create New Table...")
- Real-time SQL preview with syntax highlighting for CREATE TABLE DDL
- Multi-database CREATE TABLE support: MySQL, PostgreSQL, SQLite, SQL Server, ClickHouse, DuckDB

### Fixed

- Globe+F (fn+F) fullscreen shortcut not working in SwiftUI lifecycle app
Expand Down
54 changes: 54 additions & 0 deletions Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,60 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
return (converted, paramMap)
}

// MARK: - Create Table DDL

func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? {
guard !definition.columns.isEmpty else { return nil }

let tableName = quoteIdentifier(definition.tableName)
let parts: [String] = definition.columns.map { clickhouseColumnDefinition($0) }

var sql = "CREATE TABLE \(tableName) (\n " +
parts.joined(separator: ",\n ") +
"\n)"

let engine = definition.engine ?? "MergeTree()"
sql += "\nENGINE = \(engine)"

let pkColumns = definition.columns.filter { $0.isPrimaryKey }
if !pkColumns.isEmpty {
let orderCols = pkColumns.map { quoteIdentifier($0.name) }.joined(separator: ", ")
sql += "\nORDER BY (\(orderCols))"
} else {
sql += "\nORDER BY tuple()"
}

return sql + ";"
}

private func clickhouseColumnDefinition(_ col: PluginColumnDefinition) -> String {
var dataType = col.dataType
if col.isNullable {
let upper = dataType.uppercased()
if !upper.hasPrefix("NULLABLE(") {
dataType = "Nullable(\(dataType))"
}
}

var def = "\(quoteIdentifier(col.name)) \(dataType)"
if let defaultValue = col.defaultValue {
def += " DEFAULT \(clickhouseDefaultValue(defaultValue))"
}
if let comment = col.comment, !comment.isEmpty {
def += " COMMENT '\(escapeStringLiteral(comment))'"
}
return def
}

private func clickhouseDefaultValue(_ value: String) -> String {
let upper = value.uppercased()
if upper == "NULL" || upper == "NOW()" || upper == "TODAY()"
|| value.hasPrefix("'") || Int64(value) != nil || Double(value) != nil {
return value
}
return "'\(escapeStringLiteral(value))'"
}

// MARK: - TLS Delegate

private class InsecureTLSDelegate: NSObject, URLSessionDelegate {
Expand Down
92 changes: 92 additions & 0 deletions Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,98 @@ final class DuckDBPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
return Set(result.rows.compactMap { $0[safe: 0] ?? nil })
}

// MARK: - Create Table DDL

func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? {
guard !definition.columns.isEmpty else { return nil }

let schema = _currentSchema
let qualifiedTable = "\(quoteIdentifier(schema)).\(quoteIdentifier(definition.tableName))"
let pkColumns = definition.columns.filter { $0.isPrimaryKey }
let inlinePK = pkColumns.count == 1
var parts: [String] = definition.columns.map { duckdbColumnDefinition($0, inlinePK: inlinePK) }

if pkColumns.count > 1 {
let pkCols = pkColumns.map { quoteIdentifier($0.name) }.joined(separator: ", ")
parts.append("PRIMARY KEY (\(pkCols))")
}

for fk in definition.foreignKeys {
parts.append(duckdbForeignKeyDefinition(fk))
}

var sql = "CREATE TABLE \(qualifiedTable) (\n " +
parts.joined(separator: ",\n ") +
"\n);"

var indexStatements: [String] = []
for index in definition.indexes {
indexStatements.append(duckdbIndexDefinition(index, qualifiedTable: qualifiedTable))
}
if !indexStatements.isEmpty {
sql += "\n\n" + indexStatements.joined(separator: ";\n") + ";"
}

return sql
}

private func duckdbColumnDefinition(_ col: PluginColumnDefinition, inlinePK: Bool) -> String {
var dataType = col.dataType
if col.autoIncrement {
let upper = dataType.uppercased()
if upper == "BIGINT" || upper == "INT8" {
dataType = "BIGSERIAL"
} else {
dataType = "SERIAL"
}
}

var def = "\(quoteIdentifier(col.name)) \(dataType)"
if !col.autoIncrement {
if col.isNullable {
def += " NULL"
} else {
def += " NOT NULL"
}
}
if let defaultValue = col.defaultValue {
def += " DEFAULT \(duckdbDefaultValue(defaultValue))"
}
if inlinePK && col.isPrimaryKey {
def += " PRIMARY KEY"
}
return def
}

private func duckdbDefaultValue(_ value: String) -> String {
let upper = value.uppercased()
if upper == "NULL" || upper == "TRUE" || upper == "FALSE"
|| upper == "CURRENT_TIMESTAMP" || upper == "NOW()"
|| value.hasPrefix("'") || Int64(value) != nil || Double(value) != nil {
return value
}
return "'\(escapeStringLiteral(value))'"
}

private func duckdbIndexDefinition(_ index: PluginIndexDefinition, qualifiedTable: String) -> String {
let cols = index.columns.map { quoteIdentifier($0) }.joined(separator: ", ")
let unique = index.isUnique ? "UNIQUE " : ""
return "CREATE \(unique)INDEX \(quoteIdentifier(index.name)) ON \(qualifiedTable) (\(cols))"
}

private func duckdbForeignKeyDefinition(_ fk: PluginForeignKeyDefinition) -> String {
let cols = fk.columns.map { quoteIdentifier($0) }.joined(separator: ", ")
let refCols = fk.referencedColumns.map { quoteIdentifier($0) }.joined(separator: ", ")
var def = "CONSTRAINT \(quoteIdentifier(fk.name)) FOREIGN KEY (\(cols)) REFERENCES \(quoteIdentifier(fk.referencedTable)) (\(refCols))"
if fk.onDelete != "NO ACTION" {
def += " ON DELETE \(fk.onDelete)"
}
if fk.onUpdate != "NO ACTION" {
def += " ON UPDATE \(fk.onUpdate)"
}
return def
}

private static let indexColumnsRegex = try? NSRegularExpression(
pattern: #"ON\s+(?:(?:"[^"]*"|[^\s(]+)\s*\.\s*)*(?:"[^"]*"|[^\s(]+)\s*\(([^)]+)\)"#,
options: .caseInsensitive
Expand Down
88 changes: 88 additions & 0 deletions Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,94 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
return false
}

// MARK: - Create Table DDL

func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? {
guard !definition.columns.isEmpty else { return nil }

let schema = _currentSchema
let qualifiedTable = "\(quoteIdentifier(schema)).\(quoteIdentifier(definition.tableName))"
let pkColumns = definition.columns.filter { $0.isPrimaryKey }
let inlinePK = pkColumns.count == 1
var parts: [String] = definition.columns.map { mssqlColumnDefinition($0, inlinePK: inlinePK) }

if pkColumns.count > 1 {
let pkCols = pkColumns.map { quoteIdentifier($0.name) }.joined(separator: ", ")
parts.append("PRIMARY KEY (\(pkCols))")
}

for fk in definition.foreignKeys {
parts.append(mssqlForeignKeyDefinition(fk))
}

var sql = "CREATE TABLE \(qualifiedTable) (\n " +
parts.joined(separator: ",\n ") +
"\n);"

var indexStatements: [String] = []
for index in definition.indexes {
indexStatements.append(mssqlIndexDefinition(index, qualifiedTable: qualifiedTable))
}
if !indexStatements.isEmpty {
sql += "\n\n" + indexStatements.joined(separator: ";\n") + ";"
}

return sql
}

private func mssqlColumnDefinition(_ col: PluginColumnDefinition, inlinePK: Bool) -> String {
var def = "\(quoteIdentifier(col.name)) \(col.dataType)"
if col.autoIncrement {
def += " IDENTITY(1,1)"
}
if col.isNullable {
def += " NULL"
} else {
def += " NOT NULL"
}
if let defaultValue = col.defaultValue {
def += " DEFAULT \(mssqlDefaultValue(defaultValue))"
}
if inlinePK && col.isPrimaryKey {
def += " PRIMARY KEY"
}
return def
}

private func mssqlDefaultValue(_ value: String) -> String {
let upper = value.uppercased()
if upper == "NULL" || upper == "GETDATE()" || upper == "NEWID()" || upper == "GETUTCDATE()"
|| value.hasPrefix("'") || value.hasPrefix("(") || Int64(value) != nil || Double(value) != nil {
return value
}
return "'\(escapeStringLiteral(value))'"
}

private func mssqlIndexDefinition(_ index: PluginIndexDefinition, qualifiedTable: String) -> String {
let cols = index.columns.map { quoteIdentifier($0) }.joined(separator: ", ")
let unique = index.isUnique ? "UNIQUE " : ""
var def = "CREATE \(unique)INDEX \(quoteIdentifier(index.name)) ON \(qualifiedTable) (\(cols))"
if let type = index.indexType?.uppercased(), type == "CLUSTERED" {
def = "CREATE \(unique)CLUSTERED INDEX \(quoteIdentifier(index.name)) ON \(qualifiedTable) (\(cols))"
} else if let type = index.indexType?.uppercased(), type == "NONCLUSTERED" {
def = "CREATE \(unique)NONCLUSTERED INDEX \(quoteIdentifier(index.name)) ON \(qualifiedTable) (\(cols))"
}
return def
}

private func mssqlForeignKeyDefinition(_ fk: PluginForeignKeyDefinition) -> String {
let cols = fk.columns.map { quoteIdentifier($0) }.joined(separator: ", ")
let refCols = fk.referencedColumns.map { quoteIdentifier($0) }.joined(separator: ", ")
var def = "CONSTRAINT \(quoteIdentifier(fk.name)) FOREIGN KEY (\(cols)) REFERENCES \(quoteIdentifier(fk.referencedTable)) (\(refCols))"
if fk.onDelete != "NO ACTION" {
def += " ON DELETE \(fk.onDelete)"
}
if fk.onUpdate != "NO ACTION" {
def += " ON UPDATE \(fk.onUpdate)"
}
return def
}

private func stripMSSQLOffsetFetch(from query: String) -> String {
let ns = query.uppercased() as NSString
let len = ns.length
Expand Down
Loading
Loading