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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Run executing an old sorted query after a column header was clicked while a query ran.
- SQL Server scripts refused, or cut to their first result set, over MCP, AppleScript and the AI assistant.
- SQL import into SQL Server sending `GO` lines to the server and splitting batches at each semicolon.
- SQL Server dumps failing on their first view, routine or trigger when restored with sqlcmd or imported.
- SQL Server dumps failing on their first view, routine or trigger when restored with sqlcmd or SSMS.
- A leading `GO` line sent to SQL Server by MCP and AI assistant tools, and a `GO n` count ignored.
- App hanging for a minute when an import stopped on a failing statement several megabytes long.

### Security

Expand Down
29 changes: 11 additions & 18 deletions Plugins/SQLExportPlugin/SQLExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,6 @@
/// says so rather than shipping a dump whose three phases disagree.
var exportSpansContainers = false

/// What ends every statement of the dump: a line break, and on an engine whose client runs a
/// script in batches cut at `GO` lines, that line too. SQL Server refuses a view, a routine or a
/// trigger that is not the first statement of its batch, and a routine's body runs to the end
/// of its batch, so each statement is written as a batch of its own, the way SQL Server
/// Management Studio writes a script.
private var statementEnd = "\n"

private static let logger = Logger(subsystem: "com.TablePro", category: "SQLExportPlugin")

required init() { loadSettings() }
Expand Down Expand Up @@ -141,7 +134,6 @@
exportSpansContainers = false
tablesUnorderedByCycle = []
emittedSequenceNames = []
statementEnd = dataSource.lexicalFeatures.contains(.batchSeparatorLines) ? "\nGO\n" : "\n"

/// Read once, because `PluginManager` hands every window the same plugin instance and a
/// second window's options pane can write `settings` while this export is still running. A
Expand Down Expand Up @@ -478,7 +470,7 @@
guard !dropTargets.isEmpty else { return }
for object in dropTargets {
guard let statement = dropStatement(for: object, dataSource: dataSource) else { continue }
try writer.write(statement + statementEnd)
try writer.write(statement + dataSource.dumpStatementEnd)
}
try writer.write("\n")
}
Expand Down Expand Up @@ -525,6 +517,7 @@
) async throws {
var emittedTypeNames: Set<String> = []
let structureTables = tables.filter { optionValue($0, at: 0) }
let statementEnd = dataSource.dumpStatementEnd

for table in structureTables {
do {
Expand Down Expand Up @@ -597,7 +590,7 @@
guard !ddl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw SQLExportObjectError.emptyDefinition
}
try writer.write(dataSource.scriptText(for: ddl) + statementEnd)
try writer.write(dataSource.scriptText(for: ddl) + dataSource.dumpStatementEnd)
try writer.write("\n")
} catch {
ddlFailures.append(sanitizedName)
Expand Down Expand Up @@ -640,7 +633,7 @@
table: object.name, databaseName: object.databaseName)
guard !statements.isEmpty else { return }
for statement in statements {
try writer.write(dataSource.scriptText(for: statement) + statementEnd)
try writer.write(dataSource.scriptText(for: statement) + dataSource.dumpStatementEnd)
}
try writer.write("\n")
} catch {
Expand Down Expand Up @@ -683,7 +676,7 @@
guard !ddl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw SQLExportObjectError.emptyDefinition
}
try writer.write(dataSource.scriptText(for: ddl) + statementEnd)
try writer.write(dataSource.scriptText(for: ddl) + dataSource.dumpStatementEnd)
try writer.write("\n")
} catch {
ddlFailures.append(sanitizedName)
Expand Down Expand Up @@ -717,7 +710,7 @@
principal: principal.name, host: principal.identity)
guard !statements.isEmpty else { continue }
for statement in statements {
try writer.write(dataSource.scriptText(for: statement) + statementEnd)
try writer.write(dataSource.scriptText(for: statement) + dataSource.dumpStatementEnd)
}
} catch {
let sanitized = PluginExportUtilities.sanitizeForSQLComment(principal.name)
Expand Down Expand Up @@ -787,7 +780,7 @@
let grouped = groupForeignKeysByConstraint(fks)
for group in grouped {
let alter = renderAddConstraintFK(table: table, group: group, dataSource: dataSource)
try writer.write(alter + statementEnd)
try writer.write(alter + dataSource.dumpStatementEnd)
emittedAnything = true
}
}
Expand All @@ -802,7 +795,7 @@
for column in columns where column.isIdentity {
let setval = renderIdentitySetval(
table: table, columnName: column.name, dataSource: dataSource)
try writer.write(setval + statementEnd)
try writer.write(setval + dataSource.dumpStatementEnd)
emittedAnything = true
}
}
Expand Down Expand Up @@ -838,7 +831,7 @@
let statements = try await dataSource.fetchIndexDDL(
table: object.name, databaseName: object.databaseName)
for statement in statements {
try writer.write(dataSource.scriptText(for: statement) + statementEnd)
try writer.write(dataSource.scriptText(for: statement) + dataSource.dumpStatementEnd)
emittedAnything = true
}
} catch {
Expand Down Expand Up @@ -959,7 +952,7 @@
let needsIdentityInsert = dataSource.databaseTypeId == "SQL Server"
&& columnInfo.contains(where: \.isIdentity)
let identityInsert = needsIdentityInsert
? SQLExportSessionScope.identityInsert(tableRef: tableRef, statementEnd: statementEnd)
? SQLExportSessionScope.identityInsert(tableRef: tableRef, statementEnd: dataSource.dumpStatementEnd)
: nil

if !table.rowScope.isUnrestricted {
Expand Down Expand Up @@ -1018,7 +1011,7 @@
if let encoder { tally.unrepresentableValues += encoder.unrepresentableValues.total }
let built = SQLExportRowValueEncoder(
columns: header.columns,
columnTypeNames: header.columnTypeNames ?? [],

Check warning on line 1014 in Plugins/SQLExportPlugin/SQLExportPlugin.swift

View workflow job for this annotation

GitHub Actions / Build for testing

left side of nil coalescing operator '??' has non-optional type '[String]', so the right side is never used
excludedColumnNames: generatedColumnNames,
databaseTypeId: dataSource.databaseTypeId,
escapeStringLiteral: dataSource.escapeStringLiteral
Expand Down Expand Up @@ -1103,7 +1096,7 @@
prefix: rendered.prefix,
suffix: rendered.suffix,
budget: statementBudget(for: dataSource.databaseTypeId, options: options),
terminator: ";\(statementEnd)\n")
terminator: ";\(dataSource.dumpStatementEnd)\n")
return (accumulator, rendered.warning)
}

