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
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
72 changes: 72 additions & 0 deletions TablePro/Models/ERDiagram/ERClusterAnalyzer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 (index, component) in ordered.enumerated() {
for member in component { result[member] = index }
}
return result
}
}
Loading
Loading