Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Connections can now have more than one tag. Assign several tags in the connection form, and filter the welcome list by tag with Match Any or Match All. (#744)
- Elasticsearch support. Connect to Elasticsearch 7.x and 8.x, browse indices, run Query DSL requests in a console, and edit documents in the data grid. Install from Settings > Plugins. (#1529)

### Changed

- The ER diagram now arranges tables in a compact layout that fills the canvas in both directions, keeps tables linked by foreign keys together, and tints each group of connected tables with its own header color. (#1755)

### Fixed

- Raw filters in the data grid now apply on document and key-value databases; the typed text was being dropped before it reached the driver. (#1529)
Expand Down
73 changes: 73 additions & 0 deletions TablePro/Models/ERDiagram/ERClusterAnalyzer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation

enum ERClusterAnalyzer {
static func assignClusters(
nodes: [ERTableNode],
edges: [EREdge],
nodeIndex: [String: UUID]
) -> [UUID: Int] {
guard !nodes.isEmpty else { return [:] }

var parent: [UUID: UUID] = [:]
var rank: [UUID: Int] = [:]
for node in nodes {
parent[node.id] = node.id
rank[node.id] = 0
}

func find(_ start: UUID) -> UUID {
var root = start
while let next = parent[root], next != root { root = next }
var current = start
while let next = parent[current], next != root {
parent[current] = root
current = next
}
return root
}

func union(_ lhs: UUID, _ rhs: UUID) {
let rootLhs = find(lhs)
let rootRhs = find(rhs)
guard rootLhs != rootRhs else { return }
let rankLhs = rank[rootLhs] ?? 0
let rankRhs = rank[rootRhs] ?? 0
if rankLhs < rankRhs {
parent[rootLhs] = rootRhs
} else if rankLhs > rankRhs {
parent[rootRhs] = rootLhs
} else {
parent[rootRhs] = rootLhs
rank[rootLhs] = rankLhs + 1
}
}

for edge in edges {
guard let from = nodeIndex[edge.fromTable],
let to = nodeIndex[edge.toTable],
from != to
else { continue }
union(from, to)
}

var members: [UUID: [UUID]] = [:]
for node in nodes {
members[find(node.id), default: []].append(node.id)
}

let nameById = Dictionary(uniqueKeysWithValues: nodes.map { ($0.id, $0.tableName) })
let multiNodeComponents = members.values.filter { $0.count >= 2 }
let ordered = multiNodeComponents.sorted { lhs, rhs in
let lhsKey = lhs.compactMap { nameById[$0] }.min() ?? ""
let rhsKey = rhs.compactMap { nameById[$0] }.min() ?? ""
return lhsKey < rhsKey
}

var result: [UUID: Int] = [:]
for node in nodes { result[node.id] = -1 }
for (index, component) in ordered.enumerated() {
for member in component { result[member] = index }
}
return result
}
}
Loading
Loading