Expand Down
21 changes: 21 additions & 0 deletions Plugins/SQLExportPlugin/SQLExportStatementEnd.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// SQLExportStatementEnd.swift
// SQLExportPlugin
//

import Foundation
import TableProPluginKit

extension PluginExportDataSource {
/// What ends every statement of a dump of this source: a line break, and on an engine whose client runs a script in
/// batches cut at `GO` lines, that line too. SQL Server refuses a view, a routine or a trigger that is not the first
/// statement of its batch, and a routine's body runs to the end of its batch, so each statement is written as a
/// batch of its own, the way SQL Server Management Studio writes a script.
///
/// Read off the source each export writes from rather than held by the plugin: `PluginManager` hands every window
/// the same plugin instance, so a second export started while one runs would otherwise change what the first
/// writes from then on.
var dumpStatementEnd: String {
lexicalFeatures.contains(.batchSeparatorLines) ? "\nGO\n" : "\n"
}
}
7 changes: 4 additions & 3 deletions TablePro/Core/Plugins/ImportDataSinkAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ final class ImportDataSinkAdapter: PluginImportDataSink, @unchecked Sendable {
try await execute(statement: statement, line: 1)
}

/// A SQL Server file arrives a batch at a time, as sqlcmd reads it, and goes to the server whole: T-SQL scopes a
/// variable, a table variable and a `TRY...CATCH` to one batch, and a routine's body runs to the end of its batch.
/// A driver that cannot send a batch whole runs its statements one by one, as the editor does.
/// A SQL Server file arrives a batch at a time, as sqlcmd reads it, or a statement at a time when it holds no `GO`
/// line, and each goes to the server whole: T-SQL scopes a variable, a table variable and a `TRY...CATCH` to one
/// batch, and a routine's body runs to the end of its batch. A driver that cannot send a batch whole runs its
/// statements one by one, as the editor does.
func execute(statement: String, line: Int) async throws {
guard grammar.contains(.batchSeparatorLines) else {
_ = try await driver.execute(query: statement)
Expand Down
Loading
Loading