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
2 changes: 2 additions & 0 deletions .claude/skills/fix-issue/scripts/worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ for framework in "$MAIN_ROOT"/Libs/ios/*.xcframework; do
[ -e "$framework" ] && ln -sfn "$framework" "$DIR/Libs/ios/$(basename "$framework")"
done
link "$MAIN_ROOT/Native/DamengBridge/lib" "$DIR/Native/DamengBridge/lib"
link "$MAIN_ROOT/Native/HanaBridge/bin" "$DIR/Native/HanaBridge/bin"

missing=""
[ -e "$DIR/Configs/Secrets.xcconfig" ] || missing="$missing Configs/Secrets.xcconfig"
[ -e "$DIR/Libs/dylibs" ] || missing="$missing Libs/dylibs"
[ -e "$DIR/Native/DamengBridge/lib" ] || missing="$missing Native/DamengBridge/lib"
[ -e "$DIR/Native/HanaBridge/bin" ] || missing="$missing Native/HanaBridge/bin"
ls "$DIR"/Libs/*.a > /dev/null 2>&1 || missing="$missing Libs/*.a"
ls "$DIR"/Libs/ios/*.xcframework > /dev/null 2>&1 || missing="$missing Libs/ios/*.xcframework"
if [ -n "$missing" ]; then
Expand Down
1 change: 1 addition & 0 deletions .github/actions/unpack-test-products/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ runs:
{
echo "XCTESTRUN=$(locate TablePro)"
echo "DAMENG_XCTESTRUN=$(locate DamengDriverTests)"
echo "HANA_XCTESTRUN=$(locate HanaDriverTests)"
} >> "$GITHUB_ENV"

# Checked here rather than discovered at test time. A missing one of these surfaces as
Expand Down
14 changes: 14 additions & 0 deletions .github/plugin-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@
"category": "database-driver",
"homepage": "https://docs.tablepro.app/databases/etcd"
},
"hana": {
"target": "HanaDriver",
"bundleName": "HanaDriver",
"bundleId": "com.TablePro.HanaDriver",
"bundled": false,
"displayName": "SAP HANA Driver",
"summary": "SAP HANA SQL driver via SAP/go-hdb",
"databaseTypeIds": [
"SAP HANA"
],
"icon": "cylinder",
"category": "database-driver",
"homepage": "https://docs.tablepro.app/databases/sap-hana"
},
"html": {
"target": "HTMLExport",
"bundleName": "HTMLExport",
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/build-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ jobs:
if: ${{ contains(matrix.tag, 'plugin-dameng-') }}
run: ./scripts/build-dameng.sh both

- name: Set up Go for the SAP HANA helper
if: ${{ contains(matrix.tag, 'plugin-hana-') }}
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: Native/HanaBridge/go.mod
cache-dependency-path: Native/HanaBridge/go.sum

# Universal, because both architecture builds below embed the same helper. build-plugin.sh
# signs it with Developer ID, the hardened runtime and a timestamp, and checks all three.
- name: Build the SAP HANA helper
if: ${{ contains(matrix.tag, 'plugin-hana-') }}
run: ./scripts/build-hana.sh both

# NOTARIZE is not optional. Gatekeeper refuses to load an unnotarized plugin bundle
# into TablePro, so a build that skips it publishes a driver no user can open.
- name: Build plugin binaries
Expand Down
66 changes: 61 additions & 5 deletions .github/workflows/macos-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ env:
jobs:
# Reproduces the paths filter this workflow used to carry on `on: pull_request`. Keep this
# list and the `push:` paths above identical, so a pull request and a push to main run the
# same suites. `Native/` is on both because the Dameng steps below build and test the Rust
# bridge that lives there, and a bridge-only change would otherwise compile nowhere.
# same suites. `Native/` is on both because the Dameng and SAP HANA steps below build and test
# the Rust and Go bridges that live there, and a bridge-only change would otherwise compile
# nowhere.
changes:
name: Detect relevant changes
runs-on: ubuntu-latest
Expand Down Expand Up @@ -194,6 +195,17 @@ jobs:
- name: Build Dameng native bridge
run: scripts/build-dameng.sh arm64

- name: Set up Go for the SAP HANA helper
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: Native/HanaBridge/go.mod
cache-dependency-path: Native/HanaBridge/go.sum

# Xcode copies tablepro-hana-helper into HanaDriver and HanaDriverTests and never builds
# it, so both targets fail with "Build input file cannot be found" until this has run.
- name: Build the SAP HANA helper
run: scripts/build-hana.sh arm64

# Only the bridge's own FFI tests live here. The DM8 protocol crates are a pinned
# revision of TableProApp/rust-dameng and carry their own suite in that repository.
# Run from the bridge directory so rustup reads its rust-toolchain.toml: rustup resolves
Expand All @@ -203,6 +215,26 @@ jobs:
working-directory: Native/DamengBridge
run: cargo test --locked

# Only the helper's own tests, none of which needs a HANA server. Run from the bridge
# directory so the go command reads its go.mod and builds with the toolchain it pins.
# staticcheck and govulncheck are pinned here and fetched by `go run`, so they never
# enter the helper's go.mod.
- name: Test the SAP HANA helper
working-directory: Native/HanaBridge
run: |
set -euo pipefail
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "::error::gofmt would rewrite: $unformatted"
exit 1
fi
go mod verify
go mod tidy -diff
go vet ./...
go test -race ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...

# Secrets.xcconfig is gitignored. Tests do not need analytics keys, and the project only
# needs the variable to resolve, so an empty value is enough and no secret is read here.
- name: Create Secrets.xcconfig
Expand Down Expand Up @@ -267,9 +299,11 @@ jobs:
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions

# DamengDriverTests is the one plugin-owned unit bundle: its suites import CDameng, so
# they cannot move into TableProTests the way pure-logic plugin tests do. Without this
# step nothing runs them and they rot silently.
# DamengDriverTests and HanaDriverTests are the plugin-owned unit bundles, so they cannot
# move into TableProTests the way pure-logic plugin tests do: the Dameng suites import the
# plugin's C bridge module (CDameng), and HanaDriverTests compiles the whole plugin and
# carries tablepro-hana-helper in its own Contents/MacOS, where the plugin looks for it.
# Without these steps nothing runs them and they rot silently.
- name: Build the Dameng driver tests
run: |
set -o pipefail
Expand All @@ -283,6 +317,19 @@ jobs:
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions

- name: Build the SAP HANA driver tests
run: |
set -o pipefail
xcodebuild build-for-testing \
-project "$XCODE_PROJECT" \
-scheme HanaDriverTests \
-destination "$TEST_DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath ~/.spm-cache \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions

# Tarred rather than uploaded as a directory. actions/upload-artifact does not preserve
# the executable bit or symlinks, and both matter to an .app bundle: the app would arrive
# unable to run. The archive measures 144 MB against 559 MB on disk.
Expand Down Expand Up @@ -356,6 +403,15 @@ jobs:
-parallel-testing-enabled NO \
| xcbeautify --renderer github-actions

- name: Run SAP HANA driver tests
run: |
set -o pipefail
xcodebuild test-without-building \
-xctestrun "$HANA_XCTESTRUN" \
-destination "$TEST_DESTINATION" \
-parallel-testing-enabled NO \
| xcbeautify --renderer github-actions

# Always, including on failure: a red run is when the durations matter most.
- name: Summarise test durations
if: ${{ !cancelled() }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Native/DamengBridge/**/target/
# slipped past it and was committed, pointing at one machine's absolute path. Every checkout
# elsewhere then got a dangling link and `mkdir -p` in build-dameng.sh failed on it.
Native/DamengBridge/lib
Native/HanaBridge/bin

