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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- ClickHouse, BigQuery, CloudflareD1, LibSQL, Etcd, and DynamoDB: long-running queries no longer fail at 30 seconds when Settings > Query timeout is set higher. The HTTP transport now uses the configured query timeout plus a 30-second grace, so the server's `max_execution_time` (or equivalent) fires before the client gives up. Setting "No limit" raises the transport ceiling to 1 hour. (#1267)
- AI Chat: DeepSeek V4 thinking content (`reasoning_content`) is now captured during streaming and passed back in subsequent turns, fixing 400 errors when using deepseek-v4-pro or deepseek-v4-flash.
- MongoDB: the connection form now shows a Username field. It was hidden for databases where authentication is optional, so connections to auth-enabled servers saved with no credentials and every query failed with "requires authentication" even though the connection looked healthy.
- SQL import dropped statements when the database executed them slower than the file was parsed, so a re-imported export could fail with errors like "relation does not exist". The parser now waits for each statement to be consumed before reading more. (#1264)
Expand Down
12 changes: 8 additions & 4 deletions Plugins/BigQueryDriverPlugin/BigQueryConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ internal final class BigQueryConnection: @unchecked Sendable {
private var _currentJobId: String?
private var _currentJobLocation: String?
private var _queryTimeoutSeconds: Int = 300
private let _queryTimeout = HttpQueryTimeoutBox()
private let location: String?
private static let logger = Logger(subsystem: "com.TablePro", category: "BigQueryConnection")
private static let baseUrl = "https://bigquery.googleapis.com/bigquery/v2"
Expand All @@ -294,6 +295,7 @@ internal final class BigQueryConnection: @unchecked Sendable {

func setQueryTimeout(_ seconds: Int) {
lock.withLock { _queryTimeoutSeconds = max(seconds, 30) }
_queryTimeout.set(serverTimeoutSeconds: seconds)
}

init(config: DriverConnectionConfig) {
Expand All @@ -306,8 +308,8 @@ internal final class BigQueryConnection: @unchecked Sendable {
let authProvider = try createAuthProvider()

let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 60
sessionConfig.timeoutIntervalForResource = 300
sessionConfig.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
sessionConfig.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout
let urlSession = URLSession(configuration: sessionConfig)

lock.withLock {
Expand Down Expand Up @@ -836,8 +838,10 @@ internal final class BigQueryConnection: @unchecked Sendable {
_ request: URLRequest,
session: URLSession
) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
let task = session.dataTask(with: request) { [weak self] data, response, error in
var timedRequest = request
timedRequest.timeoutInterval = _queryTimeout.requestTimeoutInterval
return try await withCheckedThrowingContinuation { continuation in
let task = session.dataTask(with: timedRequest) { [weak self] data, response, error in
self?.lock.withLock { self?._currentTask = nil }
if let error {
if (error as? URLError)?.code == .cancelled {
Expand Down
2 changes: 2 additions & 0 deletions Plugins/BigQueryDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>TableProPluginKitVersion</key>
<integer>12</integer>
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
</dict>
</plist>
12 changes: 8 additions & 4 deletions Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
private var currentTask: URLSessionDataTask?
private var _currentDatabase: String
private var _lastQueryId: String?
private let _queryTimeout = HttpQueryTimeoutBox()

private static let logger = Logger(subsystem: "com.TablePro", category: "ClickHousePluginDriver")

Expand Down Expand Up @@ -196,8 +197,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {

func connect() async throws {
let urlConfig = URLSessionConfiguration.default
urlConfig.timeoutIntervalForRequest = 30
urlConfig.timeoutIntervalForResource = 300
urlConfig.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
urlConfig.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout

lock.lock()
if let delegate = ClickHouseTLSDelegate.make(for: config.ssl) {
Expand Down Expand Up @@ -732,6 +733,7 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}

func applyQueryTimeout(_ seconds: Int) async throws {
_queryTimeout.set(serverTimeoutSeconds: seconds)
guard seconds > 0 else { return }
_ = try await execute(query: "SET max_execution_time = \(seconds)")
}
Expand Down Expand Up @@ -802,7 +804,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}
lock.unlock()

let request = try buildRequest(query: query, database: database, queryId: queryId)
var request = try buildRequest(query: query, database: database, queryId: queryId)
request.timeoutInterval = _queryTimeout.requestTimeoutInterval
let isSelect = Self.isSelectLikeQuery(query)

let (data, response) = try await withTaskCancellationHandler {
Expand Down Expand Up @@ -861,7 +864,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
}
lock.unlock()

let request = try buildRequest(query: query, database: database, queryId: queryId, params: params)
var request = try buildRequest(query: query, database: database, queryId: queryId, params: params)
request.timeoutInterval = _queryTimeout.requestTimeoutInterval
let isSelect = Self.isSelectLikeQuery(query)

let (data, response) = try await withTaskCancellationHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ final class CloudflareD1PluginDriver: PluginDatabaseDriver, @unchecked Sendable
lock.unlock()
}

func applyQueryTimeout(_ seconds: Int) async throws {
lock.lock()
let client = httpClient
lock.unlock()
client?.setQueryTimeout(seconds)
}

// MARK: - Streaming

func streamRows(query: String) -> AsyncThrowingStream<PluginStreamElement, Error> {
Expand Down
11 changes: 9 additions & 2 deletions Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import Foundation
import os
import TableProPluginKit

// MARK: - API Response Types

Expand Down Expand Up @@ -136,6 +137,7 @@ final class D1HttpClient: @unchecked Sendable {
private var _databaseId: String
private var session: URLSession?
private var currentTask: URLSessionDataTask?
private let queryTimeout = HttpQueryTimeoutBox()

var databaseId: String {
get {
Expand All @@ -156,10 +158,14 @@ final class D1HttpClient: @unchecked Sendable {
self._databaseId = databaseId
}

func setQueryTimeout(_ seconds: Int) {
queryTimeout.set(serverTimeoutSeconds: seconds)
}

func createSession() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
config.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout

lock.lock()
session = URLSession(configuration: config)
Expand Down Expand Up @@ -307,6 +313,7 @@ final class D1HttpClient: @unchecked Sendable {

var request = URLRequest(url: url)
request.httpMethod = method
request.timeoutInterval = queryTimeout.requestTimeoutInterval
request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
Expand Down
2 changes: 2 additions & 0 deletions Plugins/CloudflareD1DriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>TableProPluginKitVersion</key>
<integer>12</integer>
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
</dict>
</plist>
10 changes: 8 additions & 2 deletions Plugins/DynamoDBDriverPlugin/DynamoDBConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ internal final class DynamoDBConnection: @unchecked Sendable {
private var _session: URLSession?
private var _credentials: AWSCredentials?
private var _currentTask: URLSessionDataTask?
private let _queryTimeout = HttpQueryTimeoutBox()
private let region: String
private let endpointUrl: String
private static let logger = Logger(subsystem: "com.TablePro", category: "DynamoDBConnection")
Expand All @@ -270,6 +271,10 @@ internal final class DynamoDBConnection: @unchecked Sendable {
lock.withLock { _session }
}

func setQueryTimeout(_ seconds: Int) {
_queryTimeout.set(serverTimeoutSeconds: seconds)
}

init(config: DriverConnectionConfig) {
self.config = config
self.region = config.additionalFields["awsRegion"] ?? "us-east-1"
Expand Down Expand Up @@ -298,8 +303,8 @@ internal final class DynamoDBConnection: @unchecked Sendable {
func connect() async throws {
let credentials = try resolveCredentials()
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 30
sessionConfig.timeoutIntervalForResource = 60
sessionConfig.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
sessionConfig.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout
let urlSession = URLSession(configuration: sessionConfig)

lock.withLock {
Expand Down Expand Up @@ -442,6 +447,7 @@ internal final class DynamoDBConnection: @unchecked Sendable {
urlRequest.setValue(hostHeader, forHTTPHeaderField: "Host")

signRequest(&urlRequest, body: bodyData, credentials: credentials)
urlRequest.timeoutInterval = _queryTimeout.requestTimeoutInterval

let (data, response) = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<(Data, URLResponse), Error>) in
Expand Down
4 changes: 3 additions & 1 deletion Plugins/DynamoDBDriverPlugin/DynamoDBPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ internal final class DynamoDBPluginDriver: PluginDatabaseDriver, @unchecked Send
connection?.cancelCurrentRequest()
}

func applyQueryTimeout(_ seconds: Int) async throws {}
func applyQueryTimeout(_ seconds: Int) async throws {
connection?.setQueryTimeout(seconds)
}

// MARK: - Schema Operations

Expand Down
2 changes: 2 additions & 0 deletions Plugins/DynamoDBDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>TableProPluginKitVersion</key>
<integer>12</integer>
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
</dict>
</plist>
10 changes: 8 additions & 2 deletions Plugins/EtcdDriverPlugin/EtcdHttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,18 @@ internal final class EtcdHttpClient: @unchecked Sendable {
private var authToken: String?
private var _isAuthenticating = false
private var apiPrefix = "v3"
private let queryTimeout = HttpQueryTimeoutBox()

private static let logger = Logger(subsystem: "com.TablePro", category: "EtcdHttpClient")

init(config: DriverConnectionConfig) {
self.config = config
}

func setQueryTimeout(_ seconds: Int) {
queryTimeout.set(serverTimeoutSeconds: seconds)
}

// MARK: - Base URL

private var tlsEnabled: Bool {
Expand All @@ -347,8 +352,8 @@ internal final class EtcdHttpClient: @unchecked Sendable {
let tlsMode = config.additionalFields["etcdTlsMode"] ?? "Disabled"

let urlConfig = URLSessionConfiguration.default
urlConfig.timeoutIntervalForRequest = 30
urlConfig.timeoutIntervalForResource = 300
urlConfig.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
urlConfig.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout

let delegate: URLSessionDelegate?
switch tlsMode {
Expand Down Expand Up @@ -718,6 +723,7 @@ internal final class EtcdHttpClient: @unchecked Sendable {

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = queryTimeout.requestTimeoutInterval
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token {
request.setValue(token, forHTTPHeaderField: "Authorization")
Expand Down
4 changes: 3 additions & 1 deletion Plugins/EtcdDriverPlugin/EtcdPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ final class EtcdPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
httpClient?.cancelCurrentRequest()
}

func applyQueryTimeout(_ seconds: Int) async throws {}
func applyQueryTimeout(_ seconds: Int) async throws {
httpClient?.setQueryTimeout(seconds)
}

// MARK: - Schema Operations

Expand Down
2 changes: 2 additions & 0 deletions Plugins/EtcdDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>TableProPluginKitVersion</key>
<integer>12</integer>
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
</dict>
</plist>
11 changes: 9 additions & 2 deletions Plugins/LibSQLDriverPlugin/HranaHttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import Foundation
import os
import TableProPluginKit

// MARK: - Hrana Protocol Types

Expand Down Expand Up @@ -114,16 +115,21 @@ final class HranaHttpClient: @unchecked Sendable {
private let lock = NSLock()
private var session: URLSession?
private var currentTask: URLSessionDataTask?
private let queryTimeout = HttpQueryTimeoutBox()

init(baseUrl: URL, authToken: String?) {
self.baseUrl = baseUrl
self.authToken = authToken
}

func setQueryTimeout(_ seconds: Int) {
queryTimeout.set(serverTimeoutSeconds: seconds)
}

func createSession() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.timeoutIntervalForRequest = HttpQueryTimeout.sessionBootstrapRequestTimeout
config.timeoutIntervalForResource = HttpQueryTimeout.sessionResourceTimeout

lock.lock()
session = URLSession(configuration: config)
Expand Down Expand Up @@ -211,6 +217,7 @@ final class HranaHttpClient: @unchecked Sendable {

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = queryTimeout.requestTimeoutInterval
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token = authToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
Expand Down
2 changes: 2 additions & 0 deletions Plugins/LibSQLDriverPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>TableProPluginKitVersion</key>
<integer>12</integer>
<key>TableProMinAppVersion</key>
<string>0.42.0</string>
</dict>
</plist>
7 changes: 7 additions & 0 deletions Plugins/LibSQLDriverPlugin/LibSQLPluginDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ final class LibSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
lock.unlock()
}

func applyQueryTimeout(_ seconds: Int) async throws {
lock.lock()
let client = httpClient
lock.unlock()
client?.setQueryTimeout(seconds)
}

// MARK: - Streaming

func streamRows(query: String) -> AsyncThrowingStream<PluginStreamElement, Error> {
Expand Down
33 changes: 33 additions & 0 deletions Plugins/TableProPluginKit/HttpQueryTimeout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

public struct HttpQueryTimeout: Sendable, Equatable {
public static let bootstrapSeconds: Int = 60
public static let defaultGraceSeconds: Int = 30
public static let resourceCeilingSeconds: Int = 3_600

public let serverTimeoutSeconds: Int
public let graceSeconds: Int

public init(
serverTimeoutSeconds: Int = Self.bootstrapSeconds,
graceSeconds: Int = Self.defaultGraceSeconds
) {
self.serverTimeoutSeconds = serverTimeoutSeconds
self.graceSeconds = max(graceSeconds, 0)
}

public var requestTimeoutInterval: TimeInterval {
guard serverTimeoutSeconds > 0 else {
return TimeInterval(Self.resourceCeilingSeconds)
}
return TimeInterval(serverTimeoutSeconds + graceSeconds)
}

public static var sessionResourceTimeout: TimeInterval {
TimeInterval(resourceCeilingSeconds)
}

public static var sessionBootstrapRequestTimeout: TimeInterval {
TimeInterval(bootstrapSeconds)
}
}
Loading
Loading