diff --git a/Plugins/CassandraDriverPlugin/Info.plist b/Plugins/CassandraDriverPlugin/Info.plist index 737aa31f..c587cfa8 100644 --- a/Plugins/CassandraDriverPlugin/Info.plist +++ b/Plugins/CassandraDriverPlugin/Info.plist @@ -18,6 +18,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + TableProPluginKitVersion + 1 NSPrincipalClass $(PRODUCT_MODULE_NAME).CassandraPlugin diff --git a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift index e3df957d..bf8d2dac 100644 --- a/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift +++ b/Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift @@ -15,7 +15,7 @@ final class ClickHousePlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "ClickHouse" static let databaseDisplayName = "ClickHouse" - static let iconName = "bolt.fill" + static let iconName = "clickhouse-icon" static let defaultPort = 8123 // MARK: - UI/Capability Metadata diff --git a/Plugins/EtcdDriverPlugin/EtcdPlugin.swift b/Plugins/EtcdDriverPlugin/EtcdPlugin.swift index 98d9f7cb..a9c39b45 100644 --- a/Plugins/EtcdDriverPlugin/EtcdPlugin.swift +++ b/Plugins/EtcdDriverPlugin/EtcdPlugin.swift @@ -17,9 +17,10 @@ final class EtcdPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "etcd" static let databaseDisplayName = "etcd" - static let iconName = "cylinder.fill" + static let iconName = "etcd-icon" static let defaultPort = 2379 static let additionalDatabaseTypeIds: [String] = [] + static let isDownloadable = true static let navigationModel: NavigationModel = .standard static let pathFieldRole: PathFieldRole = .database diff --git a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift index a45f97d0..460141b0 100644 --- a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift +++ b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift @@ -16,7 +16,7 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "SQL Server" static let databaseDisplayName = "SQL Server" - static let iconName = "server.rack" + static let iconName = "mssql-icon" static let defaultPort = 1433 static let additionalConnectionFields: [ConnectionField] = [ ConnectionField(id: "mssqlSchema", label: "Schema", placeholder: "dbo", defaultValue: "dbo") diff --git a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift index 668fcb19..a7b01ef3 100644 --- a/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift +++ b/Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift @@ -14,7 +14,7 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "MongoDB" static let databaseDisplayName = "MongoDB" - static let iconName = "leaf.fill" + static let iconName = "mongodb-icon" static let defaultPort = 27017 static let additionalConnectionFields: [ConnectionField] = [ ConnectionField(id: "mongoAuthSource", label: "Auth Database", placeholder: "admin"), diff --git a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift index b5f1cefc..c0cbf9cf 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPlugin.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPlugin.swift @@ -20,7 +20,7 @@ final class MySQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "MySQL" static let databaseDisplayName = "MySQL" - static let iconName = "cylinder.fill" + static let iconName = "mysql-icon" static let defaultPort = 3306 static let additionalConnectionFields: [ConnectionField] = [] static let additionalDatabaseTypeIds: [String] = ["MariaDB"] diff --git a/Plugins/OracleDriverPlugin/OraclePlugin.swift b/Plugins/OracleDriverPlugin/OraclePlugin.swift index 0e9afa99..331d347c 100644 --- a/Plugins/OracleDriverPlugin/OraclePlugin.swift +++ b/Plugins/OracleDriverPlugin/OraclePlugin.swift @@ -15,7 +15,7 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "Oracle" static let databaseDisplayName = "Oracle" - static let iconName = "server.rack" + static let iconName = "oracle-icon" static let defaultPort = 1521 static let additionalConnectionFields: [ConnectionField] = [ ConnectionField(id: "oracleServiceName", label: "Service Name", placeholder: "ORCL") diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift index d77935ff..6b8b646e 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift @@ -19,7 +19,7 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "PostgreSQL" static let databaseDisplayName = "PostgreSQL" - static let iconName = "cylinder.fill" + static let iconName = "postgresql-icon" static let defaultPort = 5432 static let additionalConnectionFields: [ConnectionField] = [ ConnectionField( diff --git a/Plugins/RedisDriverPlugin/Info.plist b/Plugins/RedisDriverPlugin/Info.plist index 2d7c7ce9..57a55450 100644 --- a/Plugins/RedisDriverPlugin/Info.plist +++ b/Plugins/RedisDriverPlugin/Info.plist @@ -18,6 +18,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + TableProPluginKitVersion + 1 NSPrincipalClass $(PRODUCT_MODULE_NAME).RedisPlugin diff --git a/Plugins/RedisDriverPlugin/RedisPlugin.swift b/Plugins/RedisDriverPlugin/RedisPlugin.swift index 2a91c02c..007dea01 100644 --- a/Plugins/RedisDriverPlugin/RedisPlugin.swift +++ b/Plugins/RedisDriverPlugin/RedisPlugin.swift @@ -19,7 +19,7 @@ final class RedisPlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "Redis" static let databaseDisplayName = "Redis" - static let iconName = "cylinder.fill" + static let iconName = "redis-icon" static let defaultPort = 6379 static let additionalConnectionFields: [ConnectionField] = [ ConnectionField( diff --git a/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift b/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift index d6ed628e..67a31892 100644 --- a/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift +++ b/Plugins/SQLiteDriverPlugin/SQLitePlugin.swift @@ -16,7 +16,7 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin { static let databaseTypeId = "SQLite" static let databaseDisplayName = "SQLite" - static let iconName = "doc.fill" + static let iconName = "sqlite-icon" static let defaultPort = 0 // MARK: - UI/Capability Metadata @@ -24,7 +24,7 @@ final class SQLitePlugin: NSObject, TableProPlugin, DriverPlugin { static let requiresAuthentication = false static let supportsSSH = false static let supportsSSL = false - static let isDownloadable = true + static let isDownloadable = false static let pathFieldRole: PathFieldRole = .filePath static let connectionMode: ConnectionMode = .fileBased static let urlSchemes: [String] = ["sqlite"] diff --git a/TablePro.xcodeproj/project.pbxproj b/TablePro.xcodeproj/project.pbxproj index 9246c9c1..3c63afc3 100644 --- a/TablePro.xcodeproj/project.pbxproj +++ b/TablePro.xcodeproj/project.pbxproj @@ -35,7 +35,7 @@ 5ACE00012F4F000000000006 /* CodeEditTextView in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000007 /* CodeEditTextView */; }; 5ACE00012F4F00000000000A /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F000000000009 /* Sparkle */; }; 5ACE00012F4F00000000000D /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5ACE00012F4F00000000000C /* MarkdownUI */; }; - 5AEA8B302F6808270040461A /* EtcdDriverPlugin.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5AEA8B2A2F6808270040461A /* EtcdDriverPlugin.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5AEA8B422F6808CA0040461A /* EtcdStatementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B402F6808CA0040461A /* EtcdStatementGenerator.swift */; }; 5AEA8B432F6808CA0040461A /* EtcdPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3D2F6808CA0040461A /* EtcdPlugin.swift */; }; 5AEA8B442F6808CA0040461A /* EtcdCommandParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA8B3B2F6808CA0040461A /* EtcdCommandParser.swift */; }; @@ -153,7 +153,6 @@ 5A86A000D00000000 /* CSVExport.tableplugin in Copy Plug-Ins */, 5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */, 5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */, - 5AEA8B302F6808270040461A /* EtcdDriverPlugin.tableplugin in Copy Plug-Ins */, ); name = "Copy Plug-Ins"; runOnlyForDeploymentPostprocessing = 0; @@ -769,7 +768,6 @@ 5A86A000C00000000 /* PBXTargetDependency */, 5A86B000C00000000 /* PBXTargetDependency */, 5A86C000C00000000 /* PBXTargetDependency */, - 5AEA8B322F6808270040461A /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 5A1091C92EF17EDC0055EA7C /* TablePro */, diff --git a/TablePro/AppDelegate+WindowConfig.swift b/TablePro/AppDelegate+WindowConfig.swift index c2320983..4a9f884d 100644 --- a/TablePro/AppDelegate+WindowConfig.swift +++ b/TablePro/AppDelegate+WindowConfig.swift @@ -38,7 +38,10 @@ extension AppDelegate { ) item.target = self item.representedObject = connection.id - if let original = NSImage(named: connection.type.iconName) { + let iconName = connection.type.iconName + let original = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) + ?? NSImage(named: iconName) + if let original { let resized = NSImage(size: NSSize(width: 16, height: 16), flipped: false) { rect in original.draw(in: rect) return true diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 7d194ec3..3692b7b7 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -249,6 +249,7 @@ final class PluginManager { try validateDriverDescriptor(type(of: driver), pluginId: pluginId) } catch { Self.logger.error("Plugin '\(pluginId)' driver rejected: \(error.localizedDescription)") + return } if !driverPlugins.keys.contains(type(of: driver).databaseTypeId) { let driverType = type(of: driver) @@ -264,9 +265,9 @@ final class PluginManager { from: driverType, isDownloadable: driverType.isDownloadable ) - PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: typeId) + PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: typeId, preserveIcon: true) for additionalId in driverType.additionalDatabaseTypeIds { - PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: additionalId) + PluginMetadataRegistry.shared.register(snapshot: snapshot, forTypeId: additionalId, preserveIcon: true) PluginMetadataRegistry.shared.registerTypeAlias(additionalId, primaryTypeId: typeId) } diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry.swift b/TablePro/Core/Plugins/PluginMetadataRegistry.swift index f5051078..4f7d5f6f 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry.swift @@ -117,6 +117,22 @@ struct PluginMetadataSnapshot: Sendable { additionalConnectionFields: [] ) } + + func withIconName(_ newIconName: String) -> PluginMetadataSnapshot { + PluginMetadataSnapshot( + displayName: displayName, iconName: newIconName, defaultPort: defaultPort, + requiresAuthentication: requiresAuthentication, supportsForeignKeys: supportsForeignKeys, + supportsSchemaEditing: supportsSchemaEditing, isDownloadable: isDownloadable, + primaryUrlScheme: primaryUrlScheme, parameterStyle: parameterStyle, + navigationModel: navigationModel, explainVariants: explainVariants, + pathFieldRole: pathFieldRole, supportsHealthMonitor: supportsHealthMonitor, + urlSchemes: urlSchemes, postConnectActions: postConnectActions, + brandColorHex: brandColorHex, queryLanguageName: queryLanguageName, + editorLanguage: editorLanguage, connectionMode: connectionMode, + supportsDatabaseSwitching: supportsDatabaseSwitching, + capabilities: capabilities, schema: schema, editor: editor, connection: connection + ) + } } final class PluginMetadataRegistry: @unchecked Sendable { @@ -513,11 +529,15 @@ final class PluginMetadataRegistry: @unchecked Sendable { reverseTypeIndex["ScyllaDB"] = "Cassandra" } - func register(snapshot: PluginMetadataSnapshot, forTypeId typeId: String) { + func register(snapshot: PluginMetadataSnapshot, forTypeId typeId: String, preserveIcon: Bool = false) { lock.lock() defer { lock.unlock() } - snapshots[typeId] = snapshot - for scheme in snapshot.urlSchemes { + var resolved = snapshot + if preserveIcon, let existingIcon = snapshots[typeId]?.iconName { + resolved = snapshot.withIconName(existingIcon) + } + snapshots[typeId] = resolved + for scheme in resolved.urlSchemes { schemeIndex[scheme.lowercased()] = typeId } } diff --git a/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift b/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift index 47106a1b..122ae1ae 100644 --- a/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift +++ b/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift @@ -71,6 +71,10 @@ enum SessionStateFactory { if payload.showStructure { tabMgr.tabs[index].showStructure = true } + if let initialFilter = payload.initialFilterState { + tabMgr.tabs[index].filterState = initialFilter + filterMgr.restoreFromTabState(initialFilter) + } } } else { tabMgr.addTab(databaseName: payload.databaseName ?? connection.database) diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index d6b61ac8..cb871b9a 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -282,6 +282,16 @@ extension DatabaseType { PluginMetadataRegistry.shared.snapshot(forTypeId: pluginTypeId)?.iconName ?? "database-icon" } + /// Returns the correct SwiftUI Image for this database type, handling both + /// SF Symbol names (e.g. "cylinder.fill") and asset catalog names (e.g. "mysql-icon"). + var iconImage: Image { + let name = iconName + if NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil { + return Image(systemName: name) + } + return Image(name) + } + var defaultPort: Int { PluginMetadataRegistry.shared.snapshot(forTypeId: pluginTypeId)?.defaultPort ?? 0 } diff --git a/TablePro/Models/Database/TableFilter.swift b/TablePro/Models/Database/TableFilter.swift index bdf17b6b..d1b09a58 100644 --- a/TablePro/Models/Database/TableFilter.swift +++ b/TablePro/Models/Database/TableFilter.swift @@ -71,7 +71,7 @@ enum FilterOperator: String, CaseIterable, Identifiable, Codable { } /// Represents a single table filter condition -struct TableFilter: Identifiable, Equatable, Codable { +struct TableFilter: Identifiable, Equatable, Hashable, Codable { let id: UUID var columnName: String // Column to filter on, or "__RAW__" for raw SQL var filterOperator: FilterOperator @@ -151,7 +151,7 @@ struct TableFilter: Identifiable, Equatable, Codable { } /// Stores per-tab filter state (preserves filters when switching tabs) -struct TabFilterState: Equatable, Codable { +struct TabFilterState: Equatable, Hashable, Codable { var filters: [TableFilter] var appliedFilters: [TableFilter] var isVisible: Bool diff --git a/TablePro/Models/Query/EditorTabPayload.swift b/TablePro/Models/Query/EditorTabPayload.swift index f2c3c32a..85e8243b 100644 --- a/TablePro/Models/Query/EditorTabPayload.swift +++ b/TablePro/Models/Query/EditorTabPayload.swift @@ -31,6 +31,8 @@ internal struct EditorTabPayload: Codable, Hashable { internal let skipAutoExecute: Bool /// Whether this tab is a preview (temporary) tab internal let isPreview: Bool + /// Initial filter state (for FK navigation — pre-applies a WHERE filter) + internal let initialFilterState: TabFilterState? internal init( id: UUID = UUID(), @@ -42,7 +44,8 @@ internal struct EditorTabPayload: Codable, Hashable { isView: Bool = false, showStructure: Bool = false, skipAutoExecute: Bool = false, - isPreview: Bool = false + isPreview: Bool = false, + initialFilterState: TabFilterState? = nil ) { self.id = id self.connectionId = connectionId @@ -54,6 +57,7 @@ internal struct EditorTabPayload: Codable, Hashable { self.showStructure = showStructure self.skipAutoExecute = skipAutoExecute self.isPreview = isPreview + self.initialFilterState = initialFilterState } internal init(from decoder: Decoder) throws { @@ -68,6 +72,7 @@ internal struct EditorTabPayload: Codable, Hashable { showStructure = try container.decodeIfPresent(Bool.self, forKey: .showStructure) ?? false skipAutoExecute = try container.decodeIfPresent(Bool.self, forKey: .skipAutoExecute) ?? false isPreview = try container.decodeIfPresent(Bool.self, forKey: .isPreview) ?? false + initialFilterState = try container.decodeIfPresent(TabFilterState.self, forKey: .initialFilterState) } /// Whether this payload is a "connection-only" payload — just a connectionId @@ -89,5 +94,6 @@ internal struct EditorTabPayload: Codable, Hashable { self.showStructure = tab.showStructure self.skipAutoExecute = skipAutoExecute self.isPreview = false + self.initialFilterState = nil } } diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index c168fdcf..8e6583d1 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -233,7 +233,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length } } } icon: { - Image(t.iconName) + t.iconImage .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) diff --git a/TablePro/Views/Connection/ConnectionSidebarHeader.swift b/TablePro/Views/Connection/ConnectionSidebarHeader.swift index 6ad1debf..a9772a84 100644 --- a/TablePro/Views/Connection/ConnectionSidebarHeader.swift +++ b/TablePro/Views/Connection/ConnectionSidebarHeader.swift @@ -34,7 +34,7 @@ struct ConnectionSidebarHeader: View { onSelectSession(session.id) }) { HStack { - Image(session.connection.type.iconName) + session.connection.type.iconImage .renderingMode(.template) .foregroundStyle(session.connection.displayColor) @@ -67,7 +67,7 @@ struct ConnectionSidebarHeader: View { onOpenConnection(connection) }) { HStack { - Image(connection.type.iconName) + connection.type.iconImage .renderingMode(.template) .foregroundStyle(connection.displayColor) @@ -90,7 +90,7 @@ struct ConnectionSidebarHeader: View { HStack(spacing: 8) { // Database icon if let session = currentSession { - Image(session.connection.type.iconName) + session.connection.type.iconImage .renderingMode(.template) .font(.system(size: ThemeEngine.shared.activeTheme.iconSizes.medium)) .foregroundStyle(session.connection.displayColor) diff --git a/TablePro/Views/Connection/WelcomeWindowView.swift b/TablePro/Views/Connection/WelcomeWindowView.swift index 54cc02fd..70eda06a 100644 --- a/TablePro/Views/Connection/WelcomeWindowView.swift +++ b/TablePro/Views/Connection/WelcomeWindowView.swift @@ -649,7 +649,7 @@ private struct ConnectionRow: View { var body: some View { HStack(spacing: 12) { // Database type icon - Image(connection.type.iconName) + connection.type.iconImage .renderingMode(.template) .font(.system(size: ThemeEngine.shared.activeTheme.iconSizes.medium)) .foregroundStyle(connection.displayColor) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift index 67e6679d..dc8b2807 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift @@ -52,12 +52,20 @@ extension MainContentCoordinator { // If current tab has unsaved changes, open in a new native tab instead of replacing if changeManager.hasChanges { + let fkFilterState = TabFilterState( + filters: [filter], + appliedFilters: [filter], + isVisible: true, + quickSearchText: "", + filterLogicMode: .and + ) let payload = EditorTabPayload( connectionId: connection.id, tabType: .table, tableName: referencedTable, databaseName: currentDatabase, - isView: false + isView: false, + initialFilterState: fkFilterState ) WindowOpener.shared.openNativeTab(payload) return diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index b86cc64b..095fd115 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -487,6 +487,20 @@ struct MainContentView: View { { Task { await coordinator.switchDatabase(to: selectedTab.databaseName) } } else { + if !selectedTab.filterState.appliedFilters.isEmpty, + let tableName = selectedTab.tableName, + let tabIndex = tabManager.selectedTabIndex + { + // columns is [] on initial load — buildFilteredQuery uses SELECT * + let filteredQuery = coordinator.queryBuilder.buildFilteredQuery( + tableName: tableName, + filters: selectedTab.filterState.appliedFilters, + columns: [], + limit: selectedTab.pagination.pageSize, + offset: selectedTab.pagination.currentOffset + ) + tabManager.tabs[tabIndex].query = filteredQuery + } coordinator.executeTableTabQueryDirectly() } } else {