# Issue analysis blueprints (local only)
.analysis/
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- SAP HANA database driver plugin. (#1966)

### Fixed

- Save disabled for Kafka connections set to Verify Identity without a CA file.

## [0.76.1] - 2026-09-29

### Changed
Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ TablePro is a native macOS database client (SwiftUI + AppKit), a fast, lightweig
- **Source**: `TablePro/` holds `Core/` (business logic, services), `Views/` (UI), `Models/` (data structures), `ViewModels/`, `Extensions/` and `Theme/`
- **Plugins**: `Plugins/` holds the `.tableplugin` bundles plus the `TableProPluginKit` shared framework.
- **Bundled in app** (the 17 targets in the app's `copy: { destination: plugins }` phase in `project.yml`): MySQL, PostgreSQL, SQLite, ClickHouse, Redis, CSV export, JSON export, SQL export, XLSX export, Markdown export, HTML export, XML export, MQL export, SQL import, JSON import, CSV import, XLSX import. These ship inside the app bundle and their updates normally ride with the next app release. Ten of them (`clickhouse`, `html`, `markdown`, `mql`, `redis`, `sqlimport`, `sqlite`, `xlsx`, `xlsximport`, `xml`) also have registry arms, marked `"bundled": true` in `.github/plugin-registry.json`, so a bundled plugin can be published when users on an already-shipped app need the fix sooner. `scripts/build-plugin.sh:10` explains the flag that makes that work.
- **Registry-only** (the other 23): MongoDB, Oracle, DuckDB, MSSQL, Cassandra, Etcd, CloudflareD1, CloudflareR2SQL, DynamoDB, BigQuery, Spanner, LibSQL, Snowflake, Elasticsearch, Typesense, Beancount, SurrealDB, Teradata, Trino, Dameng, Kafka, Weaviate, Parquet export. Parquet is registry-only because it links its own copy of DuckDB, which does the encoding, and that is too large to ship in the app for one format. Distributed via [TableProApp/plugins](https://github.com/TableProApp/plugins) `plugins.json`, installed into the user plugins directory.
- **Registry-only** (the other 24): MongoDB, Oracle, DuckDB, MSSQL, Cassandra, Etcd, CloudflareD1, CloudflareR2SQL, DynamoDB, BigQuery, Spanner, LibSQL, Snowflake, Elasticsearch, Typesense, Beancount, SurrealDB, Teradata, Trino, Dameng, SAP HANA, Kafka, Weaviate, Parquet export. Parquet is registry-only because it links its own copy of DuckDB, which does the encoding, and that is too large to ship in the app for one format. Distributed via [TableProApp/plugins](https://github.com/TableProApp/plugins) `plugins.json`, installed into the user plugins directory.
- **C bridges**: Each plugin contains its own C bridge module (e.g., `Plugins/MySQLDriverPlugin/CMariaDB/`, `Plugins/PostgreSQLDriverPlugin/CLibPQ/`)
- **Static libs**: `Libs/` holds pre-built `.a` files and `Libs/ios/` holds the iOS xcframeworks. Both are downloaded by `scripts/download-libs.sh` and are not in git.
- **SPM deps**: declared in `project.yml`. Every local package lives under `Packages/` (TableProCore, TableProOracle, TableProEditor, TableProGrammars); remote packages are Sparkle, swift-certificates and Yams. Revisions are pinned by the tracked `Package.resolved` inside each generated `.xcodeproj`.
Expand Down Expand Up @@ -104,7 +104,7 @@ git add Libs/ios/checksums.sha256 && git commit -m "build: update iOS xcframewor

Run `scripts/generate-project.sh` after editing any of those, and after adding, moving, or deleting a source file: XcodeGen globs sources at generation time, so a new file is not in the project until you regenerate. Changing signing in the Xcode UI is pointless, because the next generate discards it; set `TABLEPRO_DEVELOPMENT_TEAM` and `TABLEPRO_APP_BUNDLE_IDENTIFIER` in `Configs/Secrets.xcconfig` instead.

The 40 plugin bundles share one `DriverPlugin` target template; a plugin declares only its folder, principal class, and any C-library link flags. Every target gets a shared scheme named after it, which is what `scripts/build-plugin.sh <PluginTarget> [arm64|x86_64|both] [version]` builds. The `AllPlugins` aggregate target compile-checks all 40, including the registry-only ones the app does not embed, and PR CI runs it: the `Compile every plugin` step in the `app-tests` job of `.github/workflows/macos-tests.yml` builds that scheme whenever the change touches `Plugins/` or any other watched path. What PR CI still does not cover is plugin packaging, signing and notarization, which only `build-plugin.yml` does and only on a release tag.
The 41 plugin bundles share one `DriverPlugin` target template; a plugin declares only its folder, principal class, any C-library link flags, and any extra template such as `HanaHelperEmbedding`. Every target gets a shared scheme named after it, which is what `scripts/build-plugin.sh <PluginTarget> [arm64|x86_64|both] [version]` builds. The `AllPlugins` aggregate target compile-checks all 41, including the registry-only ones the app does not embed, and PR CI runs it: the `Compile every plugin` step in the `app-tests` job of `.github/workflows/macos-tests.yml` builds that scheme whenever the change touches `Plugins/` or any other watched path. What PR CI still does not cover is plugin packaging, signing and notarization, which only `build-plugin.yml` does and only on a release tag.

### Plugin System

Expand Down Expand Up @@ -148,6 +148,12 @@ When adding a new method to the driver protocol: add to `PluginDatabaseDriver` (

**A driver fix does not need an app release.** `scripts/release-plugin-for-shipped-app.sh <pluginTag> [appTag]` builds a plugin at a shipped app's tag and publishes it against that release's kit, which is the supported way to reach users who have not updated. Never relabel a binary built from `main` with an older `TableProPluginKitVersion`: the older app accepts the stamp and then fails `Bundle.loadAndReturnError`, which is #1917 and the 0.49.0 breakage.

### Native bridges

A driver whose protocol library is not C keeps its first-party code in `Native/<X>Bridge`, built by `scripts/build-<x>.sh`: `Native/DamengBridge` (Rust, `build-dameng.sh`) and `Native/HanaBridge` (Go, `build-hana.sh`). Output stays beside the source in a gitignored folder of the bridge, never in `Libs/`, which holds only prebuilt third-party C libraries that `download-libs.sh` checksum-verifies and `publish-libs.sh` guards. Upstream code comes in unmodified and pinned (`Cargo.lock` with `--locked`, `go.sum`), and so does the toolchain (Rust 1.91.1 in `rust-toolchain.toml`, Go 1.27.1 through the `toolchain` line of `go.mod` and `GOTOOLCHAIN` in `build-hana.sh`). A fork exists only when TablePro carries patches, as `TableProApp/rust-dameng` does: a protocol change lands on the fork, then the pinned `rev` moves (`Native/DamengBridge/README.md`).

**Rust may run in the app's process; Go may not.** The Dameng staticlib is built with `panic = "unwind"` and force-loaded into the plugin. The eight exports that do work, `tp_dm_connect`, `tp_dm_disconnect`, `tp_dm_execute`, `tp_dm_cancel`, and `tp_dm_begin`, `tp_dm_commit`, `tp_dm_rollback` and `tp_dm_ping` through `transaction_operation`, run their driver call inside `catch_unwind`, so a panic there comes back as an error. The argument checks before that call and the `client.close()` after a failed one run outside it. The other fourteen exports, the two frees and the error and result accessors, have no guard: they only read fields and index with `.get`, and must stay that way, so a new export that does work takes the guard. Go has no such guard, because `recover()` covers only its own goroutine, and go-hdb runs every query and ping on goroutines it starts itself, so one decode panic in a c-archive aborted the host (measured, SIGABRT, with the export's `recover()` in place). A c-archive also installs Go's SIGSEGV, SIGBUS, SIGPIPE and SIGURG handlers in the app, and a second Go plugin would put two Go runtimes in one process, which the Go team does not support (golang/go#65050, closed as not planned). So Go runs only out of process, as `tablepro-hana-helper`: a CGO-free executable in `HanaDriver.tableplugin/Contents/MacOS`, one per session and never shared, speaking length-prefixed JSON frames over stdin and stdout, where a panic costs that driver's session. That is not one per connection: `MetadataConnectionPool` opens drivers of its own, so one connection can run several helpers. The `HanaHelperEmbedding` template in `project.yml` copies it into the bundle and signs it in a `mktemp -d` directory first, because the user-script sandbox lets a phase write its declared output and nothing beside it, and codesign writes a temporary file beside what it signs and reads every parent directory. `scripts/build-plugin.sh` signs every nested Mach-O in `Contents/MacOS` and `Contents/Helpers` with Developer ID, the hardened runtime and a timestamp before the main binary, then checks all three with `codesign -dvvv`: `codesign --verify --deep --strict` accepts an ad-hoc signed helper, which notarization rejects.

### DatabaseType (String-Based Struct)

`DatabaseType` is a string-based struct (not an enum):
Expand Down
25 changes: 25 additions & 0 deletions Native/HanaBridge/THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Third-party notices

`scripts/build-hana.sh` builds this bridge with `CGO_ENABLED=0` into the `tablepro-hana-helper` executable, so
everything below is statically linked into it. The helper ships inside the SAP HANA plugin, in
`HanaDriver.tableplugin/Contents/MacOS`. The versions are the ones `go.mod` and `go.sum` pin.

| Component | Version | License | Copyright |
| --- | --- | --- | --- |
| [go-hdb](https://github.com/SAP/go-hdb) | v1.18.12 | Apache-2.0 | 2014-2026 SAP SE or an SAP affiliate company and go-hdb contributors |
| [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) | v0.42.0 | BSD-3-Clause | 2009 The Go Authors |
| [Go runtime and standard library](https://go.dev) | go1.27.1 | BSD-3-Clause | 2009 The Go Authors |

The Go standard library vendors golang.org/x/crypto, x/net, x/sys and x/text. They carry the same BSD-3-Clause notice
from The Go Authors as the standard library itself.

go-hdb has no NOTICE file. Its `REUSE.toml` adds that calls to the APIs of SAP products are not licensed under
Apache-2.0 and are governed by the user's own agreement with SAP.

The full license texts:

- go-hdb: <https://github.com/SAP/go-hdb/blob/v1.18.12/LICENSE.md>
- golang.org/x/text: <https://github.com/golang/text/blob/v0.42.0/LICENSE>
- Go: <https://github.com/golang/go/blob/go1.27.1/LICENSE>

The app's acknowledgements carry the same three texts in `TablePro/Resources/ThirdPartyLicenses/texts/`.
10 changes: 10 additions & 0 deletions Native/HanaBridge/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/TableProApp/TablePro/Native/HanaBridge

go 1.27.0

toolchain go1.27.1

require (
github.com/SAP/go-hdb v1.18.12
golang.org/x/text v0.42.0
)
4 changes: 4 additions & 0 deletions Native/HanaBridge/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/SAP/go-hdb v1.18.12 h1:nkM5nbIy3IHbWrsT1mIBdwj0lxJw45+0x8zKIR6Whfw=
github.com/SAP/go-hdb v1.18.12/go.mod h1:p9ia2k+4e36G46T1wti/PoqoRenX6P8OGg6vSIn87kQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
Loading
Loading