Skip to content

Commit 7d7499a

Browse files
authored
feat(datagrid): show whether a materialized view can be refreshed concurrently, and gate its structure edits by kind (#3063)
1 parent 561cecf commit 7d7499a

37 files changed

Lines changed: 797 additions & 49 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4343
- **Extensions** for SQLite and local libSQL connections, loading sqlite-vec, SpatiaLite and other libraries on connect. (#2502)
4444
- Version history for saved queries, with **Restore This Version**. (#2505)
4545
- Git status letters, history and **Discard Changes…** for files in a linked SQL folder. (#2505)
46+
- Whether a materialized view can be refreshed concurrently, on its **Indexes** tab. (#2522)
4647

4748
### Changed
4849

@@ -117,6 +118,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
117118
- **File > Import > Import Data…** importing every file as SQL. (#3047)
118119
- Saved query longer than 500,000 characters silently cut short when saved.
119120
- A file the import panel dimmed still opening, and reaching the wrong importer.
121+
- Materialized view opened from Open Quickly edited as a plain view. (#2522)
122+
- Index edits refused on a PGlite materialized view.
123+
- Structure grid and inspector taking edits the object or engine refuses, such as a materialized view's Type.
124+
- **Delete** and **Duplicate** in a structure row's menu doing nothing on an object that refuses them.
125+
- **New Trigger** offered on a materialized view.
120126
- Compressed dump named `.GZ` rather than `.gz` reaching the parser still compressed.
121127
- **SQL** offered as an import format on MongoDB.
122128
- **Save** permanently dim on a Custom provider for an OpenAI-compatible server that wants no API key.

TablePro/Core/Database/MaterializedViewRefreshing.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ enum MaterializedViewRefreshing {
1414
/// Nil where the engine has no refresh that leaves readers alone, so the prompt offers no
1515
/// option at all rather than one that is always off.
1616
static func concurrentRefreshAvailability(
17-
of target: DatabaseObjectTarget
17+
of target: DatabaseObjectTarget,
18+
provider: any ScopedMetadataProviding = DatabaseManager.shared
1819
) async throws -> PluginConcurrentRefreshAvailability? {
1920
let name = target.name
2021
let schema = target.schema
21-
return try await DatabaseManager.shared.withMetadataDriver(scope: target.scope) { driver in
22+
return try await provider.withMetadataDriver(scope: target.scope) { driver in
2223
try await driver.concurrentRefreshAvailability(materializedView: name, schema: schema)
2324
}
2425
}

TablePro/Core/Plugins/PluginMetadataRegistry+CuratedDefaults.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ extension PluginMetadataRegistry {
590590
brandColorHex: "#F4B942",
591591
queryLanguageName: "SQL", editorLanguage: .sql,
592592
connectionMode: .network, supportsDatabaseSwitching: true,
593-
structureEditing: SchemaEditingSupport(foreignKeyEdit: .alter),
593+
structureEditing: SchemaEditingSupport(foreignKeyEdit: .alter, structureEdits: .postgreSQL),
594594
capabilities: PluginMetadataSnapshot.CapabilityFlags(
595595
supportsSchemaSwitching: true,
596596
supportsImport: true,

TablePro/Core/Services/Infrastructure/LaunchIntent.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import TableProImport
88

99
internal enum LaunchIntent: @unchecked Sendable {
1010
case openConnection(UUID)
11-
case openTable(connectionId: UUID, database: String?, schema: String?, table: String, isView: Bool)
11+
case openTable(
12+
connectionId: UUID,
13+
database: String?,
14+
schema: String?,
15+
table: String,
16+
isView: Bool,
17+
objectType: TableInfo.TableType? = nil
18+
)
1219
case openQuery(connectionId: UUID, sql: String)
1320
/// Open a connection in Agent mode, optionally carrying the question the user already typed.
1421
case openAgentSession(connectionId: UUID, prompt: String?)
@@ -27,7 +34,7 @@ internal enum LaunchIntent: @unchecked Sendable {
2734
internal var targetConnectionId: UUID? {
2835
switch self {
2936
case .openConnection(let id),
30-
.openTable(let id, _, _, _, _),
37+
.openTable(let id, _, _, _, _, _),
3138
.openQuery(let id, _),
3239
.openAgentSession(let id, _):
3340
return id

TablePro/Core/Services/Infrastructure/LaunchIntentRouter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ internal final class LaunchIntentRouter {
109109
switch intent {
110110
case .openConnection(let id):
111111
return id
112-
case .openTable(let id, _, _, _, _):
112+
case .openTable(let id, _, _, _, _, _):
113113
return id
114114
case .openQuery(let id, _):
115115
return id

TablePro/Core/Services/Infrastructure/TabRouter.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ internal final class TabRouter {
6161
case .openConnection(let id):
6262
try await openConnection(id: id)
6363

64-
case .openTable(let id, let database, let schema, let table, let isView):
64+
case .openTable(let id, let database, let schema, let table, let isView, let objectType):
6565
try await openTable(
6666
connectionId: id, transientConnection: nil,
67-
database: database, schema: schema, table: table, isView: isView
67+
database: database, schema: schema, table: table, isView: isView, objectType: objectType
6868
)
6969

7070
case .openQuery(let id, let sql):
@@ -217,6 +217,7 @@ internal final class TabRouter {
217217
private func openTable(
218218
connectionId: UUID, transientConnection: DatabaseConnection? = nil,
219219
database: String?, schema: String?, table: String, isView: Bool,
220+
objectType: TableInfo.TableType? = nil,
220221
passwordOverride: String? = nil, sshPasswordOverride: String? = nil
221222
) async throws {
222223
let connection: DatabaseConnection
@@ -241,7 +242,8 @@ internal final class TabRouter {
241242
tableName: table,
242243
databaseName: database,
243244
schemaName: schema,
244-
isView: isView
245+
isView: isView,
246+
objectType: objectType
245247
)
246248
DatabaseManager.shared.registerPendingSession(connection)
247249
WindowManager.shared.openTab(payload: payload)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// MaterializedViewConcurrentRefreshNote.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
import TableProPluginKit
8+
9+
internal struct MaterializedViewConcurrentRefreshNote: Equatable {
10+
internal let systemImage: String
11+
internal let text: String
12+
13+
internal init?(state: MetadataLoadState<PluginConcurrentRefreshAvailability?>) {
14+
switch state {
15+
case .idle, .loading:
16+
return nil
17+
case .failed:
18+
systemImage = "exclamationmark.triangle"
19+
text = String(localized: "Couldn't check whether this view can be refreshed concurrently.")
20+
case .loaded(let availability):
21+
guard let availability else { return nil }
22+
switch availability {
23+
case .available:
24+
systemImage = "checkmark.circle"
25+
text = String(localized: "This view can be refreshed concurrently.")
26+
case .requiresUniqueIndex:
27+
systemImage = "info.circle"
28+
text = String(
29+
localized: """
30+
Concurrent refresh needs a valid unique index on the view's columns, with no WHERE \
31+
clause and no expressions.
32+
"""
33+
)
34+
case .requiresPopulatedView:
35+
systemImage = "info.circle"
36+
text = String(
37+
localized: "Concurrent refresh needs the view to be populated first. Refresh it once without that option."
38+
)
39+
@unknown default:
40+
systemImage = "info.circle"
41+
text = String(localized: "This view can't be refreshed concurrently.")
42+
}
43+
}
44+
}
45+
}

TablePro/Models/Database/MaterializedViewRefreshPrompt.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ internal struct MaterializedViewRefreshPrompt: Equatable {
7373
case .requiresUniqueIndex:
7474
return String(localized: "Needs a valid unique index on the view's columns, with no WHERE clause and no expressions.")
7575
case .requiresPopulatedView:
76-
return String(localized: "Available once the view holds rows. Refresh it once without this option first.")
76+
return String(localized: "Available once the view is populated. Refresh it once without this option first.")
7777
case .none:
7878
return ""
7979
@unknown default:

TablePro/Models/Database/StructureEditEligibility.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ struct StructureObjectEditMatrix: Sendable, Equatable {
109109
])
110110
}
111111

112+
enum TriggerEditEligibility {
113+
static func kindAcceptsTriggers(_ kind: TableInfo.TableType) -> Bool {
114+
switch kind {
115+
case .table, .partitionedTable, .view, .foreignTable:
116+
return true
117+
case .materializedView, .systemTable, .externalTable, .sequence:
118+
return false
119+
}
120+
}
121+
}
122+
112123
/// Whether the Structure tab may offer one edit right now, and what to say when it may not.
113124
///
114125
/// Mirrors `ForeignKeyEditAvailability` so every refusal in this tab carries its own sentence. A

TablePro/Models/UI/InspectorRowSource.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ internal struct InspectorRowField: Equatable {
1414
let value: String?
1515
let editor: FieldEditorKind
1616
let isModified: Bool
17+
let isEditable: Bool
1718

18-
init(name: String, value: String?, editor: FieldEditorKind, isModified: Bool = false) {
19+
init(name: String, value: String?, editor: FieldEditorKind, isModified: Bool = false, isEditable: Bool = true) {
1920
self.name = name
2021
self.value = value
2122
self.editor = editor
2223
self.isModified = isModified
24+
self.isEditable = isEditable
2325
}
2426
}
2527

0 commit comments

Comments
 (0)