-
-
Notifications
You must be signed in to change notification settings - Fork 297
feat(plugin-duckdb): add remote Quack protocol connections #1720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // | ||
| // QuackConnectBuilder.swift | ||
| // DuckDBDriverPlugin | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum QuackConnectBuilder { | ||
| static let defaultPort = 9_494 | ||
|
|
||
| static func isValidHost(_ host: String) -> Bool { | ||
| !host.trimmingCharacters(in: .whitespaces).isEmpty | ||
| } | ||
|
|
||
| static func normalizedPort(_ raw: String) -> Int? { | ||
| let trimmed = raw.trimmingCharacters(in: .whitespaces) | ||
| if trimmed.isEmpty { return defaultPort } | ||
| guard let port = Int(trimmed), (1...65_535).contains(port) else { return nil } | ||
| return port | ||
| } | ||
|
|
||
| static func secretSQL(token: String) -> String { | ||
| "CREATE OR REPLACE SECRET (TYPE quack, TOKEN '\(escapeLiteral(token))')" | ||
| } | ||
|
|
||
| static func attachSQL(host: String, port: Int, alias: String) -> String { | ||
| "ATTACH '\(quackTarget(host: host, port: port))' AS \(quoteIdentifier(alias))" | ||
| } | ||
|
|
||
| static func useSQL(alias: String) -> String { | ||
| "USE \(quoteIdentifier(alias))" | ||
| } | ||
|
|
||
| private static func quackTarget(host: String, port: Int) -> String { | ||
| "quack:\(escapeLiteral(host)):\(port)" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For an IPv6 host parsed from a URL such as Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private static func escapeLiteral(_ value: String) -> String { | ||
| value | ||
| .replacingOccurrences(of: "\0", with: "") | ||
| .replacingOccurrences(of: "'", with: "''") | ||
| } | ||
|
|
||
| private static func quoteIdentifier(_ name: String) -> String { | ||
| "\"\(name.replacingOccurrences(of: "\"", with: "\"\""))\"" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // | ||
| // PluginMetadataRegistry+DuckDBConnectionFields.swift | ||
| // TablePro | ||
| // | ||
|
|
||
| import Foundation | ||
| import TableProPluginKit | ||
|
|
||
| extension PluginMetadataRegistry { | ||
| static var duckdbConnectionFields: [ConnectionField] { | ||
| [ | ||
| ConnectionField( | ||
| id: "duckdbMode", | ||
| label: String(localized: "Connection Type"), | ||
| defaultValue: "local", | ||
| fieldType: .dropdown(options: [ | ||
| ConnectionField.DropdownOption(value: "local", label: String(localized: "Local File")), | ||
| ConnectionField.DropdownOption(value: "remote", label: String(localized: "Remote (Quack, experimental)")) | ||
| ]), | ||
| section: .authentication | ||
| ), | ||
| ConnectionField( | ||
| id: "duckdbFilePath", | ||
| label: String(localized: "Database File"), | ||
| placeholder: "/path/to/database.duckdb", | ||
| required: true, | ||
| section: .authentication, | ||
| visibleWhen: FieldVisibilityRule(fieldId: "duckdbMode", values: ["local"]) | ||
| ), | ||
| ConnectionField( | ||
| id: "duckdbHost", | ||
| label: String(localized: "Host"), | ||
| placeholder: "localhost", | ||
| required: true, | ||
| section: .authentication, | ||
| visibleWhen: FieldVisibilityRule(fieldId: "duckdbMode", values: ["remote"]) | ||
| ), | ||
| ConnectionField( | ||
| id: "duckdbPort", | ||
| label: String(localized: "Port"), | ||
| placeholder: "9494", | ||
| defaultValue: "9494", | ||
| fieldType: .number, | ||
| section: .authentication, | ||
| visibleWhen: FieldVisibilityRule(fieldId: "duckdbMode", values: ["remote"]) | ||
| ), | ||
| ConnectionField( | ||
| id: "duckdbToken", | ||
| label: String(localized: "Token"), | ||
| fieldType: .secure, | ||
| section: .authentication, | ||
| hidesPassword: true, | ||
| visibleWhen: FieldVisibilityRule(fieldId: "duckdbMode", values: ["remote"]) | ||
| ), | ||
| ConnectionField( | ||
| id: "duckdbAlias", | ||
| label: String(localized: "Database Alias"), | ||
| placeholder: "remotedb", | ||
| required: true, | ||
| defaultValue: "remotedb", | ||
| section: .authentication, | ||
| visibleWhen: FieldVisibilityRule(fieldId: "duckdbMode", values: ["remote"]) | ||
| ) | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the host is non-local but the server is exposed as plain HTTP (for example
CALL quack_serve('quack:0.0.0.0:9494', allow_other_hostname => true)on a LAN/dev box), DuckDB's Quack client assumes HTTPS unless theATTACHincludesDISABLE_SSL true(DuckDB docs). This builder never emits that option and the new fields do not expose any way to request it, so those remote connections fail at attach time even with the correct host, port, and token.Useful? React with 👍 / 👎.