Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e805e11
feat(plugin-teradata): add pure-Swift TD2 protocol core
datlechin Jul 16, 2026
d2b2be5
feat(plugin-teradata): add Teradata connection and session layer
datlechin Jul 16, 2026
dda9096
feat(plugin-teradata): map Teradata column types for structure and grid
datlechin Jul 16, 2026
1252c84
feat(plugin-teradata): add the Teradata driver plugin bundle
datlechin Jul 16, 2026
a678294
docs(plugin-teradata): document Teradata driver and wire up plugin CI
datlechin Jul 16, 2026
2ad84d7
build(plugin-teradata): add TeradataDriver Xcode target and fix plugi…
datlechin Jul 16, 2026
1ec1bf3
feat(plugin-teradata): show Teradata in the database chooser before i…
datlechin Jul 16, 2026
ff8f386
feat(plugin-teradata): generate INSERT/UPDATE/DELETE for grid editing
datlechin Jul 16, 2026
5a1cf51
feat(plugin-teradata): generate CREATE/ALTER TABLE and index DDL for …
datlechin Jul 16, 2026
9fec9b2
feat(plugin-teradata): add Teradata brand icon asset
datlechin Jul 16, 2026
4500ce9
feat(plugin-teradata): reject unsupported logon mechanisms before con…
datlechin Jul 16, 2026
779369d
feat(plugin-teradata): add TLS transport over the gateway port
datlechin Jul 16, 2026
bdecf5e
fix(plugin-teradata): carry TLS over the gateway WebSocket transport …
datlechin Jul 16, 2026
5c837a9
fix(plugin-teradata): surface server errors instead of looping on fai…
datlechin Jul 16, 2026
a170ca4
chore(plugin-teradata): extract Teradata tagline string
datlechin Jul 16, 2026
8b22a6f
fix(plugin-teradata): show the server error message instead of an err…
datlechin Jul 16, 2026
397f798
fix(plugin-teradata): list only tables and views, not procedures or m…
datlechin Jul 16, 2026
367a45d
fix(plugin-teradata): decode DECIMAL result columns
datlechin Jul 16, 2026
9bc0849
fix(plugin-teradata): pin Verify CA to the configured CA and stop a p…
datlechin Jul 16, 2026
66a2f23
fix(plugin-teradata): quote text cell values so numeric strings keep …
datlechin Jul 16, 2026
770e82a
fix(plugin-teradata): reject an out-of-range port instead of crashing…
datlechin Jul 16, 2026
dec820b
fix(plugin-teradata): read to end-of-request and guard short encrypte…
datlechin Jul 16, 2026
014b900
test(plugin-teradata): count Teradata in the known database types
datlechin Jul 16, 2026
379071d
fix(plugin-teradata): rename the activity-count helper so it stops sh…
datlechin Jul 16, 2026
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
5 changes: 5 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ jobs:
DISPLAY_NAME="MSSQL Driver"; SUMMARY="Microsoft SQL Server driver via FreeTDS"
DB_TYPE_IDS='["SQL Server"]'; ICON="mssql-icon"; BUNDLE_NAME="MSSQLDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/mssql" ;;
teradata)
TARGET="TeradataDriver"; BUNDLE_ID="com.TablePro.TeradataDriver"
DISPLAY_NAME="Teradata Driver"; SUMMARY="Teradata Vantage driver via a native Swift TD2 client"
DB_TYPE_IDS='["Teradata"]'; ICON="teradata-icon"; BUNDLE_NAME="TeradataDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/teradata" ;;
mongodb)
TARGET="MongoDBDriver"; BUNDLE_ID="com.TablePro.MongoDBDriver"
DISPLAY_NAME="MongoDB Driver"; SUMMARY="MongoDB document database driver via libmongoc"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Teradata support through a downloadable driver written in native Swift. Connect over TD2 or TDNEGO logon, optionally with TLS, browse databases, tables, and columns, run SQL, edit rows, and create or alter tables. (#1867)

### Fixed

- Fixed SSH tunnels that could accept a database connection and then go quiet instead of forwarding it or failing, which showed up as MySQL and MariaDB connections timing out while reading the server greeting. A tunnel that cannot open its forwarding channel now gives up after 10 seconds, closes the connection, and logs the reason, instead of leaving the driver to wait out its own timeout with no explanation. (#1883)
Expand Down
13 changes: 12 additions & 1 deletion Packages/TableProCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ let package = Package(
.library(name: "TableProQuery", targets: ["TableProQuery"]),
.library(name: "TableProSync", targets: ["TableProSync"]),
.library(name: "TableProAnalytics", targets: ["TableProAnalytics"]),
.library(name: "TableProMSSQLCore", targets: ["TableProMSSQLCore"])
.library(name: "TableProMSSQLCore", targets: ["TableProMSSQLCore"]),
.library(name: "TableProTeradataCore", targets: ["TableProTeradataCore"])
],
targets: [
.target(
Expand Down Expand Up @@ -66,6 +67,11 @@ let package = Package(
dependencies: [],
path: "Sources/TableProMSSQLCore"
),
.target(
name: "TableProTeradataCore",
dependencies: [],
path: "Sources/TableProTeradataCore"
),
.testTarget(
name: "TableProModelsTests",
dependencies: ["TableProModels", "TableProPluginKit"],
Expand Down Expand Up @@ -96,6 +102,11 @@ let package = Package(
dependencies: ["TableProMSSQLCore"],
path: "Tests/TableProMSSQLCoreTests"
),
.testTarget(
name: "TableProTeradataCoreTests",
dependencies: ["TableProTeradataCore"],
path: "Tests/TableProTeradataCoreTests"
),
.testTarget(
name: "TableProSyncTests",
dependencies: ["TableProSync", "TableProModels"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
public static let scylladb = DatabaseType(rawValue: "ScyllaDB")
public static let turso = DatabaseType(rawValue: "Turso")
public static let surrealdb = DatabaseType(rawValue: "SurrealDB")
public static let teradata = DatabaseType(rawValue: "Teradata")

public static let allKnownTypes: [DatabaseType] = [
.mysql, .mariadb, .postgresql, .sqlite, .redis, .mongodb,
.clickhouse, .mssql, .oracle, .duckdb, .cassandra, .redshift,
.etcd, .cloudflareD1, .dynamodb, .bigquery, .snowflake, .libsql, .beancount,
.surrealdb
.surrealdb, .teradata
]

/// Icon name for this database type — asset catalog name (e.g. "mysql-icon") or SF Symbol fallback
Expand All @@ -63,6 +64,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable {
case .libsql: return "libsql-icon"
case .beancount: return "beancount-icon"
case .surrealdb: return "surrealdb-icon"
case .teradata: return "teradata-icon"
default: return "externaldrive"
}
}
Expand Down
236 changes: 236 additions & 0 deletions Packages/TableProCore/Sources/TableProTeradataCore/BigUInt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import Foundation

struct BigUInt: Equatable, CustomStringConvertible {
private(set) var limbs: [UInt32]

init() { limbs = [] }

init(_ value: UInt32) { limbs = value == 0 ? [] : [value] }

init(limbs: [UInt32]) {
var normalized = limbs
while normalized.last == 0 { normalized.removeLast() }
self.limbs = normalized
}

init(bytesBE bytes: [UInt8]) {
var built: [UInt32] = []
var index = bytes.count
while index > 0 {
var limb: UInt32 = 0
var shift: UInt32 = 0
var taken = 0
while taken < 4 && index > 0 {
index -= 1
limb |= UInt32(bytes[index]) << shift
shift += 8
taken += 1
}
built.append(limb)
}
self.init(limbs: built)
}

init?(hex: String) {
var trimmed = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
if trimmed.count % 2 == 1 { trimmed = "0" + trimmed }
var bytes: [UInt8] = []
var cursor = trimmed.startIndex
while cursor < trimmed.endIndex {
let next = trimmed.index(cursor, offsetBy: 2)
guard let byte = UInt8(trimmed[cursor..<next], radix: 16) else { return nil }
bytes.append(byte)
cursor = next
}
self.init(bytesBE: bytes)
}

var isZero: Bool { limbs.isEmpty }

var byteCount: Int {
guard let top = limbs.last else { return 0 }
let topBytes: Int
switch top {
case 0...0xFF: topBytes = 1
case 0x100...0xFFFF: topBytes = 2
case 0x10000...0xFFFFFF: topBytes = 3
default: topBytes = 4
}
return (limbs.count - 1) * 4 + topBytes
}

var bitLength: Int {
guard let top = limbs.last else { return 0 }
return (limbs.count - 1) * 32 + (32 - top.leadingZeroBitCount)
}

func bytesBE(minCount: Int = 0) -> [UInt8] {
let count = max(byteCount, minCount)
guard count > 0 else { return [] }
var out = [UInt8]()
out.reserveCapacity(count)
for byteIndex in stride(from: count - 1, through: 0, by: -1) {
let limbIndex = byteIndex / 4
let shift = UInt32((byteIndex % 4) * 8)
let limb = limbIndex < limbs.count ? limbs[limbIndex] : 0
out.append(UInt8((limb >> shift) & 0xFF))
}
return out
}

func bit(_ position: Int) -> Bool {
let limbIndex = position / 32
guard limbIndex < limbs.count else { return false }
return (limbs[limbIndex] >> UInt32(position % 32)) & 1 == 1
}

var description: String {
isZero ? "0" : bytesBE().map { String(format: "%02x", $0) }.joined()
}

static func compare(_ lhs: [UInt32], _ rhs: [UInt32]) -> Int {
let count = max(lhs.count, rhs.count)
for i in stride(from: count - 1, through: 0, by: -1) {
let left = i < lhs.count ? lhs[i] : 0
let right = i < rhs.count ? rhs[i] : 0
if left != right { return left < right ? -1 : 1 }
}
return 0
}

func shiftedLeftOneBit() -> BigUInt {
var out = [UInt32]()
out.reserveCapacity(limbs.count + 1)
var carry: UInt32 = 0
for limb in limbs {
out.append((limb << 1) | carry)
carry = limb >> 31
}
if carry != 0 { out.append(carry) }
return BigUInt(limbs: out)
}

func settingLowBit() -> BigUInt {
var out = limbs
if out.isEmpty { out = [1] } else { out[0] |= 1 }
return BigUInt(limbs: out)
}

func subtracting(_ other: BigUInt) -> BigUInt {
var out = [UInt32]()
out.reserveCapacity(limbs.count)
var borrow: UInt64 = 0
for i in 0..<limbs.count {
let a = UInt64(limbs[i])
let b = (i < other.limbs.count ? UInt64(other.limbs[i]) : 0) + borrow
if a >= b {
out.append(UInt32(a - b))
borrow = 0
} else {
out.append(UInt32(a + 0x1_0000_0000 - b))
borrow = 1
}
}
return BigUInt(limbs: out)
}

func mod(_ modulus: BigUInt) -> BigUInt {
precondition(!modulus.isZero, "modulo by zero")
var remainder = BigUInt()
for position in stride(from: bitLength - 1, through: 0, by: -1) {
remainder = remainder.shiftedLeftOneBit()
if bit(position) { remainder = remainder.settingLowBit() }
if BigUInt.compare(remainder.limbs, modulus.limbs) >= 0 {
remainder = remainder.subtracting(modulus)
}
}
return remainder
}

func modPow(_ exponent: BigUInt, modulus: BigUInt) -> BigUInt {
precondition(!modulus.isZero, "modulo by zero")
if modulus.limbs == [1] { return BigUInt() }
precondition(modulus.limbs[0] & 1 == 1, "Montgomery modPow requires an odd modulus")
if exponent.isZero { return BigUInt(1).mod(modulus) }

let size = modulus.limbs.count
let n = BigUInt.padded(modulus.limbs, to: size)
let nInv = BigUInt.montgomeryInverse(n[0])

let rSquaredLimbs = [UInt32](repeating: 0, count: 2 * size) + [1]
let rSquared = BigUInt.padded(BigUInt(limbs: rSquaredLimbs).mod(modulus).limbs, to: size)

let baseMont = BigUInt.montgomeryMultiply(
BigUInt.padded(mod(modulus).limbs, to: size), rSquared, n, nInv, size)
var accumulator = BigUInt.montgomeryMultiply(
BigUInt.padded([1], to: size), rSquared, n, nInv, size)

for position in stride(from: exponent.bitLength - 1, through: 0, by: -1) {
accumulator = BigUInt.montgomeryMultiply(accumulator, accumulator, n, nInv, size)
if exponent.bit(position) {
accumulator = BigUInt.montgomeryMultiply(accumulator, baseMont, n, nInv, size)
}
}

let result = BigUInt.montgomeryMultiply(
accumulator, BigUInt.padded([1], to: size), n, nInv, size)
return BigUInt(limbs: result)
}

private static func padded(_ limbs: [UInt32], to size: Int) -> [UInt32] {
if limbs.count >= size { return Array(limbs[0..<size]) }
return limbs + [UInt32](repeating: 0, count: size - limbs.count)
}

private static func montgomeryInverse(_ n0: UInt32) -> UInt32 {
var inverse: UInt32 = 1
for _ in 0..<5 { inverse = inverse &* (2 &- n0 &* inverse) }
return 0 &- inverse
}

private static func montgomeryMultiply(
_ a: [UInt32], _ b: [UInt32], _ n: [UInt32], _ nInv: UInt32, _ size: Int
) -> [UInt32] {
var t = [UInt32](repeating: 0, count: size + 2)
for i in 0..<size {
var carry: UInt64 = 0
let bi = UInt64(b[i])
for j in 0..<size {
let product = UInt64(t[j]) + UInt64(a[j]) * bi + carry
t[j] = UInt32(truncatingIfNeeded: product)
carry = product >> 32
}
let sum = UInt64(t[size]) + carry
t[size] = UInt32(truncatingIfNeeded: sum)
t[size + 1] = UInt32(sum >> 32)

let m = UInt64(UInt32(truncatingIfNeeded: UInt64(t[0]) &* UInt64(nInv)))
var reduceCarry = (UInt64(t[0]) + m * UInt64(n[0])) >> 32
for j in 1..<size {
let product = UInt64(t[j]) + m * UInt64(n[j]) + reduceCarry
t[j - 1] = UInt32(truncatingIfNeeded: product)
reduceCarry = product >> 32
}
let tail = UInt64(t[size]) + reduceCarry
t[size - 1] = UInt32(truncatingIfNeeded: tail)
t[size] = t[size + 1] &+ UInt32(tail >> 32)
}

var result = Array(t[0..<size])
if t[size] != 0 || BigUInt.compare(result, n) >= 0 {
var borrow: UInt64 = 0
for j in 0..<size {
let a = UInt64(result[j])
let b = UInt64(n[j]) + borrow
if a >= b {
result[j] = UInt32(a - b)
borrow = 0
} else {
result[j] = UInt32(a + 0x1_0000_0000 - b)
borrow = 1
}
}
}
return result
}
}
Loading
Loading