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

- MySQL/MariaDB: `BIT(N)` columns now display as decimal numbers (`0`, `1`, `255`) instead of raw bytes that showed up as control characters like `^A` in the data grid. (#1272)
- 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.
Expand Down
20 changes: 17 additions & 3 deletions Plugins/MySQLDriverPlugin/MariaDBFieldClassifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ internal enum MariaDBFieldClassifier {
private static let blobOrStringTypes: Set<UInt32> = [249, 250, 251, 252, 253, 254]

static func isBinary(typeRaw: UInt32, charset: UInt32) -> Bool {
if typeRaw == bitType {
return true
}
guard charset == binaryCharset else {
return false
}
return blobOrStringTypes.contains(typeRaw)
}

static func isBit(typeRaw: UInt32) -> Bool {
typeRaw == bitType
}

static func bitFieldToString(_ buffer: UnsafeRawBufferPointer) -> String {
guard !buffer.isEmpty else { return "0" }
var value: UInt64 = 0
for byte in buffer {
value = (value << 8) | UInt64(byte)
}
return String(value)
}

static func bitFieldToString(_ data: Data) -> String {
data.withUnsafeBytes { bitFieldToString($0) }
}
}
8 changes: 7 additions & 1 deletion Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,8 @@ final class MariaDBPluginConnection: @unchecked Sendable {

if columnTypes[i] == 255 {
row.append(.text(GeometryWKBParser.parse(bufferPtr)))
} else if MariaDBFieldClassifier.isBit(typeRaw: columnTypes[i]) {
row.append(.text(MariaDBFieldClassifier.bitFieldToString(bufferPtr)))
} else if columnIsBinary[i] {
row.append(.bytes(Data(bufferPtr)))
} else if let str = String(bytes: bufferPtr, encoding: .utf8) {
Expand Down Expand Up @@ -710,7 +712,9 @@ final class MariaDBPluginConnection: @unchecked Sendable {
let length = Int(resultBinds[i].length?.pointee ?? 0)
let buffer = resultBuffers[i].assumingMemoryBound(to: UInt8.self)
let data = Data(bytes: buffer, count: length)
if columnIsBinary[i] {
if MariaDBFieldClassifier.isBit(typeRaw: columnTypes[i]) {
row.append(.text(MariaDBFieldClassifier.bitFieldToString(data)))
} else if columnIsBinary[i] {
row.append(.bytes(data))
} else if let str = String(data: data, encoding: .utf8) {
row.append(.text(str))
Expand Down Expand Up @@ -956,6 +960,8 @@ final class MariaDBPluginConnection: @unchecked Sendable {

if columnTypes[i] == 255 {
row.append(.text(GeometryWKBParser.parse(bufferPtr)))
} else if MariaDBFieldClassifier.isBit(typeRaw: columnTypes[i]) {
row.append(.text(MariaDBFieldClassifier.bitFieldToString(bufferPtr)))
} else if columnIsBinary[i] {
row.append(.bytes(Data(bufferPtr)))
} else if let str = String(bytes: bufferPtr, encoding: .utf8) {
Expand Down
58 changes: 54 additions & 4 deletions TableProTests/Plugins/MariaDBFieldClassifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,68 @@
//

#if canImport(MySQLDriverPlugin)
import Foundation
import Testing

@testable import MySQLDriverPlugin

@Suite("MariaDBFieldClassifier")
struct MariaDBFieldClassifierTests {
@Test("BIT routes to binary regardless of charset")
func bitIsBinary() {
#expect(MariaDBFieldClassifier.isBinary(typeRaw: 16, charset: 63))
#expect(MariaDBFieldClassifier.isBinary(typeRaw: 16, charset: 33))
@Test("BIT no longer routes to binary (it was rendering as raw control characters in the data grid)")
func bitIsNotBinary() {
#expect(!MariaDBFieldClassifier.isBinary(typeRaw: 16, charset: 63))
#expect(!MariaDBFieldClassifier.isBinary(typeRaw: 16, charset: 33))
}

@Test("isBit identifies type code 16 and only type code 16")
func isBitMatchesOnlyBitType() {
#expect(MariaDBFieldClassifier.isBit(typeRaw: 16))
for typeRaw: UInt32 in [0, 1, 2, 3, 7, 12, 15, 17, 245, 252, 255] {
#expect(!MariaDBFieldClassifier.isBit(typeRaw: typeRaw))
}
}

@Test("bitFieldToString decodes a single byte big-endian")
func bitFieldSingleByte() {
#expect(decodeBitField([0x00]) == "0")
#expect(decodeBitField([0x01]) == "1")
#expect(decodeBitField([0x7f]) == "127")
#expect(decodeBitField([0xff]) == "255")
}

@Test("bitFieldToString decodes multi-byte values MSB-first")
func bitFieldMultiByte() {
#expect(decodeBitField([0x01, 0x00]) == "256")
#expect(decodeBitField([0xff, 0xff]) == "65535")
#expect(decodeBitField([0x12, 0x34]) == "4660")
}

@Test("bitFieldToString handles BIT(64) up to UInt64.max")
func bitField64Bits() {
let allOnes: [UInt8] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
#expect(decodeBitField(allOnes) == "18446744073709551615")
let halfPlusOne: [UInt8] = [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
#expect(decodeBitField(halfPlusOne) == "9223372036854775808")
}

@Test("bitFieldToString on an empty buffer returns 0")
func bitFieldEmptyBuffer() {
#expect(decodeBitField([]) == "0")
}

@Test("Data overload matches the raw-pointer overload")
func bitFieldDataOverload() {
let bytes: [UInt8] = [0x01, 0xff]
let dataResult = MariaDBFieldClassifier.bitFieldToString(Data(bytes))
#expect(dataResult == "511")
#expect(dataResult == decodeBitField(bytes))
}

private func decodeBitField(_ bytes: [UInt8]) -> String {
bytes.withUnsafeBytes { MariaDBFieldClassifier.bitFieldToString($0) }
}


@Test("BLOB family with binary charset routes to binary")
func blobFamilyBinary() {
for typeRaw: UInt32 in [249, 250, 251, 252] {
Expand Down
Loading