Skip to content

Commit 6823ecc

Browse files
committed
perf: fix N+1 fetchAllForeignKeys with bulk queries (HIGH-6)
- fetchForeignKeys(forTables:) default now calls fetchAllForeignKeys() once and filters, leveraging existing bulk SQL in MySQL/PostgreSQL/MSSQL - Add bulk fetchAllForeignKeys for SQLite using pragma_foreign_key_list joined with sqlite_master (single query instead of per-table pragma) - Add performance guidance docs to PluginDatabaseDriver default impls
1 parent e24b15e commit 6823ecc

3 files changed

Lines changed: 44 additions & 17 deletions

File tree

Plugins/SQLiteDriverPlugin/SQLitePlugin.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -551,13 +551,44 @@ final class SQLitePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
551551
}
552552

553553
func fetchAllForeignKeys(schema: String?) async throws -> [String: [PluginForeignKeyInfo]] {
554-
let tables = try await fetchTables(schema: schema)
555-
var result: [String: [PluginForeignKeyInfo]] = [:]
556-
for table in tables {
557-
let fks = try await fetchForeignKeys(table: table.name, schema: schema)
558-
if !fks.isEmpty { result[table.name] = fks }
554+
let query = """
555+
SELECT m.name AS table_name, p.id, p."table" AS referenced_table,
556+
p."from" AS column_name, p."to" AS referenced_column,
557+
p.on_update, p.on_delete
558+
FROM sqlite_master m, pragma_foreign_key_list(m.name) p
559+
WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%'
560+
ORDER BY m.name, p.id, p.seq
561+
"""
562+
let result = try await execute(query: query)
563+
564+
var allForeignKeys: [String: [PluginForeignKeyInfo]] = [:]
565+
566+
for row in result.rows {
567+
guard row.count >= 7,
568+
let tableName = row[0],
569+
let id = row[1],
570+
let refTable = row[2],
571+
let fromCol = row[3],
572+
let toCol = row[4] else {
573+
continue
574+
}
575+
576+
let onUpdate = row[5] ?? "NO ACTION"
577+
let onDelete = row[6] ?? "NO ACTION"
578+
579+
let fk = PluginForeignKeyInfo(
580+
name: "fk_\(tableName)_\(id)",
581+
column: fromCol,
582+
referencedTable: refTable,
583+
referencedColumn: toCol,
584+
onDelete: onDelete,
585+
onUpdate: onUpdate
586+
)
587+
588+
allForeignKeys[tableName, default: []].append(fk)
559589
}
560-
return result
590+
591+
return allForeignKeys
561592
}
562593

563594
func fetchIndexes(table: String, schema: String?) async throws -> [PluginIndexInfo] {

Plugins/TableProPluginKit/PluginDatabaseDriver.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ public extension PluginDatabaseDriver {
165165

166166
func fetchApproximateRowCount(table: String, schema: String?) async throws -> Int? { nil }
167167

168+
/// Default: fetches columns per-table sequentially (N+1 round-trips).
169+
/// SQL drivers should override with a single bulk query (e.g. INFORMATION_SCHEMA.COLUMNS).
168170
func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] {
169171
let tables = try await fetchTables(schema: schema)
170172
var result: [String: [PluginColumnInfo]] = [:]
@@ -174,6 +176,8 @@ public extension PluginDatabaseDriver {
174176
return result
175177
}
176178

179+
/// Default: fetches foreign keys per-table sequentially (N+1 round-trips).
180+
/// SQL drivers should override with a single bulk query (e.g. INFORMATION_SCHEMA.KEY_COLUMN_USAGE).
177181
func fetchAllForeignKeys(schema: String?) async throws -> [String: [PluginForeignKeyInfo]] {
178182
let tables = try await fetchTables(schema: schema)
179183
var result: [String: [PluginForeignKeyInfo]] = [:]

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -231,17 +231,9 @@ extension DatabaseDriver {
231231
}
232232

233233
func fetchForeignKeys(forTables tableNames: [String]) async throws -> [String: [ForeignKeyInfo]] {
234-
var result: [String: [ForeignKeyInfo]] = [:]
235-
for name in tableNames {
236-
do {
237-
let fks = try await fetchForeignKeys(table: name)
238-
if !fks.isEmpty { result[name] = fks }
239-
} catch {
240-
Logger(subsystem: "com.TablePro", category: "DatabaseDriver")
241-
.debug("Failed to fetch foreign keys for \(name): \(error.localizedDescription)")
242-
}
243-
}
244-
return result
234+
let all = try await fetchAllForeignKeys()
235+
let nameSet = Set(tableNames)
236+
return all.filter { nameSet.contains($0.key) }
245237
}
246238

247239
/// Default fetchAllColumns: falls back to per-table fetchColumns (N+1).

0 commit comments

Comments
 (0)