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
1 change: 1 addition & 0 deletions .claude/rules/data-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Three layers, knowledge pointing one way: an owner's adapter, then `DataGridMode
- **Row height, row count, reloads, inserts and removes reach both panes in one call**, so their document heights never differ.
- **Vertical scroll stays in step through bounds-change notifications**: each pane's clip moves the other with `clipView.scroll(to:)` and `reflectScrolledClipView`, only when its y differs. Anywhere else scroll with `NSView.scroll(_:)`: moving a clip's x that way leaves the header behind.
- **The frozen pane forwards every wheel event to the main scroll view**, so one momentum engine moves both.
- **A `fitsRows` grid is as tall as its rows and scrolls only sideways.** It publishes `model.fittingHeight` for its host to size it, hands a vertical gesture to the enclosing scroll view (the axis is decided once per gesture), and reveals a row by scrolling that page. The Structure tab's sections use it.
- **Frozen columns take at most two thirds of the grid** (`DataGridMetrics.maximumFrozenShare`). Columns past that scroll, and Freeze Up to This Column is refused in the menu.
- **`DataGridTableView.scrollToVisible(_:)` widens the rect leftward by the frozen width**, so a revealed column never parks under the frozen pane.
- **A column moves only within its side of the freeze line.** A freeze or unfreeze rebuilds both panes' column lists from the set, writing width and position explicitly: remove-then-add alone strands a column at the end and resets its width.
Expand Down
2 changes: 1 addition & 1 deletion apple/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ custom_rules:
severity: error
appstorage_key_namespace:
name: "AppStorage Key Namespace"
regex: '@AppStorage\(\s*"(?!com\.TablePro)(?!(?:hideExportSuccessDialog|skipSchemaPreview|structureCodeFontSize)")[^"\\]+"'
regex: '@AppStorage\(\s*"(?!com\.TablePro)(?!(?:hideExportSuccessDialog|structureCodeFontSize)")[^"\\]+"'
excluded: ['.*/(Packages|Plugins)/']
message: "Route an @AppStorage key through the preferences layer, under com.TablePro."
severity: error
Expand Down
7 changes: 7 additions & 0 deletions apple/TablePro/Core/DataGrid/DataGridModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ final class DataGridModel: ObservableObject {
@Published private(set) var displayRevision = 0
@Published private(set) var selectionRevision = 0
@Published private(set) var editabilityRevision = 0
// What a fitsRows grid needs to show every row; the mounted view measures it.
@Published private(set) var fittingHeight: CGFloat = 0

private(set) weak var mountedView: (any DataGridMountedView)?
private var orderRevision = 0
Expand Down Expand Up @@ -456,6 +458,11 @@ final class DataGridModel: ObservableObject {

var isMounted: Bool { mountedView != nil }

func setFittingHeight(_ height: CGFloat) {
guard height != fittingHeight else { return }
fittingHeight = height
}

func revealColumn(_ id: DataGridColumnID) {
guard let position = columns.displayPosition(of: id) else { return }
mountedView?.revealColumn(position)
Expand Down
4 changes: 4 additions & 0 deletions apple/TablePro/Core/Menu/FileMenuBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ enum FileMenuBuilder {
shortcut: .saveChanges,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Discard Changes…"),
action: #selector(MainSplitViewController.discardChanges(_:))
),
MenuItemFactory.item(
String(localized: "Save As…"),
action: #selector(MainSplitViewController.saveDocumentAs(_:)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ import TableProPluginKit
struct CreateTableStatements {
let statements: [String]
let issues: [SchemaDraftIssue]
/// The name the statement actually creates, trimmed. The draft's own field may carry
/// surrounding whitespace, and opening the tab under that text loads a table nobody made.
// Trimmed: opening a tab under the draft's untrimmed text loads a table nobody made.
let tableName: String?

var preview: String {
statements.map { $0.hasSuffix(";") ? $0 : $0 + ";" }.joined(separator: "\n\n")
}
}

/// One statement per element, because a driver is only obliged to run one statement per
/// `execute(query:)`: SQLite prepares with a nil tail, so anything after the first semicolon never runs.
// One statement per element: a driver need only run one per execute, and SQLite drops anything after the first.
@MainActor
enum CreateTableStatementComposer {
static func compose(
Expand All @@ -33,37 +31,48 @@ enum CreateTableStatementComposer {

var issues = plan.issues
let columnRefusals = definition.columns.compactMap { driver.schemaOperationRefusal(.addColumn($0)) }
issues += columnRefusals.map { SchemaDraftIssue(tab: .columns, row: nil, message: $0) }
var refusedIndexRows: Set<Int> = []
for (row, index) in plan.indexes.enumerated() {
issues += columnRefusals.map { SchemaDraftIssue(section: .columns, row: nil, message: $0) }
var refusedIndexes: Set<Int> = []
for (position, index) in plan.indexes.enumerated() {
guard let reason = driver.schemaOperationRefusal(.addIndex(index)) else { continue }
refusedIndexRows.insert(row)
issues.append(SchemaDraftIssue(tab: .indexes, row: row, message: reason))
refusedIndexes.insert(position)
issues.append(SchemaDraftIssue(section: .indexes, row: plan.indexRow(at: position), message: reason))
}
guard columnRefusals.isEmpty else {
return CreateTableStatements(statements: [], issues: issues, tableName: definition.tableName)
}

guard let createTable = driver.generateCreateTableStatements(definition: definition), !createTable.isEmpty else {
issues.append(SchemaDraftIssue(
tab: .columns, row: nil,
section: .columns, row: nil,
message: String(localized: "This database cannot create a table from the visual editor.")
))
return CreateTableStatements(statements: [], issues: issues, tableName: definition.tableName)
}

var statements = createTable
for (row, index) in plan.indexes.enumerated() where !refusedIndexRows.contains(row) {
for (position, index) in plan.indexes.enumerated() where !refusedIndexes.contains(position) {
guard let sql = driver.generateAddIndexSQL(table: definition.tableName, index: index) else {
issues.append(SchemaDraftIssue(
tab: .indexes, row: row,
section: .indexes, row: plan.indexRow(at: position),
message: String(localized: "Create Table cannot add an index on this database.")
))
continue
}
statements.append(sql)
}

for (position, constraint) in plan.checkConstraints.enumerated() {
guard let sql = driver.generateAddCheckConstraintSQL(table: definition.tableName, constraint: constraint) else {
issues.append(SchemaDraftIssue(
section: .checkConstraints, row: plan.checkConstraintRow(at: position),
message: String(localized: "Create Table cannot add a check constraint on this database.")
))
continue
}
statements.append(sql)
}

if let comment = plan.tableComment {
if let sql = driver.objectCommentStatement(
name: definition.tableName,
Expand All @@ -74,7 +83,7 @@ enum CreateTableStatementComposer {
statements.append(sql)
} else {
issues.append(SchemaDraftIssue(
tab: .columns, row: nil,
section: .columns, row: nil,
message: String(localized: "This database cannot store a table comment.")
))
}
Expand Down
39 changes: 36 additions & 3 deletions apple/TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct SchemaStatementGenerator {
throw NSError(
domain: "SchemaStatementGenerator",
code: -1,
userInfo: [NSLocalizedDescriptionKey: String(format: String(localized: "Unsupported schema operation: %@"), change.description)]
userInfo: [NSLocalizedDescriptionKey: String(format: String(localized: "Unsupported schema operation: %@"), change.localizedDescription)]
)
}
for stmt in stmts {
Expand Down Expand Up @@ -175,10 +175,43 @@ struct SchemaStatementGenerator {

let constraintHandoffs = Self.inNameOrder(constraintModifies)
let indexHandoffs = Self.inNameOrder(indexAdds)
let keyHandoff = Self.keyDropsBeforeColumnDrops(pkChanges, columnDeletes: columnDeletes)

return constraintDeletes + constraintHandoffs.drops + constraintHandoffs.ordered + fkDeletes
+ indexDeletes + indexHandoffs.drops + columnDeletes + characterSetChanges + columnModifies + columnAdds
+ pkChanges + autoIncrementChanges + indexHandoffs.ordered + fkAdds + constraintAdds + commentChanges
+ indexDeletes + indexHandoffs.drops + keyHandoff.drops + columnDeletes + characterSetChanges
+ columnModifies + columnAdds + keyHandoff.adds + autoIncrementChanges + indexHandoffs.ordered + fkAdds
+ constraintAdds + commentChanges
}

// Dropping a key column takes the key with it, so the key's own DROP would then fail. A key whose every column is
// dropped is left to the database and only the new key is added; one that keeps some is dropped first.
private static func keyDropsBeforeColumnDrops(
_ keyChanges: [SchemaChange],
columnDeletes: [SchemaChange]
) -> (drops: [SchemaChange], adds: [SchemaChange]) {
let dropped = Set(columnDeletes.compactMap { change -> String? in
guard case .deleteColumn(let column) = change else { return nil }
return column.name
})
var drops: [SchemaChange] = []
var adds: [SchemaChange] = []
for change in keyChanges {
guard case .modifyPrimaryKey(let old, let new) = change, !old.isEmpty, old.contains(where: dropped.contains) else {
adds.append(change)
continue
}
if old.allSatisfy(dropped.contains) {
if !new.isEmpty {
adds.append(.modifyPrimaryKey(old: [], new: new))
}
continue
}
drops.append(.modifyPrimaryKey(old: old, new: []))
if !new.isEmpty {
adds.append(.modifyPrimaryKey(old: [], new: new))
}
}
return (drops, adds)
}

/// Orders changes that each give up one name and take another in a single step, a rename or a
Expand Down
Loading
Loading