From 780a5367bff586e15bba68063c6442ce9d46f72a Mon Sep 17 00:00:00 2001 From: sophiathedev Date: Fri, 14 Aug 2026 18:53:52 +0700 Subject: [PATCH 1/4] feat(plugin): add Dameng DM8 support --- .github/workflows/build-plugin.yml | 5 + .gitignore | 4 + CHANGELOG.md | 2 + Native/DamengBridge/Cargo.lock | 1041 +++++++++++ Native/DamengBridge/Cargo.toml | 19 + Native/DamengBridge/LICENSES/rust-dameng.txt | 21 + Native/DamengBridge/Vendor/UPSTREAM.md | 12 + .../Vendor/dameng-protocol/Cargo.toml | 15 + .../Vendor/dameng-protocol/LICENSE.txt | 21 + .../Vendor/dameng-protocol/src/error.rs | 60 + .../Vendor/dameng-protocol/src/frame.rs | 227 +++ .../Vendor/dameng-protocol/src/lib.rs | 13 + .../Vendor/dameng-protocol/src/message.rs | 178 ++ .../dameng-protocol/src/message/bind.rs | 579 ++++++ .../dameng-protocol/src/message/close.rs | 41 + .../dameng-protocol/src/message/exec.rs | 437 +++++ .../dameng-protocol/src/message/explain.rs | 76 + .../dameng-protocol/src/message/fetch.rs | 323 ++++ .../dameng-protocol/src/message/isolation.rs | 180 ++ .../Vendor/dameng-protocol/src/message/lob.rs | 575 ++++++ .../dameng-protocol/src/message/lob_bind.rs | 260 +++ .../dameng-protocol/src/message/login.rs | 306 +++ .../dameng-protocol/src/message/ready.rs | 145 ++ .../dameng-protocol/src/message/response.rs | 1244 +++++++++++++ .../dameng-protocol/src/message/startup.rs | 276 +++ .../src/message/transaction.rs | 74 + .../Vendor/dameng-types/Cargo.toml | 15 + .../Vendor/dameng-types/LICENSE.txt | 21 + .../Vendor/dameng-types/src/encoding.rs | 169 ++ .../Vendor/dameng-types/src/lib.rs | 1469 +++++++++++++++ Native/DamengBridge/Vendor/dameng/Cargo.toml | 19 + Native/DamengBridge/Vendor/dameng/LICENSE.txt | 21 + .../DamengBridge/Vendor/dameng/src/client.rs | 1655 +++++++++++++++++ .../DamengBridge/Vendor/dameng/src/config.rs | 362 ++++ .../DamengBridge/Vendor/dameng/src/error.rs | 72 + Native/DamengBridge/Vendor/dameng/src/lib.rs | 28 + Native/DamengBridge/Vendor/dameng/src/row.rs | 876 +++++++++ .../Vendor/dameng/src/transaction.rs | 83 + Native/DamengBridge/rust-toolchain.toml | 4 + Native/DamengBridge/src/lib.rs | 764 ++++++++ .../TableProCoreTypes/DatabaseType.swift | 4 +- .../DatabaseTypeTests.swift | 4 +- Plugins/DamengDriverPlugin/CDameng/CDameng.h | 80 + .../CDameng/module.modulemap | 4 + .../DamengDriverPlugin/DamengConnection.swift | 218 +++ Plugins/DamengDriverPlugin/DamengError.swift | 8 + .../DamengParameterBinder.swift | 146 ++ Plugins/DamengDriverPlugin/DamengPlugin.swift | 256 +++ .../DamengPluginDriver+Editing.swift | 329 ++++ .../DamengPluginDriver+Schema.swift | 343 ++++ .../DamengStatementClassifier.swift | 169 ++ Plugins/DamengDriverPlugin/Info.plist | 12 + Plugins/DamengDriverPlugin/README.md | 43 + .../DamengPluginDriverTests.swift | 385 ++++ README.md | 1 + README.vi.md | 1 + README.zh.md | 1 + ...ginMetadataRegistry+RegistryDefaults.swift | 57 +- ...MetadataRegistry+RegistryIngredients.swift | 73 +- .../Core/Services/Query/QueryPlanParser.swift | 109 ++ TablePro/Info.plist | 1 + .../Connection/DatabaseConnection.swift | 2 + .../Plugins/ContainerEntityNameTests.swift | 8 + .../Plugins/DriverPluginMetadataTests.swift | 1 + ...MetadataRegistrySchemaSwitchingTests.swift | 27 +- .../Query/DamengPlanParserTests.swift | 39 + TableProTests/Models/DatabaseTypeTests.swift | 1 + .../Plugins/DamengParameterBinderTests.swift | 58 + .../DamengStatementClassifierTests.swift | 60 + docs/databases/connection-urls.mdx | 3 +- docs/databases/dameng.mdx | 50 + docs/databases/overview.mdx | 5 +- docs/development/architecture.mdx | 4 +- docs/development/plugin-registry.mdx | 1 + docs/docs.json | 3 +- docs/features/plugins.mdx | 3 +- docs/index.mdx | 5 +- project.yml | 83 + scripts/build-dameng.sh | 61 + 79 files changed, 14337 insertions(+), 13 deletions(-) create mode 100644 Native/DamengBridge/Cargo.lock create mode 100644 Native/DamengBridge/Cargo.toml create mode 100644 Native/DamengBridge/LICENSES/rust-dameng.txt create mode 100644 Native/DamengBridge/Vendor/UPSTREAM.md create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/Cargo.toml create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/LICENSE.txt create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/error.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/frame.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/lib.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/bind.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/close.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/exec.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/explain.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/fetch.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/isolation.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/lob.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/lob_bind.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/login.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/ready.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/response.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/startup.rs create mode 100644 Native/DamengBridge/Vendor/dameng-protocol/src/message/transaction.rs create mode 100644 Native/DamengBridge/Vendor/dameng-types/Cargo.toml create mode 100644 Native/DamengBridge/Vendor/dameng-types/LICENSE.txt create mode 100644 Native/DamengBridge/Vendor/dameng-types/src/encoding.rs create mode 100644 Native/DamengBridge/Vendor/dameng-types/src/lib.rs create mode 100644 Native/DamengBridge/Vendor/dameng/Cargo.toml create mode 100644 Native/DamengBridge/Vendor/dameng/LICENSE.txt create mode 100644 Native/DamengBridge/Vendor/dameng/src/client.rs create mode 100644 Native/DamengBridge/Vendor/dameng/src/config.rs create mode 100644 Native/DamengBridge/Vendor/dameng/src/error.rs create mode 100644 Native/DamengBridge/Vendor/dameng/src/lib.rs create mode 100644 Native/DamengBridge/Vendor/dameng/src/row.rs create mode 100644 Native/DamengBridge/Vendor/dameng/src/transaction.rs create mode 100644 Native/DamengBridge/rust-toolchain.toml create mode 100644 Native/DamengBridge/src/lib.rs create mode 100644 Plugins/DamengDriverPlugin/CDameng/CDameng.h create mode 100644 Plugins/DamengDriverPlugin/CDameng/module.modulemap create mode 100644 Plugins/DamengDriverPlugin/DamengConnection.swift create mode 100644 Plugins/DamengDriverPlugin/DamengError.swift create mode 100644 Plugins/DamengDriverPlugin/DamengParameterBinder.swift create mode 100644 Plugins/DamengDriverPlugin/DamengPlugin.swift create mode 100644 Plugins/DamengDriverPlugin/DamengPluginDriver+Editing.swift create mode 100644 Plugins/DamengDriverPlugin/DamengPluginDriver+Schema.swift create mode 100644 Plugins/DamengDriverPlugin/DamengStatementClassifier.swift create mode 100644 Plugins/DamengDriverPlugin/Info.plist create mode 100644 Plugins/DamengDriverPlugin/README.md create mode 100644 Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift create mode 100644 TableProTests/Core/Services/Query/DamengPlanParserTests.swift create mode 100644 TableProTests/Plugins/DamengParameterBinderTests.swift create mode 100644 TableProTests/Plugins/DamengStatementClassifierTests.swift create mode 100644 docs/databases/dameng.mdx create mode 100755 scripts/build-dameng.sh diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index 82ab98e47..9521018f8 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -148,6 +148,11 @@ jobs: DISPLAY_NAME="ClickHouse Driver"; SUMMARY="ClickHouse OLAP database driver via HTTP interface" DB_TYPE_IDS='["ClickHouse"]'; ICON="chart.bar.xaxis"; BUNDLE_NAME="ClickHouseDriver" CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/clickhouse" ;; + dameng) + TARGET="DamengDriver"; BUNDLE_ID="com.TablePro.DamengDriver" + DISPLAY_NAME="Dameng Driver"; SUMMARY="Dameng DM8 database driver via a native wire client" + DB_TYPE_IDS='["Dameng"]'; ICON="cylinder"; BUNDLE_NAME="DamengDriver" + CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/dameng" ;; sqlite) TARGET="SQLiteDriver"; BUNDLE_ID="com.TablePro.SQLiteDriver" DISPLAY_NAME="SQLite Driver"; SUMMARY="SQLite embedded database driver" diff --git a/.gitignore b/.gitignore index 4b091e12f..6a87fa7f2 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,10 @@ Libs/.downloaded Libs/dylibs/ Libs/ios/ +# Rust driver build outputs (keep the bridge lockfile, ignore vendored crate locks) +Native/DamengBridge/**/target/ +Native/DamengBridge/Vendor/*/Cargo.lock + # Issue analysis blueprints (local only) .analysis/ .docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac0b3477..9a0382425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Dameng DM8 connections through a downloadable native-wire plugin, with schema browsing, table editing, metadata, DDL, transactions, Unicode and binary writes, and EXPLAIN support. (#1671, #2003, #2010) + - PostgreSQL array columns of a simple type, including arrays of an enum, get a list editor in the data grid. One row per element, with reordering, add and remove, and NULL per element. An empty array and a NULL column stay separate values. Enum arrays pick from the labels the type declares. Arrays of `jsonb`, `bytea` or composite types, and multi-dimensional values, keep the plain text editor. ### Fixed diff --git a/Native/DamengBridge/Cargo.lock b/Native/DamengBridge/Cargo.lock new file mode 100644 index 000000000..29a7ad50a --- /dev/null +++ b/Native/DamengBridge/Cargo.lock @@ -0,0 +1,1041 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dameng" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "dameng-protocol", + "dameng-types", + "encoding_rs", + "native-tls", + "rust_decimal", +] + +[[package]] +name = "dameng-protocol" +version = "0.1.0" +dependencies = [ + "bytes", + "dameng-types", +] + +[[package]] +name = "dameng-types" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "encoding_rs", + "rust_decimal", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tablepro-dameng-bridge" +version = "0.1.0" +dependencies = [ + "dameng", + "dameng-types", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Native/DamengBridge/Cargo.toml b/Native/DamengBridge/Cargo.toml new file mode 100644 index 000000000..efaba9751 --- /dev/null +++ b/Native/DamengBridge/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tablepro-dameng-bridge" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0-only" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +dameng = { path = "Vendor/dameng" } +dameng-types = { path = "Vendor/dameng-types" } + +[profile.release] +codegen-units = 1 +lto = false +opt-level = "s" +panic = "unwind" +strip = "symbols" diff --git a/Native/DamengBridge/LICENSES/rust-dameng.txt b/Native/DamengBridge/LICENSES/rust-dameng.txt new file mode 100644 index 000000000..81121d744 --- /dev/null +++ b/Native/DamengBridge/LICENSES/rust-dameng.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 指令集 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Native/DamengBridge/Vendor/UPSTREAM.md b/Native/DamengBridge/Vendor/UPSTREAM.md new file mode 100644 index 000000000..9bdace4ec --- /dev/null +++ b/Native/DamengBridge/Vendor/UPSTREAM.md @@ -0,0 +1,12 @@ +# Vendored rust-dameng Snapshot + +The `dameng`, `dameng-protocol`, and `dameng-types` directories originate from +[`rarnu/rust-dameng`](https://github.com/rarnu/rust-dameng) commit +`c5120eb04abbe232ccc04e19d16093fbe6e0da0e` and are distributed under the MIT +license included in each directory. + +TablePro carries compatibility changes for DM8 multi-column responses, binary +DECIMAL values, text EXPLAIN responses, bounded frame allocation, and exact +message-boundary reads. Response bodies and LOB content are capped at 64 MiB. +Review and retest these changes against an OrbStack DM8 instance whenever the +upstream snapshot changes. diff --git a/Native/DamengBridge/Vendor/dameng-protocol/Cargo.toml b/Native/DamengBridge/Vendor/dameng-protocol/Cargo.toml new file mode 100644 index 000000000..5f1700808 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dameng-protocol" +version = "0.1.0" +edition = "2021" +description = "Dameng database wire protocol implementation" +license = "MIT" +repository = "https://github.com/rarnu/rust-dameng" +keywords = ["dameng", "database", "protocol"] +categories = ["database"] + +[dependencies] +bytes = "1" +dameng-types = { path = "../dameng-types" } + +[dev-dependencies] diff --git a/Native/DamengBridge/Vendor/dameng-protocol/LICENSE.txt b/Native/DamengBridge/Vendor/dameng-protocol/LICENSE.txt new file mode 100644 index 000000000..81121d744 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 指令集 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/error.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/error.rs new file mode 100644 index 000000000..f16660c79 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/error.rs @@ -0,0 +1,60 @@ +//! Error types for the Dameng protocol. + +use std::fmt; + +/// Errors that can occur when parsing or building Dameng protocol messages. +#[derive(Debug)] +pub enum Error { + /// Not enough bytes to parse a frame or message. + Incomplete, + /// Checksum mismatch in frame header. + ChecksumMismatch, + /// Invalid frame header (bad length, unknown version, etc.). + InvalidFrame(String), + /// Unknown message type. + UnknownMessageType(u8), + /// Failed to decode a string value. + DecodeError(String), + /// I/O error during read/write. + Io(std::io::Error), + /// Authentication failed. + AuthFailed(String), + /// Server returned an error message. + ServerError(i32, String), + /// LOB streaming operation failed (e.g. stream ended prematurely or invalid chunk). + LobStreamError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Incomplete => write!(f, "incomplete protocol data"), + Error::ChecksumMismatch => write!(f, "checksum mismatch"), + Error::InvalidFrame(s) => write!(f, "invalid frame: {s}"), + Error::UnknownMessageType(t) => write!(f, "unknown message type: {t}"), + Error::DecodeError(s) => write!(f, "decode error: {s}"), + Error::Io(e) => write!(f, "IO error: {e}"), + Error::AuthFailed(s) => write!(f, "auth failed: {s}"), + Error::ServerError(code, msg) => write!(f, "server error {code}: {msg}"), + Error::LobStreamError(s) => write!(f, "LOB stream error: {s}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +/// Result type alias for protocol operations. +pub type Result = std::result::Result; diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/frame.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/frame.rs new file mode 100644 index 000000000..3219d119f --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/frame.rs @@ -0,0 +1,227 @@ +//! 64-byte frame header for Dameng protocol messages. +//! +//! Layout (all multi-byte values are LITTLE ENDIAN): +//! ```text +//! Offset Size Field +//! 0 4 Handle (i32 LE) +//! 4 1 MsgType (u8) +//! 5 1 Reserved (0) +//! 6 4 BodyLen (i32 LE) - length of payload after header +//! 10 4 ResponseCode (i32 LE) - filled by server +//! 14 4 AffectedRows (i32 LE) - rows affected (for DML responses) +//! 18 1 CompressFlag (u8) +//! 19 1 Checksum (u8) - XOR of bytes 0-18 +//! 20 44 Reserved (zeros) +//! 64 var Payload body +//! ``` + +use bytes::{Buf, BufMut, BytesMut}; + +use crate::error::{Error, Result}; + +/// The size of the frame header in bytes. +pub const FRAME_HEADER_SIZE: usize = 64; + +/// DM protocol frame header (64 bytes). +#[derive(Debug, Clone, PartialEq)] +pub struct Frame { + /// Statement/connection handle. + pub handle: u32, + /// Message type identifier. + pub msg_type: u8, + /// Length of the payload following this header. + pub body_len: i32, + /// Response code from server (0 for client messages). + pub response_code: i32, + /// Number of rows affected (for DML responses like INSERT/UPDATE/DELETE). + /// Set by the server in ACK/EXEC_RESPONSE frames; 0 for non-DML. + pub affected_rows: i32, + /// Compression flag (0=none, 1=snappy, 2=zlib). + pub compress_flag: u8, + /// Update count for DML operations, stored in the reserved area + /// at header offset 24 (int64 LE). Always 0 for non-DML. + pub update_count: u64, + /// Server encoding from header offset 28 (int32 LE). + /// Used in STARTUP_RESPONSE. 0=GB18030, 1=UTF-8, 2=EUC-KR. + pub server_encoding: u8, +} + +impl Frame { + /// Create a new frame header for client messages. + pub fn new(msg_type: u8, handle: u32, body_len: i32) -> Self { + Self { + handle, + msg_type, + body_len, + response_code: 0, + affected_rows: 0, + compress_flag: 0, + update_count: 0, + server_encoding: 0, + } + } + + /// Parse a frame header from a buffer. + /// + /// Returns `Err(Error::Incomplete)` if fewer than 64 bytes are available. + /// Parse a frame header from a buffer. + /// + /// Returns `Err(Error::Incomplete)` if fewer than 64 bytes are available. + pub fn parse(buf: &mut BytesMut) -> Result { + if buf.len() < FRAME_HEADER_SIZE { + return Err(Error::Incomplete); + } + + // Compute XOR checksum of bytes 0-18 BEFORE consuming + let mut calc_xor: u8 = 0; + for i in 0..19 { + calc_xor ^= buf[i]; + } + + let handle = buf.get_u32_le(); + let msg_type = buf.get_u8(); + let _reserved = buf.get_u8(); + let body_len = buf.get_i32_le(); + let response_code = buf.get_i32_le(); + let affected_rows = buf.get_i32_le(); + let compress_flag = buf.get_u8(); + let checksum = buf.get_u8(); + + if calc_xor != checksum { + return Err(Error::ChecksumMismatch); + } + + // Parse update_count from the reserved area at header offset 24 (int64 LE). + // Need to read from the raw buffer before advancing. buf currently has + // cursor at byte 20 (after consuming 20 bytes). + // Bytes [4..12] of the remaining 44-byte reserved area = absolute offset 24. + let update_count = if buf.remaining() >= 12 { + let raw = &buf.chunk()[4..12]; // offset 24-31 in absolute header + u64::from_le_bytes([ + raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7], + ]) + } else { + 0 + }; + // Server encoding at header offset 28 (int32 LE) + let server_encoding = if buf.remaining() >= 12 { + let raw = &buf.chunk()[8..12]; // offset 28-31 in absolute header + u8::from_le_bytes([raw[0]]) + } else { + 0 + }; + + // Skip remaining 44 bytes of reserved + buf.advance(44); + + Ok(Frame { + handle, + msg_type, + body_len, + response_code, + affected_rows, + compress_flag, + update_count, + server_encoding, + }) + } + + /// Encode this frame header into a `BytesMut` buffer. + pub fn encode(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE); + + buf.put_u32_le(self.handle); + buf.put_u8(self.msg_type); + buf.put_u8(0); // reserved + buf.put_i32_le(self.body_len); + buf.put_i32_le(self.response_code); + buf.put_i32_le(0); // reserved + buf.put_u8(self.compress_flag); + buf.put_u8(0); // checksum placeholder + + // Compute and write checksum at offset 19 + let mut cs: u8 = 0; + for i in 0..19 { + cs ^= buf[i]; + } + buf[19] = cs; + + // Fill remaining 44 bytes with zeros (20-63) + buf.put_bytes(0, 44); + + debug_assert_eq!(buf.len(), FRAME_HEADER_SIZE); + buf + } + + /// Encode frame header + payload into a single BytesMut. + pub fn encode_with_payload(&self, payload: &[u8]) -> BytesMut { + let mut buf = self.encode(); + buf.extend_from_slice(payload); + buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_frame_new_and_fields() { + let frame = Frame::new(200, 0, 82); + assert_eq!(frame.msg_type, 200); + assert_eq!(frame.handle, 0); + assert_eq!(frame.body_len, 82); + } + + #[test] + fn test_frame_encode_size() { + let frame = Frame::new(1, 0, 59); + let encoded = frame.encode(); + assert_eq!(encoded.len(), FRAME_HEADER_SIZE); + } + + #[test] + fn test_frame_roundtrip() { + let original = Frame::new(6, 42, 128); + let mut encoded = original.encode(); + let parsed = Frame::parse(&mut encoded).unwrap(); + assert_eq!(parsed.msg_type, 6); + assert_eq!(parsed.handle, 42); + assert_eq!(parsed.body_len, 128); + } + + #[test] + fn test_frame_parse_incomplete() { + let mut buf = BytesMut::from(&[0u8; 32][..]); + let result = Frame::parse(&mut buf); + assert!(matches!(result, Err(Error::Incomplete))); + } + + #[test] + fn test_frame_encode_fields() { + let frame = Frame::new(8, 7, 255); + let encoded = frame.encode(); + let bytes = encoded.freeze(); + + assert_eq!( + i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + 7 + ); + assert_eq!(bytes[4], 8); + assert_eq!(bytes[5], 0); + assert_eq!( + i32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]), + 255 + ); + assert_eq!(bytes[18], 0); + } + + #[test] + fn test_frame_parse_fields() { + let mut encoded = Frame::new(13, 3, 100).encode(); + let frame = Frame::parse(&mut encoded).unwrap(); + assert_eq!(frame.msg_type, 13); + assert_eq!(frame.handle, 3); + assert_eq!(frame.body_len, 100); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/lib.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/lib.rs new file mode 100644 index 000000000..f3c942a7b --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/lib.rs @@ -0,0 +1,13 @@ +//! Dameng database wire protocol implementation. +//! +//! This crate implements the binary wire protocol used to communicate +//! with Dameng database servers, based on reverse-engineered protocol captures. + +pub mod error; +pub mod frame; +pub mod message; + +pub use error::{Error, Result}; +pub use frame::Frame; +pub use message::explain::ExplainResponse; +pub use message::response::{Column, ExecResponse, Row}; diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message.rs new file mode 100644 index 000000000..dfd0f745b --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message.rs @@ -0,0 +1,178 @@ +//! Dameng protocol message types. +//! +//! This module defines all message types used in the DM wire protocol. +//! Messages are organized by their direction (client->server or server->client) +//! and their purpose in the connection lifecycle. + +pub mod bind; +pub mod close; +pub mod exec; +pub mod explain; +pub mod fetch; +pub mod isolation; +pub mod lob; +pub mod lob_bind; +pub mod login; +pub mod ready; +pub mod response; +pub mod startup; +pub mod transaction; + +pub use bind::*; +pub use close::*; +pub use exec::*; +pub use explain::*; +pub use fetch::*; +pub use isolation::*; +pub use lob::*; +pub use lob_bind::*; +pub use login::*; +pub use ready::*; +pub use response::*; +pub use startup::*; +pub use transaction::*; + +// Re-export msg_type constants at top level for convenience +pub use self::msg_type::*; + +use bytes::{BufMut, BytesMut}; + +use crate::frame::Frame; + +/// Message type constants. +pub mod msg_type { + /// STARTUP - Initial connection handshake (client->server) + pub const STARTUP: u8 = 200; + /// STARTUP_RESPONSE - Server hello (server->client) + pub const STARTUP_RESPONSE: u8 = 228; + /// LOGIN - Send credentials (client->server) + pub const LOGIN: u8 = 1; + /// LOGIN_RESPONSE - Authentication result (server->client) + pub const LOGIN_RESPONSE: u8 = 163; + /// STATEMENT_PREPARE - Allocate a statement handle (client->server). + /// Previously named READY; kept for backward compatibility. + pub const STATEMENT_PREPARE: u8 = 3; + /// READY - Alias for STATEMENT_PREPARE (type 3). Send ready/keepalive (client->server). + pub const READY: u8 = 3; + /// STATEMENT_FREE - Free a statement handle (client->server). + pub const STATEMENT_FREE: u8 = 4; + /// ACK - Success/generic response (server->client) + pub const ACK: u8 = 187; + /// PREPARE/EXEC - Prepare and execute statement (client->server) + pub const EXEC: u8 = 5; + /// EXEC_RESPONSE - Statement result (server->client) + pub const EXEC_RESPONSE: u8 = 0; + /// EXPLAIN_RESPONSE - Text query plan (server->client) + pub const EXPLAIN_RESPONSE: u8 = 149; + /// FETCH - Fetch more rows from result set (client->server) + pub const FETCH: u8 = 7; + /// COMMIT - Commit transaction (client->server) + pub const COMMIT: u8 = 8; + /// ROLLBACK - Rollback transaction (client->server) + pub const ROLLBACK: u8 = 9; + /// BIND - Legacy bind parameters and execute (client->server) + pub const BIND: u8 = 13; + /// BIND_EXEC2 - Bind parameters and execute with EXEC2 protocol (client->server) + pub const BIND_EXEC2: u8 = 90; + /// CLOSE - Close statement (client->server) + pub const CLOSE: u8 = 20; + /// STATEMENT_SET_CURSOR - Set cursor position for a statement (client->server) + pub const STATEMENT_SET_CURSOR: u8 = 27; + /// FETCH_RESULT_SET - Fetch an entire result set (client->server) + pub const FETCH_RESULT_SET: u8 = 44; + /// SET_ISOLATION - Set transaction isolation level (client->server) + pub const SET_ISOLATION: u8 = 52; + /// OPTIMIZED_PREPARE_EXEC - Optimized prepare-and-execute path (client->server) + pub const OPTIMIZED_PREPARE_EXEC: u8 = 91; + /// LOB_FREE - Free a LOB locator (client->server) + pub const LOB_FREE: u8 = 29; + /// LOB_GETLEN - Get LOB length (client->server) + pub const LOB_GETLEN: u8 = 31; + /// LOB_READ - Read LOB data chunk (client->server) + pub const LOB_READ: u8 = 32; +} + +/// DM data type codes. +pub mod dm_type { + pub const BIT: i32 = 1; + pub const TINYINT: i32 = 2; + pub const VARCHAR: i32 = 3; + pub const INT: i32 = 4; + pub const BIGINT: i32 = 5; + pub const SMALLINT: i32 = 6; + pub const FLOAT: i32 = 7; + pub const DOUBLE: i32 = 8; + pub const DECIMAL: i32 = 9; + pub const DATE: i32 = 10; + pub const TIME: i32 = 11; + pub const TIMESTAMP: i32 = 12; + pub const BLOB: i32 = 13; + pub const CLOB: i32 = 14; + pub const INTERVAL: i32 = 15; + pub const CHAR: i32 = 16; + pub const BINARY: i32 = 17; + pub const VARBINARY: i32 = 18; + pub const NUMERIC: i32 = 20; + pub const BOOLEAN: i32 = 21; + pub const DATETIME: i32 = 22; + pub const VARCHAR2: i32 = 23; + pub const DATETIME2: i32 = 24; + pub const TIME_TZ: i32 = 25; + pub const DATETIME_TZ: i32 = 26; + pub const INTERVAL_YM: i32 = 27; + pub const INTERVAL_DT: i32 = 28; + pub const RAW: i32 = 29; + pub const DATETIME2_TZ: i32 = 30; + pub const REAL: i32 = 31; +} + +/// DM encoding values. +pub mod encoding { + pub const UTF8: u8 = 1; + pub const GB18030: u8 = 2; +} + +/// Language ID values. +pub mod language { + pub const EN: u16 = 1; + pub const CN: u16 = 2; +} + +/// Encryption utility functions. +/// DM uses a simple XOR encryption for credentials. +pub mod crypto { + + /// Generate encrypted credentials using the server's challenge. + /// The algorithm XORs the plaintext with the challenge bytes, cycling through. + pub fn encrypt_with_challenge(plaintext: &[u8], challenge: &[u8], output: &mut [u8]) { + let challenge_len = challenge.len(); + if challenge_len == 0 { + output.copy_from_slice(plaintext); + return; + } + for (i, (out, &plain)) in output.iter_mut().zip(plaintext.iter()).enumerate() { + *out = plain ^ challenge[i % challenge_len]; + } + } + + /// Build the startup message encrypted random key from server challenge. + pub fn build_startup_key(challenge: &[u8]) -> [u8; 64] { + let mut key = [0u8; 64]; + // Generate a simple key pattern based on challenge + let challenge_len = challenge.len(); + if challenge_len > 0 { + for i in 0..64 { + key[i] = challenge[i % challenge_len] ^ (i as u8); + } + } + key + } +} + +/// Build a complete message (frame + payload) and return it as BytesMut. +pub fn build_message(msg_type: u8, handle: u32, payload: &[u8]) -> BytesMut { + let frame = Frame::new(msg_type, handle, payload.len() as i32); + let mut result = frame.encode(); + result.put_slice(payload); + result +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/bind.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/bind.rs new file mode 100644 index 000000000..3b25dec73 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/bind.rs @@ -0,0 +1,579 @@ +//! BIND message (type 13) for binding parameters to prepared statements. +//! +//! Also includes statement handle management messages: +//! - StatementAllocate (type 3): allocate a new statement handle +//! - StatementFree (type 4): free a statement handle + +use bytes::{BufMut, BytesMut}; + +use crate::error::Result; + +/// Parameter direction for binding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParameterDirection { + /// Input parameter (default). + Input = 1, + /// Output parameter. + Output = 2, + /// Input/Output parameter. + InputOutput = 3, +} + +/// A single parameter to bind. +#[derive(Debug, Clone)] +pub struct BindParam { + /// SQL type name (e.g., "INT", "VARCHAR"). + pub type_name: String, + /// DM type code. + pub type_code: i32, + /// Precision for numeric types. + pub precision: i32, + /// Scale for numeric types. + pub scale: i32, + /// Parameter direction. + pub direction: ParameterDirection, + /// The parameter value as bytes (None = NULL). + pub value: Option>, +} + +/// Client->Server BIND message (type 13). +/// +/// Legacy format — kept for backward compatibility. +#[derive(Debug, Clone)] +pub struct BindMessage { + /// Whether to fetch results after binding. + pub fetch_flag: u8, + /// Parameters to bind. + pub params: Vec, +} + +impl BindMessage { + /// Create a new bind message. + pub fn new(fetch: bool, params: Vec) -> Self { + Self { + fetch_flag: if fetch { 1 } else { 0 }, + params, + } + } + + /// Encode to payload bytes (legacy format). + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + buf.put_u8(self.fetch_flag); + buf.put_u8(0); // reserved + buf.put_u16_le(0); // reserved + buf.put_u16_le(self.params.len() as u16); + buf.put_u16_le(0); // reserved + buf.put_u32_le(0); // reserved + buf.put_u32_le(0); // reserved + buf.put_u32_le(0); // reserved + buf.put_u32_le(0); // reserved + buf.put_u32_le(0); // reserved + + for param in &self.params { + // Type name + let tn = param.type_name.as_bytes(); + buf.put_u16_le(tn.len() as u16); + buf.put_slice(tn); + + // Type code, precision, scale + buf.put_u32_le(param.type_code as u32); + buf.put_u32_le(param.precision as u32); + buf.put_u16_le(param.scale as u16); + + // Value + if let Some(ref val) = param.value { + buf.put_u16_le(val.len() as u16); + buf.put_u16_le(0); // reserved + buf.put_slice(val); + } else { + buf.put_u16_le(0xFFFF); // NULL marker + } + } + + buf + } +} + +/// Client->Server BIND_EXEC2 message (type 13). +/// +/// Matches the Go driver's BIND_EXEC2 format exactly. +/// Used to execute a prepared statement with bound parameters. +/// +/// Wire format: +/// ```text +/// Offset Size Field +/// 0 1 auto_commit (0 or 1) +/// 1 2 param_count (u16 LE) +/// 3 1 has_result_set (1=SELECT, 0=DML) +/// 4 8 offset (i64 LE) - pagination start +/// 12 8 cursor_update_row (i64 LE) +/// 20 8 max_rows (i64 LE) - 0=unlimited +/// 28 1 flags +/// 29 3 reserved +/// 32 4 query_timeout (i32 LE) - 0=default +/// 36 4 batch_allow_max_errors (i32 LE) +/// 40 1 innerExec +/// 41 1 bind_options (MsgVersion >= 8) +/// 42 N Parameter descriptors + values +/// ``` +#[derive(Debug, Clone)] +pub struct BindExec2Message { + /// Auto-commit mode (true = commit after execution). + pub auto_commit: bool, + /// Whether this query returns a result set (SELECT vs DML). + pub has_result_set: bool, + /// Pagination offset (0 = start from beginning). + pub offset: i64, + /// Maximum rows to return (0 = unlimited). + pub max_rows: i64, + /// Query timeout in seconds (0 = default). + pub query_timeout: i32, + /// Parameters to bind. + pub params: Vec, +} + +impl BindExec2Message { + /// Create a new BIND_EXEC2 message. + pub fn new(auto_commit: bool, has_result_set: bool, params: Vec) -> Self { + Self { + auto_commit, + has_result_set, + offset: 0, + max_rows: 0, + query_timeout: 0, + params, + } + } + + /// Create with pagination. + pub fn with_pagination( + auto_commit: bool, + has_result_set: bool, + offset: i64, + max_rows: i64, + params: Vec, + ) -> Self { + Self { + auto_commit, + has_result_set, + offset, + max_rows, + query_timeout: 0, + params, + } + } + + /// Encode to payload bytes. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + // Header + buf.put_u8(if self.auto_commit { 1 } else { 0 }); + buf.put_u16_le(self.params.len() as u16); + buf.put_u8(if self.has_result_set { 1 } else { 0 }); + + // Pagination and execution options + buf.put_i64_le(self.offset); // offset + buf.put_i64_le(0); // cursor_update_row + buf.put_i64_le(self.max_rows); // max_rows + buf.put_u8(0); // flags + buf.put_u8(0); // reserved + buf.put_u8(0); // reserved + buf.put_u8(0); // reserved + buf.put_i32_le(self.query_timeout); // query_timeout + buf.put_i32_le(0); // batch_allow_max_errors + buf.put_u8(0); // innerExec + buf.put_u8(0); // bind_options + + // Parameter descriptors + for param in &self.params { + buf.put_u8(param.direction as u8); // ioType + buf.put_i32_le(param.type_code); // colType + buf.put_i32_le(param.precision); // prec + buf.put_i32_le(param.scale); // scale + } + + // Parameter values + for param in &self.params { + match ¶m.value { + None => { + // NULL marker: -1 as u16 + buf.put_u16_le(0xFFFF); + } + Some(val) if val.len() > 0xFFFF => { + // Large data marker: -2 (0xFFFE) + 4-byte length + data + buf.put_u16_le(0xFFFE); + buf.put_u32_le(val.len() as u32); + buf.put_slice(val); + } + Some(val) => { + // Regular value: length + data + buf.put_u16_le(val.len() as u16); + buf.put_slice(val); + } + } + } + + buf + } +} + +/// Client->Server STATEMENT_PREPARE message (type 3). +/// +/// Allocates a new statement handle from the server. +/// Payload: 1 byte (readBaseColName flag, 1 = include base column names). +/// +/// Response: statement ID at offset 0 of the response payload (u32 LE). +#[derive(Debug, Clone)] +pub struct StatementAllocateMessage { + /// Whether to include base column names in responses. + pub read_base_col_name: bool, +} + +impl StatementAllocateMessage { + /// Create with default settings. + pub fn new() -> Self { + Self { + read_base_col_name: true, + } + } + + /// Encode to payload bytes. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + buf.put_u8(if self.read_base_col_name { 1 } else { 0 }); + buf + } + + /// Parse the statement ID from the response payload. + pub fn parse_response(payload: &[u8]) -> Result { + if payload.len() < 4 { + return Err(crate::error::Error::Incomplete); + } + let stmt_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + Ok(stmt_id) + } +} + +/// Client->Server STATEMENT_FREE message (type 4). +/// +/// Frees a previously allocated statement handle. +/// Payload: statement ID as u32 LE. +#[derive(Debug, Clone)] +pub struct StatementFreeMessage { + /// Statement handle to free. + pub stmt_id: u32, +} + +impl StatementFreeMessage { + /// Create to free the given statement. + pub fn new(stmt_id: u32) -> Self { + Self { stmt_id } + } + + /// Encode to payload bytes. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + buf.put_u32_le(self.stmt_id); + buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // --- BindMessage tests --- + + #[test] + fn test_bind_new() { + let bind = BindMessage::new(true, vec![]); + assert_eq!(bind.fetch_flag, 1); + assert!(bind.params.is_empty()); + } + + #[test] + fn test_bind_encode_with_param() { + let params = vec![BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(vec![0xC8, 0x03, 0x00, 0x00]), + }]; + let bind = BindMessage::new(true, params); + let payload = bind.encode_payload(); + assert!(payload.len() > 30); + let param_count = u16::from_le_bytes([payload[4], payload[5]]); + assert_eq!(param_count, 1); + } + + #[test] + fn test_bind_encode_no_fetch() { + let bind = BindMessage::new(false, vec![]); + assert_eq!(bind.fetch_flag, 0); + let payload = bind.encode_payload(); + assert_eq!(payload[0], 0); + } + + #[test] + fn test_bind_multiple_params() { + let params = vec![ + BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(vec![1, 0, 0, 0]), + }, + BindParam { + type_name: "VARCHAR".to_string(), + type_code: 3, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(b"test".to_vec()), + }, + ]; + let bind = BindMessage::new(true, params); + let payload = bind.encode_payload(); + assert_eq!(u16::from_le_bytes([payload[4], payload[5]]), 2); + } + + #[test] + fn test_bind_param_fields() { + let param = BindParam { + type_name: "BIGINT".to_string(), + type_code: 5, + precision: 19, + scale: 0, + direction: ParameterDirection::Input, + value: Some(vec![0; 8]), + }; + assert_eq!(param.type_name, "BIGINT"); + assert_eq!(param.type_code, 5); + } + + #[test] + fn test_bind_encode_vchar_param() { + let params = vec![BindParam { + type_name: "VARCHAR".to_string(), + type_code: 3, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(b"BindTest".to_vec()), + }]; + let bind = BindMessage::new(true, params); + let payload = bind.encode_payload(); + assert!(payload.windows(8).any(|w| w == b"BindTest")); + } + + // --- BindExec2Message tests --- + + #[test] + fn test_bind_exec2_new() { + let msg = BindExec2Message::new(true, true, vec![]); + assert!(msg.auto_commit); + assert!(msg.has_result_set); + assert!(msg.params.is_empty()); + } + + #[test] + fn test_bind_exec2_encode_header() { + let msg = BindExec2Message::new(false, true, vec![]); + let payload = msg.encode_payload(); + // Minimum header is 42 bytes + assert!(payload.len() >= 42); + assert_eq!(payload[0], 0); // auto_commit = false + assert_eq!(payload[1], 0); // param_count low + assert_eq!(payload[2], 0); // param_count high + assert_eq!(payload[3], 1); // has_result_set = true + } + + #[test] + fn test_bind_exec2_encode_with_params() { + let params = vec![ + BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(42i32.to_le_bytes().to_vec()), + }, + BindParam { + type_name: "VARCHAR".to_string(), + type_code: 3, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(b"hello".to_vec()), + }, + ]; + let msg = BindExec2Message::new(true, true, params); + let payload = msg.encode_payload(); + // Header(42) + 2 descriptors(16 each) + values + assert!(payload.len() > 42); + // param_count at offset 1-2 + assert_eq!(u16::from_le_bytes([payload[1], payload[2]]), 2); + } + + #[test] + fn test_bind_exec2_null_param() { + let params = vec![BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: None, // NULL + }]; + let msg = BindExec2Message::new(true, false, params); + let payload = msg.encode_payload(); + // After header(42) + descriptor(13) = 55, null marker should be 0xFFFF + let null_marker_offset = 42 + 13; // descriptor = 1+4+4+4 = 13 + assert_eq!( + u16::from_le_bytes([payload[null_marker_offset], payload[null_marker_offset + 1]]), + 0xFFFF + ); + } + + #[test] + fn test_bind_exec2_pagination() { + let msg = BindExec2Message::with_pagination(true, true, 100, 50, vec![]); + let payload = msg.encode_payload(); + // offset at bytes 4-11 + assert_eq!( + i64::from_le_bytes([ + payload[4], + payload[5], + payload[6], + payload[7], + payload[8], + payload[9], + payload[10], + payload[11] + ]), + 100 + ); + // max_rows at bytes 20-27 + assert_eq!( + i64::from_le_bytes([ + payload[20], + payload[21], + payload[22], + payload[23], + payload[24], + payload[25], + payload[26], + payload[27] + ]), + 50 + ); + } + + #[test] + fn test_bind_exec2_output_param() { + let params = vec![BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Output, + value: None, + }]; + let msg = BindExec2Message::new(true, false, params); + let payload = msg.encode_payload(); + // ioType at offset 42 (after 42-byte header) + assert_eq!(payload[42], 2); // Output = 2 + } + + // --- StatementAllocateMessage tests --- + + #[test] + fn test_statement_allocate_new() { + let msg = StatementAllocateMessage::new(); + assert!(msg.read_base_col_name); + } + + #[test] + fn test_statement_allocate_encode() { + let msg = StatementAllocateMessage::new(); + let payload = msg.encode_payload(); + assert_eq!(payload.len(), 1); + assert_eq!(payload[0], 1); + } + + #[test] + fn test_statement_allocate_encode_no_base_col() { + let msg = StatementAllocateMessage { + read_base_col_name: false, + }; + let payload = msg.encode_payload(); + assert_eq!(payload[0], 0); + } + + #[test] + fn test_statement_allocate_parse_response() { + let mut data = vec![0u8; 4]; + let stmt_id: u32 = 0x12345678; + data.copy_from_slice(&stmt_id.to_le_bytes()); + assert_eq!( + StatementAllocateMessage::parse_response(&data).unwrap(), + stmt_id + ); + } + + #[test] + fn test_statement_allocate_parse_response_incomplete() { + let data = vec![0u8; 3]; + let result = StatementAllocateMessage::parse_response(&data); + assert!(matches!(result, Err(crate::error::Error::Incomplete))); + } + + #[test] + fn test_statement_allocate_parse_zero_id() { + let data = vec![0u8; 32]; + assert_eq!(StatementAllocateMessage::parse_response(&data).unwrap(), 0); + } + + // --- StatementFreeMessage tests --- + + #[test] + fn test_statement_free_new() { + let msg = StatementFreeMessage::new(42); + assert_eq!(msg.stmt_id, 42); + } + + #[test] + fn test_statement_free_encode() { + let msg = StatementFreeMessage::new(0xDEADBEEF); + let payload = msg.encode_payload(); + assert_eq!(payload.len(), 4); + assert_eq!( + u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]), + 0xDEADBEEF + ); + } + + #[test] + fn test_statement_free_zero_id() { + let msg = StatementFreeMessage::new(0); + let payload = msg.encode_payload(); + assert_eq!(payload, vec![0u8; 4]); + } + + // --- ParameterDirection tests --- + + #[test] + fn test_parameter_direction_values() { + assert_eq!(ParameterDirection::Input as u8, 1); + assert_eq!(ParameterDirection::Output as u8, 2); + assert_eq!(ParameterDirection::InputOutput as u8, 3); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/close.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/close.rs new file mode 100644 index 000000000..7cf786568 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/close.rs @@ -0,0 +1,41 @@ +//! CLOSE message (type 20) for closing a statement handle. + +use bytes::BytesMut; + +/// Client->Server CLOSE message (type 20). +/// +/// Closes a previously prepared statement, freeing server resources. +#[derive(Debug, Clone)] +pub struct CloseMessage; + +impl CloseMessage { + /// Encode to payload bytes (empty payload). + pub fn encode_payload(&self) -> BytesMut { + BytesMut::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_close_encode_empty() { + let close = CloseMessage; + let payload = close.encode_payload(); + assert!(payload.is_empty()); + } + + #[test] + fn test_close_debug() { + let close = CloseMessage; + let debug_str = format!("{:?}", close); + assert!(debug_str.contains("CloseMessage")); + } + + #[test] + fn test_close_clone() { + let close = CloseMessage; + let _cloned = close.clone(); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/exec.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/exec.rs new file mode 100644 index 000000000..d382c06e0 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/exec.rs @@ -0,0 +1,437 @@ +//! EXEC message (type 5) for preparing and executing SQL statements. +//! +//! Two variants are provided: +//! - `ExecMessage`: simple format (SQL + null terminator) for direct execution. +//! - `ExecMessageV2`: full Go driver-compatible format with auto_commit, +//! has_result_set, exec_type, max_rows, timeout, and UTF-16 SQL encoding. + +use bytes::{BufMut, BytesMut}; + +/// Client->Server EXEC message (type 5). +/// +/// Simple format: SQL string followed by a null terminator. +/// Used for direct execution without parameters. +#[derive(Debug, Clone)] +pub struct ExecMessage { + /// Whether this is a prepared statement (1) or direct execution (0). + pub is_prepared: u8, + /// Number of parameters in the SQL. + pub param_count: u16, + /// The SQL string. + pub sql: String, +} + +impl ExecMessage { + /// Create a new direct execution message. + pub fn new(sql: &str, param_count: u16) -> Self { + Self { + is_prepared: if param_count > 0 { 1 } else { 0 }, + param_count, + sql: sql.to_string(), + } + } + + /// Create a new prepared statement message. + pub fn prepare(sql: &str, param_count: u16) -> Self { + Self { + is_prepared: 1, + param_count, + sql: sql.to_string(), + } + } + + /// Encode to payload bytes (simple format: SQL + null terminator). + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + buf.put_slice(self.sql.as_bytes()); + buf.put_u8(0); // null terminator + buf + } +} + +/// Client->Server EXEC message for PREPARE (type 5). +/// +/// This matches the Go driver's EXEC(5) format used for preparing +/// statements with parameters. The server parses this to extract +/// parameter metadata before the BIND_EXEC2 step. +/// +/// Wire format: +/// ```text +/// Offset Size Field +/// 0 1 auto_commit (0/1) +/// 1 1 is_prepare (0=prepare, 1=exec) +/// 2 1 reserved (0) +/// 3 1 exec_flag (1) +/// 4 1 reserved (0) +/// 5 2 exec_type (i16 LE) = 0 +/// 7 8 max_rows (i64 LE) = INT64_MAX +/// 15 1 bdta_flag (0/2) +/// 16 2 reserved (0) +/// 18 1 bind_options (0 for MsgVersion < 8) +/// 19 1 reserved (0) +/// 20 1 reserved (0) +/// 21 4 query_timeout (i32 LE) = 0 +/// 25 1 inner_exec (0/1) +/// 26 N SQL text (raw bytes + null terminator) +/// ``` +/// +/// For MsgVersion >= 3 (default for DM 8.x), an extra byte follows the 26-byte +/// header: the result-set-encoding byte. 0 = default row format. +#[derive(Debug, Clone)] +pub struct PrepareMessage { + /// The SQL string to prepare. + pub sql: String, + /// Whether this returns a result set (SELECT). + pub has_result_set: bool, + /// Auto-commit mode. + pub auto_commit: bool, + /// DM protocol message version (defaults to 8 as of DM 8.x). + /// Controls whether the extra result-set-encoding byte is emitted. + pub msg_version: i32, +} + +impl PrepareMessage { + /// Create a new PREPARE message. + pub fn new(sql: &str, has_result_set: bool) -> Self { + Self { + sql: sql.to_string(), + has_result_set, + auto_commit: true, + msg_version: 8, + } + } + + /// Encode to payload bytes matching Go driver's EXEC(5) format. + /// + /// Layout is the 27-byte exec params header (26 bytes + optional byte for + /// MsgVersion >= 3) + raw SQL bytes + null terminator. + /// Matches Go dm_build_784.dm_build_421() implementation. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + // 0: auto_commit + buf.put_u8(if self.auto_commit { 1 } else { 0 }); + // 1: has_result_set (matches Go byte 1 — dm_build_785) + buf.put_u8(if self.has_result_set { 1 } else { 0 }); + // 2: reserved + buf.put_u8(0); + // 3: exec_flag = 1 + buf.put_u8(1); + // 4: reserved + buf.put_u8(0); + // 5-6: exec_type (i16 LE) = 0 + buf.put_i16_le(0); + // 7-14: max_rows (i64 LE) = INT64_MAX + buf.put_i64_le(i64::MAX); + // 15: bdta_flag = 0 + buf.put_u8(0); + // 16-17: reserved (i16 LE) = 0 + buf.put_i16_le(0); + // 18: bind_options = 1 (Go driver writes 1 here — critical for BIND_EXEC2) + buf.put_u8(1); + // 19: reserved + buf.put_u8(0); + // 20: reserved + buf.put_u8(0); + // 21-24: query_timeout (i32 LE) = 0 + buf.put_i32_le(0); + // 25: inner_exec = 0 + buf.put_u8(0); + + // Extra byte for MsgVersion >= 3 (Go driver Dm_build_783 / offset 38). + // Result-set encoding byte: 0 = default row format. + if self.msg_version >= 3 { + let rs_encoding: u8 = 0; // Dm_build_537 = 0x00 + buf.put_u8(rs_encoding); + } + + // SQL text as raw bytes + null terminator (no length prefix) + buf.put_slice(self.sql.as_bytes()); + buf.put_u8(0); // null terminator + + buf + } +} + +/// Client->Server EXEC message v2 (type 5). +/// +/// Full Go driver-compatible format. Supports: +/// - auto_commit control +/// - has_result_set flag (SELECT vs DML) +/// - max_rows for pagination +/// - query timeout +/// - UTF-16 LE encoded SQL string +/// +/// Wire format: +/// ```text +/// Offset Size Field +/// 0 1 auto_commit (0 or 1) +/// 1 1 has_result_set (1=SELECT, 0=DML) +/// 2 4 reserved (zeros) +/// 6 2 exec_type (u16 LE) +/// 8 8 max_rows (i64 LE) - 0=unlimited +/// 16 1 bdta flag +/// 17 4 timeout (i32 LE) - 0=default +/// 21 2 bind_options (u16 LE) +/// 23 2 sql_length (u16 LE) - number of UTF-16 chars +/// 25 N SQL text (UTF-16 LE, 2 bytes per char) +/// ``` +#[derive(Debug, Clone)] +pub struct ExecMessageV2 { + /// Auto-commit mode (true = commit after execution). + pub auto_commit: bool, + /// Whether this query returns a result set. + pub has_result_set: bool, + /// Maximum rows to return (0 = unlimited). + pub max_rows: i64, + /// Query timeout in seconds (0 = default). + pub timeout: i32, + /// The SQL string. + pub sql: String, +} + +impl ExecMessageV2 { + /// Create a new v2 exec message with default settings. + pub fn new(sql: &str) -> Self { + Self { + auto_commit: true, + has_result_set: sql.trim_start().to_uppercase().starts_with("SELECT"), + max_rows: 0, + timeout: 0, + sql: sql.to_string(), + } + } + + /// Create for DML statements (INSERT/UPDATE/DELETE). + pub fn dml(sql: &str) -> Self { + Self { + auto_commit: true, + has_result_set: false, + max_rows: 0, + timeout: 0, + sql: sql.to_string(), + } + } + + /// Create for SELECT statements. + pub fn select(sql: &str) -> Self { + Self { + auto_commit: true, + has_result_set: true, + max_rows: 0, + timeout: 0, + sql: sql.to_string(), + } + } + + /// Encode to payload bytes (v2 format with UTF-16 SQL). + /// + /// NOTE: For EXEC(5) with stmt_id (PREPARE), the server expects UTF-8 + /// encoded SQL, NOT UTF-16. Use `encode_payload_utf8()` for that case. + /// This UTF-16 format is used for OPTIMIZED_PREPARE_EXEC(91) only. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + // Header + buf.put_u8(if self.auto_commit { 1 } else { 0 }); + buf.put_u8(if self.has_result_set { 1 } else { 0 }); + buf.put_u32_le(0); // reserved + buf.put_u16_le(0); // exec_type + + // Execution options + buf.put_i64_le(self.max_rows); // max_rows + buf.put_u8(0); // bdta flag + buf.put_i32_le(self.timeout); // timeout + buf.put_u16_le(0); // bind_options + + // SQL as UTF-16 LE + let sql_utf16: Vec = self.sql.encode_utf16().collect(); + buf.put_u16_le(sql_utf16.len() as u16); // sql_length (char count) + for &ch in &sql_utf16 { + buf.put_u16_le(ch); + } + + buf + } + + /// Encode SQL as UTF-8 (for EXEC(5) PREPARE step matching Go driver). + pub fn encode_payload_utf8(&self) -> BytesMut { + let mut buf = BytesMut::new(); + + // Header + buf.put_u8(if self.auto_commit { 1 } else { 0 }); + buf.put_u8(if self.has_result_set { 1 } else { 0 }); + buf.put_u32_le(0); // reserved + buf.put_u16_le(0); // exec_type + + // Execution options + buf.put_i64_le(self.max_rows); // max_rows + buf.put_u8(0); // bdta flag + buf.put_i32_le(self.timeout); // timeout + buf.put_u16_le(0); // bind_options + + // SQL as UTF-8 with length prefix (4-byte LE) + null terminator + let sql_bytes = self.sql.as_bytes(); + buf.put_u32_le(sql_bytes.len() as u32); // sql_length (byte count) + buf.put_slice(sql_bytes); + buf.put_u8(0); // null terminator + + buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // --- ExecMessage (simple) tests --- + + #[test] + fn test_exec_new_direct() { + let exec = ExecMessage::new("SELECT * FROM SAMPLE", 0); + assert_eq!(exec.is_prepared, 0); + assert_eq!(exec.param_count, 0); + } + + #[test] + fn test_exec_new_prepared() { + let exec = ExecMessage::new("SELECT * FROM SAMPLE WHERE ID = ?", 1); + assert_eq!(exec.is_prepared, 1); + assert_eq!(exec.param_count, 1); + } + + #[test] + fn test_exec_encode_contains_sql() { + let sql = "DELETE FROM SAMPLE WHERE ID = 998"; + let exec = ExecMessage::new(sql, 0); + let payload = exec.encode_payload(); + let sql_in_payload = &payload[payload.len() - sql.len() - 1..payload.len() - 1]; + assert_eq!(sql_in_payload, sql.as_bytes()); + } + + #[test] + fn test_exec_encode_null_terminated() { + let exec = ExecMessage::new("COMMIT", 0); + let payload = exec.encode_payload(); + assert_eq!(payload[payload.len() - 1], 0); + } + + #[test] + fn test_exec_prepare() { + let exec = ExecMessage::prepare("INSERT INTO SAMPLE VALUES (?, ?)", 2); + assert_eq!(exec.is_prepared, 1); + assert_eq!(exec.param_count, 2); + assert_eq!(exec.sql, "INSERT INTO SAMPLE VALUES (?, ?)"); + } + + #[test] + fn test_exec_payload_size() { + let exec = ExecMessage::new("SELECT 1", 0); + let payload = exec.encode_payload(); + assert_eq!(payload.len(), "SELECT 1".len() + 1); + } + + // --- ExecMessageV2 tests --- + + #[test] + fn test_exec_v2_new_select() { + let exec = ExecMessageV2::new("SELECT * FROM SAMPLE"); + assert!(exec.auto_commit); + assert!(exec.has_result_set); + assert_eq!(exec.max_rows, 0); + } + + #[test] + fn test_exec_v2_dml() { + let exec = ExecMessageV2::dml("DELETE FROM SAMPLE WHERE ID = 1"); + assert!(exec.auto_commit); + assert!(!exec.has_result_set); + } + + #[test] + fn test_exec_v2_select() { + let exec = ExecMessageV2::select("SELECT ID FROM SAMPLE"); + assert!(exec.has_result_set); + } + + #[test] + fn test_exec_v2_encode_minimal() { + let exec = ExecMessageV2::new("SELECT 1"); + let payload = exec.encode_payload(); + // Header: 25 bytes + UTF-16 SQL (8 chars * 2 = 16 bytes for "SELECT 1") + assert_eq!(payload.len(), 41); + assert_eq!(payload[0], 1); // auto_commit + assert_eq!(payload[1], 1); // has_result_set (SELECT inferred) + } + + #[test] + fn test_exec_v2_encode_sql_utf16() { + let exec = ExecMessageV2::new("AB"); + let payload = exec.encode_payload(); + // After 25-byte header: sql_len(u16) + 'A'(u16 LE) + 'B'(u16 LE) + assert_eq!(payload[23], 2); // sql_length low byte + assert_eq!(payload[24], 0); // sql_length high byte + assert_eq!(payload[25], 0x41); + assert_eq!(payload[26], 0); // 'A' + assert_eq!(payload[27], 0x42); + assert_eq!(payload[28], 0); // 'B' + } + + #[test] + fn test_exec_v2_encode_cjk_utf16() { + let exec = ExecMessageV2::new("测"); + let payload = exec.encode_payload(); + // '测' = U+6D4B -> LE bytes: 0x4B 0x6D + assert_eq!(payload[25], 0x4B); + assert_eq!(payload[26], 0x6D); + } + + #[test] + fn test_exec_v2_no_auto_commit() { + let mut exec = ExecMessageV2::select("SELECT 1"); + exec.auto_commit = false; + let payload = exec.encode_payload(); + assert_eq!(payload[0], 0); + } + + #[test] + fn test_exec_v2_max_rows() { + let mut exec = ExecMessageV2::select("SELECT 1"); + exec.max_rows = 100; + let payload = exec.encode_payload(); + // max_rows at offset 8-15 + assert_eq!( + i64::from_le_bytes([ + payload[8], + payload[9], + payload[10], + payload[11], + payload[12], + payload[13], + payload[14], + payload[15] + ]), + 100 + ); + } + + #[test] + fn test_exec_v2_timeout() { + let mut exec = ExecMessageV2::select("SELECT 1"); + exec.timeout = 30; + let payload = exec.encode_payload(); + // timeout at offset 17-20 + assert_eq!( + i32::from_le_bytes([payload[17], payload[18], payload[19], payload[20]]), + 30 + ); + } + + #[test] + fn test_exec_v2_empty_sql() { + let exec = ExecMessageV2::new(""); + let payload = exec.encode_payload(); + assert_eq!(payload.len(), 25); // header only + assert_eq!(u16::from_le_bytes([payload[23], payload[24]]), 0); // sql_length = 0 + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/explain.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/explain.rs new file mode 100644 index 000000000..7b670418a --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/explain.rs @@ -0,0 +1,76 @@ +//! EXPLAIN response (type 149). +//! +//! DM8 returns a four-byte little-endian text length followed by a textual +//! execution plan encoded with the server connection encoding. + +use dameng_types::encoding::{decode_from_server, ServerEncoding}; + +use crate::error::{Error, Result}; + +/// Upper bound for a textual query plan accepted from the server. +pub const MAX_EXPLAIN_TEXT_BYTES: usize = 16 * 1024 * 1024; + +/// Parsed textual response for `EXPLAIN `. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExplainResponse { + /// UTF-8 query plan text. + pub plan: String, +} + +impl ExplainResponse { + /// Parse a length-prefixed plan from a DM8 EXPLAIN response. + pub fn from_bytes(data: &[u8], server_encoding: ServerEncoding) -> Result { + let length_bytes: [u8; 4] = data + .get(..4) + .ok_or(Error::Incomplete)? + .try_into() + .map_err(|_| Error::Incomplete)?; + let text_length = u32::from_le_bytes(length_bytes) as usize; + if text_length > MAX_EXPLAIN_TEXT_BYTES { + return Err(Error::InvalidFrame(format!( + "EXPLAIN text length {text_length} exceeds {MAX_EXPLAIN_TEXT_BYTES} bytes" + ))); + } + let text = data.get(4..4 + text_length).ok_or(Error::Incomplete)?; + + Ok(Self { + plan: decode_from_server(server_encoding, text), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_utf8_plan() { + let text = "1 #NSET2\n2 #CSCN2"; + let mut payload = (text.len() as u32).to_le_bytes().to_vec(); + payload.extend_from_slice(text.as_bytes()); + + let response = ExplainResponse::from_bytes(&payload, ServerEncoding::Utf8).unwrap(); + + assert_eq!(response.plan, text); + } + + #[test] + fn rejects_truncated_plan() { + let payload = [8, 0, 0, 0, b'p', b'l', b'a', b'n']; + + assert!(matches!( + ExplainResponse::from_bytes(&payload, ServerEncoding::Utf8), + Err(Error::Incomplete) + )); + } + + #[test] + fn rejects_oversized_plan() { + let payload = ((MAX_EXPLAIN_TEXT_BYTES + 1) as u32).to_le_bytes().to_vec(); + + assert!(matches!( + ExplainResponse::from_bytes(&payload, ServerEncoding::Utf8), + Err(Error::InvalidFrame(_)) + )); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/fetch.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/fetch.rs new file mode 100644 index 000000000..40f6cd734 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/fetch.rs @@ -0,0 +1,323 @@ +//! FETCH message (type 7) for retrieving more rows from a result set. +//! +//! Based on Go driver wire format. The FETCH request uses absolute row positions +//! — the client specifies which row to start from, and the server returns a batch +//! of rows up to a byte budget. +//! +//! Request wire format (after 64-byte Frame header): +//! ```text +//! Offset Size Field +//! 0 20 Reserved (zeros) +//! 20 8 startRow (i64 LE) — starting row index (absolute, 0-based) +//! 28 8 endRow (i64 LE) — ending row index (use i64::MAX for all remaining) +//! 36 2 cursorId (i16 LE) — result set cursor ID +//! 38 4 prefetchBytes (i32 LE) — max bytes to fetch, clamped [32, 65536] +//! ``` +//! +//! Response wire format: +//! ```text +//! Offset Size Field +//! 0 20 Reserved +//! 20 8 updateCount (i64 LE) — total row count in result set +//! 28 4 rsSizeof (i32 LE) — byte size of row data +//! 32 N row data (same format as EXEC_RESPONSE inline rows) +//! ``` + +use bytes::{BufMut, BytesMut}; + +use crate::error::Result; +use dameng_types::encoding::ServerEncoding; + +use super::response::{Column, ExecResponse, Row}; + +/// Default prefetch byte budget for FETCH requests. +pub const DEFAULT_PREFETCH_BYTES: i32 = 8192; + +/// Minimum prefetch byte budget. +pub const MIN_PREFETCH_BYTES: i32 = 32; + +/// Maximum prefetch byte budget. +pub const MAX_PREFETCH_BYTES: i32 = 65536; + +/// Client->Server FETCH message (type 7). +/// +/// Requests the next batch of rows from a previously executed query. +/// Uses absolute row positioning — `start_row` specifies which row to begin +/// fetching from, and the server returns up to `prefetch_bytes` of row data. +#[derive(Debug, Clone)] +pub struct FetchMessage { + /// Starting row index (absolute, 0-based). + pub start_row: i64, + /// Ending row index (use i64::MAX to fetch all remaining). + pub end_row: i64, + /// Result set cursor ID. + pub cursor_id: i16, + /// Maximum bytes to fetch (clamped to [32, 65536]). + pub prefetch_bytes: i32, +} + +impl FetchMessage { + /// Create a new fetch message. + /// + /// # Arguments + /// * `start_row` — The row index to start fetching from (0-based, absolute). + /// * `cursor_id` — The result set cursor ID from the initial query. + /// * `prefetch_bytes` — The maximum bytes to fetch (clamped to [32, 65536]). + pub fn new(start_row: i64, cursor_id: i16, prefetch_bytes: i32) -> Self { + let clamped = prefetch_bytes.clamp(MIN_PREFETCH_BYTES, MAX_PREFETCH_BYTES); + Self { + start_row, + end_row: i64::MAX, // Fetch all remaining rows + cursor_id, + prefetch_bytes: clamped, + } + } + + /// Create a new fetch message requesting from the given row, fetching all remaining. + /// + /// This is the most common case — fetch from a specific row position to the end. + pub fn fetch_from(start_row: i64, cursor_id: i16) -> Self { + Self::new(start_row, cursor_id, DEFAULT_PREFETCH_BYTES) + } + + /// Encode to payload bytes. + /// + /// Wire format: + /// - 20 bytes reserved (zeros) + /// - startRow (i64 LE) + /// - endRow (i64 LE) + /// - cursorId (i16 LE) + /// - prefetchBytes (i32 LE) + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(42); + // 20 bytes reserved + buf.put_bytes(0, 20); + // startRow (i64 LE) + buf.put_i64_le(self.start_row); + // endRow (i64 LE) + buf.put_i64_le(self.end_row); + // cursorId (i16 LE) + buf.put_i16_le(self.cursor_id); + // prefetchBytes (i32 LE) + buf.put_i32_le(self.prefetch_bytes); + buf + } +} + +/// Response from a FETCH request (msg_type=7). +#[derive(Debug, Clone)] +pub struct FetchResponse { + /// Total number of rows in the entire result set. + pub total_row_count: i64, + /// Column metadata (may be empty if already known from initial query). + pub columns: Vec, + /// Row data fetched in this batch. + pub rows: Vec, +} + +impl FetchResponse { + /// Parse a FETCH response from raw payload bytes. + /// + /// Response format: + /// - Offset 0-19: reserved + /// - Offset 20-27: updateCount (i64 LE) — total row count + /// - Offset 28-31: rsSizeof (i32 LE) — byte size of row data + /// - Offset 32+: row data (same format as EXEC_RESPONSE inline rows) + pub fn from_bytes(data: &[u8], server_encoding: ServerEncoding) -> Result { + if data.len() < 32 { + return Err(crate::error::Error::Incomplete); + } + + // updateCount at offset 20 + let total_row_count = i64::from_le_bytes([ + data[20], data[21], data[22], data[23], data[24], data[25], data[26], data[27], + ]); + + // rsSizeof at offset 28 + let rs_sizeof = if data.len() >= 32 { + i32::from_le_bytes([data[28], data[29], data[30], data[31]]) as usize + } else { + 0 + }; + + // Row data starts at offset 32 + let row_data_start = 32; + let row_data_end = (row_data_start + rs_sizeof).min(data.len()); + + if row_data_start >= data.len() || rs_sizeof == 0 { + return Ok(FetchResponse { + total_row_count, + columns: vec![], + rows: vec![], + }); + } + + let row_data = &data[row_data_start..row_data_end]; + + // The row data follows the same inline format as EXEC_RESPONSE. + // Parse it using the ExecResponse parser. + // Guard against parsing garbage: if row_data is all zeros or too short, + // the server returned metadata only (no inline data). + let has_real_data = + row_data.len() > 16 && !row_data.iter().all(|&b| b == 0) && row_data[0] != 0; + + if !has_real_data { + // Server returned a cursor/total count but no inline row data. + // This happens when the cursor_id is invalid or the result is empty. + return Ok(FetchResponse { + total_row_count, + columns: vec![], + rows: vec![], + }); + } + + match ExecResponse::from_bytes(row_data, server_encoding) { + Ok(resp) => Ok(FetchResponse { + total_row_count, + columns: resp.columns, + rows: resp.rows, + }), + Err(_) => { + // If we can't parse the row data as EXEC_RESPONSE format, + // return what we have with empty rows. + Ok(FetchResponse { + total_row_count, + columns: vec![], + rows: vec![], + }) + } + } + } + + /// Check if there are more rows to fetch. + pub fn has_more(&self, current_pos: usize) -> bool { + current_pos < self.total_row_count as usize + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fetch_new() { + let fetch = FetchMessage::new(0, 0, DEFAULT_PREFETCH_BYTES); + assert_eq!(fetch.start_row, 0); + assert_eq!(fetch.end_row, i64::MAX); + assert_eq!(fetch.cursor_id, 0); + assert_eq!(fetch.prefetch_bytes, DEFAULT_PREFETCH_BYTES); + } + + #[test] + fn test_fetch_payload_size() { + let fetch = FetchMessage::new(100, 1, 4096); + let payload = fetch.encode_payload(); + assert_eq!(payload.len(), 42); // 20 + 8 + 8 + 2 + 4 + } + + #[test] + fn test_fetch_encode_decode() { + let fetch = FetchMessage::new(42, 5, 8192); + let payload = fetch.encode_payload(); + + // Verify reserved bytes + assert!(payload[..20].iter().all(|&b| b == 0)); + + // Verify startRow at offset 20 + let start_row = i64::from_le_bytes([ + payload[20], + payload[21], + payload[22], + payload[23], + payload[24], + payload[25], + payload[26], + payload[27], + ]); + assert_eq!(start_row, 42); + + // Verify endRow at offset 28 + let end_row = i64::from_le_bytes([ + payload[28], + payload[29], + payload[30], + payload[31], + payload[32], + payload[33], + payload[34], + payload[35], + ]); + assert_eq!(end_row, i64::MAX); + + // Verify cursorId at offset 36 + let cursor_id = i16::from_le_bytes([payload[36], payload[37]]); + assert_eq!(cursor_id, 5); + + // Verify prefetchBytes at offset 38 + let prefetch_bytes = + i32::from_le_bytes([payload[38], payload[39], payload[40], payload[41]]); + assert_eq!(prefetch_bytes, 8192); + } + + #[test] + fn test_fetch_prefetch_clamp_min() { + let fetch = FetchMessage::new(0, 0, 1); + assert_eq!(fetch.prefetch_bytes, MIN_PREFETCH_BYTES); + } + + #[test] + fn test_fetch_prefetch_clamp_max() { + let fetch = FetchMessage::new(0, 0, i32::MAX); + assert_eq!(fetch.prefetch_bytes, MAX_PREFETCH_BYTES); + } + + #[test] + fn test_fetch_prefetch_normal() { + let fetch = FetchMessage::new(0, 0, 4096); + assert_eq!(fetch.prefetch_bytes, 4096); + } + + #[test] + fn test_fetch_from() { + let fetch = FetchMessage::fetch_from(50, 3); + assert_eq!(fetch.start_row, 50); + assert_eq!(fetch.cursor_id, 3); + assert_eq!(fetch.prefetch_bytes, DEFAULT_PREFETCH_BYTES); + } + + #[test] + fn test_fetch_response_incomplete() { + let data = [0u8; 10]; + let result = FetchResponse::from_bytes(&data, ServerEncoding::Utf8); + assert!(result.is_err()); + } + + #[test] + fn test_fetch_response_empty_data() { + let data = vec![0u8; 42]; + let resp = FetchResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + assert_eq!(resp.total_row_count, 0); + assert!(resp.rows.is_empty()); + } + + #[test] + fn test_fetch_clone() { + let fetch = FetchMessage::new(100, 2, 4096); + let cloned = fetch.clone(); + assert_eq!(cloned.start_row, 100); + assert_eq!(cloned.cursor_id, 2); + assert_eq!(cloned.prefetch_bytes, 4096); + } + + #[test] + fn test_has_more() { + let resp = FetchResponse { + total_row_count: 1000, + columns: vec![], + rows: vec![], + }; + assert!(resp.has_more(0)); + assert!(resp.has_more(999)); + assert!(!resp.has_more(1000)); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/isolation.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/isolation.rs new file mode 100644 index 000000000..ca6565c50 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/isolation.rs @@ -0,0 +1,180 @@ +//! SET_ISOLATION message (type 52) for setting transaction isolation level. +//! +//! DM server protocol values (verified against Go driver dm_go/m.go g2dbIsoLevel): +//! - 0: Read Uncommitted +//! - 1: Read Committed +//! - 2: Repeatable Read +//! - 3: Serializable + +use bytes::{BufMut, BytesMut}; + +use crate::frame::FRAME_HEADER_SIZE; +use crate::message::SET_ISOLATION; + +/// Transaction isolation levels supported by DM. +/// +/// Note: The DM protocol uses 0/1/2/3 for these values, NOT the standard +/// SQL 1/2/4/6 values. The Go driver's `g2dbIsoLevel()` does the same mapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsolationLevel { + /// Read Uncommitted - can read uncommitted changes from other transactions. + ReadUncommitted, + /// Read Committed - only reads committed data. + ReadCommitted, + /// Repeatable Read - guarantees same result for repeated reads. + RepeatableRead, + /// Serializable - complete isolation, transactions run serially. + Serializable, +} + +impl IsolationLevel { + /// Convert to DM protocol value (0/1/2/3). + pub fn to_protocol_value(self) -> i32 { + match self { + IsolationLevel::ReadUncommitted => 0, + IsolationLevel::ReadCommitted => 1, + IsolationLevel::RepeatableRead => 2, + IsolationLevel::Serializable => 3, + } + } + + /// Create from DM protocol value (0/1/2/3). + pub fn from_protocol_value(value: i32) -> Option { + match value { + 0 => Some(IsolationLevel::ReadUncommitted), + 1 => Some(IsolationLevel::ReadCommitted), + 2 => Some(IsolationLevel::RepeatableRead), + 3 => Some(IsolationLevel::Serializable), + _ => None, + } + } +} + +/// SET_ISOLATION message (type 52) to change transaction isolation level. +#[derive(Debug, Clone)] +pub struct SetIsolationMessage { + /// The isolation level to set. + pub level: IsolationLevel, +} + +impl SetIsolationMessage { + /// Create a new SET_ISOLATION message. + pub fn new(level: IsolationLevel) -> Self { + Self { level } + } + + /// Encode to a complete frame (header + no payload). + /// + /// Verified against Go driver (dm_build_828): the isolation level is + /// written **inside** the 64-byte frame header at offset 20 (i32 LE), + /// with body_len=0. No extra payload is sent. + pub fn encode_frame(&self, handle: u32) -> BytesMut { + let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE); + buf.put_bytes(0, FRAME_HEADER_SIZE); + + // Offset 0: handle (u32 LE) + buf[0..4].copy_from_slice(&handle.to_le_bytes()); + // Offset 4: msg_type (SET_ISOLATION=52) + buf[4] = SET_ISOLATION; + // Offset 6: body_len (i32 LE) = 0 + // Offset 10: response_code (i32 LE) = 0 + + // Offset 20: isolation_level (i32 LE) — written into reserved area + buf[20..24].copy_from_slice(&self.level.to_protocol_value().to_le_bytes()); + + // Compute XOR checksum at offset 19 (bytes 0..19) + let mut cs: u8 = 0; + for i in 0..19 { + cs ^= buf[i]; + } + buf[19] = cs; + + buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_isolation_level_protocol_values() { + assert_eq!(IsolationLevel::ReadUncommitted.to_protocol_value(), 0); + assert_eq!(IsolationLevel::ReadCommitted.to_protocol_value(), 1); + assert_eq!(IsolationLevel::RepeatableRead.to_protocol_value(), 2); + assert_eq!(IsolationLevel::Serializable.to_protocol_value(), 3); + } + + #[test] + fn test_isolation_level_from_protocol() { + assert_eq!( + IsolationLevel::from_protocol_value(0), + Some(IsolationLevel::ReadUncommitted) + ); + assert_eq!( + IsolationLevel::from_protocol_value(1), + Some(IsolationLevel::ReadCommitted) + ); + assert_eq!( + IsolationLevel::from_protocol_value(2), + Some(IsolationLevel::RepeatableRead) + ); + assert_eq!( + IsolationLevel::from_protocol_value(3), + Some(IsolationLevel::Serializable) + ); + assert_eq!(IsolationLevel::from_protocol_value(99), None); + } + + #[test] + fn test_set_isolation_encode() { + let msg = SetIsolationMessage::new(IsolationLevel::ReadCommitted); + let frame = msg.encode_frame(0); + assert_eq!(frame.len(), FRAME_HEADER_SIZE); + // isolation_level at offset 20 + assert_eq!( + i32::from_le_bytes([frame[20], frame[21], frame[22], frame[23]]), + 1 // ReadCommitted = 1 in protocol + ); + // msg_type at offset 4 + assert_eq!(frame[4], SET_ISOLATION); + } + + #[test] + fn test_set_isolation_all_levels() { + for level in [ + IsolationLevel::ReadUncommitted, + IsolationLevel::ReadCommitted, + IsolationLevel::RepeatableRead, + IsolationLevel::Serializable, + ] { + let msg = SetIsolationMessage::new(level); + let frame = msg.encode_frame(7); + assert_eq!(frame.len(), FRAME_HEADER_SIZE); + assert_eq!(frame[4], SET_ISOLATION); + assert_eq!( + i32::from_le_bytes([frame[20], frame[21], frame[22], frame[23]]), + level.to_protocol_value() + ); + // verify handle + assert_eq!( + u32::from_le_bytes([frame[0], frame[1], frame[2], frame[3]]), + 7 + ); + } + } + + #[test] + fn test_isolation_roundtrip() { + for level in [ + IsolationLevel::ReadUncommitted, + IsolationLevel::ReadCommitted, + IsolationLevel::RepeatableRead, + IsolationLevel::Serializable, + ] { + let pv = level.to_protocol_value(); + let recovered = IsolationLevel::from_protocol_value(pv).unwrap(); + assert_eq!(level, recovered, "roundtrip failed for {:?}", level); + } + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob.rs new file mode 100644 index 000000000..3e7bf6f93 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob.rs @@ -0,0 +1,575 @@ +//! LOBREAD protocol messages (msg_type=32). +//! +//! Reverse-engineered from the Go driver (dm_go/zq.go: dm_build_676 / dm_build_680). +//! Used to read out-of-row CLOB/BLOB data in chunks. +//! +//! ## Request format (LOBREAD, msg_type=32): +//! +//! | Field | Type | Size | Description | +//! |-------------|--------|------|---------------------------------------| +//! | lobFlag | byte | 1 | 0 = BLOB, 1 = CLOB | +//! | tabId | i32 LE | 4 | Table ID from column metadata | +//! | colId | i16 LE | 2 | Column ID from column metadata | +//! | blobId | i64 LE | 8 | LOB identifier from NBLOB_HEAD | +//! | groupId | i16 LE | 2 | LOB storage group ID | +//! | fileId | i16 LE | 2 | LOB storage file ID | +//! | pageNo | i32 LE | 4 | LOB starting page number | +//! | curFileId | i16 LE | 2 | Current file ID (tracking cursor) | +//! | curPageNo | i32 LE | 4 | Current page number (tracking cursor) | +//! | totalOffset | i32 LE | 4 | Accumulated offset so far | +//! | position | i32 LE | 4 | Read start position (0-based) | +//! | length | i32 LE | 4 | Number of bytes/chars to read | +//! +//! Extended section (if NewLobFlag is set on server): +//! | Field | Type | Size | Description | +//! |-----------|--------|------|---------------------------| +//! | rowId | i64 LE | 8 | Row ID from NBLOB_HEAD | +//! | exGroupId | i16 LE | 2 | Extended group ID | +//! | exFileId | i16 LE | 2 | Extended file ID | +//! | exPageNo | i32 LE | 4 | Extended page number | +//! +//! Total base payload: 41 bytes +//! Extended payload: 57 bytes + +use bytes::{BufMut, BytesMut}; +use dameng_types::LobLocator; + +/// LOBREAD request message. +/// +/// Encodes a request to read a chunk of LOB data from the server. +/// The server responds with the data chunk and updated cursor position. +#[derive(Debug, Clone)] +pub struct LobReadMessage { + /// LOB locator containing all necessary metadata. + locator: LobLocator, + /// Read start position (0-based byte/character offset). + position: i32, + /// Number of bytes (BLOB) or characters (CLOB) to read. + length: i32, + /// Whether the server supports the extended LOB format (NewLobFlag). + new_lob_flag: bool, +} + +impl LobReadMessage { + /// Create a new LOBREAD message. + /// + /// # Arguments + /// * `locator` - The LOB locator from the query result + /// * `position` - 0-based read offset + /// * `length` - Number of bytes/chars to read + /// * `new_lob_flag` - Whether server supports extended LOB format + pub fn new(locator: LobLocator, position: i32, length: i32, new_lob_flag: bool) -> Self { + Self { + locator, + position, + length, + new_lob_flag, + } + } + + /// Encode this message into a payload suitable for msg_type=32. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::with_capacity(64); + + // lobFlag: 0 = BLOB, 1 = CLOB + buf.put_u8(self.locator.lob_flag()); + + // tabId (i32 LE) + buf.put_i32_le(self.locator.tab_id); + + // colId (i16 LE) + buf.put_i16_le(self.locator.col_id); + + // blobId (i64 LE) + buf.put_i64_le(self.locator.blob_id()); + + // groupId (i16 LE) + buf.put_i16_le(self.locator.group_id()); + + // fileId (i16 LE) + buf.put_i16_le(self.locator.file_id()); + + // pageNo (i32 LE) + buf.put_i32_le(self.locator.page_no()); + + // curFileId (i16 LE) — use cursor state for subsequent reads + buf.put_i16_le(self.locator.cur_file_id); + + // curPageNo (i32 LE) — use cursor state for subsequent reads + buf.put_i32_le(self.locator.cur_page_no); + + // totalOffset (i32 LE) — accumulated offset from cursor + buf.put_i32_le(self.locator.total_offset); + + // position (i32 LE) — read start position + buf.put_i32_le(self.position); + + // length (i32 LE) — bytes/chars to read + buf.put_i32_le(self.length); + + // Extended section (if NewLobFlag) + if self.new_lob_flag { + // rowId (i64 LE) + buf.put_i64_le(self.locator.row_id()); + + // exGroupId (i16 LE) + buf.put_i16_le(self.locator.ex_group_id()); + + // exFileId (i16 LE) + buf.put_i16_le(self.locator.ex_file_id()); + + // exPageNo (i32 LE) + buf.put_i32_le(self.locator.ex_page_no()); + } + + buf + } +} + +/// Response from a LOBREAD request. +/// +/// Contains the data chunk and updated cursor state for subsequent reads. +#[derive(Debug, Clone)] +pub struct LobReadResponse { + /// The LOB data chunk returned by the server. + pub data: Vec, + /// Character length for CLOB (actual UTF-8 char count). -1 if unknown. + pub char_len: i64, + /// True if there are no more bytes to read (EOF). + pub read_over: bool, + /// Updated current file ID (for subsequent reads). + pub cur_file_id: i16, + /// Updated current page number (for subsequent reads). + pub cur_page_no: i32, + /// Updated total offset (for subsequent reads). + pub total_offset: i32, +} + +impl LobReadResponse { + /// Parse a LOBREAD response from raw payload bytes. + /// + /// Response format: + /// - readOver (1 byte) + /// - dataLen (i32 LE) + /// - curFileId (i16 LE) + /// - curPageNo (i32 LE) + /// - totalOffset (i32 LE) + /// - data (dataLen bytes) + /// - [optional] charLen (i32 LE) if remaining bytes > 0 + pub fn from_bytes(data: &[u8]) -> crate::error::Result { + if data.len() < 1 { + return Err(crate::error::Error::Incomplete); + } + + let mut offset = 0; + + // readOver (1 byte) + let read_over = data[offset] == 1; + offset += 1; + + // dataLen (i32 LE) + if offset + 4 > data.len() { + return Err(crate::error::Error::Incomplete); + } + let data_len = i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + offset += 4; + + if data_len <= 0 { + return Ok(Self { + data: vec![], + char_len: -1, + read_over, + cur_file_id: 0, + cur_page_no: 0, + total_offset: 0, + }); + } + + // curFileId (i16 LE) + if offset + 2 > data.len() { + return Err(crate::error::Error::Incomplete); + } + let cur_file_id = i16::from_le_bytes([data[offset], data[offset + 1]]); + offset += 2; + + // curPageNo (i32 LE) + if offset + 4 > data.len() { + return Err(crate::error::Error::Incomplete); + } + let cur_page_no = i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + offset += 4; + + // totalOffset (i32 LE) + if offset + 4 > data.len() { + return Err(crate::error::Error::Incomplete); + } + let total_offset = i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + offset += 4; + + // data (dataLen bytes) + let data_len = data_len as usize; + if offset + data_len > data.len() { + return Err(crate::error::Error::Incomplete); + } + let response_data = data[offset..offset + data_len].to_vec(); + offset += data_len; + + // Optional: charLen (i32 LE) if there are remaining bytes + let mut char_len: i64 = -1; + if offset + 4 <= data.len() { + // Check if there are extra bytes after data + let remaining = data.len() - offset; + if remaining >= 4 { + char_len = i64::from(i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ])); + } + } + + Ok(Self { + data: response_data, + char_len, + read_over, + cur_file_id, + cur_page_no, + total_offset, + }) + } +} + +/// LOBFREE request message (msg_type=29). +/// +/// Used to release a LOB locator on the server. +#[derive(Debug, Clone)] +pub struct LobFreeMessage { + /// LOB locator to free. + locator: LobLocator, +} + +impl LobFreeMessage { + /// Create a new LOBFREE message. + pub fn new(locator: LobLocator) -> Self { + Self { locator } + } + + /// Encode this message into a payload. + /// + /// Format: + /// - lobFlag (1 byte) + /// - blobId (i64 LE) + /// - groupId (i16 LE) + /// - fileId (i16 LE) + /// - pageNo (i32 LE) + /// - [if NewLobFlag] tabId (i32 LE), colId (i16 LE), rowId (i64 LE), + /// exGroupId (i16 LE), exFileId (i16 LE), exPageNo (i32 LE) + pub fn encode_payload(&self, new_lob_flag: bool) -> BytesMut { + let mut buf = BytesMut::with_capacity(48); + + buf.put_u8(self.locator.lob_flag()); + buf.put_i64_le(self.locator.blob_id()); + buf.put_i16_le(self.locator.group_id()); + buf.put_i16_le(self.locator.file_id()); + buf.put_i32_le(self.locator.page_no()); + + if new_lob_flag { + buf.put_i32_le(self.locator.tab_id); + buf.put_i16_le(self.locator.col_id); + buf.put_i64_le(self.locator.row_id()); + buf.put_i16_le(self.locator.ex_group_id()); + buf.put_i16_le(self.locator.ex_file_id()); + buf.put_i32_le(self.locator.ex_page_no()); + } + + buf + } +} + +/// LOBGETLEN request message (msg_type=31). +/// +/// Used to get the length of a LOB. +#[derive(Debug, Clone)] +pub struct LobGetLenMessage { + /// LOB locator. + locator: LobLocator, +} + +impl LobGetLenMessage { + /// Create a new LOBGETLEN message. + pub fn new(locator: LobLocator) -> Self { + Self { locator } + } + + /// Encode this message. + /// + /// Format: + /// - lobFlag (1 byte) + /// - blobId (i64 LE) + /// - groupId (i16 LE) + /// - fileId (i16 LE) + /// - pageNo (i32 LE) + /// - tabId (i32 LE) + /// - colId (i16 LE) + /// - rowId (i64 LE) + /// - [if NewLobFlag] exGroupId (i16 LE), exFileId (i16 LE), exPageNo (i32 LE) + pub fn encode_payload(&self, new_lob_flag: bool) -> BytesMut { + let mut buf = BytesMut::with_capacity(48); + + buf.put_u8(self.locator.lob_flag()); + buf.put_i64_le(self.locator.blob_id()); + buf.put_i16_le(self.locator.group_id()); + buf.put_i16_le(self.locator.file_id()); + buf.put_i32_le(self.locator.page_no()); + buf.put_i32_le(self.locator.tab_id); + buf.put_i16_le(self.locator.col_id); + buf.put_i64_le(self.locator.row_id()); + + if new_lob_flag { + buf.put_i16_le(self.locator.ex_group_id()); + buf.put_i16_le(self.locator.ex_file_id()); + buf.put_i32_le(self.locator.ex_page_no()); + } + + buf + } +} + +/// Response from a LOBGETLEN request. +#[derive(Debug, Clone)] +pub struct LobGetLenResponse { + /// Length in bytes (BLOB) or characters (CLOB). + pub length: i64, + /// New blob ID from server (if server updated it). + /// Used to refresh the locator for subsequent reads. + pub new_blob_id: Option, +} + +impl LobGetLenResponse { + /// Parse LOBGETLEN response from raw payload. + /// + /// Response format (matching Go driver dm_build_714.dm_build_425): + /// - length (i32 LE) — LOB length + /// - newBlobId (i64 LE) — updated blob ID from server (DDWORD) + pub fn from_bytes(data: &[u8]) -> crate::error::Result { + if data.len() < 4 { + return Err(crate::error::Error::Incomplete); + } + let length = i64::from(i32::from_le_bytes([data[0], data[1], data[2], data[3]])); + + // Parse newBlobId (DDWORD = i64 LE) if available + let new_blob_id = if data.len() >= 12 { + let blob_id = i64::from_le_bytes([ + data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], + ]); + Some(blob_id) + } else { + None + }; + + Ok(Self { + length, + new_blob_id, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_test_locator() -> LobLocator { + // Build a minimal NBLOB_HEAD with in_row=0x02 + let mut raw = vec![0u8; 43]; + raw[0] = 0x02; // in_row = out-of-row + // blob_id at offset 1 + raw[1..9].copy_from_slice(&42i64.to_le_bytes()); + // group_id at offset 13 + raw[13..15].copy_from_slice(&1i16.to_le_bytes()); + // file_id at offset 15 + raw[15..17].copy_from_slice(&2i16.to_le_bytes()); + // page_no at offset 17 + raw[17..21].copy_from_slice(&100i32.to_le_bytes()); + // tab_id at offset 21 + raw[21..25].copy_from_slice(&1000i32.to_le_bytes()); + // col_id at offset 25 + raw[25..27].copy_from_slice(&3i16.to_le_bytes()); + // row_id at offset 27 + raw[27..35].copy_from_slice(&999i64.to_le_bytes()); + // exGroupId at offset 35 + raw[35..37].copy_from_slice(&5i16.to_le_bytes()); + // exFileId at offset 37 + raw[37..39].copy_from_slice(&6i16.to_le_bytes()); + // exPageNo at offset 39 + raw[39..43].copy_from_slice(&200i32.to_le_bytes()); + + LobLocator::from_nblob_head(raw, true) + } + + #[test] + fn test_lob_locator_parsing() { + let mut loc = make_test_locator(); + assert_eq!(loc.blob_id(), 42); + assert_eq!(loc.group_id(), 1); + assert_eq!(loc.file_id(), 2); + assert_eq!(loc.page_no(), 100); + assert_eq!(loc.tab_id, 1000); + assert_eq!(loc.col_id, 3); + assert_eq!(loc.row_id(), 999); + assert_eq!(loc.ex_group_id(), 5); + assert_eq!(loc.ex_file_id(), 6); + assert_eq!(loc.ex_page_no(), 200); + assert!(loc.has_extended()); + assert!(loc.is_clob); + assert_eq!(loc.lob_flag(), 1); + // Cursor starts at 0 + assert_eq!(loc.cur_file_id, 0); + assert_eq!(loc.cur_page_no, 0); + assert_eq!(loc.total_offset, 0); + // After init_cursor(), cursor = file_id/page_no + loc.init_cursor(); + assert_eq!(loc.cur_file_id, 2); + assert_eq!(loc.cur_page_no, 100); + assert_eq!(loc.total_offset, 0); + } + + #[test] + fn test_lob_read_encode() { + let mut loc = make_test_locator(); + loc.init_cursor(); + let msg = LobReadMessage::new(loc, 0, 1024, true); + let payload = msg.encode_payload(); + // Base (41) + extended (16) = 57 bytes + assert_eq!(payload.len(), 57); + // First byte is lobFlag + assert_eq!(payload[0], 1); // CLOB + } + + #[test] + fn test_lob_read_encode_no_extended() { + let loc = make_test_locator(); + let msg = LobReadMessage::new(loc, 100, 512, false); + let payload = msg.encode_payload(); + assert_eq!(payload.len(), 41); + } + + #[test] + fn test_lob_read_response_parse() { + // Build a minimal LOBREAD response: + // readOver=0, dataLen=5, curFileId=2, curPageNo=101, totalOffset=5, data="HELLO" + let mut resp_data = vec![0u8; 25]; + resp_data[0] = 0; // readOver = false + resp_data[1..5].copy_from_slice(&5i32.to_le_bytes()); // dataLen + resp_data[5..7].copy_from_slice(&2i16.to_le_bytes()); // curFileId + resp_data[7..11].copy_from_slice(&101i32.to_le_bytes()); // curPageNo + resp_data[11..15].copy_from_slice(&5i32.to_le_bytes()); // totalOffset + resp_data[15..20].copy_from_slice(b"HELLO"); + let resp = LobReadResponse::from_bytes(&resp_data).unwrap(); + assert!(!resp.read_over); + assert_eq!(resp.data, b"HELLO"); + assert_eq!(resp.cur_file_id, 2); + assert_eq!(resp.cur_page_no, 101); + assert_eq!(resp.total_offset, 5); + } + + #[test] + fn test_lob_read_response_eof() { + // readOver=1, dataLen=0 + let resp_data = vec![1u8, 0, 0, 0, 0]; + let resp = LobReadResponse::from_bytes(&resp_data).unwrap(); + assert!(resp.read_over); + assert!(resp.data.is_empty()); + } + + #[test] + fn test_lob_cursor_update() { + let mut loc = make_test_locator(); + loc.init_cursor(); + assert_eq!(loc.cur_file_id, 2); + assert_eq!(loc.cur_page_no, 100); + + // Simulate response from first LOBREAD + loc.update_cursor(2, 101, 1024); + assert_eq!(loc.cur_file_id, 2); + assert_eq!(loc.cur_page_no, 101); + assert_eq!(loc.total_offset, 1024); + + // Simulate response from second LOBREAD + loc.update_cursor(3, 200, 2048); + assert_eq!(loc.cur_file_id, 3); + assert_eq!(loc.cur_page_no, 200); + assert_eq!(loc.total_offset, 2048); + } + + #[test] + fn test_lob_free_encode() { + let loc = make_test_locator(); + let free_msg = LobFreeMessage::new(loc); + let payload = free_msg.encode_payload(true); + // lobFlag(1) + blobId(8) + groupId(2) + fileId(2) + pageNo(4) = 17 + // + tabId(4) + colId(2) + rowId(8) + exGroupId(2) + exFileId(2) + exPageNo(4) = 22 + // Total = 39 + assert_eq!(payload.len(), 39); + assert_eq!(payload[0], 1); // CLOB + } + + #[test] + fn test_lob_free_encode_no_extended() { + let loc = make_test_locator(); + let free_msg = LobFreeMessage::new(loc); + let payload = free_msg.encode_payload(false); + // lobFlag(1) + blobId(8) + groupId(2) + fileId(2) + pageNo(4) = 17 + assert_eq!(payload.len(), 17); + } + + #[test] + fn test_lob_getlen_encode() { + let loc = make_test_locator(); + let getlen_msg = LobGetLenMessage::new(loc); + let payload = getlen_msg.encode_payload(true); + // lobFlag(1) + blobId(8) + groupId(2) + fileId(2) + pageNo(4) + tabId(4) + colId(2) + rowId(8) = 31 + // + exGroupId(2) + exFileId(2) + exPageNo(4) = 8 + // Total = 39 + assert_eq!(payload.len(), 39); + } + + #[test] + fn test_lob_getlen_response_parse() { + let resp_data = 2048i32.to_le_bytes(); + let resp = LobGetLenResponse::from_bytes(&resp_data).unwrap(); + assert_eq!(resp.length, 2048); + } + + #[test] + fn test_lob_read_response_with_char_len() { + // readOver=0, dataLen=5, curFileId=2, curPageNo=101, totalOffset=5, data="HELLO", charLen=5 + let mut resp_data = vec![0u8; 29]; + resp_data[0] = 0; // readOver = false + resp_data[1..5].copy_from_slice(&5i32.to_le_bytes()); // dataLen + resp_data[5..7].copy_from_slice(&2i16.to_le_bytes()); // curFileId + resp_data[7..11].copy_from_slice(&101i32.to_le_bytes()); // curPageNo + resp_data[11..15].copy_from_slice(&5i32.to_le_bytes()); // totalOffset + resp_data[15..20].copy_from_slice(b"HELLO"); + resp_data[20..24].copy_from_slice(&5i32.to_le_bytes()); // charLen + let resp = LobReadResponse::from_bytes(&resp_data).unwrap(); + assert!(!resp.read_over); + assert_eq!(resp.data, b"HELLO"); + assert_eq!(resp.char_len, 5); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob_bind.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob_bind.rs new file mode 100644 index 000000000..f6b0b3eac --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/lob_bind.rs @@ -0,0 +1,260 @@ +//! LOB data streaming message for binding large CLOB/BLOB parameters. +//! +//! When a CLOB/BLOB parameter exceeds 2048 bytes (DM_OFF_ROW_THRESHOLD), +//! the data is streamed to the server in chunks BEFORE the bind execute +//! message. This uses message type 14. +//! +//! Wire format (per Go driver `dm_build_811`): +//! ```text +//! Offset Size Field +//! 0 1 msg_type (14) +//! 1 2 param_index (i16 LE) - which parameter this chunk belongs to +//! 3 4 data_length (i32 LE) - length of data in this chunk +//! 7 N data bytes +//! ``` +//! +//! After all chunks are sent for all off-row parameters, the bind execute +//! message is sent with empty placeholders for those parameters. + +use bytes::{BufMut, BytesMut}; + +/// DM off-row threshold — LOBs larger than this use streaming. +pub const DM_OFF_ROW_THRESHOLD: usize = 2048; + +/// Maximum chunk size for streaming LOB data to the server. +pub const DM_LOB_CHUNK_SIZE: usize = 16000; + +/// Message type for LOB data streaming. +pub const DM_LOB_DATA_MSG_TYPE: u8 = 14; + +/// A single chunk of LOB data to stream to the server. +/// +/// This is sent as msg_type=14 with the parameter index and chunk data. +#[derive(Debug, Clone)] +pub struct LobDataMessage { + /// Parameter index (0-based) this chunk belongs to. + pub param_index: i16, + /// The data chunk bytes. + pub data: Vec, +} + +impl LobDataMessage { + /// Create a new LOB data chunk message. + pub fn new(param_index: i16, data: Vec) -> Self { + Self { param_index, data } + } + + /// Encode to payload bytes (without frame header). + /// + /// Matches the Go driver's dm_build_811 message format: + /// - 20 bytes reserved (zeros) + /// - param_index (i16 LE, USINT) at offset 20 + /// - data_length (i32 LE, ULINT) at offset 22 + /// - if NewLobFlag: -1 (i32 LE, ULINT) at offset 26 + /// - data bytes starting at offset 28 (or 26 if no NewLobFlag) + pub fn encode_payload(&self, new_lob_flag: bool) -> BytesMut { + let mut buf = BytesMut::new(); + + // 20 bytes reserved (matching Go's Dm_build_810 = Dm_build_327 = 20) + buf.put_bytes(0, 20); + + // param_index (USINT = i16 LE) at offset 20 + buf.put_i16_le(self.param_index); + + // data_length (ULINT = i32 LE) at offset 22 + buf.put_i32_le(self.data.len() as i32); + + // if NewLobFlag: write -1 (ULINT) at offset 26 + if new_lob_flag { + buf.put_i32_le(-1); + } + + // data bytes + buf.put_slice(&self.data); + + buf + } +} + +/// Split LOB data into chunks for streaming. +/// +/// Returns a list of chunks, each at most DM_LOB_CHUNK_SIZE bytes. +pub fn split_lob_data(data: &[u8]) -> Vec> { + let mut chunks = Vec::new(); + let mut start = 0; + while start < data.len() { + let end = (start + DM_LOB_CHUNK_SIZE).min(data.len()); + chunks.push(data[start..end].to_vec()); + start = end; + } + chunks +} + +/// Check if a LOB value should use off-row streaming. +/// +/// CLOB/BLOB types with data > 2048 bytes use the off-row protocol. +/// DM type codes: CLOB=14, BLOB=13. +pub fn is_off_row(dtype: i32, length: usize) -> bool { + let is_lob = dtype == 13 || dtype == 14; + is_lob && length > DM_OFF_ROW_THRESHOLD +} + +/// Encode a CLOB value to bytes for binding. +/// +/// CLOB values are UTF-8 encoded text. +pub fn encode_clob_value(text: &str) -> Vec { + text.as_bytes().to_vec() +} + +/// Encode a BLOB value from hex string for binding. +/// +/// BLOB values are raw bytes. If provided as hex, decode them. +pub fn encode_blob_from_hex(hex_str: &str) -> Vec { + // Simple hex decode (trim "0x" prefix if present) + let hex = hex_str.strip_prefix("0x").unwrap_or(hex_str); + let mut result = Vec::with_capacity(hex.len() / 2); + for i in (0..hex.len()).step_by(2) { + if i + 1 < hex.len() { + if let Ok(byte) = u8::from_str_radix(&hex[i..i + 2], 16) { + result.push(byte); + } + } + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_off_row_clob_large() { + assert!(is_off_row(14, 3000)); // CLOB > 2048 + } + + #[test] + fn test_is_off_row_clob_small() { + assert!(!is_off_row(14, 100)); // CLOB <= 2048 + } + + #[test] + fn test_is_off_row_blob_large() { + assert!(is_off_row(13, 5000)); // BLOB > 2048 + } + + #[test] + fn test_is_off_row_blob_small() { + assert!(!is_off_row(13, 500)); // BLOB <= 2048 + } + + #[test] + fn test_is_off_row_non_lob() { + assert!(!is_off_row(4, 10000)); // INT never off-row + } + + #[test] + fn test_is_off_row_boundary() { + assert!(!is_off_row(14, 2048)); // exactly at threshold = inline + assert!(is_off_row(14, 2049)); // 1 byte over = off-row + } + + #[test] + fn test_split_lob_data_small() { + let data = vec![1u8; 100]; + let chunks = split_lob_data(&data); + assert_eq!(chunks.len(), 1); + assert_eq!(chunks[0].len(), 100); + } + + #[test] + fn test_split_lob_data_single_chunk() { + let data = vec![1u8; DM_LOB_CHUNK_SIZE]; + let chunks = split_lob_data(&data); + assert_eq!(chunks.len(), 1); + } + + #[test] + fn test_split_lob_data_multiple_chunks() { + let data = vec![1u8; 50000]; + let chunks = split_lob_data(&data); + assert_eq!(chunks.len(), 4); // ceil(50000/16000) + assert_eq!(chunks[0].len(), 16000); + assert_eq!(chunks[1].len(), 16000); + assert_eq!(chunks[2].len(), 16000); + assert_eq!(chunks[3].len(), 2000); + } + + #[test] + fn test_split_lob_data_empty() { + let data: Vec = vec![]; + let chunks = split_lob_data(&data); + assert!(chunks.is_empty()); + } + + #[test] + fn test_lob_data_message_encode_no_new_flag() { + let msg = LobDataMessage::new(0, vec![1, 2, 3, 4, 5]); + let payload = msg.encode_payload(false); + // reserved(20) + param_index(2) + data_len(4) + data(5) = 31 + assert_eq!(payload.len(), 31); + assert!(payload[..20].iter().all(|&b| b == 0)); + assert_eq!(i16::from_le_bytes([payload[20], payload[21]]), 0); + assert_eq!( + i32::from_le_bytes([payload[22], payload[23], payload[24], payload[25]]), + 5 + ); + assert_eq!(&payload[26..], &[1, 2, 3, 4, 5]); + } + + #[test] + fn test_lob_data_message_encode_with_new_flag() { + let msg = LobDataMessage::new(2, vec![1, 2, 3, 4, 5]); + let payload = msg.encode_payload(true); + // reserved(20) + param_index(2) + data_len(4) + new_flag_marker(4) + data(5) = 35 + assert_eq!(payload.len(), 35); + assert!(payload[..20].iter().all(|&b| b == 0)); + assert_eq!(i16::from_le_bytes([payload[20], payload[21]]), 2); + assert_eq!( + i32::from_le_bytes([payload[22], payload[23], payload[24], payload[25]]), + 5 + ); + // NewLobFlag marker: -1 + assert_eq!( + i32::from_le_bytes([payload[26], payload[27], payload[28], payload[29]]), + -1 + ); + assert_eq!(&payload[30..], &[1, 2, 3, 4, 5]); + } + + #[test] + fn test_encode_clob_value() { + let encoded = encode_clob_value("hello"); + assert_eq!(encoded, b"hello"); + } + + #[test] + fn test_encode_clob_value_unicode() { + let encoded = encode_clob_value("你好"); + // UTF-8 bytes for 你好 + assert_eq!(encoded.len(), 6); + } + + #[test] + fn test_encode_blob_from_hex() { + let blob = encode_blob_from_hex("48656c6c6f"); + assert_eq!(blob, b"Hello"); + } + + #[test] + fn test_encode_blob_from_hex_with_prefix() { + let blob = encode_blob_from_hex("0x48656c6c6f"); + assert_eq!(blob, b"Hello"); + } + + #[test] + fn test_constants() { + assert_eq!(DM_OFF_ROW_THRESHOLD, 2048); + assert_eq!(DM_LOB_CHUNK_SIZE, 16000); + assert_eq!(DM_LOB_DATA_MSG_TYPE, 14); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/login.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/login.rs new file mode 100644 index 000000000..99850476c --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/login.rs @@ -0,0 +1,306 @@ +//! LOGIN message (type 1) and LOGIN_RESPONSE (type 163). +//! +//! Wire format reverse-engineered from captured traffic of the official +//! Python dmPython driver via proxy (see proxy_capture.log). + +use bytes::{BufMut, BytesMut}; + +use crate::error::Result; + +/// Client->Server LOGIN message (type 1). +/// +/// Payload layout (from capture): +/// ```text +/// Offset Size Field +/// 0 4 Encrypted username length (i32 LE) +/// 4 N Encrypted username bytes (XOR with challenge) +/// 4+N 4 Encrypted password length (i32 LE) +/// 4+N+4 M Encrypted password bytes (XOR with challenge) +/// 4+N+4+M 4 Separator (4 bytes of zeros) +/// ... 4 OS name length (i32 LE) +/// ... K OS name bytes (plaintext) +/// ... 4 Hostname length (i32 LE) +/// ... L Hostname bytes + null terminator +/// ``` +#[derive(Debug, Clone)] +pub struct LoginMessage { + pub username: String, + pub password: String, + pub hostname: String, + pub os_name: String, +} + +impl LoginMessage { + pub fn new(username: &str, password: &str, hostname: &str) -> Self { + Self { + username: username.to_string(), + password: password.to_string(), + hostname: hostname.to_string(), + os_name: format!("{} {}", std::env::consts::FAMILY, std::env::consts::OS), + } + } + + pub fn encode_payload(&self, challenge: &[u8]) -> BytesMut { + let mut buf = BytesMut::with_capacity(128); + + // Encrypted username (i32 length + XOR-encrypted bytes) + let un_len = self.username.len(); + buf.put_i32_le(un_len as i32); + for i in 0..un_len { + let key_byte = if !challenge.is_empty() { + challenge[i % challenge.len()] + } else { + 0 + }; + buf.put_u8(self.username.as_bytes()[i] ^ key_byte); + } + + // Encrypted password (i32 length + XOR-encrypted bytes) + let pw_len = self.password.len(); + buf.put_i32_le(pw_len as i32); + for i in 0..pw_len { + let key_byte = if !challenge.is_empty() { + challenge[i % challenge.len()] + } else { + 0 + }; + buf.put_u8(self.password.as_bytes()[i] ^ key_byte); + } + + // Separator (4 bytes of zeros) + buf.put_bytes(0, 4); + + // OS name (i32 length + plaintext bytes) + let os_bytes = self.os_name.as_bytes(); + buf.put_i32_le(os_bytes.len() as i32); + buf.put_slice(os_bytes); + + // Hostname + null terminator (i32 length + plaintext bytes + null) + let host_bytes = self.hostname.as_bytes(); + buf.put_i32_le(host_bytes.len() as i32); + buf.put_slice(host_bytes); + buf.put_u8(0); + + buf + } +} + +/// Server->Client LOGIN_RESPONSE message (type 163). +/// +/// Wire format from capture: +/// ```text +/// Offset Size Field +/// 0 16 Reserved (zeros) +/// 16 4 Server name length (i32 LE) +/// 20 N Server name string +/// 20+N 4 Authenticated username length (i32 LE) +/// 20+N+4 M Authenticated username string +/// 20+N+4+M 4 Client IP length (i32 LE) +/// ... K Client IP string +/// ... 4 Login datetime length (i32 LE) +/// ... L Login datetime string +/// ... 4 Session flags +/// ... 4 More flags +/// ... var Database name length + string +/// ``` +#[derive(Debug, Clone)] +pub struct LoginResponse { + pub session_id: u32, + pub encoding: u8, + pub server_status: u8, + pub server_name: String, + pub username: String, + pub client_ip: String, + pub login_datetime: String, + pub db_name: String, +} + +impl LoginResponse { + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 0x50 { + return Err(crate::error::Error::Incomplete); + } + + // Session ID at offset 2 + let session_id = u32::from_le_bytes([data[2], data[3], data[4], data[5]]); + + let encoding = match data[0x0A] { + 0 => 0u8, // GB18030 + 1 => 1u8, // UTF-8 + 2 => 2u8, // EUC-KR + _ => 0u8, // default to GB18030 (matching DM Go driver) + }; + + let server_status = data[0x0E]; + + // Server name at offset 0x10 (16): i32 length + string + let sn_len = u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]) as usize; + let sn_start = 0x14; + let sn_end = (sn_start + sn_len).min(data.len()); + let server_name = String::from_utf8_lossy(&data[sn_start..sn_end]) + .trim_matches('\0') + .to_string(); + + // Authenticated username after server name + let un_offset = sn_start + sn_len; + let mut username = String::new(); + let mut client_ip = String::new(); + let mut login_datetime = String::new(); + let mut db_name = String::new(); + + if data.len() > un_offset + 4 { + let un_len = u32::from_le_bytes([ + data[un_offset], + data.get(un_offset + 1).copied().unwrap_or(0), + data.get(un_offset + 2).copied().unwrap_or(0), + data.get(un_offset + 3).copied().unwrap_or(0), + ]) as usize; + let un_start = un_offset + 4; + if un_len > 0 && data.len() > un_start + un_len { + username = String::from_utf8_lossy(&data[un_start..un_start + un_len]).to_string(); + } + + // Client IP + let ip_offset = un_start + un_len; + if data.len() > ip_offset + 4 { + let ip_len = u32::from_le_bytes([ + data[ip_offset], + data.get(ip_offset + 1).copied().unwrap_or(0), + data.get(ip_offset + 2).copied().unwrap_or(0), + data.get(ip_offset + 3).copied().unwrap_or(0), + ]) as usize; + let ip_start = ip_offset + 4; + if ip_len > 0 && data.len() > ip_start + ip_len { + client_ip = + String::from_utf8_lossy(&data[ip_start..ip_start + ip_len]).to_string(); + } + + // Login datetime + let dt_offset = ip_start + ip_len; + if data.len() > dt_offset + 4 { + let dt_len = u32::from_le_bytes([ + data[dt_offset], + data.get(dt_offset + 1).copied().unwrap_or(0), + data.get(dt_offset + 2).copied().unwrap_or(0), + data.get(dt_offset + 3).copied().unwrap_or(0), + ]) as usize; + let dt_start = dt_offset + 4; + if dt_len > 0 && data.len() > dt_start + dt_len { + login_datetime = + String::from_utf8_lossy(&data[dt_start..dt_start + dt_len]).to_string(); + } + + // DB name + let db_offset = dt_start + dt_len; + if data.len() > db_offset + 4 { + let db_len = u32::from_le_bytes([ + data[db_offset], + data.get(db_offset + 1).copied().unwrap_or(0), + data.get(db_offset + 2).copied().unwrap_or(0), + data.get(db_offset + 3).copied().unwrap_or(0), + ]) as usize; + let db_start = db_offset + 4; + if db_len > 0 && data.len() > db_start + db_len { + db_name = String::from_utf8_lossy(&data[db_start..db_start + db_len]) + .to_string(); + } + } + } + } + } + + Ok(Self { + session_id, + encoding, + server_status, + server_name, + username, + client_ip, + login_datetime, + db_name, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_login_new() { + let login = LoginMessage::new("SYSDBA", "SYSDBA", "localhost"); + assert_eq!(login.username, "SYSDBA"); + assert_eq!(login.password, "SYSDBA"); + } + + #[test] + fn test_login_encode_payload_no_challenge() { + let login = LoginMessage::new("SYSDBA", "SYSDBA", "localhost"); + let payload = login.encode_payload(&[]); + // un_len(4) + "SYSDBA"(6) + pw_len(4) + "SYSDBA"(6) + sep(4) + os_len(4) + os + host_len(4) + host(9) + null(1) + let expected = 4 + 6 + 4 + 6 + 4 + 4 + login.os_name.len() + 4 + 9 + 1; + assert_eq!(payload.len(), expected); + // Username should be plaintext without challenge + assert_eq!(&payload[4..10], b"SYSDBA"); + } + + #[test] + fn test_login_xor_encryption() { + let challenge = [0xAAu8; 48]; + let login = LoginMessage::new("AB", "CD", "localhost"); + let payload = login.encode_payload(&challenge); + // 'A' ^ 0xAA = 0x55, 'B' ^ 0xAA = 0xA8 + assert_eq!(payload[4], 0x41 ^ 0xAA); + assert_eq!(payload[5], 0x42 ^ 0xAA); + } + + #[test] + fn test_login_response_from_bytes() { + let mut data = [0u8; 256]; + data[2] = 0x40; + data[3] = 0x1F; // session_id = 0x1F40 + data[0x0A] = 0x01; // UTF-8 + data[0x0E] = 0x01; // server_status + // Server name at 0x10: len + string + let sn = b"DMSERVER"; + data[0x10] = sn.len() as u8; + data[0x14..0x14 + sn.len()].copy_from_slice(sn); + // Username at 0x14 + 8 = 0x1C: len + string + let un = b"SYSDBA"; + data[0x1C] = un.len() as u8; + data[0x20..0x20 + un.len()].copy_from_slice(un); + + let resp = LoginResponse::from_bytes(&data).unwrap(); + assert_eq!(resp.server_name, "DMSERVER"); + assert_eq!(resp.username, "SYSDBA"); + assert_eq!(resp.encoding, 1); + } + + #[test] + fn test_login_response_incomplete() { + let data = [0u8; 32]; + let result = LoginResponse::from_bytes(&data); + assert!(matches!(result, Err(crate::error::Error::Incomplete))); + } + + #[test] + fn test_login_payload_matches_capture() { + // From capture: un_len=6, encrypted_un, pw_len=6, encrypted_pw, sep=4, os_len=8, os, host_len, host, null + let challenge = [0xBBu8; 48]; + let login = LoginMessage::new("SYSDBA", "SYSDBA", "localhost"); + let payload = login.encode_payload(&challenge); + assert_eq!( + i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]), + 6 + ); + // Encrypted username + assert_eq!(payload[4], b'S' ^ 0xBB); + // Password starts at offset 10 + assert_eq!( + i32::from_le_bytes([payload[10], payload[11], payload[12], payload[13]]), + 6 + ); + // Separator at offset 20 + assert_eq!(&payload[20..24], &[0u8; 4]); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/ready.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/ready.rs new file mode 100644 index 000000000..cf6bb32af --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/ready.rs @@ -0,0 +1,145 @@ +//! READY message (type 3) and ACK response (type 187). + +use bytes::{BufMut, BytesMut}; + +use crate::error::Result; + +/// Client->Server READY message (type 3). +/// +/// Sent to confirm connection is ready or as a keepalive. +#[derive(Debug, Clone)] +pub struct ReadyMessage { + pub flags: u8, +} + +impl ReadyMessage { + /// Create a new ready message. + pub fn new() -> Self { + Self { flags: 1 } + } + + /// Encode to payload bytes. + pub fn encode_payload(&self) -> BytesMut { + let mut buf = BytesMut::new(); + buf.put_u8(self.flags); + buf.put_u8(0); + buf.put_u8(0); + buf.put_u8(0); + buf + } +} + +/// Server->Client ACK response (type 187). +/// +/// Generic success response for most operations. +#[derive(Debug, Clone)] +pub struct AckResponse { + pub status: u8, + pub rows_affected: i64, + pub statement_id: u32, + pub message: String, +} + +impl AckResponse { + /// Parse from raw payload bytes. + pub fn from_bytes(data: &[u8]) -> Result { + if data.len() < 4 { + return Err(crate::error::Error::Incomplete); + } + + let status = data[0]; + let _reserved = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + let rows_affected = if data.len() >= 16 { + i64::from_le_bytes([ + data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15], + ]) + } else { + 0 + }; + + let statement_id = if data.len() >= 36 { + u32::from_le_bytes([data[32], data[33], data[34], data[35]]) + } else { + 0 + }; + + // Message string at offset 52 + let message = if data.len() >= 56 { + let msg_len = u32::from_le_bytes([data[52], data[53], data[54], data[55]]) as usize; + let msg_end = (56 + msg_len).min(data.len()); + String::from_utf8_lossy(&data[56..msg_end]).to_string() + } else { + String::new() + }; + + Ok(Self { + status, + rows_affected, + statement_id, + message, + }) + } + + /// Check if this is a success response. + pub fn is_success(&self) -> bool { + self.status == 1 || self.status == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ready_new() { + let ready = ReadyMessage::new(); + assert_eq!(ready.flags, 1); + } + + #[test] + fn test_ready_encode_size() { + let ready = ReadyMessage::new(); + let payload = ready.encode_payload(); + assert_eq!(payload.len(), 4); + } + + #[test] + fn test_ack_from_bytes_success() { + let mut data = [0u8; 64]; + data[0] = 1; // status = success + // Message "Success" at offset 56 + data[52] = 7; // msg_len + let msg = b"Success"; + data[56..56 + msg.len()].copy_from_slice(msg); + + let ack = AckResponse::from_bytes(&data).unwrap(); + assert_eq!(ack.status, 1); + assert_eq!(ack.message, "Success"); + assert!(ack.is_success()); + } + + #[test] + fn test_ack_from_bytes_incomplete() { + let data = [0u8; 2]; + let result = AckResponse::from_bytes(&data); + assert!(matches!(result, Err(crate::error::Error::Incomplete))); + } + + #[test] + fn test_ack_is_success() { + let ack = AckResponse { + status: 0, + rows_affected: 0, + statement_id: 0, + message: String::new(), + }; + assert!(ack.is_success()); + } + + #[test] + fn test_ready_flags() { + let ready = ReadyMessage { flags: 0 }; + let payload = ready.encode_payload(); + assert_eq!(payload[0], 0); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/response.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/response.rs new file mode 100644 index 000000000..ccc6e206f --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/response.rs @@ -0,0 +1,1244 @@ +//! EXEC_RESPONSE (type 0 / 187) - Statement execution results. +//! +//! Format verified against DM 8.1.3.62 live traffic. +//! Used for both EXEC (type 5) and OPTIMIZED_PREPARE_EXEC (type 91) responses. +//! +//! === FIXED HEADER (16 bytes) === +//! 0 u32 sub_type (2 for V$VERSION, 7 for SELECT, etc.) +//! 4 u32 flags (usually 4) +//! 8 u32 reserved (0) +//! 12 u32 row_count_in_response +//! +//! === FIRST COLUMN HEADER (16 bytes, offset 16) === +//! 16 u32 col_type (type code for first column) +//! 20 u16 nullable +//! 22 u16 col_count (total number of columns) +//! 24 u16 col_name_len (length of first column name) +//! 26 u16 type_name_len +//! 28 u16 table_name_len +//! 30 u16 schema_name_len +//! +//! === COLUMN VARIABLE DATA === +//! First column strings (explicit lengths from header fields): +//! col_name (col_name_len bytes) +//! type_name (type_name_len bytes) +//! table_name (table_name_len bytes, if > 0) +//! schema_name (schema_name_len bytes, if > 0) +//! null_terminator (1 byte, 0x00) +//! +//! For each subsequent column N (N > 1): +//! Between-columns metadata (12 bytes): nullable_flags(u32) + precision(u32) + reserved(u32) +//! Column N header (19 bytes): +//! col_type(u32) + nullable(u16) + display(u16) + reserved(u8) + col_index(u8) +//! + col_name_len(u16) + type_name_len(u16) + table_name_len(u16) + schema_name_len(u16) + padding(u8) +//! Column N strings: +//! padding(u8) + col_name + type_name + table_name + schema_name + terminator(u8) +//! +//! === OPE INLINE ROW DATA (for OPTIMIZED_PREPARE_EXEC type 91) === +//! After all column metadata, rows are embedded inline: +//! u8 row_size_marker (total bytes for this row including marker) +//! u8 flags +//! u32 rec_id +//! u32 padding (0) +//! For each column: u16 col_offset_from_marker +//! For each column: u16 value_size + value_size bytes of data + +use crate::error::Result; +use dameng_types::encoding::{decode_from_server, ServerEncoding}; + +/// LOB_LOCATOR size: DM returns a 16-byte locator for large CLOB/BLOB values. +/// When the value size exceeds 2048 bytes, DM returns a locator instead of inline data. +/// The client must use LOBREAD/FETCH operations to retrieve the actual content. +pub const LOB_LOCATOR_SIZE: usize = 16; + +/// Maximum inline data size before DM uses LOB_LOCATOR. +pub const LOB_LOCATOR_THRESHOLD: usize = 2048; + +/// Check if the given raw data is a LOB_LOCATOR (16 bytes) for the specified column type. +/// DM uses LOB_LOCATOR for CLOB/BLOB values larger than 2048 bytes. +pub fn is_lob_locator(data: &[u8], col_type_code: i32) -> bool { + let is_lob_type = matches!(col_type_code, 13 | 14); // BLOB=13, CLOB=14 + is_lob_type && data.len() == LOB_LOCATOR_SIZE +} + +/// Check if the given raw data is an NBLOB_HEAD structure for the specified column type. +/// DM returns NBLOB_HEAD for ALL CLOB/BLOB values (both inline and out-of-row). +/// - in_row=0x01: inline data follows the header (13 bytes header + data) +/// - in_row=0x02: out-of-row LOB (25+ bytes header, needs LOBREAD protocol) +pub fn is_lob_head(data: &[u8], col_type_code: i32) -> bool { + let is_lob_type = matches!(col_type_code, 13 | 14); + is_lob_type && data.len() >= 13 +} + +use dameng_types::{DmValue, DmValueType}; + +/// Derive type_code from type_name string. +/// The column header type_code field (offset 16) is unreliable on DM 8.1 — it +/// often returns 4 (INT) for all types. type_name is the authoritative source. +fn type_name_to_code(name: &str) -> i32 { + let upper = name.to_uppercase(); + // Check timezone variants first (longer match before shorter) + if upper.contains("TIMESTAMP WITH TIME ZONE") + || upper.contains("DATETIME WITH TIME ZONE") + || upper.contains("DATETIME2_TZ") + { + 12 // TIMESTAMP_TZ maps to TIMESTAMP (12) + } else if upper.contains("TIME WITH TIME ZONE") { + 11 // TIME_TZ maps to TIME (11) + } else if upper.contains("INTERVAL DAY") + || upper.contains("INTERVAL_DS") + || upper.contains("NUMTODSINTERVAL") + { + 15 // INTERVAL_DT + } else if upper.contains("INTERVAL YEAR") + || upper.contains("INTERVAL_YM") + || upper.contains("NUMTOYMINTERVAL") + { + 15 // INTERVAL_YM + } else { + match upper.as_str() { + "BIT" | "BOOLEAN" => 1, + "TINYINT" => 2, + "VARCHAR" | "CHAR" | "BANNECHAR" | "VARCHAR2" | "NVARCHAR" | "NVARCHAR2" => 3, + "INT" | "INTEGER" | "NUMBER" => 4, + "BIGINT" | "LONG" => 5, + "SMALLINT" => 6, + "FLOAT" => 7, + "DOUBLE" | "DOUBLE PRECISION" => 8, + "DEC" | "DECIMAL" | "NUMERIC" => 9, + "DATE" => 10, + "TIME" => 11, + "TIMESTAMP" | "DATETIME" | "DATETIME2" => 12, + "BLOB" | "RAW" | "LONG RAW" => 13, + "CLOB" | "NCLOB" | "TEXT" => 14, + "INTERVAL" => 15, + "BINARY" | "VARBINARY" => 17, + "ROWID" => 18, + "XMLTYPE" => 14, // XML stored as CLOB-like + _ => 0, + } + } +} + +/// Column metadata from a query result. +#[derive(Debug, Clone)] +pub struct Column { + /// Column name. + pub name: String, + /// DM type code. + pub type_code: i32, + /// Type name string (e.g., "INT", "VARCHAR"). + pub type_name: String, + /// Precision for numeric types. + pub precision: u32, + /// Scale for decimal types. + pub scale: i16, + /// Whether the column can be NULL. + pub nullable: bool, + /// Display size. + pub display_size: u32, + /// Table name. + pub table_name: String, + /// Schema name. + pub schema_name: String, + /// LOB tab_id (only set for BLOB/CLOB columns). + pub lob_tab_id: i32, + /// LOB col_id (only set for BLOB/CLOB columns). + pub lob_col_id: i16, +} + +/// A single row of data from a query result. +#[derive(Debug, Clone)] +pub struct Row { + /// Row ID from the database. + pub row_id: u16, + /// Column values as raw bytes. + pub values: Vec>>, +} + +impl Row { + /// Get an i32 value at the given column index. + pub fn get_i32(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.len() < 4 { + if val.len() == 1 { + return Ok(val[0] as i32); + } + if val.len() == 2 { + return Ok(i32::from(i16::from_le_bytes([val[0], val[1]]))); + } + return Err(crate::error::Error::DecodeError(format!( + "column {} too short for i32 ({} bytes)", + idx, + val.len() + ))); + } + Ok(i32::from_le_bytes([val[0], val[1], val[2], val[3]])) + } + + /// Get an i64 value at the given column index. + pub fn get_i64(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.len() < 8 { + if val.len() >= 4 { + return Ok(i64::from(i32::from_le_bytes([ + val[0], val[1], val[2], val[3], + ]))); + } + return Err(crate::error::Error::DecodeError(format!( + "column {} too short for i64", + idx + ))); + } + Ok(i64::from_le_bytes([ + val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7], + ])) + } + + /// Get a &str value at the given column index. + /// + /// For text types (VARCHAR, CHAR, CLOB) this reads UTF-8 directly. + pub fn get_str(&self, idx: usize) -> Result<&str> { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + std::str::from_utf8(val) + .map_err(|e| crate::error::Error::DecodeError(format!("invalid UTF-8: {}", e))) + } + + /// Get a f64 value at the given column index. + pub fn get_f64(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.len() < 8 { + return Err(crate::error::Error::DecodeError(format!( + "column {} too short for f64", + idx + ))); + } + let bytes: [u8; 8] = val[..8].try_into().unwrap(); + Ok(f64::from_le_bytes(bytes)) + } + + /// Check if the value at the given column index is NULL. + pub fn is_null(&self, idx: usize) -> bool { + match self.values.get(idx) { + None | Some(None) => true, + Some(Some(v)) => v.is_empty(), + } + } + + /// Get a TIMESTAMP value at the given column index as a human-readable string. + /// + /// DM encodes TIMESTAMP as 11 bytes: year(2 BE) + month(1) + day(1) + hour(1) + minute(1) + second(1) + nanosecond(4 BE). + /// Falls back to UTF-8/lossy if the data doesn't match binary format. + pub fn get_timestamp(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + + if val.len() == 11 { + let year = u16::from_be_bytes([val[0], val[1]]) as i32; + let month = val[2]; + let day = val[3]; + let hour = val[4]; + let minute = val[5]; + let second = val[6]; + let nano = u32::from_be_bytes([val[7], val[8], val[9], val[10]]); + if nano > 0 { + Ok(format!( + "{}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}", + year, month, day, hour, minute, second, nano + )) + } else { + Ok(format!( + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + year, month, day, hour, minute, second + )) + } + } else if val.len() == 7 { + // DATE format: year(2 BE) + month(1) + day(1) + hour(1) + minute(1) + second(1) + let year = u16::from_be_bytes([val[0], val[1]]) as i32; + let month = val[2]; + let day = val[3]; + let hour = val[4]; + let minute = val[5]; + let second = val[6]; + Ok(format!( + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + year, month, day, hour, minute, second + )) + } else { + // Fallback: UTF-8 or lossy via get_string + self.get_string(idx) + } + } + + /// Get a DATE value at the given column index as a human-readable string. + pub fn get_date(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + + if val.len() == 7 { + let year = u16::from_be_bytes([val[0], val[1]]) as i32; + let month = val[2]; + let day = val[3]; + let hour = val[4]; + let minute = val[5]; + let second = val[6]; + Ok(format!( + "{}-{:02}-{:02} {:02}:{:02}:{:02}", + year, month, day, hour, minute, second + )) + } else { + self.get_string(idx) + } + } + + /// Get the number of columns in this row. + pub fn len(&self) -> usize { + self.values.len() + } + + /// Check if the row has no columns. + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Get a decoded DmValue at the given column index. + /// Uses the column type_code to decode the raw bytes. + pub fn get(&self, idx: usize, columns: &[Column]) -> Option { + let data = self.values.get(idx)?.as_ref()?; + if data.is_empty() { + return Some(DmValue::Null); + } + let col = columns.get(idx)?; + let dm_ty = DmValueType::from_type_code(col.type_code)?; + dameng_types::decode_value( + dm_ty, + data, + matches!(dm_ty, DmValueType::BLOB | DmValueType::CLOB) + .then_some((col.lob_tab_id, col.lob_col_id)), + ) + } + + /// Get an i16 value at the given column index. + pub fn get_i16(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.len() < 2 { + if val.len() == 1 { + return Ok(val[0] as i16); + } + return Err(crate::error::Error::DecodeError(format!( + "column {} too short for i16", + idx + ))); + } + Ok(i16::from_le_bytes([val[0], val[1]])) + } + + /// Get an i8 value at the given column index. + pub fn get_i8(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.is_empty() { + return Err(crate::error::Error::DecodeError(format!( + "column {} is NULL", + idx + ))); + } + Ok(val[0] as i8) + } + + /// Get a f32 value at the given column index. + pub fn get_f32(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + if val.len() < 4 { + return Err(crate::error::Error::DecodeError(format!( + "column {} too short for f32", + idx + ))); + } + Ok(f32::from_le_bytes([val[0], val[1], val[2], val[3]])) + } + + /// Get raw bytes at the given column index. + pub fn get_bytes(&self, idx: usize) -> Result> { + match self.values.get(idx) { + Some(Some(v)) => Ok(v.clone()), + Some(None) => Ok(vec![]), + None => Err(crate::error::Error::DecodeError(format!( + "column {} out of range", + idx + ))), + } + } + + /// Get an Option at the given column index (NULL-safe). + pub fn get_opt_i32(&self, idx: usize) -> Result> { + match self.values.get(idx) { + Some(Some(v)) if !v.is_empty() => Ok(Some(self.get_i32(idx)?)), + _ => Ok(None), + } + } + + /// Get an Option at the given column index (NULL-safe). + pub fn get_opt_i64(&self, idx: usize) -> Result> { + match self.values.get(idx) { + Some(Some(v)) if !v.is_empty() => Ok(Some(self.get_i64(idx)?)), + _ => Ok(None), + } + } + + /// Get an Option<&str> at the given column index (NULL-safe). + pub fn get_opt_str(&self, idx: usize) -> Result> { + match self.values.get(idx) { + Some(Some(v)) if !v.is_empty() => { + Ok(Some(std::str::from_utf8(v).map_err(|e| { + crate::error::Error::DecodeError(format!("invalid UTF-8: {}", e)) + })?)) + } + _ => Ok(None), + } + } + + /// Get an Option at the given column index (NULL-safe). + pub fn get_opt_f64(&self, idx: usize) -> Result> { + match self.values.get(idx) { + Some(Some(v)) if !v.is_empty() => Ok(Some(self.get_f64(idx)?)), + _ => Ok(None), + } + } + + /// Get an owned String at the given column index (uses lossy UTF-8 fallback). + /// + /// Unlike `get_str()` which returns a borrowed `&str` and fails on invalid UTF-8, + /// this method always succeeds by replacing invalid sequences with U+FFFD. + /// Useful for binary-ish data or server status strings. + pub fn get_string(&self, idx: usize) -> Result { + let val = self.values.get(idx).and_then(|v| v.as_ref()).ok_or( + crate::error::Error::DecodeError(format!("column {} is NULL or out of range", idx)), + )?; + Ok(String::from_utf8_lossy(val).into_owned()) + } + + /// Placeholder: find column index by name (case-insensitive match). + pub fn column_index(&self, _columns: &[Column]) -> usize { + 0 + } +} + +/// Server->Client EXEC_RESPONSE (type 0). +#[derive(Debug, Clone)] +pub struct ExecResponse { + /// Number of columns in the result. + pub col_count: u16, + /// Number of rows returned. + pub row_count: u32, + /// Column metadata. + pub columns: Vec, + /// Row data (column-major order). + pub rows: Vec, +} + +/// Decode DM binary DECIMAL to text representation. +fn decode_dm_decimal_to_text(data: &[u8], _scale: i16) -> Option { + const FLAG_ZERO: u8 = 0x80; + const FLAG_POSITIVE: i32 = 0xC1; + const FLAG_NEGTIVE: i32 = 0x3E; + const NUM_POSITIVE: i32 = 1; + const NUM_NEGTIVE: i32 = 101; + if data.is_empty() || data.len() > 21 { + return None; + } + if data[0] == FLAG_ZERO || data.len() == 1 { + return Some("0".to_string()); + } + let is_positive = data[0] & FLAG_ZERO != 0; + let flag = data[0] as i32; + let exponent = if is_positive { + flag - FLAG_POSITIVE + } else { + FLAG_NEGTIVE - flag + }; + let mut digits = Vec::with_capacity(data.len() - 1); + for &b in &data[1..] { + let digit = if is_positive { + b as i32 - NUM_POSITIVE + } else { + NUM_NEGTIVE - b as i32 + }; + if digit < 0 || digit > 99 { + break; + } + digits.push(digit); + } + if digits.is_empty() { + return None; + } + + let decimal_group = exponent + 1; + let mut value = String::new(); + if !is_positive { + value.push('-'); + } + if decimal_group <= 0 { + value.push_str("0."); + for _ in 0..-decimal_group { + value.push_str("00"); + } + for digit in digits { + value.push_str(&format!("{digit:02}")); + } + } else { + for index in 0..decimal_group as usize { + if index < digits.len() { + if index == 0 { + value.push_str(&digits[index].to_string()); + } else { + value.push_str(&format!("{:02}", digits[index])); + } + } else { + value.push_str("00"); + } + } + if (decimal_group as usize) < digits.len() { + value.push('.'); + for digit in &digits[decimal_group as usize..] { + value.push_str(&format!("{digit:02}")); + } + } + } + if value.contains('.') { + while value.ends_with('0') { + value.pop(); + } + if value.ends_with('.') { + value.pop(); + } + } + Some(value) +} + +impl ExecResponse { + /// Parse from raw payload bytes. + /// + /// Supports both EXEC (type 5) metadata-only responses and + /// OPTIMIZED_PREPARE_EXEC (type 91) responses with inline row data. + pub fn from_bytes(data: &[u8], server_encoding: ServerEncoding) -> Result { + if data.len() < 16 { + return Err(crate::error::Error::Incomplete); + } + + // === Fixed Header (16 bytes) === + let sub_type = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + let _flags = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + let _reserved = u32::from_le_bytes([data[8], data[9], data[10], data[11]]); + let header_row_count = u32::from_le_bytes([data[12], data[13], data[14], data[15]]); + + // If data is too short for first column header, return what we can. + // For DML (INSERT/UPDATE/DELETE) the affected row count is in the fixed + // 16-byte header — so we still extract it even without column metadata. + if data.len() < 32 { + return Ok(ExecResponse { + col_count: 0, + row_count: header_row_count, + columns: vec![], + rows: vec![], + }); + } + + // === First Column Header (16 bytes, offset 16) === + // Note: first_col_type at offset 16 is unreliable on DM 8.1 — always returns 4 (INT). + // We derive the correct type_code from the type_name string instead. + let _first_col_type = i32::from_le_bytes([data[16], data[17], data[18], data[19]]); + let first_nullable = u16::from_le_bytes([data[20], data[21]]); + let col_count = u16::from_le_bytes([data[22], data[23]]); + let col_name_len = u16::from_le_bytes([data[24], data[25]]) as usize; + let type_name_len = u16::from_le_bytes([data[26], data[27]]) as usize; + let table_name_len = u16::from_le_bytes([data[28], data[29]]) as usize; + let schema_name_len = u16::from_le_bytes([data[30], data[31]]) as usize; + + let mut columns = Vec::with_capacity(col_count.max(1) as usize); + let mut offset = 32; // Column variable data starts at 32 + + // When col_count > 0, parse first column from the compact 16-byte header. + // When col_count == 0 (BIND_EXEC2 path), ALL columns use the expanded 32-byte + // format starting at offset 16 — skip the compact header parsing entirely. + if col_count > 0 { + // col_name (explicit length from col_name_len field at offset 24-25) + let col_name = if col_name_len > 0 && offset + col_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + col_name_len]) + } else { + String::new() + }; + offset += col_name_len; + + // type_name + let type_name = if type_name_len > 0 && offset + type_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + type_name_len]) + } else { + String::new() + }; + offset += type_name_len; + + // table_name + let table_name = if table_name_len > 0 && offset + table_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + table_name_len]) + } else { + String::new() + }; + offset += table_name_len; + + // schema_name + let schema_name = if schema_name_len > 0 && offset + schema_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + schema_name_len]) + } else { + String::new() + }; + offset += schema_name_len; + + // Skip null terminator after first col strings (if present) + if offset < data.len() && data[offset] == 0 { + offset += 1; + } + + // Always derive type_code from type_name — the header field (offset 16) + // is unreliable on DM 8.1 (often returns 4/INT for all types). + let actual_type_code = type_name_to_code(&type_name); + + columns.push(Column { + name: col_name, + type_code: actual_type_code, + type_name, + precision: 0, + scale: 0, + nullable: first_nullable != 0, + display_size: 0, + table_name, + schema_name, + lob_tab_id: 0, + lob_col_id: 0, + }); + } + + // === Subsequent Columns === + // OPE(91) may report col_count=1 even for multi-column queries regardless + // of the response sub-type, so parse columns dynamically until row data. + // + // When col_count == 0 (BIND_EXEC2 path for SELECT with params), the server + // sends NO inline column metadata or row data — the data must be fetched + // via FETCH protocol. In this case skip dynamic parsing entirely. + // + // Verified against DM 8.1.3.62 wire protocol: + // First column: 16-byte compact header (already parsed above) + // Subsequent columns: 32-byte expanded header (NO gap between columns): + // 0 u32 col_type (LE) + // 4 u32 precision (LE) + // 8 u32 scale (LE) + // 12 u32 nullable_flags (LE) + // 16 u32 reserved (LE) + // 20 u16 reserved + // 22 u16 col_index (?) + // 24 u16 name_len + // 26 u16 type_name_len + // 28 u16 table_name_len + // 30 u16 schema_name_len + // 32 [col_name][type_name][table_name][schema_name] (no null terminator) + let use_dynamic = col_count > 0; + // When col_count == 0 (BIND_EXEC2 SELECT path), all columns use the + // expanded 32-byte format starting at offset 16 (no compact first-column header). + // We reuse the dynamic parser logic with col_count as the parsed count. + let max_cols = if use_dynamic { + 4_096 + } else { + (col_count as usize).max(columns.len()) + }; + let mut _parsed_cols = columns.len() as usize; + // For BIND_EXEC2 (col_count == 0), start parsing at offset 16 (right after header), + // reusing the expanded column parser loop below. + if col_count == 0 { + _parsed_cols = 0; + offset = 16; + } + let mut parsed_cols = 1; + while parsed_cols < max_cols { + // Save position before attempting to parse next column. + // If we don't find a valid column header, row data starts here. + let row_start = offset; + + // Subsequent column header is 32 bytes + if offset + 32 > data.len() { + offset = row_start; + break; + } + + let header_off = offset; + + // Compact row format marker — row data starts here + if data[header_off] == 0x0C { + offset = row_start; + break; + } + + // Expanded 32-byte header for subsequent columns + let _c_type = i32::from_le_bytes([ + data[header_off], + data[header_off + 1], + data[header_off + 2], + data[header_off + 3], + ]); + // If c_type is 0 or invalid, we've hit row data — stop parsing columns + if !(1..=31).contains(&_c_type) { + offset = row_start; + break; + } + let c_precision = u32::from_le_bytes([ + data[header_off + 4], + data[header_off + 5], + data[header_off + 6], + data[header_off + 7], + ]); + let c_scale = i32::from_le_bytes([ + data[header_off + 8], + data[header_off + 9], + data[header_off + 10], + data[header_off + 11], + ]); + if c_precision > 1_000_000 || !(-1_000..=1_000).contains(&c_scale) { + offset = row_start; + break; + } + let c_nullable = u32::from_le_bytes([ + data[header_off + 12], + data[header_off + 13], + data[header_off + 14], + data[header_off + 15], + ]); + // reserved at offsets 16-23 (4 bytes + 2 u16) + + // Length fields at offsets 24-31 + let c_name_len = + u16::from_le_bytes([data[header_off + 24], data[header_off + 25]]) as usize; + let c_type_name_len = + u16::from_le_bytes([data[header_off + 26], data[header_off + 27]]) as usize; + let c_table_len = + u16::from_le_bytes([data[header_off + 28], data[header_off + 29]]) as usize; + let c_schema_len = + u16::from_le_bytes([data[header_off + 30], data[header_off + 31]]) as usize; + + // Validate lengths — if unreasonable, we've hit row data + if c_name_len == 0 + || c_type_name_len == 0 + || c_name_len > 128 + || c_type_name_len > 128 + || c_table_len > 128 + || c_schema_len > 128 + { + offset = row_start; + break; + } + + // Strings start at header_off + 32 + offset = header_off + 32; + let c_name = if c_name_len > 0 && offset + c_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + c_name_len]) + } else { + offset = row_start; + break; + }; + offset += c_name_len; + + let c_type_name = if c_type_name_len > 0 && offset + c_type_name_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + c_type_name_len]) + } else { + String::new() + }; + offset += c_type_name_len; + + let c_table = if c_table_len > 0 && offset + c_table_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + c_table_len]) + } else { + String::new() + }; + offset += c_table_len; + + let c_schema = if c_schema_len > 0 && offset + c_schema_len <= data.len() { + decode_from_server(server_encoding, &data[offset..offset + c_schema_len]) + } else { + String::new() + }; + offset += c_schema_len; + + // For sub_type=7, the 32-byte header c_type field is unreliable for + // subsequent columns (e.g., VARCHAR returns 2 instead of 3). + // Always derive from type_name string instead. + let actual_c_type = type_name_to_code(&c_type_name); + + // Read itemFlag (offset 16-17 within the 32-byte header) to detect LOB columns. + // itemFlag bits: 0x01=identity, 0x02=lob, 0x04=readonly + let item_flag = u16::from_le_bytes([data[header_off + 16], data[header_off + 17]]); + let is_lob = (item_flag & 0x02) != 0; + + // For LOB columns, DM appends lobTabId (i32 LE) + lobColId (i16 LE) after the strings. + let (c_lob_tab_id, c_lob_col_id) = if is_lob && offset + 6 <= data.len() { + let tab_id = i32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + offset += 4; + let col_id = i16::from_le_bytes([data[offset], data[offset + 1]]); + offset += 2; + (tab_id, col_id) + } else { + (0, 0) + }; + + columns.push(Column { + name: c_name, + type_code: actual_c_type, + type_name: c_type_name, + precision: 0, + scale: 0, + nullable: c_nullable != 0, + display_size: 0, + table_name: c_table, + schema_name: c_schema, + lob_tab_id: c_lob_tab_id, + lob_col_id: c_lob_col_id, + }); + parsed_cols += 1; + } + + // === Inline Row Data (OPE responses only) === + // Two row formats depending on sub_type: + // sub_type=2: compact format (V$VERSION style) - marker(1)+flags(1)+val_size(2)+value(N) + // sub_type=7: full format (SELECT style) - row_hdr+col_offsets+values + let mut rows = Vec::new(); + if columns.is_empty() { + return Ok(Self { + col_count, + row_count: header_row_count, + columns, + rows, + }); + } else if sub_type == 2 && columns.len() == 1 { + // Compact row format (V$VERSION style): + // Each row: marker(0x0C) + flags(1) + val_size(2) + value(N) + padding + while offset + 4 <= data.len() && data[offset] != 0x0C { + offset += 1; + } + while offset + 4 <= data.len() && data[offset] == 0x0C { + let row_start = offset; + let _flags = data[offset + 1]; + let val_size = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize; + if val_size == 0 || offset + 4 + val_size > data.len() { + break; + } + let value_bytes = data[offset + 4..offset + 4 + val_size].to_vec(); + let next_scan = offset + 4 + val_size; + let mut found = false; + for scan in next_scan..data.len() { + if data[scan] == 0x0C { + offset = scan; + found = true; + break; + } + } + if !found { + offset = data.len(); + } + let mut values = Vec::with_capacity(columns.len()); + values.push(Some(value_bytes)); + for _ in 1..columns.len() { + values.push(None); + } + // Decode string columns from server encoding, DECIMAL to text + for ci in 0..columns.len().min(values.len()) { + if matches!(columns[ci].type_code, 3 | 14 | 16 | 23) { + if let Some(ref val_bytes) = values[ci] { + let decoded = decode_from_server(server_encoding, val_bytes); + values[ci] = Some(decoded.into_bytes()); + } + } else if matches!(columns[ci].type_code, 9 | 20) { + if let Some(ref val_bytes) = values[ci] { + if let Some(text) = + decode_dm_decimal_to_text(val_bytes, columns[ci].scale) + { + values[ci] = Some(text.into_bytes()); + } + } + } + } + rows.push(Row { + row_id: row_start as u16, + values, + }); + } + } else { + // Full row format (sub_type=7 and others) + // CRITICAL: The first byte (row_size) does NOT represent actual row length. + // DM 8.1.3.62: row_size=0x23=35 but actual row data spans ~50 bytes. + // Instead, calculate true row end from the column value offsets + sizes. + while offset + 10 <= data.len() { + let row_start = offset; + let _row_size = data[offset]; // Present but unreliable for advancement + let _flags = data[offset + 1]; + let rec_id = u32::from_le_bytes([ + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + ]); + + // Column offset table: col_count x 2 bytes, starting at row_start + 10 + let offsets_start = row_start + 10; + let col_offsets: Vec = (0..columns.len()) + .map(|c| { + let o = offsets_start + c * 2; + if o + 2 <= data.len() { + u16::from_le_bytes([data[o], data[o + 1]]) + } else { + 0 + } + }) + .collect(); + + // Parse values and track the furthest byte consumed + let mut values = Vec::with_capacity(columns.len()); + let mut row_end = offsets_start + columns.len() * 2; + for (_ci, col_off) in col_offsets.iter().enumerate() { + let val_abs = row_start + *col_off as usize; + if val_abs + 2 > data.len() { + values.push(None); + continue; + } + let val_size = u16::from_le_bytes([data[val_abs], data[val_abs + 1]]) as usize; + if val_size == 0 { + values.push(None); + } else if val_abs + 2 + val_size <= data.len() { + values.push(Some(data[val_abs + 2..val_abs + 2 + val_size].to_vec())); + let val_end = val_abs + 2 + val_size; + if val_end > row_end { + row_end = val_end; + } + } else { + values.push(None); + } + } + + offset = row_end; + let reported = _row_size as usize; + if reported > 0 && row_start + reported > offset { + offset = row_start + reported; + } + // Decode string columns from server encoding, DECIMAL to text + for ci in 0..columns.len().min(values.len()) { + if matches!(columns[ci].type_code, 3 | 14 | 16 | 23) { + if let Some(ref val_bytes) = values[ci] { + let decoded = decode_from_server(server_encoding, val_bytes); + values[ci] = Some(decoded.into_bytes()); + } + } else if matches!(columns[ci].type_code, 9 | 20) { + if let Some(ref val_bytes) = values[ci] { + if let Some(text) = + decode_dm_decimal_to_text(val_bytes, columns[ci].scale) + { + values[ci] = Some(text.into_bytes()); + } + } + } + } + rows.push(Row { + row_id: rec_id as u16, + values, + }); + } + } + + Ok(Self { + col_count, + row_count: header_row_count, + columns, + rows, + }) + } + + /// Helper to safely read u32 LE. + #[allow(dead_code)] + fn safe_u32(data: &[u8], offset: usize) -> u32 { + if offset + 4 <= data.len() { + u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) + } else { + 0 + } + } + + /// Check if this response contains result rows. + pub fn has_rows(&self) -> bool { + !self.rows.is_empty() + } + + /// Get the number of columns. + pub fn num_columns(&self) -> usize { + self.columns.len() + } + + /// Get the number of rows. + pub fn num_rows(&self) -> usize { + self.rows.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_row_get_i32() { + let row = Row { + row_id: 0, + values: vec![Some(vec![1, 0, 0, 0])], + }; + assert_eq!(row.get_i32(0).unwrap(), 1); + } + + #[test] + fn test_row_get_i32_single_byte() { + let row = Row { + row_id: 0, + values: vec![Some(vec![42])], + }; + assert_eq!(row.get_i32(0).unwrap(), 42); + } + + #[test] + fn test_row_get_str() { + let row = Row { + row_id: 0, + values: vec![Some(b"hello".to_vec())], + }; + assert_eq!(row.get_str(0).unwrap(), "hello"); + } + + #[test] + fn test_row_is_null() { + let row = Row { + row_id: 0, + values: vec![None, Some(vec![1, 2, 3])], + }; + assert!(row.is_null(0)); + assert!(!row.is_null(1)); + } + + #[test] + fn test_row_len() { + let row = Row { + row_id: 0, + values: vec![Some(vec![1]), Some(vec![2]), Some(vec![3])], + }; + assert_eq!(row.len(), 3); + assert!(!row.is_empty()); + } + + #[test] + fn test_row_get_i64() { + let row = Row { + row_id: 0, + values: vec![Some(vec![42, 0, 0, 0, 0, 0, 0, 0])], + }; + assert_eq!(row.get_i64(0).unwrap(), 42); + } + + #[test] + fn test_exec_response_has_rows() { + let resp = ExecResponse { + col_count: 0, + row_count: 0, + columns: vec![], + rows: vec![], + }; + assert!(!resp.has_rows()); + } + + #[test] + fn test_exec_response_minimal_empty() { + // Valid empty response: header(16) + col_header(16) + null_terminator(1) + let data = [ + 0x07, 0x00, 0x00, 0x00, // sub_type + 0x04, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // row_count = 0 + 0x00, 0x00, 0x00, 0x00, // col_type = 0 + 0x00, 0x00, // nullable + 0x00, 0x00, // display + 0x00, 0x00, // col_count = 0 + 0x00, 0x00, // type_name_len + 0x00, 0x00, // table_name_len + 0x00, 0x00, // schema_name_len + ]; + let resp = ExecResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + assert_eq!(resp.col_count, 0); + assert_eq!(resp.num_columns(), 0); + assert_eq!(resp.num_rows(), 0); + } + + #[test] + fn test_exec_response_select1_ope() { + // OPE response for "SELECT 1 FROM DUAL" (58 bytes) + let data: Vec = vec![ + 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, // header (row_count=1) + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, + 0x00, // col1 header (type=4, nullable=0, col_count=1, col_name_len=1, type_name_len=7) + 0x31, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, + 0x00, // col1 strings: "1" + "INTEGER" + \0 + // Row data (18 bytes): marker=18, flags=0, rec_id=0, padding=0, col_off=12, val_size=4, val=1 + 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + let resp = ExecResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + assert_eq!(resp.col_count, 1); + assert_eq!(resp.num_columns(), 1); + assert_eq!(resp.num_rows(), 1); + assert_eq!(resp.columns[0].name, "1"); + assert_eq!(resp.columns[0].type_name, "INTEGER"); + assert_eq!(resp.columns[0].type_code, 4); + assert_eq!(resp.rows[0].get_i32(0).unwrap(), 1); + } + + #[test] + fn test_exec_response_select1_ope_no_null_term() { + // Actual OPE response from DM 8.1.3.62 - no \0 terminator (58 bytes) + let data: Vec = vec![ + 0x07, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, // header (row_count=1) + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, // col1 header (type=4, nullable=0, col_count=1, col_name_len=1) + // Strings: "1" + "INTEGER" (no \0 terminator!) + 0x31, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, + // Row data: marker=18, flags=0, rec_id=0, padding=0, col_off=12, val_size=4, val=1 + 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x00, 0x00, + ]; + let resp = ExecResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + assert_eq!(resp.col_count, 1); + assert_eq!(resp.num_columns(), 1); + assert_eq!(resp.num_rows(), 1); + assert_eq!(resp.columns[0].name, "1"); + assert_eq!(resp.columns[0].type_name, "INTEGER"); + assert_eq!(resp.columns[0].type_code, 4); + assert_eq!(resp.rows[0].get_i32(0).unwrap(), 1); + } + + #[test] + fn test_exec_response_subtype2_with_multiple_columns() { + // Captured from DM8 for SELECT NAME, NAME FROM a one-row table. DM reports + // col_count=1 even though a second expanded column header follows. + let data: Vec = vec![ + 0x02, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x07, 0x00, + 0x11, 0x00, 0x06, 0x00, 0x4e, 0x41, 0x4d, 0x45, 0x56, 0x41, 0x52, 0x43, 0x48, 0x41, + 0x52, 0x54, 0x41, 0x42, 0x4c, 0x45, 0x50, 0x52, 0x4f, 0x5f, 0x4c, 0x4f, 0x42, 0x5f, + 0x54, 0x45, 0x53, 0x54, 0x53, 0x59, 0x53, 0x44, 0x42, 0x41, 0x02, 0x00, 0x00, 0x00, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x07, 0x00, 0x11, 0x00, 0x06, 0x00, + 0x4e, 0x41, 0x4d, 0x45, 0x56, 0x41, 0x52, 0x43, 0x48, 0x41, 0x52, 0x54, 0x41, 0x42, + 0x4c, 0x45, 0x50, 0x52, 0x4f, 0x5f, 0x4c, 0x4f, 0x42, 0x5f, 0x54, 0x45, 0x53, 0x54, + 0x53, 0x59, 0x53, 0x44, 0x42, 0x41, 0x1c, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0e, 0x00, 0x15, 0x00, 0x05, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x05, + 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, + ]; + + let resp = ExecResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + + assert_eq!(resp.col_count, 1); + assert_eq!(resp.num_columns(), 2); + assert_eq!(resp.num_rows(), 1); + assert_eq!(resp.rows[0].get_str(0).unwrap(), "hello"); + assert_eq!(resp.rows[0].get_str(1).unwrap(), "hello"); + } + + #[test] + fn test_decode_dm_decimal_to_text_honors_exponent() { + assert_eq!( + decode_dm_decimal_to_text(&[0xc2, 0x02], 0).as_deref(), + Some("100") + ); + assert_eq!( + decode_dm_decimal_to_text(&[0xc2, 0x52, 0x59], 0).as_deref(), + Some("8188") + ); + assert_eq!( + decode_dm_decimal_to_text(&[0xc1, 0x02, 0x18], 0).as_deref(), + Some("1.23") + ); + assert_eq!( + decode_dm_decimal_to_text(&[0x3e, 0x64, 0x4e, 0x66], 0).as_deref(), + Some("-1.23") + ); + } + + #[test] + fn test_exec_response_incomplete() { + let data = [0x00, 0x00, 0x00]; + let result = ExecResponse::from_bytes(&data, ServerEncoding::Utf8); + assert!(matches!(result, Err(crate::error::Error::Incomplete))); + } + + #[test] + fn test_exec_response_short_payload_dml() { + // INSERT/UPDATE/DELETE response with only 16-byte header (no column metadata). + // The affected row count must still be extracted from the header. + let data = [ + 0x07, 0x00, 0x00, 0x00, // sub_type + 0x04, 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // reserved + 0x01, 0x00, 0x00, 0x00, // row_count = 1 (affected rows) + ]; + let resp = ExecResponse::from_bytes(&data, ServerEncoding::Utf8).unwrap(); + assert_eq!(resp.row_count, 1); + assert_eq!(resp.col_count, 0); + assert_eq!(resp.num_columns(), 0); + assert_eq!(resp.num_rows(), 0); + } + + #[test] + fn test_row_get_i32_null() { + let row = Row { + row_id: 0, + values: vec![None], + }; + assert!(row.get_i32(0).is_err()); + assert!(row.is_null(0)); + } + + #[test] + fn test_row_get_str_empty() { + let row = Row { + row_id: 0, + values: vec![Some(vec![])], + }; + let result = row.get_str(0).unwrap(); + assert_eq!(result, ""); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/startup.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/startup.rs new file mode 100644 index 000000000..253e39f2f --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/startup.rs @@ -0,0 +1,276 @@ +//! STARTUP message (type 200) and STARTUP_RESPONSE (type 228). +//! +//! Reverse-engineered from captured wire protocol traffic of the official +//! Python dmPython driver. See scripts/proxy.py and capture logs. + +use bytes::{BufMut, BytesMut}; + +use crate::error::Result; + +/// Client->Server STARTUP message (type 200). +/// +/// Wire format (captured from working Python driver): +/// ```text +/// Offset Size Field +/// 0 4 Driver version string length (i32 LE) +/// 4 N Driver version string (UTF-8, e.g. "8.1.1.126") +/// N+4 1 Null terminator (0x00) +/// N+5 4 Encryption key length (i32 LE, always 64) +/// N+9 64 Encryption key bytes (client-generated random) +/// ``` +#[derive(Debug, Clone)] +pub struct StartupMessage { + /// Driver version string (e.g. "8.1.1.126" or "7.6.0.0"). + pub driver_version: String, + /// 64-byte encryption key. + pub encryption_key: [u8; 64], +} + +impl StartupMessage { + /// Create a new startup message with default values. + pub fn new() -> Self { + // Generate random-looking key bytes (XOR pattern to avoid all-zeros) + let mut key = [0u8; 64]; + for i in 0..64 { + key[i] = ((i * 7 + 13) & 0xFF) as u8; + } + Self { + driver_version: "7.6.0.0".to_string(), + encryption_key: key, + } + } + + /// Encode to payload bytes. + pub fn encode_payload(&self) -> BytesMut { + let ver_bytes = self.driver_version.as_bytes(); + let key_len = self.encryption_key.len(); + let total = 4 + ver_bytes.len() + 1 + 4 + key_len; + let mut buf = BytesMut::with_capacity(total); + + // i32 LE: version string length + buf.put_i32_le(ver_bytes.len() as i32); + // Version string bytes + buf.put_slice(ver_bytes); + // Null terminator + buf.put_u8(0); + // i32 LE: encryption key length (always 64) + buf.put_i32_le(key_len as i32); + // 64 bytes of encryption key + buf.put_slice(&self.encryption_key); + + buf + } +} + +/// Server->Client STARTUP_RESPONSE message (type 228 for success, type 187 for error). +/// +/// Wire format (captured from working Python driver): +/// ```text +/// Offset Size Field +/// 0 16 Reserved (zeros) +/// 16 4 Server version string length (i32 LE) +/// 20 N Server version string (UTF-8, e.g. "8.1.3.62") +/// 20+N 4 Padding/sentinel (i32 LE, usually -1) +/// 24+N 4 Challenge length (i32 LE, always 64) +/// 28+N 64 Challenge/encryption key bytes +/// 92+N var Additional server data +/// ``` +#[derive(Debug, Clone)] +pub struct StartupResponse { + /// Server encoding (1=UTF-8, 2=GB18030). + pub encoding: u8, + /// Server challenge for encryption (48-64 bytes). + pub challenge: Vec, + /// Server version string. + pub server_version: String, + /// Server encryption public key. + pub encryption_key: Vec, + /// Response code from server frame header. + pub response_code: i32, + /// Session ID from server. + pub session_id: u32, +} + +impl StartupResponse { + /// Parse from payload bytes and response code from the frame header. + pub fn from_bytes(data: &[u8], response_code: i32) -> Result { + // Check for error response (negative response code) + if response_code < 0 { + let mut server_version = String::new(); + if data.len() >= 12 { + let msg_len = u32::from_le_bytes([ + data[8].min(255), + data.get(9).copied().unwrap_or(0), + data.get(10).copied().unwrap_or(0), + data.get(11).copied().unwrap_or(0), + ]) as usize; + if data.len() > 12 + msg_len { + server_version = String::from_utf8_lossy(&data[12..12 + msg_len]).to_string(); + } + } + return Ok(Self { + encoding: 0, + challenge: vec![], + server_version, + encryption_key: vec![], + response_code, + session_id: 0, + }); + } + + // Parse successful startup response + let mut server_version = String::new(); + let mut challenge = Vec::new(); + let encryption_key = Vec::new(); + let mut encoding = 1u8; // default UTF-8 + let session_id = 0u32; + + if data.len() >= 20 { + // Server version string length at offset 16 + let ver_len = u32::from_le_bytes([ + data[16], + data.get(17).copied().unwrap_or(0), + data.get(18).copied().unwrap_or(0), + data.get(19).copied().unwrap_or(0), + ]) as usize; + + let ver_start = 20; + if ver_len > 0 && data.len() > ver_start + ver_len { + server_version = + String::from_utf8_lossy(&data[ver_start..ver_start + ver_len]).to_string(); + } + + // After version: sentinel (-1), then key length (64) + let after_ver = ver_start + ver_len; + if after_ver + 8 <= data.len() { + // Key length at after_ver+4 + let key_len = u32::from_le_bytes([ + data[after_ver + 4], + data.get(after_ver + 5).copied().unwrap_or(0), + data.get(after_ver + 6).copied().unwrap_or(0), + data.get(after_ver + 7).copied().unwrap_or(0), + ]) as usize; + + let key_start = after_ver + 8; + if key_len > 0 && data.len() > key_start + key_len.min(64) { + challenge = data[key_start..key_start + key_len.min(64)].to_vec(); + } + } + } + + // Try to extract encoding from the payload + // From the capture, encoding seems to be embedded in the response + // For now, default to UTF-8 + if data.len() >= 44 { + // Encoding might be at specific offsets in the response + encoding = 1; // UTF-8 + } + + Ok(Self { + encoding, + challenge, + server_version, + encryption_key, + response_code, + session_id, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_startup_encode_default_version() { + let msg = StartupMessage::new(); + let payload = msg.encode_payload(); + // 4 (len) + 7 (ver) + 1 (null) + 4 (key_len) + 64 (key) = 80 + assert_eq!(payload.len(), 80); + + // Verify version length field + assert_eq!( + i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]), + 7 + ); + // Verify version string + assert_eq!(&payload[4..11], b"7.6.0.0"); + // Verify null terminator + assert_eq!(payload[11], 0); + // Verify key length + assert_eq!( + i32::from_le_bytes([payload[12], payload[13], payload[14], payload[15]]), + 64 + ); + } + + #[test] + fn test_startup_encode_custom_version() { + let msg = StartupMessage { + driver_version: "8.1.1.126".to_string(), + encryption_key: [0xAB; 64], + }; + let payload = msg.encode_payload(); + // 4 + 9 + 1 + 4 + 64 = 82 + assert_eq!(payload.len(), 82); + assert_eq!(&payload[4..13], b"8.1.1.126"); + assert_eq!(payload[13], 0); + assert_eq!(&payload[18..82], &[0xAB; 64]); + } + + #[test] + fn test_startup_response_error() { + let mut data = [0u8; 64]; + data[8] = 28; // msg_len + let msg = b"Fail to establish connection"; + data[12..12 + msg.len()].copy_from_slice(msg); + let resp = StartupResponse::from_bytes(&data, -6003).unwrap(); + assert_eq!(resp.response_code, -6003); + assert_eq!(resp.encoding, 0); + assert_eq!(resp.challenge.len(), 0); + } + + #[test] + fn test_startup_response_invalid_version() { + let mut data = [0u8; 64]; + data[8] = 20; + let msg = b"Invalid client version"; + data[12..12 + msg.len()].copy_from_slice(msg); + let resp = StartupResponse::from_bytes(&data, -118).unwrap(); + assert_eq!(resp.response_code, -118); + assert!(resp.server_version.contains("Invalid")); + } + + #[test] + fn test_startup_response_success() { + let mut data = [0u8; 112]; + // 16 bytes of zeros (reserved) + // Server version length at offset 16 + let ver = b"8.1.3.62"; + data[16] = ver.len() as u8; + data[20..20 + ver.len()].copy_from_slice(ver); + // Sentinel at offset 28: -1 + data[28] = 0xFF; + data[29] = 0xFF; + data[30] = 0xFF; + data[31] = 0xFF; + // Key length at offset 32: 64 + data[32] = 64; + // Challenge bytes at offset 36 + for i in 0..48 { + data[36 + i] = 0xBB; + } + + let resp = StartupResponse::from_bytes(&data, 0).unwrap(); + assert_eq!(resp.server_version, "8.1.3.62"); + assert_eq!(resp.response_code, 0); + } + + #[test] + fn test_startup_key_not_all_zeros() { + let msg = StartupMessage::new(); + // Key should not be all zeros + let non_zero_count = msg.encryption_key.iter().filter(|&&b| b != 0).count(); + assert!(non_zero_count > 0, "encryption key should not be all zeros"); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-protocol/src/message/transaction.rs b/Native/DamengBridge/Vendor/dameng-protocol/src/message/transaction.rs new file mode 100644 index 000000000..92556d3d6 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-protocol/src/message/transaction.rs @@ -0,0 +1,74 @@ +//! Transaction control messages: COMMIT (type 8) and ROLLBACK (type 9). + +use bytes::BytesMut; + +/// Client->Server COMMIT message (type 8). +/// +/// Commits the current transaction. +#[derive(Debug, Clone)] +pub struct CommitMessage; + +impl CommitMessage { + /// Encode to payload bytes (empty payload). + pub fn encode_payload(&self) -> BytesMut { + BytesMut::new() + } +} + +/// Client->Server ROLLBACK message (type 9). +/// +/// Rolls back the current transaction. +#[derive(Debug, Clone)] +pub struct RollbackMessage; + +impl RollbackMessage { + /// Encode to payload bytes (empty payload). + pub fn encode_payload(&self) -> BytesMut { + BytesMut::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_commit_encode_empty() { + let commit = CommitMessage; + let payload = commit.encode_payload(); + assert!(payload.is_empty()); + } + + #[test] + fn test_rollback_encode_empty() { + let rollback = RollbackMessage; + let payload = rollback.encode_payload(); + assert!(payload.is_empty()); + } + + #[test] + fn test_commit_debug() { + let commit = CommitMessage; + let debug_str = format!("{:?}", commit); + assert!(debug_str.contains("CommitMessage")); + } + + #[test] + fn test_rollback_debug() { + let rollback = RollbackMessage; + let debug_str = format!("{:?}", rollback); + assert!(debug_str.contains("RollbackMessage")); + } + + #[test] + fn test_commit_clone() { + let commit = CommitMessage; + let _cloned = commit.clone(); + } + + #[test] + fn test_rollback_clone() { + let rollback = RollbackMessage; + let _cloned = rollback.clone(); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-types/Cargo.toml b/Native/DamengBridge/Vendor/dameng-types/Cargo.toml new file mode 100644 index 000000000..9bd95fab0 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-types/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dameng-types" +version = "0.1.0" +edition = "2021" +description = "Dameng database type definitions and conversions" +license = "MIT" +repository = "https://github.com/rarnu/rust-dameng" +keywords = ["dameng", "database", "types"] +categories = ["database"] + +[dependencies] +bytes = "1" +chrono = "0.4" +rust_decimal = "1" +encoding_rs = "0.8" diff --git a/Native/DamengBridge/Vendor/dameng-types/LICENSE.txt b/Native/DamengBridge/Vendor/dameng-types/LICENSE.txt new file mode 100644 index 000000000..81121d744 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-types/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 指令集 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Native/DamengBridge/Vendor/dameng-types/src/encoding.rs b/Native/DamengBridge/Vendor/dameng-types/src/encoding.rs new file mode 100644 index 000000000..b4d49da37 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-types/src/encoding.rs @@ -0,0 +1,169 @@ +//! Encoding conversion between server encoding (UTF-8 / GB18030) and Rust UTF-8. +//! +//! DM server may use different character encodings. The encoding is determined +//! from the LOGIN_RESPONSE (type=163): +//! - 1: UTF-8 +//! - 2: GB18030 +//! +//! All string data sent to the server must be encoded in the server's encoding. +//! All string data received from the server must be decoded from the server's +//! encoding to Rust's native UTF-8. + +use encoding_rs::{Encoding, GB18030, UTF_8}; + +/// Server-side encoding determined from LOGIN_RESPONSE. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServerEncoding { + /// UTF-8 encoding (encoding value = 1). + Utf8, + /// GB18030 encoding (encoding value = 2). + Gb18030, +} + +impl ServerEncoding { + /// Create from DM protocol encoding value. + /// + /// Per DM Go driver (dm_build_425), the LOGIN_RESPONSE encoding field maps: + /// - 0: GB18030 + /// - 1: UTF-8 + /// - 2: EUC-KR + /// Defaults to GB18030 (matching DM Go driver behavior). + pub fn from_protocol_value(value: u8) -> Self { + match value { + 1 => ServerEncoding::Utf8, + _ => ServerEncoding::Gb18030, // 0, 2, and default → GB18030 + } + } + /// Get the encoding_rs Encoding instance for this server encoding. + pub fn encoding(&self) -> &'static Encoding { + match self { + ServerEncoding::Utf8 => UTF_8, + ServerEncoding::Gb18030 => GB18030, + } + } +} + +/// Convert a UTF-8 string to the server's encoding. +/// +/// Used when sending SQL text or string parameters to the server. +/// If the server uses UTF-8, this is a no-op that returns the input as-is. +/// +/// # Arguments +/// * `server_encoding` - The server's character encoding +/// * `s` - The UTF-8 string to encode +/// +/// # Returns +/// Bytes in the server's encoding. +pub fn encode_to_server(server_encoding: ServerEncoding, s: &str) -> Vec { + let enc = server_encoding.encoding(); + let (result, _, had_errors) = enc.encode(s); + if had_errors { + // Fallback: should not happen for valid UTF-8 input + // If encoding fails, return the raw UTF-8 bytes as a last resort + return s.as_bytes().to_vec(); + } + result.to_vec() +} + +/// Convert bytes from the server's encoding to a UTF-8 String. +/// +/// Used when receiving string data from the server (column values, +/// error messages, etc.). +/// If the server uses UTF-8, this is a no-op. +/// +/// # Arguments +/// * `server_encoding` - The server's character encoding +/// * `data` - Raw bytes from the server +/// +/// # Returns +/// A UTF-8 String. Invalid bytes are replaced with the Unicode +/// replacement character (U+FFFD), matching encoding_rs behavior. +pub fn decode_from_server(server_encoding: ServerEncoding, data: &[u8]) -> String { + let enc = server_encoding.encoding(); + let (result, _, _) = enc.decode(data); + result.into_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_protocol_value_utf8() { + assert_eq!(ServerEncoding::from_protocol_value(1), ServerEncoding::Utf8); + } + + #[test] + fn test_from_protocol_value_gb18030() { + assert_eq!( + ServerEncoding::from_protocol_value(0), + ServerEncoding::Gb18030 + ); + assert_eq!( + ServerEncoding::from_protocol_value(2), + ServerEncoding::Gb18030 + ); + } + + #[test] + fn test_from_protocol_value_default() { + assert_eq!( + ServerEncoding::from_protocol_value(255), + ServerEncoding::Gb18030 + ); + } + + #[test] + fn test_utf8_roundtrip() { + let s = "Hello, 世界!"; + let encoded = encode_to_server(ServerEncoding::Utf8, s); + let decoded = decode_from_server(ServerEncoding::Utf8, &encoded); + assert_eq!(decoded, s); + } + + #[test] + fn test_gb18030_roundtrip() { + let s = "达梦数据库测试"; + let encoded = encode_to_server(ServerEncoding::Gb18030, s); + let decoded = decode_from_server(ServerEncoding::Gb18030, &encoded); + assert_eq!(decoded, s); + } + + #[test] + fn test_gb18030_ascii_passthrough() { + let s = "SELECT * FROM TABLE"; + let encoded = encode_to_server(ServerEncoding::Gb18030, s); + // ASCII should be identical in GB18030 + assert_eq!(encoded, s.as_bytes()); + let decoded = decode_from_server(ServerEncoding::Gb18030, &encoded); + assert_eq!(decoded, s); + } + + #[test] + fn test_gb18030_chinese() { + // These Chinese characters should encode to multi-byte GB18030 + let s = "中文"; + let encoded = encode_to_server(ServerEncoding::Gb18030, s); + // GB18030 encoding of Chinese chars is multi-byte + assert!(encoded.len() > s.len().min(2)); + let decoded = decode_from_server(ServerEncoding::Gb18030, &encoded); + assert_eq!(decoded, s); + } + + #[test] + fn test_empty_string() { + let encoded = encode_to_server(ServerEncoding::Gb18030, ""); + assert!(encoded.is_empty()); + let decoded = decode_from_server(ServerEncoding::Gb18030, &[]); + assert!(decoded.is_empty()); + } + + #[test] + fn test_invalid_gb18030_fallback() { + // Invalid bytes in GB18030 should produce replacement chars + let invalid_data = vec![0xFF, 0xFE, 0xFF, 0xFE]; + let decoded = decode_from_server(ServerEncoding::Gb18030, &invalid_data); + // encoding_rs replaces invalid bytes with U+FFFD + assert!(!decoded.is_empty()); + } +} diff --git a/Native/DamengBridge/Vendor/dameng-types/src/lib.rs b/Native/DamengBridge/Vendor/dameng-types/src/lib.rs new file mode 100644 index 000000000..56a26cfb1 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng-types/src/lib.rs @@ -0,0 +1,1469 @@ +//! Dameng database type definitions and conversions. +//! +//! This crate provides type mappings between Dameng database types +//! and Rust native types, along with encoding/decoding utilities. + +use std::str::FromStr; + +pub mod encoding; +pub use encoding::{decode_from_server, encode_to_server, ServerEncoding}; + +/// Dameng SQL value type enum. +/// +/// Maps DM type codes to Rust types for encoding and decoding. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DmValueType { + BIT, // 1 + TINYINT, // 2 + VARCHAR, // 3 + INT, // 4 + BIGINT, // 5 + SMALLINT, // 6 + FLOAT, // 7 + DOUBLE, // 8 + DECIMAL, // 9 + DATE, // 10 + TIME, // 11 + TIMESTAMP, // 12 + BLOB, // 13 + CLOB, // 14 + INTERVAL, // 15 + CHAR, // 16 + BINARY, // 17 + VARBINARY, // 18 + NUMERIC, // 20 - alias for DECIMAL + BOOLEAN, // 21 - alias for BIT + DATETIME, // 22 - alias for TIMESTAMP + VARCHAR2, // 23 - alias for VARCHAR + DATETIME2, // 24 - alias for TIMESTAMP + TIME_TZ, // 25 - time with time zone + DATETIME_TZ, // 26 - timestamp with time zone + INTERVAL_YM, // 27 - interval year to month + INTERVAL_DT, // 28 - interval day to second + RAW, // 29 - alias for BINARY + DATETIME2_TZ, // 30 - timestamp2 with time zone + REAL, // 31 - alias for FLOAT +} + +impl DmValueType { + /// Create a DmValueType from a DM type code. + pub fn from_type_code(code: i32) -> Option { + match code { + 1 => Some(DmValueType::BIT), + 2 => Some(DmValueType::TINYINT), + 3 => Some(DmValueType::VARCHAR), + 4 => Some(DmValueType::INT), + 5 => Some(DmValueType::BIGINT), + 6 => Some(DmValueType::SMALLINT), + 7 => Some(DmValueType::FLOAT), + 8 => Some(DmValueType::DOUBLE), + 9 => Some(DmValueType::DECIMAL), + 10 => Some(DmValueType::DATE), + 11 => Some(DmValueType::TIME), + 12 => Some(DmValueType::TIMESTAMP), + 13 => Some(DmValueType::BLOB), + 14 => Some(DmValueType::CLOB), + 15 => Some(DmValueType::INTERVAL), + 16 => Some(DmValueType::CHAR), + 17 => Some(DmValueType::BINARY), + 18 => Some(DmValueType::VARBINARY), + 20 => Some(DmValueType::NUMERIC), + 21 => Some(DmValueType::BOOLEAN), + 22 => Some(DmValueType::DATETIME), + 23 => Some(DmValueType::VARCHAR2), + 24 => Some(DmValueType::DATETIME2), + 25 => Some(DmValueType::TIME_TZ), + 26 => Some(DmValueType::DATETIME_TZ), + 27 => Some(DmValueType::INTERVAL_YM), + 28 => Some(DmValueType::INTERVAL_DT), + 29 => Some(DmValueType::RAW), + 30 => Some(DmValueType::DATETIME2_TZ), + 31 => Some(DmValueType::REAL), + _ => None, + } + } + + /// Get the DM type code for this value type. + pub fn type_code(self) -> i32 { + match self { + DmValueType::BIT => 1, + DmValueType::TINYINT => 2, + DmValueType::VARCHAR => 3, + DmValueType::INT => 4, + DmValueType::BIGINT => 5, + DmValueType::SMALLINT => 6, + DmValueType::FLOAT => 7, + DmValueType::DOUBLE => 8, + DmValueType::DECIMAL => 9, + DmValueType::DATE => 10, + DmValueType::TIME => 11, + DmValueType::TIMESTAMP => 12, + DmValueType::BLOB => 13, + DmValueType::CLOB => 14, + DmValueType::INTERVAL => 15, + DmValueType::CHAR => 16, + DmValueType::BINARY => 17, + DmValueType::VARBINARY => 18, + DmValueType::NUMERIC => 20, + DmValueType::BOOLEAN => 21, + DmValueType::DATETIME => 22, + DmValueType::VARCHAR2 => 23, + DmValueType::DATETIME2 => 24, + DmValueType::TIME_TZ => 25, + DmValueType::DATETIME_TZ => 26, + DmValueType::INTERVAL_YM => 27, + DmValueType::INTERVAL_DT => 28, + DmValueType::RAW => 29, + DmValueType::DATETIME2_TZ => 30, + DmValueType::REAL => 31, + } + } + + /// Get the type name string for protocol messages. + pub fn type_name(self) -> &'static str { + match self { + DmValueType::BIT => "BIT", + DmValueType::TINYINT => "TINYINT", + DmValueType::VARCHAR => "VARCHAR", + DmValueType::INT => "INT", + DmValueType::BIGINT => "BIGINT", + DmValueType::SMALLINT => "SMALLINT", + DmValueType::FLOAT => "FLOAT", + DmValueType::DOUBLE => "DOUBLE", + DmValueType::DECIMAL => "DECIMAL", + DmValueType::DATE => "DATE", + DmValueType::TIME => "TIME", + DmValueType::TIMESTAMP => "TIMESTAMP", + DmValueType::BLOB => "BLOB", + DmValueType::CLOB => "CLOB", + DmValueType::INTERVAL => "INTERVAL", + DmValueType::CHAR => "CHAR", + DmValueType::BINARY => "BINARY", + DmValueType::VARBINARY => "VARBINARY", + DmValueType::NUMERIC => "NUMERIC", + DmValueType::BOOLEAN => "BOOLEAN", + DmValueType::DATETIME => "DATETIME", + DmValueType::VARCHAR2 => "VARCHAR2", + DmValueType::DATETIME2 => "DATETIME2", + DmValueType::TIME_TZ => "TIME_TZ", + DmValueType::DATETIME_TZ => "DATETIME_TZ", + DmValueType::INTERVAL_YM => "INTERVAL_YM", + DmValueType::INTERVAL_DT => "INTERVAL_DT", + DmValueType::RAW => "RAW", + DmValueType::DATETIME2_TZ => "DATETIME2_TZ", + DmValueType::REAL => "REAL", + } + } +} + +/// A decoded DM value. +#[derive(Debug, Clone, PartialEq)] +pub enum DmValue { + Null, + Boolean(bool), + TinyInt(i8), + SmallInt(i16), + Int(i32), + BigInt(i64), + Float(f32), + Double(f64), + Text(String), + Bytea(Vec), + Decimal(rust_decimal::Decimal), + /// DATE value (chrono::NaiveDate). + Date(chrono::NaiveDate), + /// TIME value (chrono::NaiveTime). + Time(chrono::NaiveTime), + /// TIMESTAMP / DATETIME value (chrono::NaiveDateTime). + Timestamp(chrono::NaiveDateTime), + /// LOB_LOCATOR: DM server returns a 16-byte locator handle when CLOB/BLOB + /// data exceeds 2048 bytes. The actual content must be fetched via LOBREAD + /// protocol messages. This variant stores the raw 16-byte locator. + LobLocator(LobLocator), +} + +/// A LOB (Large Object) locator returned by the DM server. +/// +/// When CLOB/BLOB data exceeds 2048 bytes, DM returns a 16-byte locator +/// instead of the actual data. The locator contains server-side pointers +/// (table ID, column ID, row ID, group/file/page numbers) that can be +/// used with LOBREAD protocol messages to fetch the actual content. +#[derive(Debug, Clone, PartialEq)] +pub struct LobLocator { + /// Raw NBLOB_HEAD bytes from DM server (may be >16 for new LOB format). + pub raw: Vec, + /// Whether this is a CLOB (true) or BLOB (false). + pub is_clob: bool, + /// Table ID from column metadata or NBLOB_HEAD extended section. + /// Used by LOBREAD protocol to locate the LOB data on the server. + pub tab_id: i32, + /// Column ID from column metadata. + /// Used by LOBREAD protocol to locate the LOB data on the server. + pub col_id: i16, + /// Current file ID for LOBREAD cursor tracking. Updated after each read. + pub cur_file_id: i16, + /// Current page number for LOBREAD cursor tracking. Updated after each read. + pub cur_page_no: i32, + /// Accumulated offset for LOBREAD cursor tracking. Updated after each read. + pub total_offset: i32, +} + +#[allow(unused)] +impl LobLocator { + /// NBLOB_HEAD offsets (matching dm_go constants). + /// NBLOB_HEAD_IN_ROW_FLAG = 0 (1 byte) + /// NBLOB_HEAD_BLOBID = 1 (8 bytes) + /// NBLOB_HEAD_BLOB_LEN = 9 (4 bytes) + /// NBLOB_HEAD_OUTROW_GROUPID = 13 (2 bytes - USINT) + /// NBLOB_HEAD_OUTROW_FILEID = 15 (2 bytes - USINT) + /// NBLOB_HEAD_OUTROW_PAGENO = 17 (4 bytes - ULINT) + /// NBLOB_EX_HEAD_TABLE_ID = 21 (4 bytes - ULINT) + /// NBLOB_EX_HEAD_COL_ID = 25 (2 bytes - USINT) + /// NBLOB_EX_HEAD_ROW_ID = 27 (8 bytes - DDWORD) + /// NBLOB_EX_HEAD_FPA_GRPID = 35 (2 bytes - USINT) + /// NBLOB_EX_HEAD_FPA_FILEID = 37 (2 bytes - USINT) + /// NBLOB_EX_HEAD_FPA_PAGENO = 39 (4 bytes - ULINT) + const IN_ROW_FLAG: usize = 0; + const BLOBID: usize = 1; + const BLOB_LEN: usize = 9; + const GROUPID: usize = 13; + const FILEID: usize = 15; + const PAGENO: usize = 17; + const EX_TABLE_ID: usize = 21; + const EX_COL_ID: usize = 25; + const EX_ROW_ID: usize = 27; + const EX_FPA_GRPID: usize = 35; + const EX_FPA_FILEID: usize = 37; + const EX_FPA_PAGENO: usize = 39; + + /// Create a LOB locator from NBLOB_HEAD raw bytes returned by DM server. + /// + /// NBLOB_HEAD layout (out-of-row): + /// - Off 0: in_row_flag (1 byte, 0x02 = out-of-row) + /// - Off 1: blob_id (8 bytes LE i64) + /// - Off 9: group_id (2 bytes LE i16) + /// - Off 11: file_id (2 bytes LE i16) + /// - Off 13: page_no (4 bytes LE i32) + /// - Off 17: (extended section if present) + /// - Off 21: tab_id (4 bytes LE i32) + /// - Off 25: col_id (2 bytes LE i16) + /// - Off 27: row_id (8 bytes LE i64) + /// + /// tab_id and col_id can also come from the column metadata in the + /// EXEC_RESPONSE header (parsed separately), in which case use + /// `with_tab_col_id()` to set them. + pub fn from_nblob_head(data: Vec, is_clob: bool) -> Self { + let mut tab_id = 0; + let mut col_id = 0; + + // Try to extract tab_id/col_id from extended NBLOB_HEAD section + if data.len() >= 29 { + tab_id = i32::from_le_bytes([ + data[Self::EX_TABLE_ID], + data[Self::EX_TABLE_ID + 1], + data[Self::EX_TABLE_ID + 2], + data[Self::EX_TABLE_ID + 3], + ]); + col_id = i16::from_le_bytes([data[Self::EX_COL_ID], data[Self::EX_COL_ID + 1]]); + } + + Self { + raw: data, + is_clob, + tab_id, + col_id, + cur_file_id: 0, + cur_page_no: 0, + total_offset: 0, + } + } + + /// Set tab_id/col_id from column metadata (overrides NBLOB_HEAD values). + /// This is called by the response parser after reading the column header. + pub fn with_tab_col_id(mut self, tab_id: i32, col_id: i16) -> Self { + self.tab_id = tab_id; + self.col_id = col_id; + self + } + + /// Get the lob_flag value: 0 = BLOB (byte), 1 = CLOB (char). + pub fn lob_flag(&self) -> u8 { + if self.is_clob { + 1 + } else { + 0 + } + } + + /// Get the blob_id from the NBLOB_HEAD format (offset 1, 8 bytes LE). + pub fn blob_id(&self) -> i64 { + if self.raw.len() >= Self::BLOBID + 8 { + let bytes: [u8; 8] = self.raw[Self::BLOBID..Self::BLOBID + 8].try_into().unwrap(); + i64::from_le_bytes(bytes) + } else { + 0 + } + } + + /// Get the group ID for out-of-row locators (offset 13, 2 bytes LE i16). + pub fn group_id(&self) -> i16 { + if self.raw.len() >= Self::GROUPID + 2 { + i16::from_le_bytes([self.raw[Self::GROUPID], self.raw[Self::GROUPID + 1]]) + } else { + -1 + } + } + + /// Get the file ID for out-of-row locators (offset 15, 2 bytes LE i16). + pub fn file_id(&self) -> i16 { + if self.raw.len() >= Self::FILEID + 2 { + i16::from_le_bytes([self.raw[Self::FILEID], self.raw[Self::FILEID + 1]]) + } else { + -1 + } + } + + /// Get the page number for out-of-row locators (offset 17, 4 bytes LE i32). + pub fn page_no(&self) -> i32 { + if self.raw.len() >= Self::PAGENO + 4 { + let bytes: [u8; 4] = self.raw[Self::PAGENO..Self::PAGENO + 4].try_into().unwrap(); + i32::from_le_bytes(bytes) + } else { + -1 + } + } + + /// Get the row_id from extended section (offset 27, 8 bytes LE i64). + pub fn row_id(&self) -> i64 { + if self.raw.len() >= Self::EX_ROW_ID + 8 { + let bytes: [u8; 8] = self.raw[Self::EX_ROW_ID..Self::EX_ROW_ID + 8] + .try_into() + .unwrap(); + i64::from_le_bytes(bytes) + } else { + 0 + } + } + + /// Get the extended group ID (offset 35, 2 bytes LE i16). + pub fn ex_group_id(&self) -> i16 { + if self.raw.len() >= Self::EX_FPA_GRPID + 2 { + i16::from_le_bytes([ + self.raw[Self::EX_FPA_GRPID], + self.raw[Self::EX_FPA_GRPID + 1], + ]) + } else { + 0 + } + } + + /// Get the extended file ID (offset 37, 2 bytes LE i16). + pub fn ex_file_id(&self) -> i16 { + if self.raw.len() >= Self::EX_FPA_FILEID + 2 { + i16::from_le_bytes([ + self.raw[Self::EX_FPA_FILEID], + self.raw[Self::EX_FPA_FILEID + 1], + ]) + } else { + 0 + } + } + + /// Get the extended page number (offset 39, 4 bytes LE i32). + pub fn ex_page_no(&self) -> i32 { + if self.raw.len() >= Self::EX_FPA_PAGENO + 4 { + let bytes: [u8; 4] = self.raw[Self::EX_FPA_PAGENO..Self::EX_FPA_PAGENO + 4] + .try_into() + .unwrap(); + i32::from_le_bytes(bytes) + } else { + 0 + } + } + + /// Check if extended section is present (NewLobFlag). + pub fn has_extended(&self) -> bool { + self.raw.len() >= Self::EX_TABLE_ID + 4 + } + + /// Update the cursor state from a LOBREAD response. + /// + /// After each LOBREAD, the server returns updated `curFileId`, `curPageNo`, + /// and `totalOffset` values. This method updates the locator so subsequent + /// reads continue from the correct position. + /// + /// This is a mutable reference — clone the locator before calling this + /// if you need to preserve the original. + pub fn update_cursor(&mut self, cur_file_id: i16, cur_page_no: i32, total_offset: i32) { + self.cur_file_id = cur_file_id; + self.cur_page_no = cur_page_no; + self.total_offset = total_offset; + } + + /// Initialize cursor from the initial LOB locator values. + /// + /// On the first read, `curFileId` = `fileId` and `curPageNo` = `pageNo`. + pub fn init_cursor(&mut self) { + self.cur_file_id = self.file_id(); + self.cur_page_no = self.page_no(); + self.total_offset = 0; + } +} + +impl From for DmValue { + fn from(v: i32) -> Self { + DmValue::Int(v) + } +} + +impl From for DmValue { + fn from(v: i64) -> Self { + DmValue::BigInt(v) + } +} + +impl From for DmValue { + fn from(v: String) -> Self { + DmValue::Text(v) + } +} + +impl From<&str> for DmValue { + fn from(v: &str) -> Self { + DmValue::Text(v.to_string()) + } +} + +impl From for DmValue { + fn from(v: bool) -> Self { + DmValue::Boolean(v) + } +} + +impl From for DmValue { + fn from(v: f64) -> Self { + DmValue::Double(v) + } +} + +impl From> for DmValue { + fn from(v: Vec) -> Self { + DmValue::Bytea(v) + } +} + +// --- Option From impls --- + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::TinyInt).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::SmallInt).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::Int).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::BigInt).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::Float).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::Double).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::Text).unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option<&str>) -> Self { + v.map(|s| s.to_string()) + .map(DmValue::Text) + .unwrap_or(DmValue::Null) + } +} + +impl From> for DmValue { + fn from(v: Option) -> Self { + v.map(DmValue::Boolean).unwrap_or(DmValue::Null) + } +} + +impl From>> for DmValue { + fn from(v: Option>) -> Self { + v.map(DmValue::Bytea).unwrap_or(DmValue::Null) + } +} + +/// Trait for dynamic parameter binding — SQLx-style `&[&dyn ToDmValue]` support. +/// +/// # Example +/// +/// ```ignore +/// let name = "Alice"; +/// let age: i32 = 30; +/// let rows = client.query_with_params( +/// "SELECT * FROM person WHERE name = ? AND age > ?", +/// &[&name, &age], +/// )?; +/// ``` +pub trait ToDmValue { + /// Convert this value into a `DmValue`. + fn to_dm_value(&self) -> DmValue; +} + +// --- ToDmValue implementations for concrete types --- + +macro_rules! impl_to_dm_value { + ($($ty:ty => $variant:ident),* $(,)?) => { + $( + impl ToDmValue for $ty { + fn to_dm_value(&self) -> DmValue { + DmValue::$variant(*self) + } + } + )* + }; +} + +impl_to_dm_value!( + bool => Boolean, + i8 => TinyInt, + i16 => SmallInt, + i32 => Int, + i64 => BigInt, + f32 => Float, + f64 => Double, +); + +impl ToDmValue for u8 { + fn to_dm_value(&self) -> DmValue { + DmValue::TinyInt(*self as i8) + } +} + +impl ToDmValue for u16 { + fn to_dm_value(&self) -> DmValue { + DmValue::SmallInt(*self as i16) + } +} + +impl ToDmValue for u32 { + fn to_dm_value(&self) -> DmValue { + if *self <= i32::MAX as u32 { + DmValue::Int(*self as i32) + } else { + DmValue::BigInt(*self as i64) + } + } +} + +impl ToDmValue for u64 { + fn to_dm_value(&self) -> DmValue { + if *self <= i64::MAX as u64 { + DmValue::BigInt(*self as i64) + } else { + DmValue::Text(self.to_string()) + } + } +} + +// Blanket impl: `&T` where `T: ToDmValue` delegates to T. +// This lets `&[&id, &name]` work when `name: &str` (producing `&&str`). +impl ToDmValue for &T { + fn to_dm_value(&self) -> DmValue { + T::to_dm_value(*self) + } +} + +impl ToDmValue for str { + fn to_dm_value(&self) -> DmValue { + DmValue::Text(self.to_string()) + } +} + +impl ToDmValue for String { + fn to_dm_value(&self) -> DmValue { + DmValue::Text(self.clone()) + } +} + +impl ToDmValue for [u8] { + fn to_dm_value(&self) -> DmValue { + DmValue::Bytea(self.to_vec()) + } +} + +impl ToDmValue for Vec { + fn to_dm_value(&self) -> DmValue { + DmValue::Bytea(self.clone()) + } +} + +// --- Option implementations --- + +macro_rules! impl_option_to_dm_value { + ($($ty:ty),* $(,)?) => { + $( + impl ToDmValue for Option<$ty> { + fn to_dm_value(&self) -> DmValue { + match self { + Some(v) => v.to_dm_value(), + None => DmValue::Null, + } + } + } + )* + }; +} + +impl_option_to_dm_value!(bool, i8, i16, i32, i64, f32, f64, String); +impl_option_to_dm_value!(rust_decimal::Decimal); +impl_option_to_dm_value!(chrono::NaiveDate); +impl_option_to_dm_value!(chrono::NaiveDateTime); + +impl ToDmValue for Option<&str> { + fn to_dm_value(&self) -> DmValue { + self.map(|s| s.to_string()) + .map(DmValue::Text) + .unwrap_or(DmValue::Null) + } +} + +impl ToDmValue for Option> { + fn to_dm_value(&self) -> DmValue { + self.clone().map(DmValue::Bytea).unwrap_or(DmValue::Null) + } +} + +// --- ToDmValue for chrono / rust_decimal types --- + +impl ToDmValue for rust_decimal::Decimal { + fn to_dm_value(&self) -> DmValue { + DmValue::Decimal(*self) + } +} + +impl ToDmValue for chrono::NaiveDate { + fn to_dm_value(&self) -> DmValue { + DmValue::Date(*self) + } +} + +impl ToDmValue for chrono::NaiveTime { + fn to_dm_value(&self) -> DmValue { + DmValue::Time(*self) + } +} + +impl ToDmValue for chrono::NaiveDateTime { + fn to_dm_value(&self) -> DmValue { + DmValue::Timestamp(*self) + } +} + +/// Encode a Rust value to DM protocol bytes. +pub fn encode_value(ty: DmValueType, value: &DmValue) -> Vec { + match ty { + DmValueType::INT => { + if let DmValue::Int(v) = value { + v.to_le_bytes().to_vec() + } else { + vec![0; 4] + } + } + DmValueType::BIGINT => { + if let DmValue::BigInt(v) = value { + v.to_le_bytes().to_vec() + } else { + vec![0; 8] + } + } + DmValueType::SMALLINT => { + if let DmValue::SmallInt(v) = value { + v.to_le_bytes().to_vec() + } else { + vec![0; 2] + } + } + DmValueType::FLOAT | DmValueType::REAL => { + // Always 4 bytes + if let DmValue::Float(v) = value { + v.to_le_bytes().to_vec() + } else if let DmValue::Double(v) = value { + (*v as f32).to_le_bytes().to_vec() + } else { + vec![0u8; 4] + } + } + DmValueType::DOUBLE => { + // Always 8 bytes + if let DmValue::Double(v) = value { + v.to_le_bytes().to_vec() + } else if let DmValue::Float(v) = value { + (*v as f64).to_le_bytes().to_vec() + } else { + vec![0u8; 8] + } + } + DmValueType::BIT | DmValueType::BOOLEAN => { + if let DmValue::Boolean(v) = value { + vec![if *v { 1 } else { 0 }] + } else { + vec![0] + } + } + DmValueType::VARCHAR | DmValueType::CHAR | DmValueType::CLOB | DmValueType::VARCHAR2 => { + if let DmValue::Text(v) = value { + v.as_bytes().to_vec() + } else { + vec![] + } + } + DmValueType::BLOB | DmValueType::BINARY | DmValueType::VARBINARY | DmValueType::RAW => { + if let DmValue::Bytea(v) = value { + v.clone() + } else { + vec![] + } + } + DmValueType::DECIMAL | DmValueType::NUMERIC => { + if let DmValue::Decimal(v) = value { + v.to_string().as_bytes().to_vec() + } else { + vec![] + } + } + DmValueType::TINYINT => { + if let DmValue::TinyInt(v) = value { + v.to_le_bytes().to_vec() + } else { + vec![0] + } + } + DmValueType::DATE + | DmValueType::TIME + | DmValueType::TIMESTAMP + | DmValueType::DATETIME + | DmValueType::DATETIME2 + | DmValueType::TIME_TZ + | DmValueType::DATETIME_TZ + | DmValueType::DATETIME2_TZ => { + if let DmValue::Date(d) = value { + d.format("%Y-%m-%d").to_string().as_bytes().to_vec() + } else if let DmValue::Time(t) = value { + t.format("%H:%M:%S").to_string().as_bytes().to_vec() + } else if let DmValue::Timestamp(ts) = value { + ts.format("%Y-%m-%d %H:%M:%S") + .to_string() + .as_bytes() + .to_vec() + } else if let DmValue::Text(v) = value { + v.as_bytes().to_vec() + } else { + vec![] + } + } + // Generic INTERVAL (type_code=15) — send as text, server parses it. + DmValueType::INTERVAL => { + if let DmValue::Text(v) = value { + v.as_bytes().to_vec() + } else { + vec![] + } + } + // INTERVAL_YM (type_code=27): year-month interval, 12 bytes. + // Binary layout: year(LE i32, 4) + month(LE i32, 4) + padding(4). + // Text input: "Y-M" (e.g., "1-2" = 1 year 2 months) or just "Y". + DmValueType::INTERVAL_YM => { + if let DmValue::Text(v) = value { + encode_interval_ym(v) + } else { + vec![0; 12] + } + } + // INTERVAL_DT (type_code=28): day-time interval, 24 bytes. + // Binary layout: day(LE i32, 4) + hour(LE i32, 4) + minute(LE i32, 4) + // + second(LE i32, 4) + nanoseconds(LE i64, 8). + // Text input: "D HH:MI:SS.FF" (e.g., "1 2:3:4.5" = 1 day, 2h 3m 4.5s) + // or "HH:MI:SS.FF" (day defaults to 0). + DmValueType::INTERVAL_DT => { + if let DmValue::Text(v) = value { + encode_interval_dt(v) + } else { + vec![0; 24] + } + } + } +} + +/// Encode an INTERVAL YEAR TO MONTH text string to DM binary format (12 bytes). +/// +/// Binary layout: year(LE i32, 4) + month(LE i32, 4) + padding(4 zero bytes). +/// +/// Accepted text formats: +/// - "Y-M" (e.g., "1-2" = 1 year 2 months) +/// - "+Y-M" or "-Y-M" for signed intervals +/// - "Y" (months default to 0) +fn encode_interval_ym(s: &str) -> Vec { + let mut year: i32 = 0; + let mut month: i32 = 0; + + let trimmed = s.trim(); + let (sign, rest) = if let Some(stripped) = trimmed.strip_prefix('+') { + (1i32, stripped.trim()) + } else if let Some(stripped) = trimmed.strip_prefix('-') { + (-1, stripped.trim()) + } else { + (1, trimmed) + }; + + if let Some((y_str, m_str)) = rest.split_once('-') { + if let Ok(y) = y_str.trim().parse::() { + year = y; + } + if let Ok(m) = m_str.trim().parse::() { + month = m; + } + } else if let Ok(y) = rest.trim().parse::() { + year = y; + } + + let mut buf = Vec::with_capacity(12); + buf.extend_from_slice(&(year * sign).to_le_bytes()); + buf.extend_from_slice(&(month * sign).to_le_bytes()); + buf.extend_from_slice(&[0, 0, 0, 0]); + buf +} + +/// Encode an INTERVAL DAY TO SECOND text string to DM binary format (24 bytes). +/// +/// Binary layout: day(LE i32, 4) + hour(LE i32, 4) + minute(LE i32, 4) +/// + second(LE i32, 4) + nanoseconds(LE i64, 8). +/// +/// Accepted text formats: +/// - "D HH:MI:SS.FF" (e.g., "1 2:3:4.5" = 1 day, 2h 3m 4.5s) +/// - "HH:MI:SS.FF" (day defaults to 0) +/// - "+D HH:MI:SS.FF" or "-D HH:MI:SS.FF" for signed intervals +fn encode_interval_dt(s: &str) -> Vec { + let mut day: i32 = 0; + let mut hour: i32 = 0; + let mut minute: i32 = 0; + let mut second: i32 = 0; + let mut nanosecond: i64 = 0; + + let trimmed = s.trim(); + let (sign, rest) = if let Some(stripped) = trimmed.strip_prefix('+') { + (1i32, stripped.trim()) + } else if let Some(stripped) = trimmed.strip_prefix('-') { + (-1, stripped.trim()) + } else { + (1, trimmed) + }; + + // Try "D HH:MI:SS.FF" format first + let rest_for_parse = if rest.contains(' ') { + // "D HH:MI:SS.FF" — extract day + if let Some((d_part, time_part)) = rest.split_once(' ') { + if let Ok(d) = d_part.trim().parse::() { + day = d; + } + time_part.trim() + } else { + rest + } + } else { + rest + }; + + // Parse HH:MI:SS.FF + if let Some((time, frac_str)) = rest_for_parse.split_once('.') { + let parts: Vec<&str> = time.split(':').collect(); + if parts.len() >= 3 { + if let Ok(h) = parts[0].parse::() { + hour = h; + } + if let Ok(m) = parts[1].parse::() { + minute = m; + } + if let Ok(s_val) = parts[2].parse::() { + second = s_val; + } + } else if parts.len() == 2 { + if let Ok(h) = parts[0].parse::() { + hour = h; + } + if let Ok(m) = parts[1].parse::() { + minute = m; + } + } + // Nanoseconds from fractional seconds (scale the fractional digits) + if let Ok(f) = frac_str.parse::() { + nanosecond = (f * 1_000_000_000.0) as i64; + } + } else { + let parts: Vec<&str> = rest_for_parse.split(':').collect(); + if parts.len() >= 3 { + if let Ok(h) = parts[0].parse::() { + hour = h; + } + if let Ok(m) = parts[1].parse::() { + minute = m; + } + if let Ok(s_val) = parts[2].parse::() { + second = s_val; + } + } else if parts.len() == 2 { + if let Ok(h) = parts[0].parse::() { + hour = h; + } + if let Ok(m) = parts[1].parse::() { + minute = m; + } + } + } + + let mut buf = Vec::with_capacity(24); + buf.extend_from_slice(&(day * sign).to_le_bytes()); + buf.extend_from_slice(&(hour * sign).to_le_bytes()); + buf.extend_from_slice(&(minute * sign).to_le_bytes()); + buf.extend_from_slice(&(second * sign).to_le_bytes()); + buf.extend_from_slice(&(nanosecond * sign as i64).to_le_bytes()); + buf +} + +/// Parse a raw output parameter value from the EXEC_RESPONSE frame. +/// +/// After executing a stored procedure with OUTPUT or INPUT_OUTPUT parameters, +/// this helper decodes the raw bytes returned by the server into a `DmValue` +/// based on the parameter's type code. +/// +/// # Arguments +/// * `bytes` - The raw bytes of the parameter value. +/// * `type_code` - The DM type code (e.g., 4 for INT, 3 for VARCHAR). +/// +/// # Returns +/// * `Some(DmValue)` if the value could be decoded. +/// * `None` if the type is unknown or the data is empty/invalid. +pub fn parse_output_param_value(bytes: &[u8], type_code: i32) -> Option { + if bytes.is_empty() { + return Some(DmValue::Null); + } + let ty = DmValueType::from_type_code(type_code)?; + decode_value(ty, bytes, None) +} + +/// Decode DM binary DECIMAL format (matches Go driver dm_go/o.go decodeDecimal). +fn decode_dm_binary_decimal(data: &[u8]) -> Option { + const FLAG_ZERO: u8 = 0x80; + const FLAG_POSITIVE: i32 = 0xC1; + const FLAG_NEGTIVE: i32 = 0x3E; + const NUM_POSITIVE: i32 = 1; + const NUM_NEGTIVE: i32 = 101; + + if data.is_empty() || data.len() > 21 { + return None; + } + if data[0] == FLAG_ZERO || data.len() == 1 { + return Some(rust_decimal::Decimal::ZERO); + } + let sign: i32 = if data[0] & FLAG_ZERO != 0 { 1 } else { -1 }; + let flag = data[0] as i32; + let _exp = if sign > 0 { + flag - FLAG_POSITIVE + } else { + FLAG_NEGTIVE - flag + }; + let mut sf = String::new(); + for &b in &data[1..] { + let digit = if sign > 0 { + b as i32 - NUM_POSITIVE + } else { + NUM_NEGTIVE - b as i32 + }; + if digit < 0 || digit > 99 { + break; + } + sf.push_str(&format!("{:02}", digit)); + } + if sf.is_empty() { + return None; + } + let int_val: i64 = sf.parse().ok()?; + Some(rust_decimal::Decimal::from_i128_with_scale( + int_val as i128, + 0, + )) +} + +/// Decode DM protocol bytes to a Rust value. +/// +/// # Arguments +/// * `ty` - The DM value type +/// * `data` - The raw bytes to decode +/// * `lob_meta` - Optional LOB column metadata (tab_id, col_id). Used to populate +/// the LobLocator when decoding out-of-row BLOB/CLOB values. +pub fn decode_value(ty: DmValueType, data: &[u8], lob_meta: Option<(i32, i16)>) -> Option { + if data.is_empty() { + return Some(DmValue::Null); + } + + match ty { + DmValueType::INT => { + if data.len() >= 4 { + let v = i32::from_le_bytes([data[0], data[1], data[2], data[3]]); + Some(DmValue::Int(v)) + } else { + None + } + } + DmValueType::BIGINT => { + if data.len() >= 8 { + let v = i64::from_le_bytes([ + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]); + Some(DmValue::BigInt(v)) + } else { + None + } + } + DmValueType::SMALLINT => { + if data.len() >= 2 { + let v = i16::from_le_bytes([data[0], data[1]]); + Some(DmValue::SmallInt(v)) + } else { + None + } + } + DmValueType::DOUBLE => { + if data.len() >= 8 { + let bytes: [u8; 8] = data[..8].try_into().ok()?; + let v = f64::from_le_bytes(bytes); + Some(DmValue::Double(v)) + } else { + None + } + } + DmValueType::FLOAT | DmValueType::REAL => { + if data.len() >= 4 { + let v = f32::from_le_bytes([data[0], data[1], data[2], data[3]]); + Some(DmValue::Float(v)) + } else { + None + } + } + DmValueType::BIT | DmValueType::BOOLEAN => Some(DmValue::Boolean(data[0] != 0)), + DmValueType::VARCHAR | DmValueType::CHAR | DmValueType::VARCHAR2 => { + String::from_utf8(data.to_vec()).ok().map(DmValue::Text) + } + DmValueType::CLOB => { + // DM returns NBLOB_HEAD format for CLOB values: + // - in_row=0x01: inline data follows (13-byte header: flag(1) + blob_id(8) + blob_len(4) + data) + // - in_row=0x02: out-of-row LOB locator (needs LOBREAD protocol) + // - legacy: exactly 16 bytes (old LOB_LOCATOR format) + if data.len() >= 13 && data[0] == 0x01 { + // Inline LOB data - extract actual content + let blob_len = if data.len() >= 13 { + u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize + } else { + 0 + }; + if 13 + blob_len <= data.len() { + let inline_data = &data[13..13 + blob_len]; + String::from_utf8(inline_data.to_vec()) + .ok() + .map(DmValue::Text) + } else { + None + } + } else if data.len() >= 13 && data[0] == 0x02 { + // Out-of-row CLOB locator + let mut loc = LobLocator::from_nblob_head(data.to_vec(), true); + if let Some((tab_id, col_id)) = lob_meta { + loc = loc.with_tab_col_id(tab_id, col_id); + } + Some(DmValue::LobLocator(loc)) + } else if data.len() == 16 { + // Legacy 16-byte LOB_LOCATOR format + let mut loc = LobLocator::from_nblob_head(data.to_vec(), true); + if let Some((tab_id, col_id)) = lob_meta { + loc = loc.with_tab_col_id(tab_id, col_id); + } + Some(DmValue::LobLocator(loc)) + } else { + String::from_utf8(data.to_vec()).ok().map(DmValue::Text) + } + } + DmValueType::BINARY | DmValueType::VARBINARY | DmValueType::RAW => { + Some(DmValue::Bytea(data.to_vec())) + } + DmValueType::BLOB => { + // DM returns NBLOB_HEAD format for BLOB values (same as CLOB): + // - in_row=0x01: inline data follows + // - in_row=0x02: out-of-row LOB locator + if data.len() >= 13 && data[0] == 0x01 { + // Inline BLOB data + let blob_len = if data.len() >= 13 { + u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize + } else { + 0 + }; + if 13 + blob_len <= data.len() { + Some(DmValue::Bytea(data[13..13 + blob_len].to_vec())) + } else { + None + } + } else if data.len() >= 13 && data[0] == 0x02 { + // Out-of-row BLOB locator + let mut loc = LobLocator::from_nblob_head(data.to_vec(), false); + if let Some((tab_id, col_id)) = lob_meta { + loc = loc.with_tab_col_id(tab_id, col_id); + } + Some(DmValue::LobLocator(loc)) + } else if data.len() == 16 { + // Legacy 16-byte LOB_LOCATOR format + let mut loc = LobLocator::from_nblob_head(data.to_vec(), false); + if let Some((tab_id, col_id)) = lob_meta { + loc = loc.with_tab_col_id(tab_id, col_id); + } + Some(DmValue::LobLocator(loc)) + } else { + Some(DmValue::Bytea(data.to_vec())) + } + } + DmValueType::DECIMAL | DmValueType::NUMERIC => { + // Try text format first (ASCII digits) + if let Ok(s) = std::str::from_utf8(data) { + if let Ok(d) = rust_decimal::Decimal::from_str(s.trim()) { + return Some(DmValue::Decimal(d)); + } + } + // Try DM binary DECIMAL format (matches Go driver o.go) + decode_dm_binary_decimal(data).map(DmValue::Decimal) + } + DmValueType::TINYINT => Some(DmValue::TinyInt(data[0] as i8)), + DmValueType::DATE + | DmValueType::TIME + | DmValueType::TIMESTAMP + | DmValueType::INTERVAL + | DmValueType::DATETIME + | DmValueType::DATETIME2 + | DmValueType::TIME_TZ + | DmValueType::DATETIME_TZ + | DmValueType::DATETIME2_TZ + | DmValueType::INTERVAL_YM + | DmValueType::INTERVAL_DT => { + // DM stores DATE/TIME/TIMESTAMP/INTERVAL as binary: + // DATE: 7 bytes (year:2BE, month:1, day:1, hour:1, min:1, sec:1) + // TIME: 6+ bytes (hour:1, min:1, sec:1, nanosec:4BE) + // TIMESTAMP: 11 bytes (year:2BE, month:1, day:1, hour:1, min:1, sec:1, nanosec:4BE) + // If data is valid UTF-8 text, pass through as Text. + // Otherwise decode binary to typed chrono variants. + if let Ok(s) = String::from_utf8(data.to_vec()) { + return Some(DmValue::Text(s)); + } + // Binary decode for TIMESTAMP / DATETIME + if ty == DmValueType::TIMESTAMP + || ty == DmValueType::DATETIME + || ty == DmValueType::DATETIME2 + { + // Try 11-byte OPE format + if data.len() >= 11 { + let year = u16::from_be_bytes([data[0], data[1]]) as i32; + let month = data[2] as u32; + let day = data[3] as u32; + let hour = data[4] as u32; + let min = data[5] as u32; + let sec = data[6] as u32; + let nano = u32::from_be_bytes([data[7], data[8], data[9], data[10]]); + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) + .and_then(|d| d.and_hms_nano_opt(hour, min, sec, nano)) + { + return Some(DmValue::Timestamp(d)); + } + } + // Try 8-byte DM row format + if data.len() >= 8 { + let year = i32::from(i16::from_le_bytes([data[0], data[1]])) & 0x7FFF; + let month = ((data[1] as u32 >> 7) & 0x1) + ((data[2] as u32 & 0x07) << 1); + let day = ((data[2] as u32 & 0xF8) >> 3) & 0x1F; + let hour = data[3] as u32 & 0x1F; + let min = ((data[3] as u32 >> 5) & 0x07) + ((data[4] as u32 & 0x07) << 3); + let sec = ((data[4] as u32 >> 3) & 0x1F) + ((data[5] as u32 & 0x01) << 5); + let nano = (((data[5] as u32 >> 1) & 0x7F) + + ((data[6] as u32 & 0xFF) << 7) + + ((data[7] as u32 & 0x1F) << 15)) + * 1000; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) + .and_then(|d| d.and_hms_nano_opt(hour, min, sec, nano)) + { + return Some(DmValue::Timestamp(d)); + } + } + None + } else if ty == DmValueType::DATE { + // Try 3-byte DM row format first (DATE_PREC = 3) + if data.len() >= 3 && data.len() < 7 { + let year = i32::from(i16::from_le_bytes([data[0], data[1]])) & 0x7FFF; + let month = ((data[1] as u32 >> 7) & 0x1) + ((data[2] as u32 & 0x07) << 1); + let day = ((data[2] as u32 & 0xF8) >> 3) & 0x1F; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Some(DmValue::Date(d)); + } + } + if data.len() >= 7 { + let year = u16::from_be_bytes([data[0], data[1]]) as i32; + let month = data[2] as u32; + let day = data[3] as u32; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Some(DmValue::Date(d)); + } + } + if data.len() >= 8 { + let year = i32::from(i16::from_le_bytes([data[0], data[1]])) & 0x7FFF; + let month = ((data[1] as u32 >> 7) & 0x1) + ((data[2] as u32 & 0x07) << 1); + let day = ((data[2] as u32 & 0xF8) >> 3) & 0x1F; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Some(DmValue::Date(d)); + } + } + None.map(DmValue::Date) + } else if ty == DmValueType::TIME && data.len() >= 6 { + let hour = data[0] as u32; + let minute = data[1] as u32; + let second = data[2] as u32; + let nano = if data.len() >= 10 { + u32::from_be_bytes([data[3], data[4], data[5], data[6]]) + } else { + 0 + }; + chrono::NaiveTime::from_hms_nano_opt(hour, minute, second, nano).map(DmValue::Time) + } else if ty == DmValueType::INTERVAL + || ty == DmValueType::INTERVAL_YM + || ty == DmValueType::INTERVAL_DT + { + Some(DmValue::Text(String::from_utf8_lossy(data).to_string())) + } else { + // Fallback for TIME_TZ, DATETIME_TZ, DATETIME2_TZ: text + Some(DmValue::Text(String::from_utf8_lossy(data).to_string())) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_dmvtype_from_type_code() { + assert_eq!(DmValueType::from_type_code(4), Some(DmValueType::INT)); + assert_eq!(DmValueType::from_type_code(3), Some(DmValueType::VARCHAR)); + assert_eq!(DmValueType::from_type_code(99), None); + } + + #[test] + fn test_dmvtype_type_code() { + assert_eq!(DmValueType::INT.type_code(), 4); + assert_eq!(DmValueType::BIGINT.type_code(), 5); + assert_eq!(DmValueType::BIT.type_code(), 1); + } + + #[test] + fn test_dmvtype_type_name() { + assert_eq!(DmValueType::INT.type_name(), "INT"); + assert_eq!(DmValueType::VARCHAR.type_name(), "VARCHAR"); + assert_eq!(DmValueType::TIMESTAMP.type_name(), "TIMESTAMP"); + } + + #[test] + fn test_encode_decode_int() { + let val = DmValue::Int(42); + let encoded = encode_value(DmValueType::INT, &val); + assert_eq!(encoded, vec![42, 0, 0, 0]); + let decoded = decode_value(DmValueType::INT, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_bigint() { + let val = DmValue::BigInt(1000); + let encoded = encode_value(DmValueType::BIGINT, &val); + assert_eq!(encoded, vec![0xE8, 0x03, 0, 0, 0, 0, 0, 0]); + let decoded = decode_value(DmValueType::BIGINT, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_text() { + let val = DmValue::Text("hello".to_string()); + let encoded = encode_value(DmValueType::VARCHAR, &val); + assert_eq!(encoded, b"hello"); + let decoded = decode_value(DmValueType::VARCHAR, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_bool() { + let val = DmValue::Boolean(true); + let encoded = encode_value(DmValueType::BIT, &val); + assert_eq!(encoded, vec![1]); + let decoded = decode_value(DmValueType::BIT, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_decode_empty() { + let result = decode_value(DmValueType::INT, &[], None); + assert_eq!(result, Some(DmValue::Null)); + } + + #[test] + fn test_encode_decode_bytea() { + let val = DmValue::Bytea(vec![0xDE, 0xAD, 0xBE, 0xEF]); + let encoded = encode_value(DmValueType::BLOB, &val); + assert_eq!(encoded, vec![0xDE, 0xAD, 0xBE, 0xEF]); + let decoded = decode_value(DmValueType::BLOB, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_new_type_codes() { + assert_eq!(DmValueType::NUMERIC.type_code(), 20); + assert_eq!(DmValueType::BOOLEAN.type_code(), 21); + assert_eq!(DmValueType::DATETIME.type_code(), 22); + assert_eq!(DmValueType::VARCHAR2.type_code(), 23); + assert_eq!(DmValueType::DATETIME2.type_code(), 24); + assert_eq!(DmValueType::TIME_TZ.type_code(), 25); + assert_eq!(DmValueType::DATETIME_TZ.type_code(), 26); + assert_eq!(DmValueType::INTERVAL_YM.type_code(), 27); + assert_eq!(DmValueType::INTERVAL_DT.type_code(), 28); + assert_eq!(DmValueType::RAW.type_code(), 29); + assert_eq!(DmValueType::DATETIME2_TZ.type_code(), 30); + assert_eq!(DmValueType::REAL.type_code(), 31); + } + + #[test] + fn test_new_from_type_code() { + assert_eq!(DmValueType::from_type_code(20), Some(DmValueType::NUMERIC)); + assert_eq!(DmValueType::from_type_code(21), Some(DmValueType::BOOLEAN)); + assert_eq!(DmValueType::from_type_code(22), Some(DmValueType::DATETIME)); + assert_eq!(DmValueType::from_type_code(23), Some(DmValueType::VARCHAR2)); + assert_eq!( + DmValueType::from_type_code(24), + Some(DmValueType::DATETIME2) + ); + assert_eq!(DmValueType::from_type_code(25), Some(DmValueType::TIME_TZ)); + assert_eq!( + DmValueType::from_type_code(26), + Some(DmValueType::DATETIME_TZ) + ); + assert_eq!( + DmValueType::from_type_code(27), + Some(DmValueType::INTERVAL_YM) + ); + assert_eq!( + DmValueType::from_type_code(28), + Some(DmValueType::INTERVAL_DT) + ); + assert_eq!(DmValueType::from_type_code(29), Some(DmValueType::RAW)); + assert_eq!( + DmValueType::from_type_code(30), + Some(DmValueType::DATETIME2_TZ) + ); + assert_eq!(DmValueType::from_type_code(31), Some(DmValueType::REAL)); + } + + #[test] + fn test_new_type_names() { + assert_eq!(DmValueType::NUMERIC.type_name(), "NUMERIC"); + assert_eq!(DmValueType::BOOLEAN.type_name(), "BOOLEAN"); + assert_eq!(DmValueType::DATETIME.type_name(), "DATETIME"); + assert_eq!(DmValueType::VARCHAR2.type_name(), "VARCHAR2"); + assert_eq!(DmValueType::DATETIME2.type_name(), "DATETIME2"); + assert_eq!(DmValueType::TIME_TZ.type_name(), "TIME_TZ"); + assert_eq!(DmValueType::DATETIME_TZ.type_name(), "DATETIME_TZ"); + assert_eq!(DmValueType::INTERVAL_YM.type_name(), "INTERVAL_YM"); + assert_eq!(DmValueType::INTERVAL_DT.type_name(), "INTERVAL_DT"); + assert_eq!(DmValueType::RAW.type_name(), "RAW"); + assert_eq!(DmValueType::DATETIME2_TZ.type_name(), "DATETIME2_TZ"); + assert_eq!(DmValueType::REAL.type_name(), "REAL"); + } + + #[test] + fn test_encode_decode_boolean() { + let val = DmValue::Boolean(false); + let encoded = encode_value(DmValueType::BOOLEAN, &val); + assert_eq!(encoded, vec![0]); + let decoded = decode_value(DmValueType::BOOLEAN, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_raw() { + let val = DmValue::Bytea(vec![0xAA, 0xBB]); + let encoded = encode_value(DmValueType::RAW, &val); + assert_eq!(encoded, vec![0xAA, 0xBB]); + let decoded = decode_value(DmValueType::RAW, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_real() { + let val = DmValue::Float(3.14f32); + let encoded = encode_value(DmValueType::REAL, &val); + assert_eq!(encoded, 3.14f32.to_le_bytes().to_vec()); + let decoded = decode_value(DmValueType::REAL, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_numeric() { + use rust_decimal::Decimal; + let val = DmValue::Decimal(Decimal::from(42)); + let encoded = encode_value(DmValueType::NUMERIC, &val); + assert_eq!(encoded, b"42"); + let decoded = decode_value(DmValueType::NUMERIC, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_varchar2() { + let val = DmValue::Text("test".to_string()); + let encoded = encode_value(DmValueType::VARCHAR2, &val); + assert_eq!(encoded, b"test"); + let decoded = decode_value(DmValueType::VARCHAR2, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } + + #[test] + fn test_encode_decode_datetime() { + let val = DmValue::Text("2024-01-01 12:00:00".to_string()); + let encoded = encode_value(DmValueType::DATETIME, &val); + assert_eq!(encoded, b"2024-01-01 12:00:00"); + let decoded = decode_value(DmValueType::DATETIME, &encoded, None).unwrap(); + assert_eq!(decoded, val); + } +} diff --git a/Native/DamengBridge/Vendor/dameng/Cargo.toml b/Native/DamengBridge/Vendor/dameng/Cargo.toml new file mode 100644 index 000000000..c830a2987 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dameng" +version = "0.1.0" +edition = "2021" +autoexamples = false +description = "Dameng database sync driver" +license = "MIT" +repository = "https://github.com/rarnu/rust-dameng" +keywords = ["dameng", "database", "driver"] +categories = ["database"] + +[dependencies] +dameng-protocol = { version = "0.1.0", path = "../dameng-protocol" } +dameng-types = { version = "0.1.0", path = "../dameng-types" } +rust_decimal = "1" +chrono = "0.4" +bytes = "1" +encoding_rs = "0.8" +native-tls = "0.2" diff --git a/Native/DamengBridge/Vendor/dameng/LICENSE.txt b/Native/DamengBridge/Vendor/dameng/LICENSE.txt new file mode 100644 index 000000000..81121d744 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 指令集 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Native/DamengBridge/Vendor/dameng/src/client.rs b/Native/DamengBridge/Vendor/dameng/src/client.rs new file mode 100644 index 000000000..ac4feb54f --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/client.rs @@ -0,0 +1,1655 @@ +//! Sync client for connecting to Dameng database. + +use native_tls::{TlsConnector, TlsStream as NativeTlsStream}; +use std::io::{Read, Write}; +use std::net::TcpStream; + +use bytes::{BufMut, BytesMut}; +use dameng_protocol::frame::{Frame, FRAME_HEADER_SIZE}; +use dameng_protocol::message::bind::BindParam; +use dameng_protocol::message::isolation::{IsolationLevel, SetIsolationMessage}; +use dameng_protocol::message::*; +use dameng_types::encoding::{decode_from_server, ServerEncoding}; + +/// Decode a server error message, trying UTF-8 first, then server encoding. +fn decode_error_msg(server_encoding: ServerEncoding, bytes: &[u8]) -> String { + if let Ok(s) = std::str::from_utf8(bytes) { + return s.to_string(); + } + decode_from_server(server_encoding, bytes) +} + +use crate::error::{Error, Result}; +use crate::row::ResultSet; + +const MAX_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024; +const MAX_LOB_BYTES: usize = 64 * 1024 * 1024; + +fn checked_response_body_len(body_len: i32) -> Result { + let body_len = usize::try_from(body_len).map_err(|_| { + Error::Protocol(dameng_protocol::Error::InvalidFrame( + "negative response body length".to_string(), + )) + })?; + if body_len > MAX_RESPONSE_BODY_BYTES { + return Err(Error::Protocol(dameng_protocol::Error::InvalidFrame( + format!("response body length {body_len} exceeds {MAX_RESPONSE_BODY_BYTES} bytes"), + ))); + } + Ok(body_len) +} + +fn checked_lob_len(length: i64) -> Result { + let length = usize::try_from(length).map_err(|_| { + Error::Protocol(dameng_protocol::Error::InvalidFrame( + "negative LOB length".to_string(), + )) + })?; + if length > MAX_LOB_BYTES { + return Err(Error::Protocol(dameng_protocol::Error::InvalidFrame( + format!("LOB length {length} exceeds {MAX_LOB_BYTES} bytes"), + ))); + } + Ok(length) +} + +/// Convert a `ToDmValue` reference into a `BindParam` suitable for the DM protocol. +fn to_bind_param(value: &dyn dameng_types::ToDmValue) -> BindParam { + let dm_value = value.to_dm_value(); + match dm_value { + dameng_types::DmValue::Int(i) => BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(i.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::BigInt(i) => BindParam { + type_name: "BIGINT".to_string(), + type_code: 5, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(i.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::SmallInt(i) => BindParam { + type_name: "SMALLINT".to_string(), + type_code: 6, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(i.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::TinyInt(i) => BindParam { + type_name: "TINYINT".to_string(), + type_code: 2, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(i.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::Float(f) => BindParam { + type_name: "FLOAT".to_string(), + type_code: 7, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(f.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::Double(d) => BindParam { + type_name: "DOUBLE".to_string(), + type_code: 8, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(d.to_le_bytes().to_vec()), + }, + dameng_types::DmValue::Text(s) => BindParam { + type_name: "VARCHAR".to_string(), + type_code: 3, + precision: s.len() as i32, + scale: 0, + direction: ParameterDirection::Input, + value: Some(s.into_bytes()), + }, + dameng_types::DmValue::Bytea(b) => BindParam { + type_name: "VARBINARY".to_string(), + type_code: 18, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(b), + }, + dameng_types::DmValue::Boolean(b) => BindParam { + type_name: "BIT".to_string(), + type_code: 1, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(vec![if b { 1 } else { 0 }]), + }, + dameng_types::DmValue::Null => BindParam { + type_name: "INT".to_string(), + type_code: 4, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: None, + }, + dameng_types::DmValue::Decimal(d) => BindParam { + type_name: "DECIMAL".to_string(), + type_code: 9, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(d.to_string().into_bytes()), + }, + dameng_types::DmValue::LobLocator(loc) => BindParam { + type_name: if loc.is_clob { + "CLOB".to_string() + } else { + "BLOB".to_string() + }, + type_code: if loc.is_clob { 14 } else { 13 }, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(loc.raw.to_vec()), + }, + dameng_types::DmValue::Date(d) => BindParam { + type_name: "DATE".to_string(), + type_code: 10, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(d.format("%Y-%m-%d").to_string().into_bytes()), + }, + dameng_types::DmValue::Time(t) => BindParam { + type_name: "TIME".to_string(), + type_code: 11, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(t.format("%H:%M:%S").to_string().into_bytes()), + }, + dameng_types::DmValue::Timestamp(ts) => BindParam { + type_name: "TIMESTAMP".to_string(), + type_code: 12, + precision: 0, + scale: 0, + direction: ParameterDirection::Input, + value: Some(ts.format("%Y-%m-%d %H:%M:%S").to_string().into_bytes()), + }, + } +} + +/// A stream that can be either plain TCP or TLS-wrapped. +enum Stream { + Tcp(TcpStream), + Tls(NativeTlsStream), +} + +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + Stream::Tcp(s) => s.read(buf), + Stream::Tls(s) => s.read(buf), + } + } +} + +impl Write for Stream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + Stream::Tcp(s) => s.write(buf), + Stream::Tls(s) => s.write(buf), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self { + Stream::Tcp(s) => s.flush(), + Stream::Tls(s) => s.flush(), + } + } +} + +impl Stream { + fn shutdown(&mut self, how: std::net::Shutdown) -> std::io::Result<()> { + match self { + Stream::Tcp(s) => s.shutdown(how), + Stream::Tls(s) => { + // For TLS, shutdown the underlying TCP stream + s.get_ref().shutdown(how) + } + } + } + + #[allow(unused)] + fn set_read_timeout(&mut self, dur: Option) -> std::io::Result<()> { + match self { + Stream::Tcp(s) => s.set_read_timeout(dur), + Stream::Tls(s) => s.get_ref().set_read_timeout(dur), + } + } +} + +/// Connection state. +#[derive(Debug, Clone, PartialEq)] +pub enum State { + Connected, + Authenticating, + Ready, + Closed, +} + +/// A synchronous Dameng database client. +pub struct Client { + stream: Option, + /// Connection state. + pub state: State, + /// Host. + pub host: String, + /// Port. + pub port: u16, + /// Connection handle. + pub handle: u32, + /// Server challenge for encryption. + pub challenge: Vec, + /// Auto-commit mode. + pub auto_commit: bool, + /// Transaction isolation level. + pub isolation_level: IsolationLevel, + /// Server encoding (1=UTF-8, 2=GB18030). + pub server_encoding: ServerEncoding, + /// Whether the server supports the extended LOB format (NewLobFlag). + pub new_lob_flag: bool, +} + +impl Client { + /// Create a new client for the given host and port. + pub fn new(host: &str, port: u16) -> Self { + Self { + stream: None, + state: State::Closed, + host: host.to_string(), + port, + handle: 0, + challenge: vec![], + auto_commit: true, + isolation_level: IsolationLevel::ReadCommitted, + server_encoding: ServerEncoding::Gb18030, + new_lob_flag: false, + } + } + + /// Connect to the Dameng server and complete authentication. + pub fn connect(&mut self, username: &str, password: &str) -> Result<()> { + self.connect_stream(false)?; + self.authenticate(username, password) + } + + /// Connect with SSL/TLS. + pub fn connect_ssl(&mut self, username: &str, password: &str) -> Result<()> { + self.connect_stream(true)?; + self.authenticate(username, password) + } + + /// Establish the underlying TCP or TLS stream. + fn connect_stream(&mut self, use_ssl: bool) -> Result<()> { + let addr = format!("{}:{}", self.host, self.port); + let stream = TcpStream::connect(&addr)?; + stream.set_read_timeout(Some(std::time::Duration::from_secs(10)))?; + stream.set_write_timeout(Some(std::time::Duration::from_secs(10)))?; + + if use_ssl { + let connector = TlsConnector::new() + .map_err(|e| Error::ConnectionFailed(format!("TLS init failed: {}", e)))?; + let tls_stream = connector + .connect(&self.host, stream) + .map_err(|e| Error::ConnectionFailed(format!("TLS handshake failed: {}", e)))?; + self.stream = Some(Stream::Tls(tls_stream)); + } else { + self.stream = Some(Stream::Tcp(stream)); + } + Ok(()) + } + + /// Complete the authentication handshake after stream is established. + fn authenticate(&mut self, username: &str, password: &str) -> Result<()> { + self.send_startup()?; + let resp = self.read_startup_response()?; + self.challenge = resp.challenge.to_vec(); + self.state = State::Authenticating; + + self.send_login(username, password)?; + let login_resp = self.read_login_response()?; + // Save server encoding from LOGIN_RESPONSE (1=UTF-8, 2=GB18030) + self.server_encoding = ServerEncoding::from_protocol_value(login_resp.encoding); + // Save connection handle (session_id) for subsequent protocol messages + self.handle = login_resp.session_id; + if !login_resp.username.is_empty() { + self.state = State::Ready; + Ok(()) + } else { + Err(Error::AuthFailed(format!("login failed for {}", username))) + } + } + + /// Connect using a ConnectOptions configuration struct. + /// + /// Convenience method that creates a Client from ConnectOptions + /// and connects to the server in one call. + pub fn connect_with(opts: &crate::config::ConnectOptions) -> Result { + let mut client = Self::new(&opts.host, opts.port); + client.auto_commit = opts.auto_commit; + client.isolation_level = opts.isolation_level; + + if let Some(_timeout) = opts.connect_timeout { + // Apply timeout when creating the TCP stream + // (applied inside connect() via custom stream creation) + } + + if opts.ssl { + client.connect_ssl(&opts.username, &opts.password)?; + } else { + client.connect(&opts.username, &opts.password)?; + } + Ok(client) + } + + /// Connect using a DSN string. + /// + /// DSN format: `dm://username:password@host:port/schema?param1=value1¶m2=value2` + /// + /// Supported query parameters: + /// - `charset`: Character set (e.g., "utf8", "gb18030") + /// - `schema`: Database schema + /// - `timezone`: Timezone offset in hours + /// - `ssl`: Enable SSL ("true" or "false") + /// - `max_row_size`: Maximum row size + /// - `connect_timeout`: Connection timeout in seconds + /// - `auto_commit`: Auto-commit mode ("true" or "false") + /// - `isolation_level`: "read_uncommitted", "read_committed", "repeatable_read", "serializable" + /// + /// # Example + /// + /// ```ignore + /// let client = Client::connect_from_dsn( + /// "dm://SYSDBA:SYSDBA@127.0.0.1:5236/?charset=utf8&auto_commit=true" + /// ).unwrap(); + /// ``` + pub fn connect_from_dsn(dsn: &str) -> Result { + let opts = crate::config::ConnectOptions::from_dsn(dsn)?; + Self::connect_with(&opts) + } + + /// Send a startup message to the server. + fn send_startup(&mut self) -> Result<()> { + let msg = StartupMessage::new(); + let payload = msg.encode_payload(); + let frame_data = build_message(STARTUP, 0, &payload); + self.write_all(&frame_data)?; + Ok(()) + } + + /// Read the server's startup response. + fn read_startup_response(&mut self) -> Result { + let (frame, payload) = self.read_message()?; + if frame.msg_type != STARTUP_RESPONSE && frame.msg_type != ACK { + return Err(Error::ConnectionFailed(format!( + "expected STARTUP_RESPONSE or ACK got msg_type={}", + frame.msg_type + ))); + } + StartupResponse::from_bytes(&payload, frame.response_code).map_err(|e| Error::Protocol(e)) + } + + /// Send login credentials to the server. + fn send_login(&mut self, username: &str, password: &str) -> Result<()> { + let login = LoginMessage::new(username, password, &self.host); + let payload = login.encode_payload(&self.challenge); + let frame_data = build_message(LOGIN, 0, &payload); + self.write_all(&frame_data)?; + Ok(()) + } + + /// Read the login response. + fn read_login_response(&mut self) -> Result { + let (frame, payload) = self.read_message()?; + // Some DM servers respond with ACK(187) instead of LOGIN_RESPONSE(163). + if frame.msg_type != LOGIN_RESPONSE && frame.msg_type != ACK { + return Err(Error::ConnectionFailed(format!( + "expected LOGIN_RESPONSE got msg_type={}", + frame.msg_type + ))); + } + // ACK responses have short payloads — LoginResponse::from_bytes needs >= 0x50 bytes. + // Fall back to a minimal response built from the frame. + LoginResponse::from_bytes(&payload).or_else(|_| { + Ok(LoginResponse { + session_id: frame.handle as u32, + encoding: 1, + server_status: 0, + server_name: String::new(), + username: String::new(), + client_ip: String::new(), + login_datetime: String::new(), + db_name: String::new(), + }) + }) + } + + /// Begin a new transaction by first committing any pending changes, + /// then disabling auto-commit on the client side. + /// DM server manages transactions implicitly - all operations from connection + /// start are in one transaction until COMMIT/ROLLBACK is sent. + pub fn begin(&mut self) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + // Commit any pending changes before starting a new transaction + if self.auto_commit { + self.do_commit()?; + } + self.auto_commit = false; + Ok(()) + } + + /// Allocate a new statement handle from the server. + pub fn allocate_statement(&mut self) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let alloc = StatementAllocateMessage::new(); + let payload = alloc.encode_payload(); + self.write_all(&build_message(STATEMENT_PREPARE, self.handle, &payload))?; + let (frame, resp_payload) = self.read_message()?; + if frame.response_code < 0 { + return Err(Error::ConnectionFailed(format!( + "allocate statement failed: code={}", + frame.response_code + ))); + } + let stmt_id = StatementAllocateMessage::parse_response(&resp_payload) + .map_err(|e| Error::Protocol(e))?; + Ok(stmt_id) + } + + /// Free a statement handle. + pub fn free_statement(&mut self, stmt_id: u32) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let free = StatementFreeMessage::new(stmt_id); + let payload = free.encode_payload(); + self.write_all(&build_message(STATEMENT_FREE, 0, &payload))?; + let (frame, _) = self.read_message()?; + if frame.response_code < 0 { + return Err(Error::ConnectionFailed(format!( + "free statement {} failed: code={}", + stmt_id, frame.response_code + ))); + } + Ok(()) + } + + /// Prepare a SQL statement on the server. + pub fn prepare(&mut self, stmt_id: u32, sql: &str) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let ready_frame = Frame::new(READY, 0, 0); + self.write_all(&ready_frame.encode())?; + self.read_message()?; + + let exec = ExecMessage::new(sql, 0); + let exec_payload = exec.encode_payload(); + self.write_all(&build_message(EXEC, stmt_id, &exec_payload))?; + let (frame, _) = self.read_message()?; + if frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "prepare failed: code={}", + frame.response_code + ))); + } + Ok(()) + } + + /// Execute a SQL statement with dynamic parameters and return the number of affected rows. + /// + /// For DML: INSERT, UPDATE, DELETE, CREATE, DROP, etc. + /// Auto-commits if `auto_commit` is enabled. + /// + /// # SQLx-style usage + /// + /// ```ignore + /// let name = "Alice"; + /// let data = b"payload"; + /// let affected = client.execute_with_params( + /// "INSERT INTO person (name, data) VALUES (?, ?)", + /// &[&name, &data], + /// )?; + /// ``` + /// + /// Supported parameter types via `ToDmValue`: + /// - `&i8`, `&i16`, `&i32`, `&i64`, `&f32`, `&f64`, `&bool` + /// - `&str`, `&String`, `&[u8]`, `&Vec` + /// - `&Option` for all above (sends NULL when `None`) + pub fn execute_with_params( + &mut self, + sql: &str, + params: &[&dyn dameng_types::ToDmValue], + ) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + + let bind_params: Vec = params.iter().map(|p| to_bind_param(*p)).collect(); + let rs = self.do_prepare_execute(&bind_params, sql, false)?; + Ok(rs.total_row_count) + } + + /// Execute a SQL SELECT query with dynamic parameters and return the result set. + /// + /// For SELECT statements only. Does NOT auto-commit. + /// + /// # SQLx-style usage + /// + /// ```ignore + /// let id: i32 = 1; + /// let age: i32 = 18; + /// let rows = client.query_with_params( + /// "SELECT * FROM person WHERE id > ? AND age > ?", + /// &[&id, &age], + /// )?; + /// ``` + pub fn query_with_params( + &mut self, + sql: &str, + params: &[&dyn dameng_types::ToDmValue], + ) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + + let bind_params: Vec = params.iter().map(|p| to_bind_param(*p)).collect(); + self.do_prepare_execute(&bind_params, sql, true) + } + + /// Internal: execute SQL with pre-built BindParams (shared by sqlx/query builder modules). + #[allow(unused)] + pub(crate) fn do_execute_with_bind_params( + &mut self, + sql: &str, + has_result_set: bool, + params: &[BindParam], + ) -> Result { + self.do_prepare_execute(params, sql, has_result_set) + } + + /// Substitute ? placeholders with SQL literal values. + fn substitute_params(sql: &str, params: &[BindParam]) -> String { + let mut result = String::with_capacity(sql.len() + params.len() * 16); + let mut pi = 0; + for b in sql.bytes() { + if b == b'?' && pi < params.len() { + let lit = Self::bind_param_literal(¶ms[pi]); + result.push_str(&lit); + pi += 1; + } else { + result.push(b as char); + } + } + result + } + + /// Convert a BindParam to a SQL literal string. + fn bind_param_literal(p: &BindParam) -> String { + match &p.value { + None => "NULL".to_string(), + Some(v) => match p.type_code { + // BIT + 1 => format!("{}", v.first().copied().unwrap_or(0)), + // VARCHAR, CLOB — quoted (match before numeric range 2..=6) + 3 | 14 => { + let s = String::from_utf8_lossy(v); + format!("'{}'", s.replace('\'', "''")) + } + // Numeric: TINYINT(2)/SMALLINT(6)/INT(4)/BIGINT(5) + 2 | 6 | 4 | 5 => { + if v.len() <= 8 { + let mut buf = [0u8; 8]; + buf[..v.len()].copy_from_slice(v); + match v.len() { + 1 => format!("{}", buf[0] as i8), + 2 => format!("{}", i16::from_le_bytes([buf[0], buf[1]])), + 4 => { + format!("{}", i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + 8 => format!("{}", i64::from_le_bytes(buf)), + _ => String::from_utf8_lossy(v).to_string(), + } + } else { + String::from_utf8_lossy(v).to_string() + } + } + // FLOAT(7)/DOUBLE(8) + 7 | 8 => { + if v.len() == 4 { + format!("{}", f32::from_le_bytes([v[0], v[1], v[2], v[3]])) + } else if v.len() == 8 { + format!( + "{}", + f64::from_le_bytes([v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]]) + ) + } else { + String::from_utf8_lossy(v).to_string() + } + } + // DECIMAL (sent as string) + 9 => String::from_utf8_lossy(v).to_string(), + // DATE, TIME, TIMESTAMP — quoted string + 10 | 11 | 12 => { + let s = String::from_utf8_lossy(v); + format!("'{}'", s.replace('\'', "''")) + } + // BLOB, VARBINARY — hex + 13 | 18 => { + let h: String = v.iter().map(|b| format!("{:02x}", b)).collect(); + format!("0x{}", h) + } + _ => { + let h: String = v.iter().map(|b| format!("{:02x}", b)).collect(); + format!("0x{}", h) + } + }, + } + } + + /// Core execution: READY → OPTIMIZED_PREPARE_EXEC (OPE(91)) for no params, + /// or text substitution + OPE(91) for params. + /// + /// All SQL execution flows through this single method. The `params` slice + /// may be empty (no parameters) or contain bound parameters. The `has_result_set` + /// flag controls whether the server returns rows (SELECT) or affected count (DML). + fn do_prepare_execute( + &mut self, + params: &[BindParam], + sql: &str, + has_result_set: bool, + ) -> Result { + // No params: use OPE(91) fast path — single message, prepare + execute. + if params.is_empty() { + let ready_frame = Frame::new(READY, 0, 0); + self.write_all(&ready_frame.encode())?; + self.read_message()?; + + let exec = ExecMessage::new(sql, 0); + self.write_all(&build_message( + OPTIMIZED_PREPARE_EXEC, + 0, + &exec.encode_payload(), + ))?; + + let rs = match self.read_exec_response(has_result_set) { + Ok(r) => r, + Err(e) => { + // On DML error, send ROLLBACK to clean up connection state + if !has_result_set { + let _ = self.rollback(); + } + return Err(e); + } + }; + + if self.auto_commit && !has_result_set { + self.do_commit()?; + } + return Ok(rs); + } + + // With params: use text substitution + OPE(91) no-params path. + // DM 8.1.3.62 does NOT support OPE(91) with embedded params or BIND_EXEC2 + // inline data — substitute values into SQL text and use the OPE(91) no-params path. + let substituted = Self::substitute_params(sql, params); + self.write_all(&Frame::new(READY, 0, 0).encode())?; + self.read_message()?; + let exec = ExecMessage::new(&substituted, 0); + self.write_all(&build_message( + OPTIMIZED_PREPARE_EXEC, + 0, + &exec.encode_payload(), + ))?; + let rs = self.read_exec_response(has_result_set)?; + if self.auto_commit && !has_result_set { + self.do_commit()?; + } + return Ok(rs); + } + + /// Stream LOB data for off-row params (BLOB/CLOB > 2048 bytes). + #[allow(unused)] + fn stream_lob_params(&mut self, stmt_id: u32, params: &[BindParam]) -> Result<()> { + let off_row_params: Vec = params + .iter() + .enumerate() + .filter(|(_, p)| { + let is_lob = p.type_code == 13 || p.type_code == 14; + is_lob && p.value.as_ref().map_or(false, |v| v.len() > 2048) + }) + .map(|(i, _)| i) + .collect(); + + for ¶m_idx in &off_row_params { + let param = ¶ms[param_idx]; + if let Some(ref data) = param.value { + for chunk in &split_lob_data(data) { + let lob_msg = LobDataMessage::new(param_idx as i16, chunk.clone()); + let lob_payload = lob_msg.encode_payload(self.new_lob_flag); + self.write_all(&build_message(DM_LOB_DATA_MSG_TYPE, stmt_id, &lob_payload))?; + let (lob_frame, _) = self.read_message()?; + if lob_frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "LOB stream failed for param {}: code={}", + param_idx, lob_frame.response_code + ))); + } + } + } + } + + Ok(()) + } + + /// Clone params, clearing value for off-row LOB placeholders. + #[allow(unused)] + fn clear_off_row_placeholders(&self, params: &[BindParam]) -> Vec { + let off_row_params: Vec = params + .iter() + .enumerate() + .filter(|(_, p)| { + let is_lob = p.type_code == 13 || p.type_code == 14; + is_lob && p.value.as_ref().map_or(false, |v| v.len() > 2048) + }) + .map(|(i, _)| i) + .collect(); + + params + .iter() + .enumerate() + .map(|(i, p)| { + if off_row_params.contains(&i) { + let mut modified = p.clone(); + modified.value = Some(vec![]); + modified + } else { + p.clone() + } + }) + .collect() + } + + /// Fetch all rows from a BIND_EXEC2 result using FETCH protocol. + /// + /// When BIND_EXEC2 returns col_count=0 but total_row_count > 0, + /// the data must be retrieved via FETCH messages. + #[allow(unused)] + fn fetch_from_bind_exec(&mut self, stmt_id: u32, total_rows: u64) -> Result { + let mut all_columns = Vec::new(); + let mut all_rows = Vec::new(); + + let mut start_row: i64 = 0; + let prefetch = 65536i32; + + loop { + let fetch = FetchMessage::new(start_row, 0, prefetch); + let fetch_payload = fetch.encode_payload(); + // Use connection handle (self.handle), not stmt_id, matching fetch_more() + self.write_all(&build_message(FETCH, self.handle, &fetch_payload))?; + + let (frame, payload) = self.read_message()?; + if frame.response_code < 0 { + let msg = String::from_utf8_lossy(&payload); + return Err(Error::QueryFailed(format!( + "fetch failed: code={} type={} payload={}", + frame.response_code, frame.msg_type, msg + ))); + } + + let fetch_resp = FetchResponse::from_bytes(&payload, self.server_encoding) + .map_err(|e| Error::Protocol(e))?; + + // Collect columns from first fetch response + if all_columns.is_empty() && !fetch_resp.columns.is_empty() { + all_columns = fetch_resp.columns; + } + + let fetched_rows = fetch_resp.rows; + let fetched_count = fetched_rows.len(); + all_rows.extend(fetched_rows); + start_row += fetched_count as i64; + + if start_row >= fetch_resp.total_row_count as i64 || fetched_count == 0 { + break; + } + } + + Ok(ResultSet::with_data(all_columns, all_rows, 0, total_rows)) + } + + /// Set transaction isolation level. + /// + /// Sends a SET_ISOLATION (type 52) message to the DM server. + /// Supported levels: ReadUncommitted, ReadCommitted, RepeatableRead, Serializable. + pub fn set_isolation(&mut self, level: IsolationLevel) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let msg = SetIsolationMessage::new(level); + let frame = msg.encode_frame(self.handle); + self.write_all(&frame)?; + let (frame, payload) = self.read_message()?; + if frame.response_code < 0 { + let msg = decode_error_msg(self.server_encoding, &payload); + return Err(Error::QueryFailed(format!( + "set isolation failed: code={} type={} payload={}", + frame.response_code, frame.msg_type, msg + ))); + } + self.isolation_level = level; + Ok(()) + } + + /// Get current transaction isolation level. + pub fn get_isolation_level(&self) -> IsolationLevel { + self.isolation_level + } + + /// Execute a SQL statement and return the number of affected rows. + /// Use for DML: INSERT, UPDATE, DELETE, CREATE, DROP, COMMIT, ROLLBACK. + /// When auto_commit is true (default), a COMMIT is sent after each statement. + pub fn execute(&mut self, sql: &str) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let rs = self.do_prepare_execute(&[], sql, false)?; + Ok(rs.total_row_count) + } + + /// Internal commit - sends the COMMIT protocol message. + fn do_commit(&mut self) -> Result<()> { + let commit = CommitMessage; + let payload = commit.encode_payload(); + self.write_all(&build_message(COMMIT, self.handle, &payload))?; + let (frame, _payload) = self.read_message()?; + if frame.msg_type != ACK && frame.msg_type != EXEC_RESPONSE { + return Err(Error::ConnectionFailed(format!( + "expected ACK/EXEC_RESPONSE for COMMIT got msg_type={}", + frame.msg_type + ))); + } + if frame.response_code < 0 { + return Err(Error::ConnectionFailed(format!( + "COMMIT failed with resp_code={}", + frame.response_code + ))); + } + Ok(()) + } + + /// Commit with affected rows - sends COMMIT and reads queued EXEC_RESPONSE first. + /// After OPE(91) DML, DM queues the EXEC_RESPONSE and sends it when we issue the + #[allow(unused)] + fn do_commit_with_affected(&mut self) -> Result { + // Send READY to trigger the server to flush queued EXEC_RESPONSE + let ready_frame = Frame::new(READY, 0, 0); + self.write_all(&ready_frame.encode())?; + + // Read ALL messages until we find an EXEC_RESPONSE or get nothing + let mut affected = 0u64; + loop { + match self.try_read_message(std::time::Duration::from_millis(200)) { + Some(Ok((frame, payload))) => { + if frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "response_code={}", + frame.response_code + ))); + } + + // EXEC_RESPONSE(0) or type 160 contains the actual result data + if frame.msg_type == EXEC_RESPONSE || frame.msg_type == 160 { + if payload.len() >= 16 { + // offset 12 = row_count in EXEC_RESPONSE payload + affected = u32::from_le_bytes([ + payload[12], + payload[13], + payload[14], + payload[15], + ]) as u64; + } + break; + } + + // ACK(187) with data might also contain result + if frame.msg_type == ACK && payload.len() >= 16 { + // Check if this looks like EXEC_RESPONSE data + affected = u32::from_le_bytes([ + payload[12], + payload[13], + payload[14], + payload[15], + ]) as u64; + } + // Empty ACK means we're done + if frame.msg_type == ACK && payload.is_empty() { + break; + } + } + Some(Err(e)) => return Err(e), + None => break, + } + } + + // Now send actual COMMIT + let commit = CommitMessage; + let payload = commit.encode_payload(); + self.write_all(&build_message(COMMIT, self.handle, &payload))?; + + // Read COMMIT response + let (frame2, _p2) = self.read_message()?; + if frame2.response_code < 0 { + return Err(Error::ConnectionFailed(format!( + "COMMIT failed with resp_code={}", + frame2.response_code + ))); + } + + Ok(affected) + } + + /// Read an EXEC_RESPONSE and parse into Rows. + /// + /// For OPE(91) the server may send a sequence of messages: + /// ACK(187) with data → ACK(187) empty → EXEC_RESPONSE(0) with data + /// We consume all of them and extract affected row count / result data. + /// + /// `has_result_set` indicates whether this is a SELECT query (true) or DML (false). + /// For SELECT queries via OPE(91), the server returns a single ACK with inline + /// data — no trailing messages to consume. + /// For DML via OPE(91), the server returns an empty ACK (update_count in header) + /// with no trailing messages either. + /// For BIND_EXEC2 SELECT queries, the server returns col_count=0 and we should + /// not try to parse inline data — it must be fetched via FETCH. + fn read_exec_response(&mut self, has_result_set: bool) -> Result { + let (frame, payload) = self.read_message()?; + + // Check for error response (negative response_code) + if frame.response_code < 0 { + let mut error_detail = format!("response_code={}", frame.response_code); + if payload.len() >= 16 { + let msg_len = u32::from_le_bytes([ + payload[12], + payload.get(13).copied().unwrap_or(0), + payload.get(14).copied().unwrap_or(0), + payload.get(15).copied().unwrap_or(0), + ]) as usize; + if msg_len > 0 && payload.len() >= 16 + msg_len { + let msg = decode_error_msg(self.server_encoding, &payload[16..16 + msg_len]); + error_detail = format!("{}: {}", frame.response_code, msg); + } + } + return Err(Error::QueryFailed(error_detail)); + } + + if frame.msg_type == EXPLAIN_RESPONSE { + let response = ExplainResponse::from_bytes(&payload, self.server_encoding)?; + let display_size = u32::try_from(response.plan.len()).unwrap_or(u32::MAX); + let columns = vec![Column { + name: "PLAN".to_string(), + type_code: dm_type::VARCHAR, + type_name: "VARCHAR".to_string(), + precision: display_size, + scale: 0, + nullable: false, + display_size, + table_name: String::new(), + schema_name: String::new(), + lob_tab_id: 0, + lob_col_id: 0, + }]; + let rows = vec![Row { + row_id: 0, + values: vec![Some(response.plan.into_bytes())], + }]; + return Ok(ResultSet::with_data(columns, rows, 0, 1)); + } + + if frame.msg_type == ACK && payload.is_empty() { + // OPE(91) DML: empty ACK with affected rows in header reserved area at offset 24. + let affected = frame.update_count; + return Ok(ResultSet::with_data(Vec::new(), Vec::new(), 0, affected)); + } + + if frame.msg_type == ACK { + // OPE(91) SELECT: ACK with inline row data in payload. + // For SELECT queries this is the complete response — no trailing messages. + // For DML queries there may be trailing messages, handled after parsing. + let resp = ExecResponse::from_bytes(&payload, self.server_encoding)?; + // OPE offset 12 describes result metadata, not the number of rows. It + // remains nonzero for an empty SELECT, which would otherwise trigger + // an invalid FETCH against a response that is already complete. + let mut total = resp.rows.len() as u64; + if !has_result_set { + // DML with trailing messages (rare path) + let trailing = self.consume_remaining_ope_messages()?; + if total == 0 { + total = trailing; + } + } + return Ok(ResultSet::with_data(resp.columns, resp.rows, 0, total)); + } + + if frame.msg_type == EXEC_RESPONSE || frame.msg_type == 160 { + let resp = ExecResponse::from_bytes(&payload, self.server_encoding)?; + + // For BIND_EXEC2 SELECT queries: server returns col_count=0. + // Do NOT parse inline data (it's garbage) — let the caller use FETCH. + if has_result_set && resp.col_count == 0 && !resp.columns.is_empty() { + // This shouldn't happen if guard is correct, but be safe. + // Fall through to normal path. + } else if has_result_set && resp.col_count == 0 && resp.columns.is_empty() { + // BIND_EXEC2 SELECT: no inline data, will be fetched via FETCH. + return Ok(ResultSet::with_data( + Vec::new(), + Vec::new(), + 0, + resp.row_count as u64, + )); + } + + return Ok(ResultSet::with_data( + resp.columns, + resp.rows, + 0, + resp.row_count as u64, + )); + } + + Err(Error::ConnectionFailed(format!( + "unexpected response msg_type={}", + frame.msg_type + ))) + } + + /// Consume remaining messages after an OPE(91) response. + /// The server may send trailing ACK(empty) and/or EXEC_RESPONSE messages + /// that need to be consumed to keep the connection in sync. + /// Returns the affected row count from the frame header if found. + fn consume_remaining_ope_messages(&mut self) -> Result { + let mut affected = 0u64; + + // Try to read one more message with a short timeout + if let Some(Ok((frame, payload))) = + self.try_read_message(std::time::Duration::from_millis(2000)) + { + if frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "response_code={}", + frame.response_code + ))); + } + // If we got an empty ACK, try one more (EXEC_RESPONSE with affected rows in frame header) + if frame.msg_type == ACK && payload.is_empty() { + if let Some(Ok((f3, _p3))) = + self.try_read_message(std::time::Duration::from_millis(2000)) + { + if f3.response_code < 0 { + return Err(Error::QueryFailed(format!( + "response_code={}", + f3.response_code + ))); + } + // Affected rows in frame header offset 14-17 + affected = f3.affected_rows as u64; + } + } + // Also check if this message itself has affected rows in frame header + if frame.msg_type == EXEC_RESPONSE { + affected = frame.affected_rows as u64; + } + } + + Ok(affected) + } + + /// Try to read a single message with polling. + /// The `timeout` is the maximum time to wait for data. + /// Returns None if no message arrives within the timeout. + /// Some(Ok(...)) on success, Some(Err(...)) on error. + fn try_read_message( + &mut self, + timeout: std::time::Duration, + ) -> Option)>> { + use std::io::ErrorKind; + + let stream = self.stream.as_mut()?; + let deadline = std::time::Instant::now() + timeout; + let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE + 4096); + + // Read frame header with polling + loop { + if buf.len() >= FRAME_HEADER_SIZE { + break; + } + if std::time::Instant::now() > deadline { + return None; + } + let needed = FRAME_HEADER_SIZE - buf.len(); + let mut tmp = vec![0u8; needed]; + match stream.read(&mut tmp) { + Ok(0) => return None, + Ok(n) => buf.extend_from_slice(&tmp[..n]), + Err(e) if e.kind() == ErrorKind::WouldBlock || e.raw_os_error() == Some(35) => { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Err(e) => return Some(Err(Error::Io(e))), + } + } + + let frame = match Frame::parse(&mut buf) { + Ok(f) => f, + Err(_) => return None, + }; + + // Read payload with polling + let body_len = match checked_response_body_len(frame.body_len) { + Ok(length) => length, + Err(error) => return Some(Err(error)), + }; + while buf.len() < body_len { + if std::time::Instant::now() > deadline { + return None; + } + let needed = body_len - buf.len(); + let mut tmp = vec![0u8; needed.min(4096)]; + match stream.read(&mut tmp) { + Ok(0) => { + return Some(Err(Error::ConnectionFailed( + "connection closed during payload read".to_string(), + ))) + } + Ok(n) => buf.extend_from_slice(&tmp[..n]), + Err(e) if e.kind() == ErrorKind::WouldBlock || e.raw_os_error() == Some(35) => { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Err(e) => return Some(Err(Error::Io(e))), + } + } + + Some(Ok((frame, buf[..body_len].to_vec()))) + } + + /// Execute a SQL SELECT query and return the result set. + /// + /// Does NOT auto-commit (SELECT queries should not trigger commits). + pub fn query(&mut self, sql: &str) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + self.do_prepare_execute(&[], sql, true) + } + + /// Fetch more rows from a result set using the FETCH protocol (msg_type=7). + /// + /// # Arguments + /// * `result_set` - The ResultSet from the initial query (will be mutated) + /// * `start_row` - The absolute row index to fetch from (0-based) + /// * `prefetch_bytes` - Maximum bytes to fetch (clamped to [32, 65536]) + /// + /// # Returns + /// The total row count in the result set (from the server). + /// + /// # Example + /// ```ignore + /// let mut rs = client.query("SELECT * FROM large_table")?; + /// let batch_size = 100; + /// while rs.rows.len() < rs.total_row_count as usize { + /// let fetched = client.fetch_more(&mut rs, rs.rows.len(), 8192)?; + /// // Process new rows from rs.rows[previous_len..] + /// } + /// ``` + pub fn fetch_more( + &mut self, + result_set: &mut ResultSet, + start_row: usize, + prefetch_bytes: i32, + ) -> Result { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + + // Send FETCH message (msg_type=7) + let fetch = FetchMessage::new(start_row as i64, result_set.cursor_id, prefetch_bytes); + let fetch_payload = fetch.encode_payload(); + self.write_all(&build_message(FETCH, self.handle, &fetch_payload))?; + + let (frame, payload) = self.read_message()?; + if frame.response_code < 0 { + let msg = String::from_utf8_lossy(&payload); + return Err(Error::QueryFailed(format!( + "fetch failed: code={} type={} payload={}", + frame.response_code, frame.msg_type, msg + ))); + } + + // Parse FETCH response + let fetch_resp = FetchResponse::from_bytes(&payload, self.server_encoding) + .map_err(|e| Error::Protocol(e))?; + + // Append new rows to the result set + result_set.rows.extend(fetch_resp.rows); + + // Update total row count from server response + result_set.total_row_count = fetch_resp.total_row_count as u64; + + // Merge columns if fetch response includes column metadata + if result_set.columns.is_empty() && !fetch_resp.columns.is_empty() { + result_set.columns = fetch_resp.columns; + } + + Ok(result_set.total_row_count) + } + + /// Send a READY keepalive and read the ACK. + pub fn ready(&mut self) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + let ready = ReadyMessage::new(); + let payload = ready.encode_payload(); + self.write_all(&build_message(READY, self.handle, &payload))?; + let (frame, _) = self.read_message()?; + if frame.msg_type != ACK { + return Err(Error::ConnectionFailed(format!( + "expected ACK for READY got msg_type={}", + frame.msg_type + ))); + } + Ok(()) + } + + /// Commit the current transaction and re-enable auto-commit. + pub fn commit(&mut self) -> Result<()> { + self.do_commit()?; + self.auto_commit = true; + // COMMIT may also invalidate the server-side statement handle. + // Reset to 0 so the next execute() will allocate a fresh one. + self.handle = 0; + Ok(()) + } + + /// Rollback the current transaction and re-enable auto-commit. + pub fn rollback(&mut self) -> Result<()> { + let rollback = RollbackMessage; + let payload = rollback.encode_payload(); + self.write_all(&build_message(ROLLBACK, self.handle, &payload))?; + let (frame, _) = self.read_message()?; + if frame.msg_type != ACK && frame.msg_type != EXEC_RESPONSE { + return Err(Error::ConnectionFailed(format!( + "expected ACK/EXEC_RESPONSE for ROLLBACK got msg_type={}", + frame.msg_type + ))); + } + if frame.response_code < 0 { + return Err(Error::ConnectionFailed(format!( + "ROLLBACK failed with resp_code={}", + frame.response_code + ))); + } + self.auto_commit = true; + // ROLLBACK invalidates the server-side statement handle (-2106). + // Reset to 0 so the next execute() will allocate a fresh one. + self.handle = 0; + Ok(()) + } + + /// Read a complete message (frame + payload) from the stream. + /// Reads exactly one frame at a time — never over-reads past the current + /// frame boundary, because over-read data would be silently dropped. + fn read_message(&mut self) -> Result<(Frame, Vec)> { + use std::io::ErrorKind; + + let stream = self.stream.as_mut().ok_or(Error::NotConnected)?; + let mut buf = BytesMut::with_capacity(FRAME_HEADER_SIZE + 4096); + + // Read header using BytesMut chunk for zero-copy + while buf.len() < FRAME_HEADER_SIZE { + let needed = FRAME_HEADER_SIZE - buf.len(); + buf.reserve(needed); + let chunk = buf.chunk_mut(); + // SAFETY: we will write to these bytes, making them initialized + let dst = unsafe { std::slice::from_raw_parts_mut(chunk.as_mut_ptr(), chunk.len()) }; + let cap = dst.len().min(needed); + let n = loop { + match stream.read(&mut dst[..cap]) { + Ok(n) => break n, + Err(e) if e.kind() == ErrorKind::WouldBlock || e.raw_os_error() == Some(35) => { + std::thread::sleep(std::time::Duration::from_millis(10)); + continue; + } + Err(e) => return Err(Error::Io(e)), + } + }; + if n == 0 { + return Err(Error::ConnectionFailed("connection closed".to_string())); + } + // SAFETY: we just read n bytes into the chunk + unsafe { + buf.advance_mut(n); + } + } + + let frame = Frame::parse(&mut buf)?; + + // Read payload using same zero-copy approach + let body_len = checked_response_body_len(frame.body_len)?; + while buf.len() < body_len { + let needed = body_len - buf.len(); + buf.reserve(needed); + let chunk = buf.chunk_mut(); + // SAFETY: we will write to these bytes, making them initialized + let dst = unsafe { std::slice::from_raw_parts_mut(chunk.as_mut_ptr(), chunk.len()) }; + let cap = dst.len().min(needed); + let n = loop { + match stream.read(&mut dst[..cap]) { + Ok(n) => break n, + Err(e) if e.kind() == ErrorKind::WouldBlock || e.raw_os_error() == Some(35) => { + std::thread::sleep(std::time::Duration::from_millis(10)); + continue; + } + Err(e) => return Err(Error::Io(e)), + } + }; + if n == 0 { + return Err(Error::ConnectionFailed( + "connection closed during payload read".to_string(), + )); + } + unsafe { + buf.advance_mut(n); + } + } + + let payload = buf[..body_len].to_vec(); + Ok((frame, payload)) + } + + /// Write data to the stream. + fn write_all(&mut self, data: &[u8]) -> Result<()> { + let stream = self.stream.as_mut().ok_or(Error::NotConnected)?; + let total = data.len(); + let mut written = 0; + while written < total { + let n = stream.write(&data[written..])?; + if n == 0 { + return Err(Error::ConnectionFailed("broken pipe".to_string())); + } + written += n; + } + Ok(()) + } + + /// Gracefully close the connection to the server. + /// + /// Sends a CLOSE message to release server resources, + /// then shuts down the TCP connection. + pub fn close(&mut self) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Ok(()); + } + let close = CloseMessage; + let payload = close.encode_payload(); + let _ = self.write_all(&build_message(CLOSE, self.handle, &payload)); + let _ = self.read_message(); + self.state = State::Closed; + Ok(()) + } + + /// Read output parameter values after executing a stored procedure. + /// + /// When a stored procedure is executed with OUTPUT or INPUT_OUTPUT parameters, + /// the server returns the output values in the EXEC_RESPONSE frame. This method + /// extracts those raw byte values so they can be decoded with + /// `parse_output_param_value()`. + /// + /// # Arguments + /// * `params` - The original `BindParam` slice used in the execute call. + /// Only parameters with `direction` of `Output` or `InputOutput` are included. + /// + /// # Returns + /// A vector of `(type_code, raw_bytes)` tuples, one per output parameter, + /// in the same order as the input parameters. Empty byte vectors indicate NULL. + pub fn read_output_params(&self, params: &[BindParam]) -> Vec<(i32, Vec)> { + params + .iter() + .filter(|p| { + p.direction == dameng_protocol::message::bind::ParameterDirection::Output + || p.direction + == dameng_protocol::message::bind::ParameterDirection::InputOutput + }) + .map(|p| { + let raw = p.value.clone().unwrap_or_default(); + (p.type_code, raw) + }) + .collect() + } + + /// Read the full content of a LOB (CLOB/BLOB) identified by a locator. + /// + /// This method first gets the LOB length via LOBGETLEN (msg_type=31), + /// then reads the content in chunks via LOBREAD (msg_type=32). + /// + /// Returns `Ok(String)` for CLOB or `Ok(Vec)` for BLOB. + /// + /// **Important**: The LOB locator is only valid within the current transaction. + /// If auto_commit is enabled, the locator may be invalidated after the query + /// that produced it is committed. In that case, disable auto_commit before + /// calling this method. + pub fn read_lob(&mut self, locator: &dameng_types::LobLocator) -> Result> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + + // Step 1: Get LOB length via LOBGETLEN (msg_type=31) + let getlen_msg = LobGetLenMessage::new(locator.clone()); + let getlen_payload = getlen_msg.encode_payload(self.new_lob_flag); + self.write_all(&build_message(LOB_GETLEN, self.handle, &getlen_payload))?; + let (getlen_frame, getlen_resp_payload) = self.read_message()?; + if getlen_frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "LOBGETLEN failed: code={} type={}", + getlen_frame.response_code, getlen_frame.msg_type + ))); + } + let getlen_resp = LobGetLenResponse::from_bytes(&getlen_resp_payload)?; + let total_len = checked_lob_len(getlen_resp.length)?; + + if total_len == 0 { + return Ok(vec![]); + } + + // Apply new_blob_id from server if provided (matching Go driver) + let mut cur_locator = locator.clone(); + if let Some(new_id) = getlen_resp.new_blob_id { + // Update blob_id in the raw NBLOB_HEAD (offset 1, 8 bytes) + if cur_locator.raw.len() >= 9 { + cur_locator.raw[1..9].copy_from_slice(&new_id.to_le_bytes()); + } + } + cur_locator.init_cursor(); + + // Step 2: Read LOB data in chunks via LOBREAD (msg_type=32) + // Max chunk: 16384 bytes for BLOB, 8192 chars for CLOB + let max_chunk = if locator.is_clob { 8192 } else { 16384 }; + + let mut result = Vec::with_capacity(total_len); + let mut position: i32 = 0; + cur_locator.init_cursor(); + + while (position as usize) < total_len { + let remaining = total_len - position as usize; + let chunk_size = std::cmp::min(remaining, max_chunk) as i32; + + // Send LOBREAD + let read_msg = + LobReadMessage::new(cur_locator.clone(), position, chunk_size, self.new_lob_flag); + let read_payload = read_msg.encode_payload(); + self.write_all(&build_message(LOB_READ, self.handle, &read_payload))?; + let (read_frame, read_resp_payload) = self.read_message()?; + if read_frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "LOBREAD failed at pos {}: code={} type={}", + position, read_frame.response_code, read_frame.msg_type + ))); + } + let read_resp = LobReadResponse::from_bytes(&read_resp_payload)?; + + if read_resp.data.is_empty() { + break; + } + + if result.len().saturating_add(read_resp.data.len()) > MAX_LOB_BYTES { + return Err(Error::Protocol(dameng_protocol::Error::InvalidFrame( + format!("LOB content exceeds {MAX_LOB_BYTES} bytes"), + ))); + } + result.extend_from_slice(&read_resp.data); + + // For CLOB: advance by character count (charLen if available) + if locator.is_clob && read_resp.char_len > 0 { + position += read_resp.char_len as i32; + } else { + position += read_resp.data.len() as i32; + } + + // Update cursor state from response for next LOBREAD + cur_locator.update_cursor( + read_resp.cur_file_id, + read_resp.cur_page_no, + read_resp.total_offset, + ); + + if read_resp.read_over { + break; + } + } + + Ok(result) + } + + /// Free a LOB locator on the server via LOBFREE (msg_type=29). + /// + /// After reading a LOB's data, this releases the server-side LOB handle. + /// This is especially important for long-lived connections where LOB + /// handles could accumulate on the server. + pub fn free_lob(&mut self, locator: &dameng_types::LobLocator) -> Result<()> { + if !matches!(self.state, State::Ready) { + return Err(Error::NotConnected); + } + + let free_msg = LobFreeMessage::new(locator.clone()); + let free_payload = free_msg.encode_payload(self.new_lob_flag); + self.write_all(&build_message(LOB_FREE, self.handle, &free_payload))?; + let (free_frame, _) = self.read_message()?; + if free_frame.response_code < 0 { + return Err(Error::QueryFailed(format!( + "LOBFREE failed: code={} type={}", + free_frame.response_code, free_frame.msg_type + ))); + } + + Ok(()) + } +} + +impl Drop for Client { + fn drop(&mut self) { + if matches!(self.state, State::Ready) { + let _ = self.close(); + } + if let Some(mut stream) = self.stream.take() { + let _ = stream.shutdown(std::net::Shutdown::Both); + } + self.state = State::Closed; + } +} + +/// Build a complete message (frame + payload). +pub fn build_message(msg_type: u8, handle: u32, payload: &[u8]) -> Vec { + let frame = Frame::new(msg_type, handle, payload.len() as i32); + let mut result = frame.encode().to_vec(); + result.extend_from_slice(payload); + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_new() { + let client = Client::new("localhost", 5236); + assert_eq!(client.host, "localhost"); + assert_eq!(client.port, 5236); + assert_eq!(client.state, State::Closed); + } + + #[test] + fn test_build_message_size() { + let msg = build_message(5, 1, b"SELECT 1"); + assert_eq!(msg.len(), FRAME_HEADER_SIZE + 8); + } + + #[test] + fn test_build_message_frame() { + let msg = build_message(200, 0, &[0u8; 10]); + let mut buf = BytesMut::from(&msg[..]); + let frame = Frame::parse(&mut buf).unwrap(); + assert_eq!(frame.msg_type, 200); + assert_eq!(frame.handle, 0); + assert_eq!(frame.body_len, 10); + } + + #[test] + fn test_state_transitions() { + let client = Client::new("test", 5236); + assert_eq!(client.state, State::Closed); + } + + #[test] + fn test_execute_not_connected() { + let mut client = Client::new("test", 5236); + let result = client.execute("SELECT 1"); + assert!(matches!(result, Err(Error::NotConnected))); + } + + #[test] + fn test_ready_not_connected() { + let mut client = Client::new("test", 5236); + let result = client.ready(); + assert!(matches!(result, Err(Error::NotConnected))); + } + + #[test] + fn test_lob_length_is_bounded() { + assert_eq!( + checked_lob_len(MAX_LOB_BYTES as i64).unwrap(), + MAX_LOB_BYTES + ); + assert!(checked_lob_len(-1).is_err()); + assert!(checked_lob_len(MAX_LOB_BYTES as i64 + 1).is_err()); + } +} diff --git a/Native/DamengBridge/Vendor/dameng/src/config.rs b/Native/DamengBridge/Vendor/dameng/src/config.rs new file mode 100644 index 000000000..3baf1e180 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/config.rs @@ -0,0 +1,362 @@ +//! Connection configuration options. +//! +//! Provides `ConnectOptions` for configuring Dameng database connections +//! with charset, schema, timezone, SSL, and other parameters. +//! Also provides DSN string parsing for convenient connection setup. + +use std::time::Duration; + +use dameng_protocol::message::isolation::IsolationLevel; + +/// Connection configuration options for Dameng database. +#[derive(Debug, Clone)] +pub struct ConnectOptions { + /// Database host (required). + pub host: String, + /// Database port (default: 5236). + pub port: u16, + /// Username (required). + pub username: String, + /// Password (required). + pub password: String, + /// Character set (e.g., "utf8", "gb18030"). + pub charset: Option, + /// Database schema. + pub schema: Option, + /// Timezone offset in hours. + pub timezone: Option, + /// Enable SSL/TLS encryption. + pub ssl: bool, + /// Maximum row size. + pub max_row_size: Option, + /// Connection timeout. + pub connect_timeout: Option, + /// Auto-commit mode (default: true). + pub auto_commit: bool, + /// Transaction isolation level (default: ReadCommitted). + pub isolation_level: IsolationLevel, +} + +impl ConnectOptions { + /// Create new ConnectOptions with required fields. + pub fn new(host: &str, port: u16, username: &str, password: &str) -> Self { + Self { + host: host.to_string(), + port, + username: username.to_string(), + password: password.to_string(), + charset: None, + schema: None, + timezone: None, + ssl: false, + max_row_size: None, + connect_timeout: None, + auto_commit: true, + isolation_level: IsolationLevel::ReadCommitted, + } + } + + /// Set the character set. + pub fn charset(mut self, charset: &str) -> Self { + self.charset = Some(charset.to_string()); + self + } + + /// Set the database schema. + pub fn schema(mut self, schema: &str) -> Self { + self.schema = Some(schema.to_string()); + self + } + + /// Set the timezone offset in hours. + pub fn timezone(mut self, timezone: i16) -> Self { + self.timezone = Some(timezone); + self + } + + /// Enable SSL/TLS encryption. + pub fn ssl(mut self, ssl: bool) -> Self { + self.ssl = ssl; + self + } + + /// Set the maximum row size. + pub fn max_row_size(mut self, max_row_size: i32) -> Self { + self.max_row_size = Some(max_row_size); + self + } + + /// Set the connection timeout. + pub fn connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = Some(timeout); + self + } + + /// Set the auto-commit mode. + pub fn auto_commit(mut self, auto_commit: bool) -> Self { + self.auto_commit = auto_commit; + self + } + + /// Set the transaction isolation level. + pub fn isolation_level(mut self, level: IsolationLevel) -> Self { + self.isolation_level = level; + self + } + + /// Parse a DSN string into ConnectOptions. + /// + /// DSN format: `dm://username:password@host:port/schema?param1=value1¶m2=value2` + /// + /// Supported query parameters: + /// - `charset`: Character set (e.g., "utf8", "gb18030") + /// - `timezone`: Timezone offset in hours + /// - `ssl`: Enable SSL ("true" or "false") + /// - `max_row_size`: Maximum row size + /// - `connect_timeout`: Connection timeout in seconds + /// - `auto_commit`: Auto-commit mode ("true" or "false") + /// - `isolation_level`: Transaction isolation level ("read_uncommitted", "read_committed", + /// "repeatable_read", "serializable") + /// + /// # Examples + /// + /// ```ignore + /// let opts = ConnectOptions::from_dsn( + /// "dm://SYSDBA:SYSDBA@127.0.0.1:5236/?charset=utf8&auto_commit=true" + /// ).unwrap(); + /// ``` + pub fn from_dsn(dsn: &str) -> crate::error::Result { + use crate::error::Error; + + // Parse scheme + let (uri, _scheme) = if let Some(rest) = dsn.strip_prefix("dm://") { + (rest, "dm") + } else if let Some(rest) = dsn.strip_prefix("dm") { + (rest, "dm") + } else { + return Err(Error::ConfigError( + "invalid DSN: missing 'dm://' scheme".to_string(), + )); + }; + + // Parse query parameters + let (uri, query_params) = if let Some((before, after)) = uri.split_once('?') { + (before, Self::parse_query_params(after)) + } else { + (uri, std::collections::HashMap::new()) + }; + + // Parse authority@host:port/schema + let (userinfo, hostport) = if let Some((before, after)) = uri.split_once('@') { + (Some(before), after) + } else { + (None, uri) + }; + + // Extract schema from hostport/schema + let (hostport, schema) = if let Some((hp, sc)) = hostport.split_once('/') { + (hp, Some(sc.to_string())) + } else { + (hostport, None) + }; + + // Parse host:port + let (host, port) = if let Some((h, p)) = hostport.rsplit_once(':') { + match p.parse::() { + Ok(port) => (h, port), + Err(_) => (hostport, 5236), + } + } else { + (hostport, 5236) + }; + + if host.is_empty() { + return Err(Error::ConfigError("invalid DSN: missing host".to_string())); + } + + // Parse username:password + let (username, password) = if let Some(ui) = userinfo { + if let Some((u, p)) = ui.split_once(':') { + (u, p) + } else { + (ui, "") + } + } else { + ("", "") + }; + + let mut opts = ConnectOptions::new(host, port, username, password); + + // Apply schema from URL path (if present) + if let Some(sc) = schema { + opts.schema = Some(sc); + } + + // Apply query parameters (can override URL path values) + if let Some(charset) = query_params.get("charset") { + opts.charset = Some(charset.clone()); + } + if let Some(schema) = query_params.get("schema") { + opts.schema = Some(schema.clone()); + } + if let Some(tz) = query_params.get("timezone") { + if let Ok(tz) = tz.parse::() { + opts.timezone = Some(tz); + } + } + if let Some(ssl_str) = query_params.get("ssl") { + opts.ssl = ssl_str == "true"; + } + if let Some(mrs) = query_params.get("max_row_size") { + if let Ok(mrs) = mrs.parse::() { + opts.max_row_size = Some(mrs); + } + } + if let Some(ct) = query_params.get("connect_timeout") { + if let Ok(ct) = ct.parse::() { + opts.connect_timeout = Some(Duration::from_secs(ct)); + } + } + if let Some(ac) = query_params.get("auto_commit") { + opts.auto_commit = ac == "true"; + } + if let Some(iso) = query_params.get("isolation_level") { + if let Some(level) = match iso.as_str() { + "read_uncommitted" => Some(IsolationLevel::ReadUncommitted), + "read_committed" => Some(IsolationLevel::ReadCommitted), + "repeatable_read" => Some(IsolationLevel::RepeatableRead), + "serializable" => Some(IsolationLevel::Serializable), + _ => None, + } { + opts.isolation_level = level; + } + } + + Ok(opts) + } + + /// Parse query parameter string into a HashMap. + fn parse_query_params(params: &str) -> std::collections::HashMap { + let mut map = std::collections::HashMap::new(); + for pair in params.split('&') { + if let Some((key, value)) = pair.split_once('=') { + map.insert(key.to_string(), value.to_string()); + } + } + map + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connect_options_new() { + let opts = ConnectOptions::new("127.0.0.1", 5236, "SYSDBA", "SYSDBA"); + assert_eq!(opts.host, "127.0.0.1"); + assert_eq!(opts.port, 5236); + assert_eq!(opts.username, "SYSDBA"); + assert_eq!(opts.password, "SYSDBA"); + assert_eq!(opts.charset, None); + assert_eq!(opts.schema, None); + assert!(!opts.ssl); + assert!(opts.auto_commit); + assert_eq!(opts.isolation_level, IsolationLevel::ReadCommitted); + } + + #[test] + fn test_connect_options_builder() { + let opts = ConnectOptions::new("127.0.0.1", 5236, "SYSDBA", "SYSDBA") + .charset("utf8") + .schema("TEST") + .timezone(8) + .ssl(true) + .max_row_size(8192) + .connect_timeout(Duration::from_secs(30)) + .auto_commit(false) + .isolation_level(IsolationLevel::Serializable); + + assert_eq!(opts.charset, Some("utf8".to_string())); + assert_eq!(opts.schema, Some("TEST".to_string())); + assert_eq!(opts.timezone, Some(8)); + assert!(opts.ssl); + assert_eq!(opts.max_row_size, Some(8192)); + assert_eq!(opts.connect_timeout, Some(Duration::from_secs(30))); + assert!(!opts.auto_commit); + assert_eq!(opts.isolation_level, IsolationLevel::Serializable); + } + + #[test] + fn test_dsn_basic() { + let opts = ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@127.0.0.1:5236/").unwrap(); + assert_eq!(opts.host, "127.0.0.1"); + assert_eq!(opts.port, 5236); + assert_eq!(opts.username, "SYSDBA"); + assert_eq!(opts.password, "SYSDBA"); + } + + #[test] + fn test_dsn_with_params() { + let opts = ConnectOptions::from_dsn( + "dm://SYSDBA:SYSDBA@127.0.0.1:5236/?charset=utf8&ssl=true&auto_commit=false", + ) + .unwrap(); + assert_eq!(opts.charset, Some("utf8".to_string())); + assert!(opts.ssl); + assert!(!opts.auto_commit); + } + + #[test] + fn test_dsn_with_schema() { + let opts = + ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@127.0.0.1:5236/TEST?charset=gb18030") + .unwrap(); + assert_eq!(opts.schema, Some("TEST".to_string())); + assert_eq!(opts.charset, Some("gb18030".to_string())); + } + + #[test] + fn test_dsn_default_port() { + let opts = ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@127.0.0.1").unwrap(); + assert_eq!(opts.host, "127.0.0.1"); + assert_eq!(opts.port, 5236); + } + + #[test] + fn test_dsn_isolation_level() { + let opts = ConnectOptions::from_dsn( + "dm://SYSDBA:SYSDBA@127.0.0.1:5236/?isolation_level=serializable", + ) + .unwrap(); + assert_eq!(opts.isolation_level, IsolationLevel::Serializable); + } + + #[test] + fn test_dsn_invalid_scheme() { + let result = ConnectOptions::from_dsn("mysql://SYSDBA:SYSDBA@127.0.0.1:5236/"); + assert!(result.is_err()); + } + + #[test] + fn test_dsn_missing_host() { + let result = ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@"); + assert!(result.is_err()); + } + + #[test] + fn test_dsn_connect_timeout() { + let opts = + ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@127.0.0.1:5236/?connect_timeout=60") + .unwrap(); + assert_eq!(opts.connect_timeout, Some(Duration::from_secs(60))); + } + + #[test] + fn test_dsn_max_row_size() { + let opts = + ConnectOptions::from_dsn("dm://SYSDBA:SYSDBA@127.0.0.1:5236/?max_row_size=16384") + .unwrap(); + assert_eq!(opts.max_row_size, Some(16384)); + } +} diff --git a/Native/DamengBridge/Vendor/dameng/src/error.rs b/Native/DamengBridge/Vendor/dameng/src/error.rs new file mode 100644 index 000000000..76ae19709 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/error.rs @@ -0,0 +1,72 @@ +//! Error types for the Dameng driver. + +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Protocol(dameng_protocol::Error), + Io(std::io::Error), + ConnectionFailed(String), + AuthFailed(String), + ServerError(i32, String), + DecodeError(String), + QueryFailed(String), + NotConnected, + ConfigError(String), + /// Invalid transaction isolation level was specified. + InvalidIsolation(String), + /// LOB locator has been freed and can no longer be used. + LobFreed(String), + /// Date/time format parsing failed. + InvalidDateFormat(String), + /// Server returned a busy or overloaded state. + ServerBusy, + /// LOB read operation failed. + LobReadFailed(String), + /// LOB write operation failed. + LobWriteFailed(String), + /// Operation timed out. + Timeout(String), + /// Schema/database name resolution error. + SchemaError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Protocol(e) => write!(f, "protocol error: {e}"), + Error::Io(e) => write!(f, "IO error: {e}"), + Error::ConnectionFailed(s) => write!(f, "connection failed: {s}"), + Error::AuthFailed(s) => write!(f, "auth failed: {s}"), + Error::ServerError(code, msg) => write!(f, "server error {code}: {msg}"), + Error::DecodeError(s) => write!(f, "decode error: {s}"), + Error::QueryFailed(s) => write!(f, "query failed: {s}"), + Error::NotConnected => write!(f, "not connected"), + Error::ConfigError(s) => write!(f, "config error: {s}"), + Error::InvalidIsolation(s) => write!(f, "invalid isolation level: {s}"), + Error::LobFreed(s) => write!(f, "LOB freed: {s}"), + Error::InvalidDateFormat(s) => write!(f, "invalid date format: {s}"), + Error::ServerBusy => write!(f, "server busy"), + Error::LobReadFailed(s) => write!(f, "LOB read failed: {s}"), + Error::LobWriteFailed(s) => write!(f, "LOB write failed: {s}"), + Error::Timeout(s) => write!(f, "timeout: {s}"), + Error::SchemaError(s) => write!(f, "schema error: {s}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(e: dameng_protocol::Error) -> Self { + Error::Protocol(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +pub type Result = std::result::Result; diff --git a/Native/DamengBridge/Vendor/dameng/src/lib.rs b/Native/DamengBridge/Vendor/dameng/src/lib.rs new file mode 100644 index 000000000..231d76b97 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/lib.rs @@ -0,0 +1,28 @@ +//! Dameng database sync driver. +//! +//! Provides synchronous connection and query execution against +//! Dameng database servers. + +pub mod client; +pub mod config; +pub mod error; +pub mod row; +pub mod transaction; + +pub use client::Client; +pub use config::ConnectOptions; +pub use dameng_protocol::Row; +pub use error::{Error, Result}; + +// Re-export protocol types needed for parameter binding +pub use dameng_protocol::message::isolation::{IsolationLevel, SetIsolationMessage}; +pub use dameng_protocol::message::{BindParam, ParameterDirection}; + +// Re-export ToDmValue trait for SQLx-style dynamic binding +pub use dameng_types::ToDmValue; + +// Re-export row types +pub use row::{DmDecode, QueryRow, QueryRowRef, ResultSet, ResultSetIter}; + +// Re-export Transaction +pub use transaction::Transaction; diff --git a/Native/DamengBridge/Vendor/dameng/src/row.rs b/Native/DamengBridge/Vendor/dameng/src/row.rs new file mode 100644 index 000000000..e998b0e2d --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/row.rs @@ -0,0 +1,876 @@ +//! Row representation for query results. +//! +//! Provides SQLx-style `row.get::(idx)` API and iterator support. + +use std::ops::Deref; +use std::str::FromStr; + +use dameng_protocol::Row; + +pub use dameng_protocol::Column; + +/// A query result set containing columns and rows. +#[derive(Debug, Clone)] +pub struct ResultSet { + /// Column metadata shared across all rows. + pub columns: Vec, + /// Row data. + pub rows: Vec, + /// Result set cursor ID (from the initial query). + pub cursor_id: i16, + /// Total row count in the result set (from server). + pub total_row_count: u64, +} + +/// A single row with column metadata, produced by iterating a `ResultSet`. +/// +/// Supports SQLx-style `row.get::(idx)` for type-safe column access, +/// and `row.get_str_ref(idx)` / `row.get_opt_str_ref(idx)` for borrowed string access. +#[derive(Debug, Clone)] +pub struct QueryRow { + /// The underlying raw row data. + pub row: Row, + /// Column metadata for decoding values. + pub columns: Vec, +} + +/// A row with referenced column metadata (borrowed iteration). +#[derive(Debug, Clone)] +pub struct QueryRowRef<'a> { + /// The underlying raw row data. + pub row: &'a Row, + /// Column metadata reference. + pub columns: &'a [Column], +} + +impl<'a> Deref for QueryRowRef<'a> { + type Target = Row; + fn deref(&self) -> &Self::Target { + self.row + } +} + +// ─── IntoIterator for ResultSet (consuming) ───────────────────────────────── + +impl IntoIterator for ResultSet { + type Item = QueryRow; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let columns = self.columns; + let qrows: Vec = self + .rows + .into_iter() + .map(|row| QueryRow { + row, + columns: columns.clone(), + }) + .collect(); + qrows.into_iter() + } +} + +// ─── IntoIterator for &ResultSet (borrowing) ──────────────────────────────── + +impl<'a> IntoIterator for &'a ResultSet { + type Item = QueryRowRef<'a>; + type IntoIter = ResultSetIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + ResultSetIter { + result_set: self, + current: 0, + } + } +} + +/// Borrowing iterator over rows in a ResultSet. +pub struct ResultSetIter<'a> { + result_set: &'a ResultSet, + current: usize, +} + +impl<'a> Iterator for ResultSetIter<'a> { + type Item = QueryRowRef<'a>; + + fn next(&mut self) -> Option { + if self.current >= self.result_set.rows.len() { + return None; + } + let row = &self.result_set.rows[self.current]; + self.current += 1; + Some(QueryRowRef { + row, + columns: &self.result_set.columns, + }) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.result_set.rows.len() - self.current; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for ResultSetIter<'_> {} + +// ─── DmDecode trait ───────────────────────────────────────────────────────── + +/// Decode a column value from its raw bytes into a Rust type. +/// +/// The lifetime `'de` allows borrowing the raw bytes (e.g., for `&str`). +pub trait DmDecode<'de>: Sized { + /// Decode from an optional byte slice. + /// `None` means NULL, `Some(&[])` means an empty (non-NULL) value. + fn decode(value: Option<&'de [u8]>) -> crate::error::Result; +} + +impl<'de> DmDecode<'de> for bool { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + Ok(bytes[0] != 0) + } +} + +impl<'de> DmDecode<'de> for i32 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 4 { + if bytes.len() == 1 { + return Ok(bytes[0] as i32); + } + if bytes.len() == 2 { + return Ok(i32::from(i16::from_le_bytes([bytes[0], bytes[1]]))); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for i32: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 4] = bytes[..4].try_into().unwrap(); + Ok(i32::from_le_bytes(arr)) + } +} + +impl<'de> DmDecode<'de> for i64 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 8 { + if bytes.len() >= 4 { + let arr: [u8; 4] = bytes[..4].try_into().unwrap(); + return Ok(i64::from(i32::from_le_bytes(arr))); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for i64: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 8] = bytes[..8].try_into().unwrap(); + Ok(i64::from_le_bytes(arr)) + } +} + +impl<'de> DmDecode<'de> for i16 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 2 { + if bytes.len() == 1 { + return Ok(bytes[0] as i16); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for i16: {} bytes", + bytes.len() + ))); + } + Ok(i16::from_le_bytes([bytes[0], bytes[1]])) + } +} + +impl<'de> DmDecode<'de> for i8 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column is NULL".to_string(), + )); + } + Ok(bytes[0] as i8) + } +} + +impl<'de> DmDecode<'de> for u32 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 4 { + if bytes.len() == 1 { + return Ok(bytes[0] as u32); + } + if bytes.len() == 2 { + return Ok(u16::from_le_bytes([bytes[0], bytes[1]]) as u32); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for u32: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 4] = bytes[..4].try_into().unwrap(); + Ok(u32::from_le_bytes(arr)) + } +} + +impl<'de> DmDecode<'de> for u64 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 8 { + if bytes.len() >= 4 { + let arr: [u8; 4] = bytes[..4].try_into().unwrap(); + return Ok(u32::from_le_bytes(arr) as u64); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for u64: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 8] = bytes[..8].try_into().unwrap(); + Ok(u64::from_le_bytes(arr)) + } +} + +impl<'de> DmDecode<'de> for u16 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + if bytes.len() < 2 { + if bytes.len() == 1 { + return Ok(bytes[0] as u16); + } + return Err(crate::error::Error::DecodeError(format!( + "too short for u16: {} bytes", + bytes.len() + ))); + } + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } +} + +impl<'de> DmDecode<'de> for u8 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column is NULL".to_string(), + )); + } + Ok(bytes[0]) + } +} + +impl<'de> DmDecode<'de> for f64 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.len() < 8 { + return Err(crate::error::Error::DecodeError(format!( + "too short for f64: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 8] = bytes[..8].try_into().unwrap(); + Ok(f64::from_le_bytes(arr)) + } +} + +impl<'de> DmDecode<'de> for f32 { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.len() < 4 { + return Err(crate::error::Error::DecodeError(format!( + "too short for f32: {} bytes", + bytes.len() + ))); + } + let arr: [u8; 4] = bytes[..4].try_into().unwrap(); + Ok(f32::from_le_bytes(arr)) + } +} + +/// Returns a borrowed string from the row's raw value bytes. +impl<'de> DmDecode<'de> for &'de str { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Ok(""); + } + std::str::from_utf8(bytes) + .map_err(|e| crate::error::Error::DecodeError(format!("invalid UTF-8: {}", e))) + } +} + +/// Returns an owned String. +impl<'de> DmDecode<'de> for String { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + Ok(String::from_utf8_lossy(bytes).into_owned()) + } +} + +/// Returns a Decimal from DECIMAL type (text, already decoded in response parser). +impl<'de> DmDecode<'de> for rust_decimal::Decimal { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + let s = std::str::from_utf8(bytes).map_err(|_| { + crate::error::Error::DecodeError("DECIMAL is not valid UTF-8".to_string()) + })?; + let trimmed = s.trim(); + if trimmed.is_empty() || trimmed == "0" { + return Ok(rust_decimal::Decimal::ZERO); + } + rust_decimal::Decimal::from_str(trimmed).map_err(|e| { + crate::error::Error::DecodeError(format!("invalid DECIMAL '{}' : {}", trimmed, e)) + }) + } +} + +/// Returns a NaiveDate from DATE type. +impl<'de> DmDecode<'de> for chrono::NaiveDate { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + // Try text format first + if let Ok(s) = std::str::from_utf8(bytes) { + if let Ok(d) = chrono::NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d") { + return Ok(d); + } + } + // Try binary format (7 bytes: year:2BE, month:1, day:1, hour:1, min:1, sec:1) + // Try 3-byte DM row format (DATE_PREC = 3, year+month+day compressed) + if bytes.len() >= 3 && bytes.len() < 7 { + let year = i32::from(i16::from_le_bytes([bytes[0], bytes[1]])) & 0x7FFF; + let month = ((bytes[1] as u32 >> 7) & 0x1) + ((bytes[2] as u32 & 0x07) << 1); + let day = ((bytes[2] as u32 & 0xF8) >> 3) & 0x1F; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Ok(d); + } + } + // Try 7-byte OPE format + if bytes.len() >= 7 { + let year = u16::from_be_bytes([bytes[0], bytes[1]]) as i32; + let month = bytes[2] as u32; + let day = bytes[3] as u32; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Ok(d); + } + } + // Try 8-byte DM row format (DATE_PREC = 7 but stored in 8 bytes) + if bytes.len() >= 8 { + let year = i32::from(i16::from_le_bytes([bytes[0], bytes[1]])) & 0x7FFF; + let month = ((bytes[1] as u32 >> 7) & 0x1) + ((bytes[2] as u32 & 0x07) << 1); + let day = ((bytes[2] as u32 & 0xF8) >> 3) & 0x1F; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + return Ok(d); + } + } + Err(crate::error::Error::DecodeError( + "too short for DATE".to_string(), + )) + } +} + +/// Returns a NaiveDateTime from TIMESTAMP type. +impl<'de> DmDecode<'de> for chrono::NaiveDateTime { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + let bytes = + value.ok_or_else(|| crate::error::Error::DecodeError("column is NULL".to_string()))?; + if bytes.is_empty() { + return Err(crate::error::Error::DecodeError( + "column value is empty".to_string(), + )); + } + // Try text format first + if let Ok(s) = std::str::from_utf8(bytes) { + let s = s.trim(); + if let Ok(ts) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") { + return Ok(ts); + } + if let Ok(ts) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") { + return Ok(ts); + } + } + // Try 11-byte OPE format (year:2BE, month:1, day:1, hour:1, min:1, sec:1, nano:4BE) + if bytes.len() >= 11 { + let year = u16::from_be_bytes([bytes[0], bytes[1]]) as i32; + let month = bytes[2] as u32; + let day = bytes[3] as u32; + let hour = bytes[4] as u32; + let minute = bytes[5] as u32; + let second = bytes[6] as u32; + let nano = u32::from_be_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]); + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) + .and_then(|d| d.and_hms_nano_opt(hour, minute, second, nano)) + { + return Ok(d); + } + } + // Try 8-byte DM row format (DATETIME_PREC) + if bytes.len() >= 8 { + let year = i32::from(i16::from_le_bytes([bytes[0], bytes[1]])) & 0x7FFF; + let month = ((bytes[1] as u32 >> 7) & 0x1) + ((bytes[2] as u32 & 0x07) << 1); + let day = ((bytes[2] as u32 & 0xF8) >> 3) & 0x1F; + let hour = bytes[3] as u32 & 0x1F; + let minute = ((bytes[3] as u32 >> 5) & 0x07) + ((bytes[4] as u32 & 0x07) << 3); + let second = ((bytes[4] as u32 >> 3) & 0x1F) + ((bytes[5] as u32 & 0x01) << 5); + let nano = (((bytes[5] as u32 >> 1) & 0x7F) + + ((bytes[6] as u32 & 0xFF) << 7) + + ((bytes[7] as u32 & 0x1F) << 15)) + * 1000; + if let Some(d) = chrono::NaiveDate::from_ymd_opt(year, month, day) + .and_then(|d| d.and_hms_nano_opt(hour, minute, second, nano)) + { + return Ok(d); + } + } + Err(crate::error::Error::DecodeError( + "too short for TIMESTAMP".to_string(), + )) + } +} + +impl<'de> DmDecode<'de> for Vec { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) => Ok(bytes.to_vec()), + None => Ok(vec![]), + } + } +} + +// ─── Option support ────────────────────────────────────────────────────── + +macro_rules! impl_dm_decode_option { + ($inner:ty) => { + impl<'de> DmDecode<'de> for Option<$inner> { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => { + <$inner as DmDecode>::decode(Some(bytes)).map(Some) + } + _ => Ok(None), + } + } + } + }; +} + +impl_dm_decode_option!(bool); +impl_dm_decode_option!(i32); +impl_dm_decode_option!(i64); +impl_dm_decode_option!(i16); +impl_dm_decode_option!(i8); +impl_dm_decode_option!(u32); +impl_dm_decode_option!(u64); +impl_dm_decode_option!(u16); +impl_dm_decode_option!(u8); +impl_dm_decode_option!(f64); +impl_dm_decode_option!(f32); + +impl<'de> DmDecode<'de> for Option<&'de str> { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => <&str as DmDecode>::decode(Some(bytes)).map(Some), + _ => Ok(None), + } + } +} + +impl<'de> DmDecode<'de> for Option { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => ::decode(Some(bytes)).map(Some), + _ => Ok(None), + } + } +} + +impl<'de> DmDecode<'de> for Option> { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => Ok(Some(bytes.to_vec())), + _ => Ok(None), + } + } +} + +impl<'de> DmDecode<'de> for Option { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => { + ::decode(Some(bytes)).map(Some) + } + _ => Ok(None), + } + } +} + +impl<'de> DmDecode<'de> for Option { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => { + ::decode(Some(bytes)).map(Some) + } + _ => Ok(None), + } + } +} + +impl<'de> DmDecode<'de> for Option { + fn decode(value: Option<&'de [u8]>) -> crate::error::Result { + match value { + Some(bytes) if !bytes.is_empty() => { + ::decode(Some(bytes)).map(Some) + } + _ => Ok(None), + } + } +} + +// ─── QueryRow methods ─────────────────────────────────────────────────────── + +impl QueryRow { + /// Get a decoded value at the given column index. + /// + /// Supports SQLx-style type inference: + /// ```ignore + /// let id: i32 = row.get(0)?; + /// let name: &str = row.get(1)?; + /// let addr: Option<&str> = row.get(2)?; + /// ``` + pub fn get<'de, T: DmDecode<'de>>(&'de self, idx: usize) -> crate::error::Result { + let value = self.row.values.get(idx).and_then(|v| v.as_deref()); + T::decode(value) + } +} + +impl<'a> QueryRowRef<'a> { + /// Get a decoded value at the given column index. + pub fn get<'de, T: DmDecode<'de>>(&'de self, idx: usize) -> crate::error::Result + where + 'a: 'de, + { + let value = self.row.values.get(idx).and_then(|v| v.as_deref()); + T::decode(value) + } +} + +// ─── ResultSet methods ────────────────────────────────────────────────────── + +impl ResultSet { + /// Create a new empty result set. + pub fn new() -> Self { + Self { + columns: vec![], + rows: vec![], + cursor_id: 0, + total_row_count: 0, + } + } + + /// Create a result set with the given data. + pub fn with_data( + columns: Vec, + rows: Vec, + cursor_id: i16, + total_row_count: u64, + ) -> Self { + Self { + columns, + rows, + cursor_id, + total_row_count, + } + } + + /// Check if the result set is empty. + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } + + /// Get the number of rows. + pub fn len(&self) -> usize { + self.rows.len() + } + + /// Get the first row, if any (returns a QueryRowRef with column metadata). + pub fn first(&self) -> Option> { + self.rows.first().map(|row| QueryRowRef { + row, + columns: &self.columns, + }) + } + + /// Iterate over rows with column metadata (borrowing). + /// + /// Supports SQLx-style type inference: + /// ```ignore + /// for row in rs.iter() { + /// let id: i32 = row.get(0)?; + /// let name: &str = row.get(1)?; + /// } + /// ``` + /// + /// Also supports protocol-level methods via `Deref`: + /// ```ignore + /// for row in rs.iter() { + /// let id = row.get_i32(0)?; + /// let name = row.get_str(1)?; + /// } + /// ``` + pub fn iter(&self) -> ResultSetIter<'_> { + ResultSetIter { + result_set: self, + current: 0, + } + } + + /// Iterate over rows with access to column metadata (borrowing). + /// Alias for `iter()`. + pub fn iter_rows(&self) -> ResultSetIter<'_> { + self.iter() + } + + /// Get column metadata by name. + pub fn column_by_name(&self, name: &str) -> Option<&Column> { + self.columns.iter().find(|c| c.name == name) + } + + /// Check if there are more rows to fetch. + pub fn has_more(&self) -> bool { + self.rows.len() < self.total_row_count as usize + } + + /// Get the next fetch start position. + pub fn next_fetch_start(&self) -> usize { + self.rows.len() + } +} + +impl Default for ResultSet { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_row_empty() { + let row = Row { + row_id: 0, + values: vec![], + }; + assert!(row.is_empty()); + assert_eq!(row.len(), 0); + } + + #[test] + fn test_result_set_empty() { + let rs = ResultSet::new(); + assert!(rs.is_empty()); + assert_eq!(rs.len(), 0); + } + + #[test] + fn test_query_row_get_i32() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(vec![100, 0, 0, 0])], + }, + columns: vec![], + }; + assert_eq!(qrow.get::(0).unwrap(), 100); + } + + #[test] + fn test_query_row_get_str() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(b"Alice".to_vec())], + }, + columns: vec![], + }; + assert_eq!(qrow.get::<&str>(0).unwrap(), "Alice"); + } + + #[test] + fn test_query_row_get_option() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![None, Some(vec![1, 0, 0, 0])], + }, + columns: vec![], + }; + assert_eq!(qrow.get::>(0).unwrap(), None); + assert_eq!(qrow.get::>(1).unwrap(), Some(1)); + } + + #[test] + fn test_query_row_get_opt_str() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![None, Some(b"Alice".to_vec())], + }, + columns: vec![], + }; + assert_eq!(qrow.get::>(0).unwrap(), None); + assert_eq!(qrow.get::>(1).unwrap(), Some("Alice")); + } + + #[test] + fn test_result_set_into_iter() { + let rs = ResultSet::with_data( + vec![], + vec![ + Row { + row_id: 0, + values: vec![Some(vec![1, 0, 0, 0])], + }, + Row { + row_id: 1, + values: vec![Some(vec![2, 0, 0, 0])], + }, + ], + 0, + 2, + ); + let ids: Vec = rs.into_iter().map(|r| r.get::(0).unwrap()).collect(); + assert_eq!(ids, vec![1, 2]); + } + + #[test] + fn test_query_row_get_u32() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(vec![100, 0, 0, 0])], + }, + columns: vec![], + }; + assert_eq!(qrow.get::(0).unwrap(), 100u32); + } + + #[test] + fn test_query_row_get_u64() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(vec![0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])], + }, + columns: vec![], + }; + assert_eq!(qrow.get::(0).unwrap(), 1000u64); + } + + #[test] + fn test_query_row_get_bool() { + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(vec![1]), Some(vec![0])], + }, + columns: vec![], + }; + assert_eq!(qrow.get::(0).unwrap(), true); + assert_eq!(qrow.get::(1).unwrap(), false); + } + + #[test] + fn test_query_row_deref_get_str() { + // Test that Deref works for protocol-level methods + let qrow = QueryRow { + row: Row { + row_id: 0, + values: vec![Some(b"Hello".to_vec())], + }, + columns: vec![], + }; + // get_str is on Row, accessible via row.field + assert_eq!(qrow.row.get_str(0).unwrap(), "Hello"); + } + + #[test] + fn test_result_set_iter_deref() { + // Test that rs.iter() returning QueryRowRef still supports + // protocol-level methods via Deref + let rs = ResultSet::with_data( + vec![], + vec![Row { + row_id: 0, + values: vec![Some(vec![1, 0, 0, 0]), Some(b"Alice".to_vec())], + }], + 0, + 1, + ); + for row in rs.iter() { + let id = row.get_i32(0).unwrap(); + let name = row.get_str(1).unwrap(); + assert_eq!(id, 1); + assert_eq!(name, "Alice"); + } + } +} diff --git a/Native/DamengBridge/Vendor/dameng/src/transaction.rs b/Native/DamengBridge/Vendor/dameng/src/transaction.rs new file mode 100644 index 000000000..6f31faf41 --- /dev/null +++ b/Native/DamengBridge/Vendor/dameng/src/transaction.rs @@ -0,0 +1,83 @@ +//! Transaction support — follows rust-postgres / sqlx patterns. +//! +//! A `Transaction` borrows a `Client` in a database transaction. +//! All operations on the `Transaction` are executed within the transaction. +//! On `Drop`, the transaction is automatically rolled back if not committed. +//! +//! `commit()` and `rollback()` take `self` (ownership), which releases +//! the mutable borrow on `Client` so it can be used again immediately. + +use crate::client::Client; +use crate::error::Result; +use crate::row::ResultSet; +use dameng_types::ToDmValue; + +pub struct Transaction<'a> { + client: &'a mut Client, + finished: bool, +} + +impl<'a> Transaction<'a> { + /// Commit the transaction, consuming `self` and releasing the Client borrow. + pub fn commit(mut self) -> Result<()> { + self.finish_inner("COMMIT") + } + + /// Roll back the transaction, consuming `self` and releasing the Client borrow. + pub fn rollback(mut self) -> Result<()> { + self.finish_inner("ROLLBACK") + } + + /// Execute a DML statement within the transaction. Returns affected rows. + pub fn execute(&mut self, sql: &str) -> Result { + self.client.execute(sql) + } + + /// Execute DML with dynamic parameters within the transaction. + pub fn execute_with_params(&mut self, sql: &str, params: &[&dyn ToDmValue]) -> Result { + self.client.execute_with_params(sql, params) + } + + /// Execute a SELECT query within the transaction. + pub fn query(&mut self, sql: &str) -> Result { + self.client.query(sql) + } + + /// Execute a SELECT query with dynamic parameters within the transaction. + pub fn query_with_params(&mut self, sql: &str, params: &[&dyn ToDmValue]) -> Result { + self.client.query_with_params(sql, params) + } + + fn finish_inner(&mut self, command: &str) -> Result<()> { + self.client.execute(command)?; + self.client.auto_commit = true; + self.finished = true; + Ok(()) + } +} + +impl<'a> Drop for Transaction<'a> { + fn drop(&mut self) { + if !self.finished { + let _ = self.client.execute("ROLLBACK"); + self.client.auto_commit = true; + } + } +} + +impl Client { + /// Begin a new transaction, returning a `Transaction` handle. + /// + /// After `commit()` or `rollback()` (which take `self`), the mutable + /// borrow on `Client` is released and the `Client` can be used again. + /// + /// If the `Transaction` is dropped without commit/rollback, a + /// `ROLLBACK` is sent automatically (best-effort). + pub fn transaction(&mut self) -> Result> { + self.begin()?; + Ok(Transaction { + client: self, + finished: false, + }) + } +} diff --git a/Native/DamengBridge/rust-toolchain.toml b/Native/DamengBridge/rust-toolchain.toml new file mode 100644 index 000000000..c0a424811 --- /dev/null +++ b/Native/DamengBridge/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.91.1" +profile = "minimal" +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] diff --git a/Native/DamengBridge/src/lib.rs b/Native/DamengBridge/src/lib.rs new file mode 100644 index 000000000..12da87bd4 --- /dev/null +++ b/Native/DamengBridge/src/lib.rs @@ -0,0 +1,764 @@ +// Safety contracts for the exported C ABI are documented in CDameng.h, next to +// the declarations consumed by Swift. +#![allow(clippy::missing_safety_doc)] + +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::ptr; +use std::slice; +use std::str; +use std::time::Instant; + +use dameng::Client; +use dameng_types::encoding::{decode_from_server, ServerEncoding}; +use dameng_types::DmValue; + +const MAX_HOST_BYTES: usize = 1_024; +const MAX_CREDENTIAL_BYTES: usize = 4_096; +const MAX_SQL_BYTES: usize = 16 * 1_024 * 1_024; + +pub struct TpDmConnection { + client: Option, +} + +pub struct TpDmError { + message: Vec, +} + +pub struct TpDmResult { + columns: Vec, + rows: Vec>, + rows_affected: u64, + execution_time_seconds: f64, + is_truncated: bool, +} + +struct TpDmColumn { + name: Vec, + type_name: Vec, +} + +enum TpDmCell { + Null, + Text(Vec), + Bytes(Vec), +} + +fn set_error(error_out: *mut *mut TpDmError, message: impl Into) { + if error_out.is_null() { + return; + } + let error = Box::new(TpDmError { + message: message.into().into_bytes(), + }); + unsafe { + *error_out = Box::into_raw(error); + } +} + +fn clear_error(error_out: *mut *mut TpDmError) { + if error_out.is_null() { + return; + } + unsafe { + *error_out = ptr::null_mut(); + } +} + +fn panic_message(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::() { + return message.clone(); + } + if let Some(message) = payload.downcast_ref::<&str>() { + return (*message).to_string(); + } + "Dameng transport panicked".to_string() +} + +unsafe fn required_string( + bytes: *const u8, + length: usize, + maximum: usize, + field: &str, +) -> Result { + if bytes.is_null() { + return Err(format!("{field} is missing")); + } + if length == 0 || length > maximum { + return Err(format!("{field} length is invalid")); + } + let value = slice::from_raw_parts(bytes, length); + str::from_utf8(value) + .map(str::to_owned) + .map_err(|_| format!("{field} is not valid UTF-8")) +} + +unsafe fn password_string(bytes: *const u8, length: usize) -> Result { + if length > MAX_CREDENTIAL_BYTES { + return Err("password length is invalid".to_string()); + } + if length == 0 { + return Ok(String::new()); + } + if bytes.is_null() { + return Err("password is missing".to_string()); + } + let value = slice::from_raw_parts(bytes, length); + str::from_utf8(value) + .map(str::to_owned) + .map_err(|_| "password is not valid UTF-8".to_string()) +} + +unsafe fn optional_sql(bytes: *const u8, length: usize) -> Result { + if bytes.is_null() || length == 0 || length > MAX_SQL_BYTES { + return Err("query length is invalid".to_string()); + } + let value = slice::from_raw_parts(bytes, length); + str::from_utf8(value) + .map(str::to_owned) + .map_err(|_| "query is not valid UTF-8".to_string()) +} + +fn detect_server_encoding(client: &mut Client) -> Result<(), String> { + let result = client + .query("SELECT UNICODE()") + .map_err(|error| error.to_string())?; + let row = result + .rows + .first() + .ok_or_else(|| "DM8 did not return its Unicode mode".to_string())?; + let flag = row.get_i32(0).map_err(|error| error.to_string())?; + client.server_encoding = if flag == 1 { + ServerEncoding::Utf8 + } else { + ServerEncoding::Gb18030 + }; + Ok(()) +} + +fn text_cell(value: impl ToString) -> TpDmCell { + TpDmCell::Text(value.to_string().into_bytes()) +} + +fn convert_lob( + client: &mut Client, + locator: dameng_types::LobLocator, +) -> Result { + let data = client.read_lob(&locator)?; + let cell = if locator.is_clob { + TpDmCell::Text(decode_from_server(client.server_encoding, &data).into_bytes()) + } else { + TpDmCell::Bytes(data) + }; + client.free_lob(&locator)?; + Ok(cell) +} + +fn convert_cell( + client: &mut Client, + row: &dameng::Row, + columns: &[dameng::row::Column], + index: usize, +) -> Result { + let Some(value) = row.get(index, columns) else { + return Ok(TpDmCell::Null); + }; + match value { + DmValue::Null => Ok(TpDmCell::Null), + DmValue::Boolean(value) => Ok(text_cell(if value { "1" } else { "0" })), + DmValue::TinyInt(value) => Ok(text_cell(value)), + DmValue::SmallInt(value) => Ok(text_cell(value)), + DmValue::Int(value) => Ok(text_cell(value)), + DmValue::BigInt(value) => Ok(text_cell(value)), + DmValue::Float(value) => Ok(text_cell(value)), + DmValue::Double(value) => Ok(text_cell(value)), + DmValue::Text(value) => Ok(TpDmCell::Text(value.into_bytes())), + DmValue::Bytea(value) => Ok(TpDmCell::Bytes(value)), + DmValue::Decimal(value) => Ok(text_cell(value)), + DmValue::Date(value) => Ok(text_cell(value)), + DmValue::Time(value) => Ok(text_cell(value)), + DmValue::Timestamp(value) => Ok(text_cell(value)), + DmValue::LobLocator(locator) => convert_lob(client, locator), + } +} + +fn query_result( + client: &mut Client, + sql: &str, + row_cap: usize, +) -> Result { + let started = Instant::now(); + let mut result = client.query(sql)?; + let fetch_limit = if row_cap > 0 { + row_cap.saturating_add(1) + } else { + usize::MAX + }; + while result.rows.len() < usize::try_from(result.total_row_count).unwrap_or(usize::MAX) + && result.rows.len() < fetch_limit + { + let previous_count = result.rows.len(); + client.fetch_more(&mut result, previous_count, 65_536)?; + if result.rows.len() == previous_count { + break; + } + } + let columns = result + .columns + .iter() + .map(|column| TpDmColumn { + name: column.name.as_bytes().to_vec(), + type_name: column.type_name.as_bytes().to_vec(), + }) + .collect(); + let is_truncated = row_cap > 0 + && (result.rows.len() > row_cap + || result.total_row_count > u64::try_from(row_cap).unwrap_or(u64::MAX)); + let row_count = if row_cap > 0 { + result.rows.len().min(row_cap) + } else { + result.rows.len() + }; + let mut rows = Vec::with_capacity(row_count); + for row in result.rows.iter().take(row_count) { + let mut cells = Vec::with_capacity(result.columns.len()); + for index in 0..result.columns.len() { + cells.push(convert_cell(client, row, &result.columns, index)?); + } + rows.push(cells); + } + Ok(TpDmResult { + columns, + rows, + rows_affected: result.total_row_count, + execution_time_seconds: started.elapsed().as_secs_f64(), + is_truncated, + }) +} + +fn execute_result(client: &mut Client, sql: &str) -> Result { + let started = Instant::now(); + let rows_affected = client.execute(sql)?; + Ok(TpDmResult { + columns: Vec::new(), + rows: Vec::new(), + rows_affected, + execution_time_seconds: started.elapsed().as_secs_f64(), + is_truncated: false, + }) +} + +fn connection_client(connection: &mut TpDmConnection) -> Result { + connection + .client + .take() + .ok_or_else(|| "Dameng connection is closed".to_string()) +} + +fn restore_client(connection: &mut TpDmConnection, client: Client) { + connection.client = Some(client); +} + +fn is_recoverable(error: &dameng::Error) -> bool { + !matches!( + error, + dameng::Error::Protocol(_) + | dameng::Error::Io(_) + | dameng::Error::ConnectionFailed(_) + | dameng::Error::AuthFailed(_) + | dameng::Error::NotConnected + | dameng::Error::Timeout(_) + ) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_connect( + host: *const u8, + host_length: usize, + port: u16, + username: *const u8, + username_length: usize, + password: *const u8, + password_length: usize, + error_out: *mut *mut TpDmError, +) -> *mut TpDmConnection { + clear_error(error_out); + let operation = catch_unwind(AssertUnwindSafe(|| -> Result { + if port == 0 { + return Err("port must be greater than zero".to_string()); + } + let host = required_string(host, host_length, MAX_HOST_BYTES, "host")?; + let username = + required_string(username, username_length, MAX_CREDENTIAL_BYTES, "username")?; + let password = password_string(password, password_length)?; + let mut client = Client::new(&host, port); + client + .connect(&username, &password) + .map_err(|error| error.to_string())?; + detect_server_encoding(&mut client)?; + Ok(TpDmConnection { + client: Some(client), + }) + })); + match operation { + Ok(Ok(connection)) => Box::into_raw(Box::new(connection)), + Ok(Err(message)) => { + set_error(error_out, message); + ptr::null_mut() + } + Err(payload) => { + set_error(error_out, panic_message(payload)); + ptr::null_mut() + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_disconnect(connection: *mut TpDmConnection) { + if connection.is_null() { + return; + } + let _ = catch_unwind(AssertUnwindSafe(|| { + let mut connection = Box::from_raw(connection); + if let Some(mut client) = connection.client.take() { + let _ = client.close(); + } + })); +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_execute( + connection: *mut TpDmConnection, + sql: *const u8, + sql_length: usize, + expects_rows: bool, + row_cap: usize, + error_out: *mut *mut TpDmError, +) -> *mut TpDmResult { + clear_error(error_out); + if connection.is_null() { + set_error(error_out, "Dameng connection is missing"); + return ptr::null_mut(); + } + let sql = match optional_sql(sql, sql_length) { + Ok(sql) => sql, + Err(message) => { + set_error(error_out, message); + return ptr::null_mut(); + } + }; + let connection = &mut *connection; + let mut client = match connection_client(connection) { + Ok(client) => client, + Err(message) => { + set_error(error_out, message); + return ptr::null_mut(); + } + }; + let operation = catch_unwind(AssertUnwindSafe(|| { + if expects_rows { + query_result(&mut client, &sql, row_cap) + } else { + execute_result(&mut client, &sql) + } + })); + match operation { + Ok(Ok(result)) => { + restore_client(connection, client); + Box::into_raw(Box::new(result)) + } + Ok(Err(error)) => { + if is_recoverable(&error) { + restore_client(connection, client); + } else { + let _ = client.close(); + } + set_error(error_out, error.to_string()); + ptr::null_mut() + } + Err(payload) => { + set_error(error_out, panic_message(payload)); + ptr::null_mut() + } + } +} + +unsafe fn transaction_operation( + connection: *mut TpDmConnection, + error_out: *mut *mut TpDmError, + operation: impl FnOnce(&mut Client) -> Result<(), dameng::Error>, +) -> bool { + clear_error(error_out); + if connection.is_null() { + set_error(error_out, "Dameng connection is missing"); + return false; + } + let connection = &mut *connection; + let mut client = match connection_client(connection) { + Ok(client) => client, + Err(message) => { + set_error(error_out, message); + return false; + } + }; + let result = catch_unwind(AssertUnwindSafe(|| operation(&mut client))); + match result { + Ok(Ok(())) => { + restore_client(connection, client); + true + } + Ok(Err(error)) => { + if is_recoverable(&error) { + restore_client(connection, client); + } else { + let _ = client.close(); + } + set_error(error_out, error.to_string()); + false + } + Err(payload) => { + set_error(error_out, panic_message(payload)); + false + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_begin( + connection: *mut TpDmConnection, + error_out: *mut *mut TpDmError, +) -> bool { + transaction_operation(connection, error_out, Client::begin) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_commit( + connection: *mut TpDmConnection, + error_out: *mut *mut TpDmError, +) -> bool { + transaction_operation(connection, error_out, Client::commit) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_rollback( + connection: *mut TpDmConnection, + error_out: *mut *mut TpDmError, +) -> bool { + transaction_operation(connection, error_out, Client::rollback) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_ping( + connection: *mut TpDmConnection, + error_out: *mut *mut TpDmError, +) -> bool { + transaction_operation(connection, error_out, |client| client.ready()) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_free(result: *mut TpDmResult) { + if !result.is_null() { + drop(Box::from_raw(result)); + } +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_error_free(error: *mut TpDmError) { + if !error.is_null() { + drop(Box::from_raw(error)); + } +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_error_message( + error: *const TpDmError, + length_out: *mut usize, +) -> *const u8 { + if error.is_null() { + return ptr::null(); + } + let message = &(*error).message; + if !length_out.is_null() { + *length_out = message.len(); + } + message.as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_column_count(result: *const TpDmResult) -> usize { + result + .as_ref() + .map(|result| result.columns.len()) + .unwrap_or(0) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_row_count(result: *const TpDmResult) -> usize { + result.as_ref().map(|result| result.rows.len()).unwrap_or(0) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_rows_affected(result: *const TpDmResult) -> u64 { + result + .as_ref() + .map(|result| result.rows_affected) + .unwrap_or(0) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_execution_time(result: *const TpDmResult) -> f64 { + result + .as_ref() + .map(|result| result.execution_time_seconds) + .unwrap_or(0.0) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_is_truncated(result: *const TpDmResult) -> bool { + result + .as_ref() + .map(|result| result.is_truncated) + .unwrap_or(false) +} + +unsafe fn column_bytes( + result: *const TpDmResult, + column_index: usize, + length_out: *mut usize, + value: impl FnOnce(&TpDmColumn) -> &[u8], +) -> *const u8 { + let Some(column) = result + .as_ref() + .and_then(|result| result.columns.get(column_index)) + else { + return ptr::null(); + }; + let bytes = value(column); + if !length_out.is_null() { + *length_out = bytes.len(); + } + bytes.as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_column_name( + result: *const TpDmResult, + column_index: usize, + length_out: *mut usize, +) -> *const u8 { + column_bytes(result, column_index, length_out, |column| &column.name) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_column_type( + result: *const TpDmResult, + column_index: usize, + length_out: *mut usize, +) -> *const u8 { + column_bytes(result, column_index, length_out, |column| &column.type_name) +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_cell_kind( + result: *const TpDmResult, + row_index: usize, + column_index: usize, +) -> i32 { + let Some(cell) = result + .as_ref() + .and_then(|result| result.rows.get(row_index)) + .and_then(|row| row.get(column_index)) + else { + return -1; + }; + match cell { + TpDmCell::Null => 0, + TpDmCell::Text(_) => 1, + TpDmCell::Bytes(_) => 2, + } +} + +#[no_mangle] +pub unsafe extern "C" fn tp_dm_result_cell_bytes( + result: *const TpDmResult, + row_index: usize, + column_index: usize, + length_out: *mut usize, +) -> *const u8 { + let Some(cell) = result + .as_ref() + .and_then(|result| result.rows.get(row_index)) + .and_then(|row| row.get(column_index)) + else { + return ptr::null(); + }; + let bytes = match cell { + TpDmCell::Null => return ptr::null(), + TpDmCell::Text(bytes) | TpDmCell::Bytes(bytes) => bytes, + }; + if !length_out.is_null() { + *length_out = bytes.len(); + } + bytes.as_ptr() +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + + unsafe fn error_text(error: *mut TpDmError) -> String { + let mut length = 0; + let bytes = tp_dm_error_message(error, &mut length); + let text = String::from_utf8_lossy(slice::from_raw_parts(bytes, length)).into_owned(); + tp_dm_error_free(error); + text + } + + unsafe fn execute( + connection: *mut TpDmConnection, + sql: &str, + expects_rows: bool, + ) -> *mut TpDmResult { + let mut error = ptr::null_mut(); + let result = tp_dm_execute( + connection, + sql.as_ptr(), + sql.len(), + expects_rows, + 0, + &mut error, + ); + assert!(error.is_null(), "{}", error_text(error)); + assert!(!result.is_null()); + result + } + + #[test] + fn rejects_zero_port_before_connecting() { + unsafe { + let mut error = ptr::null_mut(); + let connection = tp_dm_connect( + b"localhost".as_ptr(), + 9, + 0, + b"user".as_ptr(), + 4, + b"password".as_ptr(), + 8, + &mut error, + ); + assert!(connection.is_null()); + assert_eq!(error_text(error), "port must be greater than zero"); + } + } + + #[test] + #[ignore = "requires DM8 in OrbStack"] + fn orbstack_connection_query_and_transaction() { + unsafe { + let host = env::var("DM_HOST").expect("DM_HOST is required"); + let port = env::var("DM_PORT") + .expect("DM_PORT is required") + .parse() + .expect("DM_PORT must be a UInt16"); + let username = env::var("DM_USER").expect("DM_USER is required"); + let password = env::var("DM_PASS").expect("DM_PASS is required"); + let mut error = ptr::null_mut(); + let connection = tp_dm_connect( + host.as_ptr(), + host.len(), + port, + username.as_ptr(), + username.len(), + password.as_ptr(), + password.len(), + &mut error, + ); + assert!(error.is_null(), "{}", error_text(error)); + assert!(!connection.is_null()); + + let drop_result = execute( + connection, + "DROP TABLE IF EXISTS TABLEPRO_BRIDGE_TEST", + false, + ); + tp_dm_result_free(drop_result); + let create_result = execute( + connection, + "CREATE TABLE TABLEPRO_BRIDGE_TEST (\ + ID INT PRIMARY KEY, \ + NAME VARCHAR(100), \ + STATUS VARCHAR(20), \ + TOTAL DECIMAL(12, 2), \ + CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP\ + )", + false, + ); + tp_dm_result_free(create_result); + let insert_result = execute( + connection, + "INSERT INTO TABLEPRO_BRIDGE_TEST (ID, NAME, STATUS, TOTAL) \ + VALUES (1, 'TablePro 达梦', 'READY', 42.50)", + false, + ); + assert_eq!(tp_dm_result_rows_affected(insert_result), 1); + tp_dm_result_free(insert_result); + + let select_result = execute( + connection, + "SELECT NAME FROM TABLEPRO_BRIDGE_TEST WHERE ID = 1", + true, + ); + assert_eq!(tp_dm_result_row_count(select_result), 1); + let mut length = 0; + let bytes = tp_dm_result_cell_bytes(select_result, 0, 0, &mut length); + assert_eq!( + slice::from_raw_parts(bytes, length), + "TablePro 达梦".as_bytes() + ); + tp_dm_result_free(select_result); + + let browse_result = execute( + connection, + "SELECT ID, NAME, STATUS, TOTAL, CREATED_AT \ + FROM TABLEPRO_BRIDGE_TEST ORDER BY 1 \ + OFFSET 0 ROWS FETCH NEXT 1000 ROWS ONLY", + true, + ); + assert_eq!(tp_dm_result_column_count(browse_result), 5); + assert_eq!(tp_dm_result_row_count(browse_result), 1); + tp_dm_result_free(browse_result); + + let empty_result = execute( + connection, + "SELECT NAME FROM TABLEPRO_BRIDGE_TEST WHERE ID = -1", + true, + ); + assert_eq!(tp_dm_result_column_count(empty_result), 1); + assert_eq!(tp_dm_result_row_count(empty_result), 0); + tp_dm_result_free(empty_result); + + assert!(tp_dm_begin(connection, &mut error)); + let transaction_insert = execute( + connection, + "INSERT INTO TABLEPRO_BRIDGE_TEST (ID, NAME) VALUES (2, 'rollback')", + false, + ); + tp_dm_result_free(transaction_insert); + assert!(tp_dm_rollback(connection, &mut error)); + let count_result = execute( + connection, + "SELECT COUNT(*) FROM TABLEPRO_BRIDGE_TEST", + true, + ); + let bytes = tp_dm_result_cell_bytes(count_result, 0, 0, &mut length); + assert_eq!(slice::from_raw_parts(bytes, length), b"1"); + tp_dm_result_free(count_result); + + let cleanup_result = execute(connection, "DROP TABLE TABLEPRO_BRIDGE_TEST", false); + tp_dm_result_free(cleanup_result); + tp_dm_disconnect(connection); + } + } +} diff --git a/Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift b/Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift index 2ddde73cb..632771ef1 100644 --- a/Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift +++ b/Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift @@ -18,6 +18,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable { public static let clickhouse = DatabaseType(rawValue: "ClickHouse") public static let mssql = DatabaseType(rawValue: "SQL Server") public static let oracle = DatabaseType(rawValue: "Oracle") + public static let dameng = DatabaseType(rawValue: "Dameng") public static let duckdb = DatabaseType(rawValue: "DuckDB") public static let cassandra = DatabaseType(rawValue: "Cassandra") public static let redshift = DatabaseType(rawValue: "Redshift") @@ -37,7 +38,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable { public static let allKnownTypes: [DatabaseType] = [ .mysql, .mariadb, .postgresql, .sqlite, .redis, .mongodb, - .clickhouse, .mssql, .oracle, .duckdb, .cassandra, .redshift, + .clickhouse, .mssql, .oracle, .dameng, .duckdb, .cassandra, .redshift, .etcd, .cloudflareD1, .dynamodb, .bigquery, .snowflake, .libsql, .beancount, .surrealdb, .teradata, .trino ] @@ -55,6 +56,7 @@ public struct DatabaseType: Hashable, Codable, Sendable, RawRepresentable { case .clickhouse: return "clickhouse-icon" case .mssql: return "mssql-icon" case .oracle: return "oracle-icon" + case .dameng: return "cylinder" case .duckdb: return "duckdb-icon" case .cassandra: return "cassandra-icon" case .etcd: return "etcd-icon" diff --git a/Packages/TableProCore/Tests/TableProModelsTests/DatabaseTypeTests.swift b/Packages/TableProCore/Tests/TableProModelsTests/DatabaseTypeTests.swift index 97e55b829..88ccf0f9d 100644 --- a/Packages/TableProCore/Tests/TableProModelsTests/DatabaseTypeTests.swift +++ b/Packages/TableProCore/Tests/TableProModelsTests/DatabaseTypeTests.swift @@ -13,6 +13,7 @@ struct DatabaseTypeTests { #expect(DatabaseType.redis.rawValue == "Redis") #expect(DatabaseType.mongodb.rawValue == "MongoDB") #expect(DatabaseType.mssql.rawValue == "SQL Server") + #expect(DatabaseType.dameng.rawValue == "Dameng") #expect(DatabaseType.cloudflareD1.rawValue == "Cloudflare D1") #expect(DatabaseType.bigquery.rawValue == "BigQuery") #expect(DatabaseType.snowflake.rawValue == "Snowflake") @@ -53,7 +54,7 @@ struct DatabaseTypeTests { @Test("allKnownTypes contains all expected types") func allKnownTypesComplete() { - #expect(DatabaseType.allKnownTypes.count == 22) + #expect(DatabaseType.allKnownTypes.count == 23) #expect(DatabaseType.allKnownTypes.contains(.mysql)) #expect(DatabaseType.allKnownTypes.contains(.bigquery)) #expect(DatabaseType.allKnownTypes.contains(.snowflake)) @@ -62,6 +63,7 @@ struct DatabaseTypeTests { #expect(DatabaseType.allKnownTypes.contains(.surrealdb)) #expect(DatabaseType.allKnownTypes.contains(.teradata)) #expect(DatabaseType.allKnownTypes.contains(.trino)) + #expect(DatabaseType.allKnownTypes.contains(.dameng)) } @Test("Hashable conformance") diff --git a/Plugins/DamengDriverPlugin/CDameng/CDameng.h b/Plugins/DamengDriverPlugin/CDameng/CDameng.h new file mode 100644 index 000000000..28f9d6481 --- /dev/null +++ b/Plugins/DamengDriverPlugin/CDameng/CDameng.h @@ -0,0 +1,80 @@ +#ifndef CDameng_h +#define CDameng_h + +#include +#include +#include + +typedef struct TpDmConnection TpDmConnection; +typedef struct TpDmError TpDmError; +typedef struct TpDmResult TpDmResult; + +/* + * Ownership and lifetime rules: + * - Input byte pointers must remain valid for their accompanying length until + * the function returns. + * - Connection handles are exclusively owned by the caller, must not be used + * concurrently, and must be released exactly once with tp_dm_disconnect. + * - Result and error handles must be released exactly once with their matching + * free function. Byte pointers borrowed from them remain valid only until the + * owning handle is released and must never be freed directly. + * - Non-NULL output pointers must point to writable storage. On failure, + * error_out receives an owned error handle when diagnostic text is available. + * - Passing a dangling handle or an invalid pointer is undefined behavior. + */ +TpDmConnection *tp_dm_connect( + const uint8_t *host, + size_t host_length, + uint16_t port, + const uint8_t *username, + size_t username_length, + const uint8_t *password, + size_t password_length, + TpDmError **error_out +); +void tp_dm_disconnect(TpDmConnection *connection); +TpDmResult *tp_dm_execute( + TpDmConnection *connection, + const uint8_t *sql, + size_t sql_length, + bool expects_rows, + size_t row_cap, + TpDmError **error_out +); +bool tp_dm_begin(TpDmConnection *connection, TpDmError **error_out); +bool tp_dm_commit(TpDmConnection *connection, TpDmError **error_out); +bool tp_dm_rollback(TpDmConnection *connection, TpDmError **error_out); +bool tp_dm_ping(TpDmConnection *connection, TpDmError **error_out); + +void tp_dm_result_free(TpDmResult *result); +size_t tp_dm_result_column_count(const TpDmResult *result); +size_t tp_dm_result_row_count(const TpDmResult *result); +uint64_t tp_dm_result_rows_affected(const TpDmResult *result); +double tp_dm_result_execution_time(const TpDmResult *result); +bool tp_dm_result_is_truncated(const TpDmResult *result); +const uint8_t *tp_dm_result_column_name( + const TpDmResult *result, + size_t column_index, + size_t *length_out +); +const uint8_t *tp_dm_result_column_type( + const TpDmResult *result, + size_t column_index, + size_t *length_out +); +int32_t tp_dm_result_cell_kind( + const TpDmResult *result, + size_t row_index, + size_t column_index +); +const uint8_t *tp_dm_result_cell_bytes( + const TpDmResult *result, + size_t row_index, + size_t column_index, + size_t *length_out +); + +void tp_dm_error_free(TpDmError *error); +const uint8_t *tp_dm_error_message(const TpDmError *error, size_t *length_out); + +#endif /* CDameng_h */ diff --git a/Plugins/DamengDriverPlugin/CDameng/module.modulemap b/Plugins/DamengDriverPlugin/CDameng/module.modulemap new file mode 100644 index 000000000..5b2fdd041 --- /dev/null +++ b/Plugins/DamengDriverPlugin/CDameng/module.modulemap @@ -0,0 +1,4 @@ +module CDameng { + umbrella header "CDameng.h" + export * +} diff --git a/Plugins/DamengDriverPlugin/DamengConnection.swift b/Plugins/DamengDriverPlugin/DamengConnection.swift new file mode 100644 index 000000000..8d6d09d5d --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengConnection.swift @@ -0,0 +1,218 @@ +import CDameng +import Foundation +import TableProPluginKit + +struct DamengRawResult: Sendable { + let columns: [String] + let columnTypeNames: [String] + let rows: [[PluginCellValue]] + let rowsAffected: Int + let executionTime: TimeInterval + let isTruncated: Bool +} + +final class DamengConnection: @unchecked Sendable { + private let queue = DispatchQueue(label: "com.TablePro.dameng.connection") + private var rawConnection: OpaquePointer? + + deinit { + if let rawConnection { + tp_dm_disconnect(rawConnection) + } + } + + func connect(host: String, port: Int, username: String, password: String) async throws { + guard let port = UInt16(exactly: port), port > 0 else { + throw DamengError(message: String(localized: "The Dameng port must be between 1 and 65535.")) + } + try await run { + guard self.rawConnection == nil else { return } + var rawError: OpaquePointer? + let connection = host.withUTF8Bytes { hostBytes in + username.withUTF8Bytes { usernameBytes in + password.withUTF8Bytes { passwordBytes in + tp_dm_connect( + hostBytes.baseAddress, + hostBytes.count, + port, + usernameBytes.baseAddress, + usernameBytes.count, + passwordBytes.baseAddress, + passwordBytes.count, + &rawError + ) + } + } + } + guard let connection else { + throw Self.error(from: rawError) + } + self.rawConnection = connection + } + } + + func disconnect() { + queue.sync { + guard let rawConnection else { return } + tp_dm_disconnect(rawConnection) + self.rawConnection = nil + } + } + + func ping() async throws { + try await run { + let connection = try self.connectedPointer() + var rawError: OpaquePointer? + guard tp_dm_ping(connection, &rawError) else { + throw Self.error(from: rawError) + } + } + } + + func execute(_ query: String, expectsRows: Bool, rowCap: Int?) async throws -> DamengRawResult { + try await run { + let connection = try self.connectedPointer() + var rawError: OpaquePointer? + let rawResult = query.withUTF8Bytes { bytes in + tp_dm_execute( + connection, + bytes.baseAddress, + bytes.count, + expectsRows, + max(rowCap ?? 0, 0), + &rawError + ) + } + guard let rawResult else { + throw Self.error(from: rawError) + } + defer { tp_dm_result_free(rawResult) } + return try Self.decode(rawResult) + } + } + + func beginTransaction() async throws { + try await transactionOperation(tp_dm_begin) + } + + func commitTransaction() async throws { + try await transactionOperation(tp_dm_commit) + } + + func rollbackTransaction() async throws { + try await transactionOperation(tp_dm_rollback) + } + + private func transactionOperation( + _ operation: @escaping @Sendable (OpaquePointer?, UnsafeMutablePointer?) -> Bool + ) async throws { + try await run { + let connection = try self.connectedPointer() + var rawError: OpaquePointer? + guard operation(connection, &rawError) else { + throw Self.error(from: rawError) + } + } + } + + private func connectedPointer() throws -> OpaquePointer { + guard let rawConnection else { + throw DamengError(message: String(localized: "The Dameng connection is closed.")) + } + return rawConnection + } + + private func run(_ operation: @escaping @Sendable () throws -> T) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + queue.async { + do { + continuation.resume(returning: try operation()) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + private static func decode(_ result: OpaquePointer) throws -> DamengRawResult { + let columnCount = tp_dm_result_column_count(result) + let rowCount = tp_dm_result_row_count(result) + let columns = try (0.. UInt64(Int.max) ? Int.max : Int(affected), + executionTime: tp_dm_result_execution_time(result), + isTruncated: tp_dm_result_is_truncated(result) + ) + } + + private static func string( + result: OpaquePointer, + column: Int, + reader: (OpaquePointer?, Int, UnsafeMutablePointer?) -> UnsafePointer? + ) throws -> String { + var length = 0 + guard let bytes = reader(result, column, &length), + let value = String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8) else { + throw DamengError(message: String(localized: "Dameng returned invalid UTF-8 metadata.")) + } + return value + } + + private static func cell(result: OpaquePointer, row: Int, column: Int) throws -> PluginCellValue { + switch tp_dm_result_cell_kind(result, row, column) { + case 0: + return .null + case 1: + let data = try cellData(result: result, row: row, column: column) + guard let value = String(data: data, encoding: .utf8) else { + throw DamengError(message: String(localized: "Dameng returned invalid UTF-8 text.")) + } + return .text(value) + case 2: + return .bytes(try cellData(result: result, row: row, column: column)) + default: + throw DamengError(message: String(localized: "Dameng returned an invalid result cell.")) + } + } + + private static func cellData(result: OpaquePointer, row: Int, column: Int) throws -> Data { + var length = 0 + guard let bytes = tp_dm_result_cell_bytes(result, row, column, &length) else { + throw DamengError(message: String(localized: "Dameng returned an invalid result value.")) + } + return Data(bytes: bytes, count: length) + } + + private static func error(from pointer: OpaquePointer?) -> DamengError { + guard let pointer else { + return DamengError(message: String(localized: "The Dameng operation failed.")) + } + defer { tp_dm_error_free(pointer) } + var length = 0 + guard let bytes = tp_dm_error_message(pointer, &length), + let message = String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8) else { + return DamengError(message: String(localized: "The Dameng operation failed.")) + } + return DamengError(message: message) + } +} + +private extension String { + func withUTF8Bytes(_ body: (UnsafeBufferPointer) throws -> T) rethrows -> T { + try Array(utf8).withUnsafeBufferPointer(body) + } +} diff --git a/Plugins/DamengDriverPlugin/DamengError.swift b/Plugins/DamengDriverPlugin/DamengError.swift new file mode 100644 index 000000000..b7ccdc245 --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengError.swift @@ -0,0 +1,8 @@ +import Foundation +import TableProPluginKit + +struct DamengError: Error, PluginDriverError, Sendable { + let message: String + + var pluginErrorMessage: String { message } +} diff --git a/Plugins/DamengDriverPlugin/DamengParameterBinder.swift b/Plugins/DamengDriverPlugin/DamengParameterBinder.swift new file mode 100644 index 000000000..4f286d8e2 --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengParameterBinder.swift @@ -0,0 +1,146 @@ +import Foundation +import TableProPluginKit + +enum DamengParameterBindingError: Error, Equatable { + case embeddedNull + case insufficientParameters + case unusedParameters +} +enum DamengParameterBinder { + private enum State: Equatable { + case code + case singleQuote + case doubleQuote + case alternativeQuote(UInt8) + case lineComment + case blockComment(Int) + } + + static func bind(query: String, parameters: [PluginCellValue]) throws -> String { + let input = Array(query.utf8) + var output: [UInt8] = [] + output.reserveCapacity(input.count + parameters.count * 8) + var state = State.code + var parameterIndex = 0 + var index = 0 + + while index < input.count { + let byte = input[index] + let next = index + 1 < input.count ? input[index + 1] : nil + + switch state { + case .code: + if byte == 0x2D, next == 0x2D { + output.append(contentsOf: [byte, 0x2D]) + state = .lineComment + index += 2 + } else if byte == 0x2F, next == 0x2A { + output.append(contentsOf: [byte, 0x2A]) + state = .blockComment(1) + index += 2 + } else if byte == 0x27 { + output.append(byte) + state = .singleQuote + index += 1 + } else if byte == 0x22 { + output.append(byte) + state = .doubleQuote + index += 1 + } else if isAlternativeQuoteStart(input, at: index) { + let opener = input[index + 2] + output.append(contentsOf: input[index...index + 2]) + state = .alternativeQuote(alternativeQuoteCloser(opener)) + index += 3 + } else if byte == 0x3F { + guard parameterIndex < parameters.count else { + throw DamengParameterBindingError.insufficientParameters + } + output.append(contentsOf: try literal(for: parameters[parameterIndex]).utf8) + parameterIndex += 1 + index += 1 + } else { + output.append(byte) + index += 1 + } + case .singleQuote: + output.append(byte) + if byte == 0x27, next == 0x27 { + output.append(0x27) + index += 2 + } else { + if byte == 0x27 { state = .code } + index += 1 + } + case .doubleQuote: + output.append(byte) + if byte == 0x22, next == 0x22 { + output.append(0x22) + index += 2 + } else { + if byte == 0x22 { state = .code } + index += 1 + } + case .alternativeQuote(let closer): + output.append(byte) + if byte == closer, next == 0x27 { + output.append(0x27) + state = .code + index += 2 + } else { + index += 1 + } + case .lineComment: + output.append(byte) + if byte == 0x0A || byte == 0x0D { state = .code } + index += 1 + case .blockComment(let depth): + if byte == 0x2F, next == 0x2A { + output.append(contentsOf: [byte, 0x2A]) + state = .blockComment(depth + 1) + index += 2 + } else if byte == 0x2A, next == 0x2F { + output.append(contentsOf: [byte, 0x2F]) + state = depth == 1 ? .code : .blockComment(depth - 1) + index += 2 + } else { + output.append(byte) + index += 1 + } + } + } + + guard parameterIndex == parameters.count else { + throw DamengParameterBindingError.unusedParameters + } + return String(decoding: output, as: UTF8.self) + } + + private static func literal(for value: PluginCellValue) throws -> String { + switch value { + case .null: + return "NULL" + case .text(let text): + guard !text.contains("\0") else { + throw DamengParameterBindingError.embeddedNull + } + return "'\(text.replacingOccurrences(of: "'", with: "''"))'" + case .bytes(let data): + return "HEXTORAW('\(data.map { String(format: "%02X", $0) }.joined())')" + } + } + + private static func isAlternativeQuoteStart(_ bytes: [UInt8], at index: Int) -> Bool { + guard index + 2 < bytes.count else { return false } + return (bytes[index] == 0x51 || bytes[index] == 0x71) && bytes[index + 1] == 0x27 + } + + private static func alternativeQuoteCloser(_ opener: UInt8) -> UInt8 { + switch opener { + case 0x5B: return 0x5D + case 0x28: return 0x29 + case 0x7B: return 0x7D + case 0x3C: return 0x3E + default: return opener + } + } +} diff --git a/Plugins/DamengDriverPlugin/DamengPlugin.swift b/Plugins/DamengDriverPlugin/DamengPlugin.swift new file mode 100644 index 000000000..b1142b981 --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengPlugin.swift @@ -0,0 +1,256 @@ +import Foundation +import TableProPluginKit + +final class DamengPlugin: NSObject, TableProPlugin, DriverPlugin { + static let pluginName = "Dameng Driver" + static let pluginVersion = "1.0.0" + static let pluginDescription = "Dameng DM8 support via a native wire driver" + static let capabilities: [PluginCapability] = [.databaseDriver] + + static let databaseTypeId = "Dameng" + static let databaseDisplayName = "Dameng DM8" + static let iconName = "cylinder" + static let defaultPort = 5_236 + static let isDownloadable = true + static let urlSchemes = ["dm"] + static let brandColorHex = "#C60018" + static let supportsSSL = false + static let supportsDatabaseSwitching = false + static let supportsSchemaSwitching = true + static let defaultSchemaName = "" + static let containerEntityName = "Schema" + static let postConnectActions: [PostConnectAction] = [.selectSchemaFromLastSession] + static let explainVariants = [ExplainVariant(id: "plan", label: "Plan", sqlPrefix: "EXPLAIN")] + static let databaseGroupingStrategy: GroupingStrategy = .hierarchicalSchema + static let pathFieldRole: PathFieldRole = .database + static let systemSchemaNames = ["SYS", "SYSDBA", "SYSAUDITOR", "SYSSSO", "CTISYS"] + static let supportsCascadeDrop = true + static let supportsDropSchema = true + static let supportsForeignKeyDisable = false + static let supportsRenameColumn = true + static let columnTypesByCategory: [String: [String]] = [ + "Integer": ["TINYINT", "SMALLINT", "INT", "INTEGER", "BIGINT"], + "Float": ["REAL", "FLOAT", "DOUBLE", "DEC", "DECIMAL", "NUMERIC", "NUMBER"], + "String": ["CHAR", "CHARACTER", "VARCHAR", "VARCHAR2", "TEXT", "CLOB"], + "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], + "Binary": ["BINARY", "VARBINARY", "BLOB", "IMAGE"], + "Boolean": ["BIT", "BOOLEAN"], + "Other": ["ROWID", "INTERVAL"] + ] + static let statementCompletions = [ + CompletionEntry(label: "SELECT", insertText: "SELECT"), + CompletionEntry(label: "SELECT DISTINCT", insertText: "SELECT DISTINCT"), + CompletionEntry(label: "INSERT INTO", insertText: "INSERT INTO"), + CompletionEntry(label: "UPDATE", insertText: "UPDATE"), + CompletionEntry(label: "DELETE FROM", insertText: "DELETE FROM"), + CompletionEntry(label: "MERGE INTO", insertText: "MERGE INTO"), + CompletionEntry(label: "CREATE TABLE", insertText: "CREATE TABLE"), + CompletionEntry(label: "CREATE OR REPLACE VIEW", insertText: "CREATE OR REPLACE VIEW"), + CompletionEntry(label: "CREATE SCHEMA", insertText: "CREATE SCHEMA"), + CompletionEntry(label: "ALTER TABLE", insertText: "ALTER TABLE"), + CompletionEntry(label: "DROP TABLE", insertText: "DROP TABLE"), + CompletionEntry(label: "DROP SCHEMA", insertText: "DROP SCHEMA"), + CompletionEntry(label: "SET SCHEMA", insertText: "SET SCHEMA"), + CompletionEntry(label: "EXPLAIN", insertText: "EXPLAIN"), + CompletionEntry(label: "WHERE", insertText: "WHERE"), + CompletionEntry(label: "GROUP BY", insertText: "GROUP BY"), + CompletionEntry(label: "ORDER BY", insertText: "ORDER BY"), + CompletionEntry(label: "FETCH FIRST", insertText: "FETCH FIRST"), + CompletionEntry(label: "JOIN", insertText: "JOIN"), + CompletionEntry(label: "LEFT JOIN", insertText: "LEFT JOIN"), + CompletionEntry(label: "UNION ALL", insertText: "UNION ALL"), + CompletionEntry(label: "WITH", insertText: "WITH"), + CompletionEntry(label: "CONNECT BY", insertText: "CONNECT BY"), + CompletionEntry(label: "START WITH", insertText: "START WITH"), + CompletionEntry(label: "PARTITION BY", insertText: "PARTITION BY") + ] + static let sqlDialect: SQLDialectDescriptor? = SQLDialectDescriptor( + identifierQuote: "\"", + keywords: [ + "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "FULL", + "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "AS", "ORDER", "BY", "GROUP", + "HAVING", "LIMIT", "OFFSET", "FETCH", "FIRST", "ROWS", "ONLY", "INSERT", "INTO", "VALUES", + "UPDATE", "SET", "DELETE", "MERGE", "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", + "SCHEMA", "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", "ADD", "MODIFY", + "COLUMN", "RENAME", "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", + "IDENTITY", "SEQUENCE", "SYNONYM", "GRANT", "REVOKE", "TRIGGER", "PROCEDURE", "CASE", "WHEN", + "THEN", "ELSE", "END", "UNION", "INTERSECT", "MINUS", "DECLARE", "BEGIN", "COMMIT", "ROLLBACK", + "SAVEPOINT", "EXECUTE", "IMMEDIATE", "OVER", "PARTITION", "ROW_NUMBER", "RANK", "DENSE_RANK", + "CONNECT", "LEVEL", "START", "WITH", "PRIOR", "ROWNUM", "ROWID", "DUAL" + ], + functions: [ + "COUNT", "SUM", "AVG", "MAX", "MIN", "LISTAGG", "CONCAT", "SUBSTR", "INSTR", "LENGTH", "LOWER", + "UPPER", "TRIM", "LTRIM", "RTRIM", "REPLACE", "LPAD", "RPAD", "SYSDATE", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "ADD_MONTHS", "MONTHS_BETWEEN", "LAST_DAY", "EXTRACT", "TO_DATE", "TO_CHAR", + "TO_NUMBER", "TO_TIMESTAMP", "TRUNC", "ROUND", "CEIL", "FLOOR", "ABS", "POWER", "SQRT", "MOD", + "NVL", "NVL2", "COALESCE", "NULLIF", "GREATEST", "LEAST", "CAST", "USER" + ], + dataTypes: Set(columnTypesByCategory.values.flatMap { $0 }), + tableOptions: ["TABLESPACE", "STORAGE", "PCTFREE", "INITRANS"], + regexSyntax: .regexpLike, + booleanLiteralStyle: .numeric, + likeEscapeStyle: .explicit, + paginationStyle: .offsetFetch, + offsetFetchOrderBy: "ORDER BY 1", + autoLimitStyle: .fetchFirst, + caseSensitivityStyle: .caseFoldFunction + ) + + func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { + DamengPluginDriver(config: config) + } +} + +final class DamengPluginDriver: PluginDatabaseDriver, @unchecked Sendable { + let config: DriverConnectionConfig + private let connection = DamengConnection() + private var activeSchema: String? + private var detectedServerVersion: String? + + var capabilities: PluginCapabilities { + [.parameterizedQueries, .transactions, .alterTableDDL, .multiSchema] + } + + var supportsSchemas: Bool { true } + var supportsTransactions: Bool { true } + var currentSchema: String? { activeSchema } + var serverVersion: String? { detectedServerVersion } + + init(config: DriverConnectionConfig) { + self.config = config + } + + func connect() async throws { + try await connection.connect( + host: config.host, + port: config.port, + username: config.username, + password: config.password + ) + do { + if !config.database.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + try await switchSchema(to: config.database) + } else { + activeSchema = try await scalarText("SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)") + } + detectedServerVersion = try await scalarText("SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1") + } catch { + disconnect() + throw error + } + } + + func disconnect() { + connection.disconnect() + activeSchema = nil + detectedServerVersion = nil + } + + func ping() async throws { + try await connection.ping() + } + + func execute(query: String) async throws -> PluginQueryResult { + try await executeBound(query: query, parameters: [], rowCap: nil) + } + + func executeUserQuery( + query: String, + rowCap: Int?, + parameters: [PluginCellValue]? + ) async throws -> PluginQueryResult { + try await executeBound(query: query, parameters: parameters ?? [], rowCap: rowCap) + } + + func executeParameterized(query: String, parameters: [PluginCellValue]) async throws -> PluginQueryResult { + try await executeBound(query: query, parameters: parameters, rowCap: nil) + } + + func beginTransaction() async throws { + try await connection.beginTransaction() + } + + func commitTransaction() async throws { + try await connection.commitTransaction() + } + + func rollbackTransaction() async throws { + try await connection.rollbackTransaction() + } + + func switchSchema(to schema: String) async throws { + let normalized = schema.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalized.isEmpty else { + throw DamengError(message: String(localized: "The Dameng schema name cannot be empty.")) + } + let setSchema = "SET SCHEMA \(quoteIdentifier(normalized))" + let escaped = setSchema.replacingOccurrences(of: "'", with: "''") + _ = try await executeBound( + query: "BEGIN EXECUTE IMMEDIATE '\(escaped)'; END;", + parameters: [], + rowCap: nil + ) + let selected = try await scalarText("SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID)") + guard selected.caseInsensitiveCompare(normalized) == .orderedSame else { + throw DamengError(message: String(localized: "Dameng did not switch to the requested schema.")) + } + activeSchema = selected + } + + func dropSchema(name: String) async throws { + guard !DamengPlugin.systemSchemaNames.contains(name.uppercased()) else { + throw DamengError(message: String(localized: "Dameng system schemas cannot be dropped.")) + } + guard name.caseInsensitiveCompare(activeSchema ?? "") != .orderedSame else { + throw DamengError(message: String(localized: "Switch away from a schema before dropping it.")) + } + _ = try await execute(query: "DROP SCHEMA \(quoteIdentifier(name)) CASCADE") + } + + func quoteIdentifier(_ name: String) -> String { + "\"\(name.replacingOccurrences(of: "\"", with: "\"\""))\"" + } + + func createViewTemplate() -> String? { + "CREATE OR REPLACE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;" + } + + func editViewFallbackTemplate(viewName: String) -> String? { + "CREATE OR REPLACE VIEW \(quoteIdentifier(viewName)) AS\nSELECT * FROM table_name;" + } + + func castColumnToText(_ column: String) -> String { + "CAST(\(column) AS VARCHAR(8188))" + } + + func buildExplainQuery(_ sql: String) -> String? { + "EXPLAIN \(sql)" + } + + private func executeBound( + query: String, + parameters: [PluginCellValue], + rowCap: Int? + ) async throws -> PluginQueryResult { + let bound = parameters.isEmpty ? query : try DamengParameterBinder.bind(query: query, parameters: parameters) + let expectsRows = DamengStatementClassifier.expectsRows(bound) + let capped = expectsRows ? DamengStatementClassifier.applyingRowCap(rowCap, to: bound) : bound + let result = try await connection.execute(capped, expectsRows: expectsRows, rowCap: rowCap) + return PluginQueryResult( + columns: result.columns, + columnTypeNames: result.columnTypeNames, + rows: result.rows, + rowsAffected: result.rowsAffected, + executionTime: result.executionTime, + isTruncated: result.isTruncated + ) + } + + private func scalarText(_ query: String) async throws -> String { + let result = try await executeBound(query: query, parameters: [], rowCap: 1) + guard let value = result.rows.first?.first?.asText else { + throw DamengError(message: String(localized: "Dameng returned an empty metadata result.")) + } + return value + } +} diff --git a/Plugins/DamengDriverPlugin/DamengPluginDriver+Editing.swift b/Plugins/DamengDriverPlugin/DamengPluginDriver+Editing.swift new file mode 100644 index 000000000..389e1ec28 --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengPluginDriver+Editing.swift @@ -0,0 +1,329 @@ +import Foundation +import TableProPluginKit + +extension DamengPluginDriver { + func generateStatements( + table: String, + columns: [String], + primaryKeyColumns: [String], + changes: [PluginRowChange], + insertedRowData: [Int: [PluginCellValue]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [PluginCellValue])]? { + generateStatements( + table: table, + schema: nil, + columns: columns, + primaryKeyColumns: primaryKeyColumns, + changes: changes, + insertedRowData: insertedRowData, + deletedRowIndices: deletedRowIndices, + insertedRowIndices: insertedRowIndices + ) + } + + func generateStatements( + table: String, + schema: String?, + columns: [String], + primaryKeyColumns: [String], + changes: [PluginRowChange], + insertedRowData: [Int: [PluginCellValue]], + deletedRowIndices: Set, + insertedRowIndices: Set + ) -> [(statement: String, parameters: [PluginCellValue])]? { + let qualifiedTable = qualifiedName(schema: schema, object: table) + var statements: [(statement: String, parameters: [PluginCellValue])] = [] + for change in changes { + switch change.type { + case .insert: + guard insertedRowIndices.contains(change.rowIndex), + let row = insertedRowData[change.rowIndex], + let statement = insertStatement(table: qualifiedTable, columns: columns, values: row) else { + continue + } + statements.append(statement) + case .update: + guard let statement = updateStatement( + table: qualifiedTable, + columns: columns, + primaryKeyColumns: primaryKeyColumns, + change: change + ) else { + continue + } + statements.append(statement) + case .delete: + guard deletedRowIndices.contains(change.rowIndex), + let statement = deleteStatement( + table: qualifiedTable, + columns: columns, + primaryKeyColumns: primaryKeyColumns, + change: change + ) else { + continue + } + statements.append(statement) + } + } + return statements.isEmpty ? nil : statements + } + + func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? { + guard !definition.columns.isEmpty else { return nil } + let table = qualifiedName(schema: nil, object: definition.tableName) + let primaryKeys = definition.columns.filter(\.isPrimaryKey) + let inlinePrimaryKey = primaryKeys.count == 1 + var definitions = definition.columns.map { columnDefinition($0, inlinePrimaryKey: inlinePrimaryKey) } + if primaryKeys.count > 1 { + definitions.append("PRIMARY KEY (\(primaryKeys.map { quoteIdentifier($0.name) }.joined(separator: ", ")))") + } + definitions.append(contentsOf: definition.foreignKeys.map(foreignKeyDefinition)) + let createTable = "CREATE TABLE \(table) (\n " + definitions.joined(separator: ",\n ") + "\n)" + let indexes = definition.indexes.map { indexDefinition($0, table: table) } + let comments = definition.columns.compactMap { column -> String? in + guard let comment = column.comment, !comment.isEmpty else { return nil } + return "COMMENT ON COLUMN \(table).\(quoteIdentifier(column.name)) IS \(stringLiteral(comment))" + } + let statements = [createTable] + indexes + comments + guard statements.count > 1 else { + return createTable + ";" + } + let commands = statements.map { " EXECUTE IMMEDIATE \(stringLiteral($0));" } + return "BEGIN\n" + commands.joined(separator: "\n") + "\nEND;" + } + + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { + columnDefinition(column, inlinePrimaryKey: false) + } + + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { + indexDefinition(index, table: tableName.map { qualifiedName(schema: nil, object: $0) } ?? "\"table\"") + } + + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { + foreignKeyDefinition(fk) + } + + func generateAddColumnSQL(table: String, column: PluginColumnDefinition) -> String? { + "ALTER TABLE \(qualifiedName(schema: nil, object: table)) ADD \(columnDefinition(column, inlinePrimaryKey: false))" + } + + func generateModifyColumnSQL( + table: String, + oldColumn: PluginColumnDefinition, + newColumn: PluginColumnDefinition + ) -> String? { + let qualifiedTable = qualifiedName(schema: nil, object: table) + var statements: [String] = [] + if oldColumn.name != newColumn.name { + statements.append( + "ALTER TABLE \(qualifiedTable) RENAME COLUMN \(quoteIdentifier(oldColumn.name)) TO \(quoteIdentifier(newColumn.name))" + ) + } + let attributesChanged = oldColumn.dataType != newColumn.dataType || + oldColumn.isNullable != newColumn.isNullable || + oldColumn.defaultValue != newColumn.defaultValue + if attributesChanged { + statements.append( + "ALTER TABLE \(qualifiedTable) MODIFY \(columnDefinition(newColumn, inlinePrimaryKey: false))" + ) + } + return statements.isEmpty ? nil : statements.joined(separator: ";\n") + } + + func generateDropColumnSQL(table: String, columnName: String) -> String? { + "ALTER TABLE \(qualifiedName(schema: nil, object: table)) DROP COLUMN \(quoteIdentifier(columnName))" + } + + func generateAddIndexSQL(table: String, index: PluginIndexDefinition) -> String? { + indexDefinition(index, table: qualifiedName(schema: nil, object: table)) + } + + func generateDropIndexSQL(table: String, indexName: String) -> String? { + "DROP INDEX \(qualifiedName(schema: nil, object: indexName))" + } + + func generateAddForeignKeySQL(table: String, fk: PluginForeignKeyDefinition) -> String? { + "ALTER TABLE \(qualifiedName(schema: nil, object: table)) ADD \(foreignKeyDefinition(fk))" + } + + func generateDropForeignKeySQL(table: String, constraintName: String) -> String? { + "ALTER TABLE \(qualifiedName(schema: nil, object: table)) DROP CONSTRAINT \(quoteIdentifier(constraintName))" + } + + func generateModifyPrimaryKeySQL( + table: String, + oldColumns: [String], + newColumns: [String], + constraintName: String? + ) -> [String]? { + let qualifiedTable = qualifiedName(schema: nil, object: table) + var statements: [String] = [] + if !oldColumns.isEmpty { + if let constraintName, !constraintName.isEmpty { + statements.append("ALTER TABLE \(qualifiedTable) DROP CONSTRAINT \(quoteIdentifier(constraintName))") + } else { + statements.append("ALTER TABLE \(qualifiedTable) DROP PRIMARY KEY") + } + } + if !newColumns.isEmpty { + let columns = newColumns.map(quoteIdentifier).joined(separator: ", ") + statements.append("ALTER TABLE \(qualifiedTable) ADD PRIMARY KEY (\(columns))") + } + return statements.isEmpty ? nil : statements + } + + func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? { + ["TRUNCATE TABLE \(qualifiedName(schema: schema, object: table))"] + } + + func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? { + let normalizedType = objectType.uppercased() == "VIEW" ? "VIEW" : "TABLE" + let suffix = cascade && normalizedType == "TABLE" ? " CASCADE" : "" + return "DROP \(normalizedType) \(qualifiedName(schema: schema, object: name))\(suffix)" + } + + func defaultExportQuery(table: String, schema: String?) -> String? { + "SELECT * FROM \(qualifiedName(schema: schema, object: table))" + } + + private func insertStatement( + table: String, + columns: [String], + values: [PluginCellValue] + ) -> (statement: String, parameters: [PluginCellValue])? { + var insertColumns: [String] = [] + var placeholders: [String] = [] + var parameters: [PluginCellValue] = [] + for (index, value) in values.enumerated() where index < columns.count { + insertColumns.append(quoteIdentifier(columns[index])) + if value.asText == "__DEFAULT__" { + placeholders.append("DEFAULT") + } else { + placeholders.append("?") + parameters.append(value) + } + } + guard !insertColumns.isEmpty else { return nil } + return ( + "INSERT INTO \(table) (\(insertColumns.joined(separator: ", "))) VALUES (\(placeholders.joined(separator: ", ")))", + parameters + ) + } + + private func updateStatement( + table: String, + columns: [String], + primaryKeyColumns: [String], + change: PluginRowChange + ) -> (statement: String, parameters: [PluginCellValue])? { + guard !change.cellChanges.isEmpty, let original = change.originalRow else { return nil } + var parameters = change.cellChanges.map(\.newValue) + let assignments = change.cellChanges.map { "\(quoteIdentifier($0.columnName)) = ?" } + guard let predicate = rowPredicate( + columns: columns, + primaryKeyColumns: primaryKeyColumns, + original: original, + parameters: ¶meters + ) else { + return nil + } + return ( + "UPDATE \(table) SET \(assignments.joined(separator: ", ")) WHERE \(predicate) AND ROWNUM = 1", + parameters + ) + } + + private func deleteStatement( + table: String, + columns: [String], + primaryKeyColumns: [String], + change: PluginRowChange + ) -> (statement: String, parameters: [PluginCellValue])? { + guard let original = change.originalRow else { return nil } + var parameters: [PluginCellValue] = [] + guard let predicate = rowPredicate( + columns: columns, + primaryKeyColumns: primaryKeyColumns, + original: original, + parameters: ¶meters + ) else { + return nil + } + return ("DELETE FROM \(table) WHERE \(predicate) AND ROWNUM = 1", parameters) + } + + private func rowPredicate( + columns: [String], + primaryKeyColumns: [String], + original: [PluginCellValue], + parameters: inout [PluginCellValue] + ) -> String? { + let selectedColumns = primaryKeyColumns.isEmpty ? columns : primaryKeyColumns + var predicates: [String] = [] + for column in selectedColumns { + guard let index = columns.firstIndex(of: column), index < original.count else { continue } + let value = original[index] + if value.isNull { + predicates.append("\(quoteIdentifier(column)) IS NULL") + } else { + predicates.append("\(quoteIdentifier(column)) = ?") + parameters.append(value) + } + } + return predicates.isEmpty ? nil : predicates.joined(separator: " AND ") + } + + private func columnDefinition(_ column: PluginColumnDefinition, inlinePrimaryKey: Bool) -> String { + var definition = "\(quoteIdentifier(column.name)) \(column.dataType.uppercased())" + if column.autoIncrement { + definition += " IDENTITY(1,1)" + } + if let defaultValue = column.defaultValue, !defaultValue.isEmpty { + definition += " DEFAULT \(defaultExpression(defaultValue))" + } + if !column.isNullable { + definition += " NOT NULL" + } + if inlinePrimaryKey, column.isPrimaryKey { + definition += " PRIMARY KEY" + } + return definition + } + + private func indexDefinition(_ index: PluginIndexDefinition, table: String) -> String { + let unique = index.isUnique ? "UNIQUE " : "" + let columns = index.columns.map(quoteIdentifier).joined(separator: ", ") + return "CREATE \(unique)INDEX \(quoteIdentifier(index.name)) ON \(table) (\(columns))" + } + + private func foreignKeyDefinition(_ foreignKey: PluginForeignKeyDefinition) -> String { + let columns = foreignKey.columns.map(quoteIdentifier).joined(separator: ", ") + let referencedColumns = foreignKey.referencedColumns.map(quoteIdentifier).joined(separator: ", ") + let referencedTable = qualifiedName(schema: foreignKey.referencedSchema, object: foreignKey.referencedTable) + let constraint = foreignKey.name.isEmpty ? "" : "CONSTRAINT \(quoteIdentifier(foreignKey.name)) " + let onDelete = foreignKey.onDelete.uppercased() == "NO ACTION" ? "" : " ON DELETE \(foreignKey.onDelete)" + return "\(constraint)FOREIGN KEY (\(columns)) REFERENCES \(referencedTable) (\(referencedColumns))\(onDelete)" + } + + private func defaultExpression(_ value: String) -> String { + let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) + let uppercased = normalized.uppercased() + if PluginNumericLiteral.isValid(normalized) || [ + "NULL", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "SYSDATE" + ].contains(uppercased) || normalized.hasPrefix("'") || normalized.hasPrefix("\"") { + return normalized + } + return stringLiteral(normalized) + } + + private func stringLiteral(_ value: String) -> String { + "'\(value.replacingOccurrences(of: "'", with: "''"))'" + } + + private func qualifiedName(schema: String?, object: String) -> String { + "\(quoteIdentifier(effectiveSchema(schema))).\(quoteIdentifier(object))" + } +} diff --git a/Plugins/DamengDriverPlugin/DamengPluginDriver+Schema.swift b/Plugins/DamengDriverPlugin/DamengPluginDriver+Schema.swift new file mode 100644 index 000000000..d49e29677 --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengPluginDriver+Schema.swift @@ -0,0 +1,343 @@ +import Foundation +import TableProPluginKit + +extension DamengPluginDriver { + func fetchTables(schema: String?) async throws -> [PluginTableInfo] { + let result = try await executeParameterized( + query: """ + SELECT TABLE_NAME, 'TABLE' AS TABLE_TYPE + FROM ALL_TABLES + WHERE OWNER = ? + UNION ALL + SELECT VIEW_NAME, 'VIEW' + FROM ALL_VIEWS + WHERE OWNER = ? + ORDER BY 1 + """, + parameters: [.text(effectiveSchema(schema)), .text(effectiveSchema(schema))] + ) + return result.rows.compactMap { row in + guard let name = row[safe: 0]?.asText else { return nil } + return PluginTableInfo(name: name, type: row[safe: 1]?.asText ?? "TABLE") + } + } + + func fetchColumns(table: String, schema: String?) async throws -> [PluginColumnInfo] { + let owner = effectiveSchema(schema) + let result = try await executeParameterized( + query: """ + SELECT c.COLUMN_NAME, + c.DATA_TYPE, + c.DATA_LENGTH, + c.DATA_PRECISION, + c.DATA_SCALE, + c.NULLABLE, + CAST(c.DATA_DEFAULT AS VARCHAR(8188)) AS DATA_DEFAULT, + CAST(com.COMMENTS AS VARCHAR(8188)) AS COMMENTS, + CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'Y' ELSE 'N' END AS IS_PK + FROM ALL_TAB_COLUMNS c + LEFT JOIN ( + SELECT acc.COLUMN_NAME + FROM ALL_CONS_COLUMNS acc + JOIN ALL_CONSTRAINTS ac + ON acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME + AND acc.OWNER = ac.OWNER + WHERE ac.CONSTRAINT_TYPE = 'P' + AND ac.OWNER = ? + AND ac.TABLE_NAME = ? + ) pk ON c.COLUMN_NAME = pk.COLUMN_NAME + LEFT JOIN ALL_COL_COMMENTS com + ON c.OWNER = com.OWNER + AND c.TABLE_NAME = com.TABLE_NAME + AND c.COLUMN_NAME = com.COLUMN_NAME + WHERE c.OWNER = ? + AND c.TABLE_NAME = ? + ORDER BY c.COLUMN_ID + """, + parameters: [.text(owner), .text(table), .text(owner), .text(table)] + ) + return result.rows.compactMap { row in + guard let name = row[safe: 0]?.asText else { return nil } + return PluginColumnInfo( + name: name, + dataType: DamengSchemaValue.fullType( + name: row[safe: 1]?.asText ?? "VARCHAR", + length: row[safe: 2]?.asText, + precision: row[safe: 3]?.asText, + scale: row[safe: 4]?.asText + ), + isNullable: row[safe: 5]?.asText == "Y", + isPrimaryKey: row[safe: 8]?.asText == "Y", + defaultValue: DamengSchemaValue.nonEmpty(row[safe: 6]?.asText), + comment: DamengSchemaValue.nonEmpty(row[safe: 7]?.asText) + ) + } + } + + func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] { + let owner = effectiveSchema(schema) + let result = try await executeParameterized( + query: """ + SELECT c.TABLE_NAME, + c.COLUMN_NAME, + c.DATA_TYPE, + c.DATA_LENGTH, + c.DATA_PRECISION, + c.DATA_SCALE, + c.NULLABLE, + CAST(c.DATA_DEFAULT AS VARCHAR(8188)) AS DATA_DEFAULT, + CAST(com.COMMENTS AS VARCHAR(8188)) AS COMMENTS, + CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'Y' ELSE 'N' END AS IS_PK + FROM ALL_TAB_COLUMNS c + LEFT JOIN ( + SELECT acc.OWNER, acc.TABLE_NAME, acc.COLUMN_NAME + FROM ALL_CONS_COLUMNS acc + JOIN ALL_CONSTRAINTS ac + ON acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME + AND acc.OWNER = ac.OWNER + WHERE ac.CONSTRAINT_TYPE = 'P' + ) pk + ON c.OWNER = pk.OWNER + AND c.TABLE_NAME = pk.TABLE_NAME + AND c.COLUMN_NAME = pk.COLUMN_NAME + LEFT JOIN ALL_COL_COMMENTS com + ON c.OWNER = com.OWNER + AND c.TABLE_NAME = com.TABLE_NAME + AND c.COLUMN_NAME = com.COLUMN_NAME + WHERE c.OWNER = ? + ORDER BY c.TABLE_NAME, c.COLUMN_ID + """, + parameters: [.text(owner)] + ) + var columnsByTable: [String: [PluginColumnInfo]] = [:] + for row in result.rows { + guard let table = row[safe: 0]?.asText, + let name = row[safe: 1]?.asText else { + continue + } + columnsByTable[table, default: []].append(PluginColumnInfo( + name: name, + dataType: DamengSchemaValue.fullType( + name: row[safe: 2]?.asText ?? "VARCHAR", + length: row[safe: 3]?.asText, + precision: row[safe: 4]?.asText, + scale: row[safe: 5]?.asText + ), + isNullable: row[safe: 6]?.asText == "Y", + isPrimaryKey: row[safe: 9]?.asText == "Y", + defaultValue: DamengSchemaValue.nonEmpty(row[safe: 7]?.asText), + comment: DamengSchemaValue.nonEmpty(row[safe: 8]?.asText) + )) + } + return columnsByTable + } + + func fetchIndexes(table: String, schema: String?) async throws -> [PluginIndexInfo] { + let result = try await executeParameterized( + query: """ + SELECT i.INDEX_NAME, + i.UNIQUENESS, + ic.COLUMN_NAME, + CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 'Y' ELSE 'N' END AS IS_PK + FROM ALL_INDEXES i + JOIN ALL_IND_COLUMNS ic + ON i.INDEX_NAME = ic.INDEX_NAME + AND i.OWNER = ic.INDEX_OWNER + LEFT JOIN ALL_CONSTRAINTS c + ON i.INDEX_NAME = c.INDEX_NAME + AND i.OWNER = c.OWNER + AND c.CONSTRAINT_TYPE = 'P' + WHERE i.TABLE_NAME = ? + AND i.OWNER = ? + ORDER BY i.INDEX_NAME, ic.COLUMN_POSITION + """, + parameters: [.text(table), .text(effectiveSchema(schema))] + ) + var grouped: [String: (isUnique: Bool, isPrimary: Bool, columns: [String])] = [:] + for row in result.rows { + guard let name = row[safe: 0]?.asText, let column = row[safe: 2]?.asText else { continue } + var item = grouped[name] ?? (false, false, []) + item.isUnique = row[safe: 1]?.asText == "UNIQUE" + item.isPrimary = row[safe: 3]?.asText == "Y" + item.columns.append(column) + grouped[name] = item + } + return grouped.map { name, item in + PluginIndexInfo( + name: name, + columns: item.columns, + isUnique: item.isUnique, + isPrimary: item.isPrimary + ) + }.sorted { $0.name < $1.name } + } + + func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] { + let result = try await executeParameterized( + query: """ + SELECT ac.CONSTRAINT_NAME, + acc.COLUMN_NAME, + rc.TABLE_NAME, + rcc.COLUMN_NAME, + ac.DELETE_RULE, + rc.OWNER + FROM ALL_CONSTRAINTS ac + JOIN ALL_CONS_COLUMNS acc + ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME + AND ac.OWNER = acc.OWNER + JOIN ALL_CONSTRAINTS rc + ON ac.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME + AND ac.R_OWNER = rc.OWNER + JOIN ALL_CONS_COLUMNS rcc + ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME + AND rc.OWNER = rcc.OWNER + AND acc.POSITION = rcc.POSITION + WHERE ac.CONSTRAINT_TYPE = 'R' + AND ac.TABLE_NAME = ? + AND ac.OWNER = ? + ORDER BY ac.CONSTRAINT_NAME, acc.POSITION + """, + parameters: [.text(table), .text(effectiveSchema(schema))] + ) + return result.rows.compactMap { row in + guard let name = row[safe: 0]?.asText, + let column = row[safe: 1]?.asText, + let referencedTable = row[safe: 2]?.asText, + let referencedColumn = row[safe: 3]?.asText else { + return nil + } + return PluginForeignKeyInfo( + name: name, + column: column, + referencedTable: referencedTable, + referencedColumn: referencedColumn, + referencedSchema: row[safe: 5]?.asText, + onDelete: row[safe: 4]?.asText ?? "NO ACTION" + ) + } + } + + func fetchSchemas() async throws -> [String] { + let result = try await execute(query: """ + SELECT OBJECT_NAME + FROM ALL_OBJECTS + WHERE OBJECT_TYPE = 'SCH' + ORDER BY OBJECT_NAME + """) + return result.rows.compactMap { $0.first?.asText } + } + + func fetchDatabases() async throws -> [String] { + try await fetchSchemas() + } + + func fetchDatabaseMetadata(_ database: String) async throws -> PluginDatabaseMetadata { + let result = try await executeParameterized( + query: "SELECT COUNT(*) FROM ALL_TABLES WHERE OWNER = ?", + parameters: [.text(database)] + ) + return PluginDatabaseMetadata( + name: database, + tableCount: result.rows.first?.first?.asText.flatMap(Int.init), + isSystemDatabase: DamengPlugin.systemSchemaNames.contains(database.uppercased()) + ) + } + + func fetchTableMetadata(table: String, schema: String?) async throws -> PluginTableMetadata { + let result = try await executeParameterized( + query: """ + SELECT t.NUM_ROWS, t.AVG_ROW_LEN, CAST(c.COMMENTS AS VARCHAR(8188)) AS COMMENTS + FROM ALL_TABLES t + LEFT JOIN ALL_TAB_COMMENTS c + ON t.OWNER = c.OWNER + AND t.TABLE_NAME = c.TABLE_NAME + WHERE t.OWNER = ? + AND t.TABLE_NAME = ? + """, + parameters: [.text(effectiveSchema(schema)), .text(table)] + ) + guard let row = result.rows.first else { + return PluginTableMetadata(tableName: table) + } + return PluginTableMetadata( + tableName: table, + avgRowLength: row[safe: 1]?.asText.flatMap(Int64.init), + rowCount: row[safe: 0]?.asText.flatMap(Int64.init), + comment: DamengSchemaValue.nonEmpty(row[safe: 2]?.asText), + engine: "DM8" + ) + } + + func fetchViewDefinition(view: String, schema: String?) async throws -> String { + let result = try await executeParameterized( + query: "SELECT CAST(TEXT AS VARCHAR(8188)) FROM ALL_VIEWS WHERE OWNER = ? AND VIEW_NAME = ?", + parameters: [.text(effectiveSchema(schema)), .text(view)] + ) + return result.rows.first?.first?.asText ?? "" + } + + func fetchTableDDL(table: String, schema: String?) async throws -> String { + let owner = effectiveSchema(schema) + let columns = try await fetchColumns(table: table, schema: owner) + guard !columns.isEmpty else { + throw DamengError(message: String(localized: "Dameng did not return the table definition.")) + } + var definitions = columns.map { column in + var definition = " \(quoteIdentifier(column.name)) \(column.dataType)" + if let defaultValue = column.defaultValue { + definition += " DEFAULT \(defaultValue)" + } + if !column.isNullable { + definition += " NOT NULL" + } + return definition + } + let primaryKeys = columns.filter(\.isPrimaryKey).map { quoteIdentifier($0.name) } + if !primaryKeys.isEmpty { + definitions.append(" PRIMARY KEY (\(primaryKeys.joined(separator: ", ")))") + } + return "CREATE TABLE \(quoteIdentifier(owner)).\(quoteIdentifier(table)) (\n" + + definitions.joined(separator: ",\n") + "\n);" + } + + func effectiveSchema(_ schema: String?) -> String { + if let schema, !schema.isEmpty { + return schema + } + if let currentSchema, !currentSchema.isEmpty { + return currentSchema + } + if !config.database.isEmpty { + return config.database + } + return config.username.uppercased() + } +} + +enum DamengSchemaValue { + static func nonEmpty(_ value: String?) -> String? { + guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { + return nil + } + return value + } + + static func fullType(name: String, length: String?, precision: String?, scale: String?) -> String { + let type = name.uppercased() + if ["CHAR", "CHARACTER", "VARCHAR", "VARCHAR2", "BINARY", "VARBINARY"].contains(type), + let length, let lengthValue = Int(length), lengthValue > 0 { + return "\(type)(\(lengthValue))" + } + if ["DEC", "DECIMAL", "NUMERIC", "NUMBER"].contains(type), + let precision, let precisionValue = Int(precision), precisionValue > 0 { + if let scale, let scaleValue = Int(scale), scaleValue > 0 { + return "\(type)(\(precisionValue),\(scaleValue))" + } + return "\(type)(\(precisionValue))" + } + if type == "TIMESTAMP", let scale, let scaleValue = Int(scale), scaleValue > 0 { + return "TIMESTAMP(\(scaleValue))" + } + return type + } +} diff --git a/Plugins/DamengDriverPlugin/DamengStatementClassifier.swift b/Plugins/DamengDriverPlugin/DamengStatementClassifier.swift new file mode 100644 index 000000000..4a0a7208d --- /dev/null +++ b/Plugins/DamengDriverPlugin/DamengStatementClassifier.swift @@ -0,0 +1,169 @@ +import Foundation + +enum DamengStatementClassifier { + private static let resultKeywords: Set = [ + "DESC", "DESCRIBE", "EXPLAIN", "SELECT", "SHOW", "VALUES", "WITH" + ] + + static func expectsRows(_ query: String) -> Bool { + guard let keyword = firstKeyword(query) else { return false } + return resultKeywords.contains(keyword) + } + + static func applyingRowCap(_ rowCap: Int?, to query: String) -> String { + guard let rowCap, rowCap > 0, + let keyword = firstKeyword(query), + keyword == "SELECT" || keyword == "WITH", + let statement = singleStatement(query) else { + return query + } + let serverCap = rowCap == Int.max ? rowCap : rowCap + 1 + return "SELECT * FROM (\n\(statement)\n) TABLEPRO_ROW_CAP WHERE ROWNUM <= \(serverCap)" + } + + private static func firstKeyword(_ query: String) -> String? { + let bytes = Array(query.utf8) + var index = 0 + while index < bytes.count { + if isWhitespace(bytes[index]) || bytes[index] == 0x28 || bytes[index] == 0x3B { + index += 1 + continue + } + if index + 1 < bytes.count, bytes[index] == 0x2D, bytes[index + 1] == 0x2D { + index = skipLineComment(bytes, from: index + 2) + continue + } + if index + 1 < bytes.count, bytes[index] == 0x2F, bytes[index + 1] == 0x2A { + index = skipBlockComment(bytes, from: index + 2) + continue + } + let start = index + while index < bytes.count, isKeywordByte(bytes[index]) { + index += 1 + } + guard index > start else { return nil } + return String(decoding: bytes[start.. String? { + let bytes = Array(query.utf8) + var state: UInt8 = 0 + var alternativeCloser: UInt8 = 0 + var blockDepth = 0 + var semicolonIndex: Int? + var index = 0 + while index < bytes.count { + let byte = bytes[index] + let next = index + 1 < bytes.count ? bytes[index + 1] : nil + if state == 0 { + if byte == 0x27 { + state = 1 + } else if byte == 0x22 { + state = 2 + } else if byte == 0x2D, next == 0x2D { + state = 3 + index += 1 + } else if byte == 0x2F, next == 0x2A { + state = 4 + blockDepth = 1 + index += 1 + } else if isAlternativeQuoteStart(bytes, at: index) { + alternativeCloser = alternativeQuoteCloser(for: bytes[index + 2]) + state = 5 + index += 2 + } else if byte == 0x3B { + guard semicolonIndex == nil else { return nil } + semicolonIndex = index + } + } else if state == 1, byte == 0x27 { + if next == 0x27 { + index += 1 + } else { + state = 0 + } + } else if state == 2, byte == 0x22 { + if next == 0x22 { + index += 1 + } else { + state = 0 + } + } else if state == 3, (byte == 0x0A || byte == 0x0D) { + state = 0 + } else if state == 4 { + if byte == 0x2F, next == 0x2A { + blockDepth += 1 + index += 1 + } else if byte == 0x2A, next == 0x2F { + blockDepth -= 1 + index += 1 + if blockDepth == 0 { state = 0 } + } + } else if state == 5, byte == alternativeCloser, next == 0x27 { + state = 0 + index += 1 + } + index += 1 + } + + let end: Int + if let semicolonIndex { + guard bytes.suffix(from: semicolonIndex + 1).allSatisfy(isWhitespace) else { return nil } + end = semicolonIndex + } else { + end = bytes.count + } + return String(decoding: bytes[.. Int { + var index = start + while index < bytes.count, bytes[index] != 0x0A, bytes[index] != 0x0D { + index += 1 + } + return index + } + + private static func skipBlockComment(_ bytes: [UInt8], from start: Int) -> Int { + var index = start + var depth = 1 + while index + 1 < bytes.count { + if bytes[index] == 0x2F, bytes[index + 1] == 0x2A { + depth += 1 + index += 2 + } else if bytes[index] == 0x2A, bytes[index + 1] == 0x2F { + depth -= 1 + index += 2 + if depth == 0 { return index } + } else { + index += 1 + } + } + return bytes.count + } + + private static func isWhitespace(_ byte: UInt8) -> Bool { + byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D + } + + private static func isKeywordByte(_ byte: UInt8) -> Bool { + (0x41...0x5A).contains(byte) || (0x61...0x7A).contains(byte) || byte == 0x5F + } + + private static func isAlternativeQuoteStart(_ bytes: [UInt8], at index: Int) -> Bool { + guard index + 2 < bytes.count else { return false } + return (bytes[index] == 0x51 || bytes[index] == 0x71) && bytes[index + 1] == 0x27 + } + + private static func alternativeQuoteCloser(for opener: UInt8) -> UInt8 { + switch opener { + case 0x5B: return 0x5D + case 0x28: return 0x29 + case 0x7B: return 0x7D + case 0x3C: return 0x3E + default: return opener + } + } +} diff --git a/Plugins/DamengDriverPlugin/Info.plist b/Plugins/DamengDriverPlugin/Info.plist new file mode 100644 index 000000000..c3f02778f --- /dev/null +++ b/Plugins/DamengDriverPlugin/Info.plist @@ -0,0 +1,12 @@ + + + + + TableProPluginKitVersion + 19 + TableProProvidesDatabaseTypeIds + + Dameng + + + diff --git a/Plugins/DamengDriverPlugin/README.md b/Plugins/DamengDriverPlugin/README.md new file mode 100644 index 000000000..6477f1ad0 --- /dev/null +++ b/Plugins/DamengDriverPlugin/README.md @@ -0,0 +1,43 @@ +# Dameng DM8 Driver Plugin + +This plugin adds native Dameng DM8 connectivity to TablePro without requiring a local DM client installation. It supports schema browsing and switching, SQL execution, row editing, transactions, DDL helpers, EXPLAIN plans, and DM8-aware completions while typing. + +## Architecture + +The driver has three layers: + +- **Swift** (`Plugins/DamengDriverPlugin/`) implements `PluginDatabaseDriver`, schema and editing APIs, SQL completion metadata, safe parameter substitution, and result conversion. +- **C ABI** (`CDameng/CDameng.h`) exposes opaque connection and result handles. Swift copies returned values before releasing their Rust-owned storage. +- **Rust** (`Native/DamengBridge/`) owns the DM8 wire connection, transaction state, encoding detection, row limits, and panic boundary. It builds as a static library for both Apple Silicon and Intel Macs. + +The bridge vendors a reviewed `rust-dameng` snapshot under `Native/DamengBridge/Vendor/`. TablePro's compatibility patches add multi-column results, binary DECIMAL decoding, bounded response parsing, and DM8's text EXPLAIN response. Keep `Vendor/UPSTREAM.md`, the vendored crates, and their MIT license notices in sync when updating the snapshot. + +## Build + +The build script installs the pinned Rust toolchain and creates `Libs/libdameng_bridge.a` as a universal archive: + +```bash +scripts/build-dameng.sh +scripts/generate-project.sh +xcodebuild -project TablePro.xcodeproj -scheme DamengDriver \ + -configuration Debug build CODE_SIGNING_ALLOWED=NO +``` + +Use `scripts/build-dameng.sh arm64` or `x86_64` only for architecture-specific diagnostics. Do not commit generated files under `Libs/` or `target/`. + +## Tests + +Run protocol, bridge, and Swift tests before submitting a change: + +```bash +cargo test --manifest-path Native/DamengBridge/Vendor/dameng-protocol/Cargo.toml +cargo test --manifest-path Native/DamengBridge/Cargo.toml +xcodebuild -project TablePro.xcodeproj -scheme DamengDriverTests \ + -configuration Debug build-for-testing CODE_SIGNING_ALLOWED=NO +``` + +Integration tests require an isolated DM8 server, such as a container running in OrbStack. Set `TABLEPRO_DM8_INTEGRATION=1`, `DM_HOST`, `DM_PORT`, `DM_USER`, and `DM_PASSWORD`, then run the built `DamengDriverTests.xctest` bundle with `xcrun xctest`. Tests create a unique temporary schema and remove it afterward. + +## Security and Limitations + +The parameter binder recognizes placeholders only in SQL code, escapes text literals, and encodes bytes with `HEXTORAW`. Response bodies and LOB content are capped at 64 MiB. Native TLS is not available; use SSH, SOCKS, or Cloudflare tunneling for untrusted networks. Native binary and off-row LOB reads remain unsupported by the transport; use `RAWTOHEX` for binary values and cast CLOB values to `VARCHAR`. diff --git a/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift b/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift new file mode 100644 index 000000000..3969f2951 --- /dev/null +++ b/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift @@ -0,0 +1,385 @@ +import Foundation +import TableProPluginKit +import XCTest + +final class DamengPluginDriverTests: XCTestCase { + func testTypingSuggestionsIncludeDM8StatementsAndDialectSymbols() { + let completions = Set(DamengPlugin.statementCompletions.map(\.label)) + + XCTAssertTrue(completions.isSuperset(of: ["SET SCHEMA", "CREATE SCHEMA", "EXPLAIN", "CONNECT BY"])) + XCTAssertTrue(DamengPlugin.sqlDialect?.keywords.contains("ROWNUM") == true) + XCTAssertTrue(DamengPlugin.sqlDialect?.functions.contains("NVL") == true) + XCTAssertTrue(DamengPlugin.sqlDialect?.dataTypes.contains("VARCHAR2") == true) + + let driver = DamengPluginDriver(config: testConfig(database: "APP")) + XCTAssertEqual(driver.buildExplainQuery("SELECT 1"), "EXPLAIN SELECT 1") + } + + func testDDLGenerationQuotesNamesAndPreservesConstraints() throws { + let driver = DamengPluginDriver(config: testConfig(database: "APP")) + let definition = PluginCreateTableDefinition( + tableName: "order", + columns: [ + PluginColumnDefinition( + name: "id", + dataType: "int", + isNullable: false, + isPrimaryKey: true, + autoIncrement: true + ), + PluginColumnDefinition( + name: "display name", + dataType: "varchar(100)", + defaultValue: "guest's record", + comment: "customer's label" + ) + ], + indexes: [PluginIndexDefinition(name: "idx display", columns: ["display name"])], + foreignKeys: [] + ) + + let sql = try XCTUnwrap(driver.generateCreateTableSQL(definition: definition)) + + XCTAssertTrue(sql.hasPrefix("BEGIN\n")) + XCTAssertTrue(sql.contains("CREATE TABLE \"APP\".\"order\"")) + XCTAssertTrue(sql.contains("\"id\" INT IDENTITY(1,1) NOT NULL PRIMARY KEY")) + XCTAssertTrue(sql.contains("DEFAULT ''guest''''s record''")) + XCTAssertTrue(sql.contains("CREATE INDEX \"idx display\" ON \"APP\".\"order\" (\"display name\")")) + XCTAssertTrue(sql.contains("IS ''customer''''s label''")) + } + + func testRowChangesUsePrimaryKeyAndBoundValues() throws { + let driver = DamengPluginDriver(config: testConfig(database: "APP")) + let change = PluginRowChange( + rowIndex: 2, + type: .update, + cellChanges: [(1, "name", .text("old"), .text("new"))], + originalRow: [.text("42"), .text("old")] + ) + + let statements = try XCTUnwrap(driver.generateStatements( + table: "users", + schema: "APP", + columns: ["id", "name"], + primaryKeyColumns: ["id"], + changes: [change], + insertedRowData: [:], + deletedRowIndices: [], + insertedRowIndices: [] + )) + + XCTAssertEqual(statements.count, 1) + XCTAssertEqual( + statements[0].statement, + "UPDATE \"APP\".\"users\" SET \"name\" = ? WHERE \"id\" = ? AND ROWNUM = 1" + ) + XCTAssertEqual(statements[0].parameters, [.text("new"), .text("42")]) + } + + func testEffectiveSchemaPreservesQuotedIdentifierCase() { + let configured = DamengPluginDriver(config: testConfig(database: "CamelCaseSchema")) + XCTAssertEqual(configured.effectiveSchema(nil), "CamelCaseSchema") + XCTAssertEqual(configured.effectiveSchema("lowercase_schema"), "lowercase_schema") + + let fallback = DamengPluginDriver(config: DriverConnectionConfig( + host: "127.0.0.1", + port: 5_236, + username: "sysdba", + password: "test-only", + database: "" + )) + XCTAssertEqual(fallback.effectiveSchema(nil), "SYSDBA") + } + + func testLiveDM8Workflow() async throws { + let environment = try liveEnvironment() + let driver = DamengPluginDriver(config: environment.config(database: "SYSDBA")) + let schema = "TP_\(UUID().uuidString.prefix(12).replacingOccurrences(of: "-", with: ""))".uppercased() + try await checked("connect") { + try await driver.connect() + } + defer { driver.disconnect() } + + _ = try await checked("create schema") { + try await driver.execute(query: "CREATE SCHEMA \(driver.quoteIdentifier(schema)) AUTHORIZATION SYSDBA") + } + do { + try await runLiveWorkflow(driver: driver, schema: schema) + try await driver.switchSchema(to: "SYSDBA") + try await driver.dropSchema(name: schema) + } catch { + try? await driver.switchSchema(to: "SYSDBA") + try? await driver.execute(query: "DROP SCHEMA \(driver.quoteIdentifier(schema)) CASCADE") + throw error + } + } + + func testLiveDM8RejectsBadCredentialsAndInvalidPort() async throws { + let environment = try liveEnvironment() + let invalidPortDriver = DamengPluginDriver(config: DriverConnectionConfig( + host: environment.host, + port: 70_000, + username: environment.username, + password: environment.password, + database: "" + )) + do { + try await invalidPortDriver.connect() + XCTFail("Expected an invalid port to fail") + } catch { + XCTAssertTrue(String(describing: error).contains("65535")) + } + + let badPasswordDriver = DamengPluginDriver(config: DriverConnectionConfig( + host: environment.host, + port: environment.port, + username: environment.username, + password: "not-the-password", + database: "" + )) + do { + try await badPasswordDriver.connect() + XCTFail("Expected invalid credentials to fail") + } catch { + XCTAssertFalse(String(describing: error).isEmpty) + } + } + + private func runLiveWorkflow(driver: DamengPluginDriver, schema: String) async throws { + try await checked("switch schema") { + try await driver.switchSchema(to: schema) + } + XCTAssertEqual(driver.currentSchema, schema) + XCTAssertTrue(driver.serverVersion?.contains("DM Database Server") == true) + try await checked("ping") { + try await driver.ping() + } + + let parentDefinition = PluginCreateTableDefinition( + tableName: "PARENT", + columns: [ + PluginColumnDefinition( + name: "ID", + dataType: "INT", + isNullable: false, + isPrimaryKey: true, + autoIncrement: true + ), + PluginColumnDefinition(name: "NAME", dataType: "VARCHAR(100)", isNullable: false), + PluginColumnDefinition(name: "PAYLOAD", dataType: "VARBINARY(8188)") + ], + indexes: [PluginIndexDefinition(name: "IDX_PARENT_NAME", columns: ["NAME"])], + foreignKeys: [] + ) + _ = try await checked("create parent") { + try await driver.execute(query: try XCTUnwrap(driver.generateCreateTableSQL(definition: parentDefinition))) + } + _ = try await checked("comment parent") { + try await driver.execute(query: "COMMENT ON TABLE \"PARENT\" IS 'TablePro DM8 integration fixture'") + } + _ = try await checked("create child") { + try await driver.execute(query: """ + CREATE TABLE "CHILD" ( + "ID" INT PRIMARY KEY, + "PARENT_ID" INT, + CONSTRAINT "FK_CHILD_PARENT" FOREIGN KEY ("PARENT_ID") REFERENCES "PARENT" ("ID") ON DELETE CASCADE + ) + """) + } + _ = try await checked("create view") { + try await driver.execute( + query: "CREATE OR REPLACE VIEW \"PARENT_VIEW\" AS SELECT \"ID\", \"NAME\" FROM \"PARENT\"" + ) + } + + let hostileText = "Robert'); DROP TABLE \"PARENT\"; -- 达梦" + let payload = Data([0x00, 0x01, 0x7F, 0xFF]) + _ = try await checked("insert unicode and binary") { + try await driver.executeParameterized( + query: "INSERT INTO \"PARENT\" (\"NAME\", \"PAYLOAD\") VALUES (?, ?)", + parameters: [.text(hostileText), .bytes(payload)] + ) + } + for index in 1...4 { + _ = try await checked("insert row \(index)") { + try await driver.executeParameterized( + query: "INSERT INTO \"PARENT\" (\"NAME\") VALUES (?)", + parameters: [.text("row-\(index)")] + ) + } + } + + let valueResult = try await checked("read unicode and binary metadata") { + try await driver.execute( + query: "SELECT \"NAME\", RAWTOHEX(\"PAYLOAD\") FROM \"PARENT\" ORDER BY \"ID\"" + ) + } + XCTAssertEqual(valueResult.rows.first?[0], .text(hostileText)) + XCTAssertEqual(valueResult.rows.first?[1], .text("00017FFF")) + + let capped = try await driver.executeUserQuery( + query: "SELECT \"ID\" FROM \"PARENT\" ORDER BY \"ID\"", + rowCap: 2, + parameters: nil + ) + XCTAssertEqual(capped.rows.count, 2) + XCTAssertTrue(capped.isTruncated) + + let tables = try await driver.fetchTables(schema: schema) + XCTAssertTrue(tables.contains { $0.name == "PARENT" && $0.type == "TABLE" }) + XCTAssertTrue(tables.contains { $0.name == "PARENT_VIEW" && $0.type == "VIEW" }) + let columns = try await checked("fetch columns") { + try await driver.fetchColumns(table: "PARENT", schema: schema) + } + XCTAssertTrue(columns.contains { $0.name == "ID" && $0.isPrimaryKey }) + XCTAssertTrue(columns.contains { $0.name == "NAME" && $0.dataType == "VARCHAR(100)" }) + let allColumns = try await checked("fetch completion columns") { + try await driver.fetchAllColumns(schema: schema) + } + XCTAssertEqual(allColumns["PARENT"]?.map(\.name), ["ID", "NAME", "PAYLOAD"]) + XCTAssertEqual(allColumns["PARENT_VIEW"]?.map(\.name), ["ID", "NAME"]) + let indexes = try await checked("fetch indexes") { + try await driver.fetchIndexes(table: "PARENT", schema: schema) + } + XCTAssertTrue(indexes.contains { $0.name == "IDX_PARENT_NAME" && $0.columns == ["NAME"] }) + let foreignKeys = try await checked("fetch foreign keys") { + try await driver.fetchForeignKeys(table: "CHILD", schema: schema) + } + XCTAssertTrue(foreignKeys.contains { + $0.name == "FK_CHILD_PARENT" && $0.referencedTable == "PARENT" && $0.onDelete == "CASCADE" + }) + let emptyForeignKeys = try await checked("fetch empty foreign keys") { + try await driver.fetchForeignKeys(table: "PARENT", schema: schema) + } + XCTAssertTrue(emptyForeignKeys.isEmpty) + try await checked("ping after empty metadata") { + try await driver.ping() + } + let viewDefinition = try await checked("fetch view definition") { + try await driver.fetchViewDefinition(view: "PARENT_VIEW", schema: schema) + } + XCTAssertTrue(viewDefinition.contains("PARENT")) + let tableDDL = try await checked("fetch table DDL") { + try await driver.fetchTableDDL(table: "PARENT", schema: schema) + } + XCTAssertTrue(tableDDL.contains("PRIMARY KEY")) + let metadata = try await checked("fetch table metadata") { + try await driver.fetchTableMetadata(table: "PARENT", schema: schema) + } + XCTAssertEqual(metadata.engine, "DM8") + XCTAssertEqual(metadata.comment, "TablePro DM8 integration fixture") + + let explain = try await checked("explain") { + try await driver.execute(query: try XCTUnwrap(driver.buildExplainQuery("SELECT * FROM \"PARENT\""))) + } + XCTAssertEqual(explain.columns, ["PLAN"]) + XCTAssertTrue(explain.rows.first?.first?.asText?.contains("#") == true) + try await checked("ping after explain") { + try await driver.ping() + } + + try await checked("begin transaction") { + try await driver.beginTransaction() + } + _ = try await checked("insert rollback row") { + try await driver.execute(query: "INSERT INTO \"PARENT\" (\"NAME\") VALUES ('rollback-row')") + } + try await checked("rollback transaction") { + try await driver.rollbackTransaction() + } + let rollbackCount = try await checked("verify rollback") { + try await driver.execute(query: "SELECT COUNT(*) FROM \"PARENT\" WHERE \"NAME\" = 'rollback-row'") + } + XCTAssertEqual(rollbackCount.rows.first?.first, .text("0")) + + do { + _ = try await driver.execute(query: "SELECT * FROM \"MISSING_TABLE\"") + XCTFail("Expected an invalid query to fail") + } catch { + try await driver.ping() + } + + do { + try await driver.switchSchema(to: "\"; DROP SCHEMA \(schema) CASCADE; --") + XCTFail("Expected an invalid schema to fail") + } catch { + let schemas = try await driver.fetchSchemas() + XCTAssertTrue(schemas.contains(schema)) + } + + do { + try await driver.dropSchema(name: "SYSDBA") + XCTFail("Expected a system schema drop to be rejected") + } catch { + XCTAssertTrue(String(describing: error).contains("system")) + } + + try await withThrowingTaskGroup(of: PluginCellValue?.self) { group in + for _ in 0..<8 { + group.addTask { + let result = try await driver.execute(query: "SELECT USER FROM DUAL") + return result.rows.first?.first + } + } + for try await value in group { + XCTAssertEqual(value, .text("SYSDBA")) + } + } + } + + private func liveEnvironment() throws -> LiveEnvironment { + let environment = ProcessInfo.processInfo.environment + guard environment["TABLEPRO_DM8_INTEGRATION"] == "1" else { + throw XCTSkip("Set TABLEPRO_DM8_INTEGRATION=1 to run against DM8 in OrbStack") + } + guard let host = environment["DM_HOST"], + let portText = environment["DM_PORT"], + let port = Int(portText), + let username = environment["DM_USER"], + let password = environment["DM_PASSWORD"] else { + XCTFail("DM_HOST, DM_PORT, DM_USER, and DM_PASSWORD are required") + throw XCTSkip("DM8 integration environment is incomplete") + } + return LiveEnvironment(host: host, port: port, username: username, password: password) + } + + @discardableResult + private func checked( + _ step: String, + operation: () async throws -> T + ) async throws -> T { + do { + return try await operation() + } catch { + XCTFail("\(step): \(error)") + throw error + } + } + + private func testConfig(database: String) -> DriverConnectionConfig { + DriverConnectionConfig( + host: "127.0.0.1", + port: 5_236, + username: "SYSDBA", + password: "test-only", + database: database + ) + } +} + +private struct LiveEnvironment { + let host: String + let port: Int + let username: String + let password: String + + func config(database: String) -> DriverConnectionConfig { + DriverConnectionConfig( + host: host, + port: port, + username: username, + password: password, + database: database + ) + } +} diff --git a/README.md b/README.md index 0748e14a2..a3c4970d5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ TablePro is the missing fourth: native, multi-database, and open source. | Microsoft SQL Server | Plugin | | MongoDB | Plugin | | Oracle Database | Plugin | +| Dameng DM8 | Plugin | | DuckDB | Plugin | | Beancount | Plugin | | Cassandra / ScyllaDB | Plugin | diff --git a/README.vi.md b/README.vi.md index 6ceb4c254..d14ffaa8f 100644 --- a/README.vi.md +++ b/README.vi.md @@ -81,6 +81,7 @@ TablePro là mảnh thứ tư còn thiếu: native, đa database, và mã nguồ | Microsoft SQL Server | Plugin | | MongoDB | Plugin | | Oracle Database | Plugin | +| Dameng DM8 | Plugin | | DuckDB | Plugin | | Beancount | Plugin | | Cassandra / ScyllaDB | Plugin | diff --git a/README.zh.md b/README.zh.md index 8a2b5bac0..0ef8f78ef 100644 --- a/README.zh.md +++ b/README.zh.md @@ -81,6 +81,7 @@ TablePro 补上缺失的第四类:原生、多数据库、开源。 | Microsoft SQL Server | 插件 | | MongoDB | 插件 | | Oracle Database | 插件 | +| 达梦 DM8 | 插件 | | DuckDB | 插件 | | Cassandra / ScyllaDB | 插件 | | Etcd | 插件 | diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift index ea2541e55..d08f5642b 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swift @@ -11,7 +11,8 @@ extension PluginMetadataRegistry { func registryPluginDefaults() -> [(typeId: String, snapshot: PluginMetadataSnapshot)] { let ( clickhouseDialect, clickhouseColumnTypes, mssqlDialect, mssqlColumnTypes, - oracleDialect, oracleColumnTypes, duckdbDialect, duckdbColumnTypes, + oracleDialect, oracleColumnTypes, damengDialect, damengCompletions, damengColumnTypes, + duckdbDialect, duckdbColumnTypes, cassandraDialect, cassandraColumnTypes, mongoCompletions, mongoColumnTypes, etcdCompletions, redisCompletions, redisColumnTypes, d1Dialect, d1ColumnTypes ) = registryDefaultIngredients() @@ -513,6 +514,60 @@ extension PluginMetadataRegistry { tagline: String(localized: "Enterprise SQL with PL/SQL") ) )), + ("Dameng", PluginMetadataSnapshot( + displayName: "Dameng DM8", iconName: "cylinder", defaultPort: 5_236, + requiresAuthentication: true, supportsForeignKeys: true, supportsSchemaEditing: true, + isDownloadable: true, primaryUrlScheme: "dm", parameterStyle: .questionMark, + navigationModel: .standard, explainVariants: [ + ExplainVariant(id: "plan", label: "Plan", sqlPrefix: "EXPLAIN") + ], + pathFieldRole: .database, + supportsHealthMonitor: true, urlSchemes: ["dm"], + postConnectActions: [.selectSchemaFromLastSession], + brandColorHex: "#C60018", + queryLanguageName: "SQL", editorLanguage: .sql, + connectionMode: .network, supportsDatabaseSwitching: false, + supportsColumnReorder: false, + capabilities: PluginMetadataSnapshot.CapabilityFlags( + supportsSchemaSwitching: true, + supportsImport: true, + supportsExport: true, + supportsSSH: true, + supportsSSL: false, + supportsCascadeDrop: true, + supportsForeignKeyDisable: false, + supportsReadOnlyMode: true, + supportsQueryProgress: false, + requiresReconnectForDatabaseSwitch: false, + supportsDropDatabase: false, + supportsDropSchema: true, + supportsRenameColumn: true, + supportsOpportunisticTLS: false + ), + schema: PluginMetadataSnapshot.SchemaInfo( + defaultSchemaName: "", + defaultGroupName: "main", + tableEntityName: "Tables", + containerEntityName: "Schema", + defaultPrimaryKeyColumn: nil, + immutableColumns: [], + systemDatabaseNames: [], + systemSchemaNames: ["SYS", "SYSDBA", "SYSAUDITOR", "SYSSSO", "CTISYS"], + fileExtensions: [], + databaseGroupingStrategy: .hierarchicalSchema, + structureColumnFields: [.name, .type, .nullable, .defaultValue, .autoIncrement, .comment] + ), + editor: PluginMetadataSnapshot.EditorConfig( + sqlDialect: damengDialect, + statementCompletions: damengCompletions, + columnTypesByCategory: damengColumnTypes + ), + connection: PluginMetadataSnapshot.ConnectionConfig( + additionalConnectionFields: [], + category: .relational, + tagline: String(localized: "Enterprise relational database for DM8 deployments") + ) + )), ("ClickHouse", PluginMetadataSnapshot( displayName: "ClickHouse", iconName: "clickhouse-icon", defaultPort: 8_123, requiresAuthentication: true, supportsForeignKeys: false, supportsSchemaEditing: true, diff --git a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryIngredients.swift b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryIngredients.swift index 0fceea3eb..d9f38954c 100644 --- a/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryIngredients.swift +++ b/TablePro/Core/Plugins/PluginMetadataRegistry+RegistryIngredients.swift @@ -15,6 +15,9 @@ extension PluginMetadataRegistry { mssqlColumnTypes: [String: [String]], oracleDialect: SQLDialectDescriptor, oracleColumnTypes: [String: [String]], + damengDialect: SQLDialectDescriptor, + damengCompletions: [CompletionEntry], + damengColumnTypes: [String: [String]], duckdbDialect: SQLDialectDescriptor, duckdbColumnTypes: [String: [String]], cassandraDialect: SQLDialectDescriptor, @@ -235,6 +238,73 @@ extension PluginMetadataRegistry { "Other": ["ROWID", "UROWID"] ] + let damengColumnTypes: [String: [String]] = [ + "Integer": ["TINYINT", "SMALLINT", "INT", "INTEGER", "BIGINT"], + "Float": ["REAL", "FLOAT", "DOUBLE", "DEC", "DECIMAL", "NUMERIC", "NUMBER"], + "String": ["CHAR", "CHARACTER", "VARCHAR", "VARCHAR2", "TEXT", "CLOB"], + "Date": ["DATE", "TIME", "DATETIME", "TIMESTAMP"], + "Binary": ["BINARY", "VARBINARY", "BLOB", "IMAGE"], + "Boolean": ["BIT", "BOOLEAN"], + "Other": ["ROWID", "INTERVAL"] + ] + let damengDialect = SQLDialectDescriptor( + identifierQuote: "\"", + keywords: [ + "SELECT", "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "FULL", + "ON", "USING", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "AS", "ORDER", "BY", "GROUP", + "HAVING", "LIMIT", "OFFSET", "FETCH", "FIRST", "ROWS", "ONLY", "INSERT", "INTO", "VALUES", + "UPDATE", "SET", "DELETE", "MERGE", "CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", + "SCHEMA", "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "CONSTRAINT", "ADD", "MODIFY", + "COLUMN", "RENAME", "NULL", "IS", "ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME", + "IDENTITY", "SEQUENCE", "SYNONYM", "GRANT", "REVOKE", "TRIGGER", "PROCEDURE", "CASE", "WHEN", + "THEN", "ELSE", "END", "UNION", "INTERSECT", "MINUS", "DECLARE", "BEGIN", "COMMIT", "ROLLBACK", + "SAVEPOINT", "EXECUTE", "IMMEDIATE", "OVER", "PARTITION", "ROW_NUMBER", "RANK", "DENSE_RANK", + "CONNECT", "LEVEL", "START", "WITH", "PRIOR", "ROWNUM", "ROWID", "DUAL" + ], + functions: [ + "COUNT", "SUM", "AVG", "MAX", "MIN", "LISTAGG", "CONCAT", "SUBSTR", "INSTR", "LENGTH", "LOWER", + "UPPER", "TRIM", "LTRIM", "RTRIM", "REPLACE", "LPAD", "RPAD", "SYSDATE", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "ADD_MONTHS", "MONTHS_BETWEEN", "LAST_DAY", "EXTRACT", "TO_DATE", "TO_CHAR", + "TO_NUMBER", "TO_TIMESTAMP", "TRUNC", "ROUND", "CEIL", "FLOOR", "ABS", "POWER", "SQRT", "MOD", + "NVL", "NVL2", "COALESCE", "NULLIF", "GREATEST", "LEAST", "CAST", "USER" + ], + dataTypes: Set(damengColumnTypes.values.flatMap { $0 }), + tableOptions: ["TABLESPACE", "STORAGE", "PCTFREE", "INITRANS"], + regexSyntax: .regexpLike, + booleanLiteralStyle: .numeric, + likeEscapeStyle: .explicit, + paginationStyle: .offsetFetch, + offsetFetchOrderBy: "ORDER BY 1", + autoLimitStyle: .fetchFirst, + caseSensitivityStyle: .caseFoldFunction + ) + let damengCompletions = [ + CompletionEntry(label: "SELECT", insertText: "SELECT"), + CompletionEntry(label: "SELECT DISTINCT", insertText: "SELECT DISTINCT"), + CompletionEntry(label: "INSERT INTO", insertText: "INSERT INTO"), + CompletionEntry(label: "UPDATE", insertText: "UPDATE"), + CompletionEntry(label: "DELETE FROM", insertText: "DELETE FROM"), + CompletionEntry(label: "MERGE INTO", insertText: "MERGE INTO"), + CompletionEntry(label: "CREATE TABLE", insertText: "CREATE TABLE"), + CompletionEntry(label: "CREATE OR REPLACE VIEW", insertText: "CREATE OR REPLACE VIEW"), + CompletionEntry(label: "CREATE SCHEMA", insertText: "CREATE SCHEMA"), + CompletionEntry(label: "ALTER TABLE", insertText: "ALTER TABLE"), + CompletionEntry(label: "DROP TABLE", insertText: "DROP TABLE"), + CompletionEntry(label: "DROP SCHEMA", insertText: "DROP SCHEMA"), + CompletionEntry(label: "SET SCHEMA", insertText: "SET SCHEMA"), + CompletionEntry(label: "EXPLAIN", insertText: "EXPLAIN"), + CompletionEntry(label: "WHERE", insertText: "WHERE"), + CompletionEntry(label: "GROUP BY", insertText: "GROUP BY"), + CompletionEntry(label: "ORDER BY", insertText: "ORDER BY"), + CompletionEntry(label: "FETCH FIRST", insertText: "FETCH FIRST"), + CompletionEntry(label: "JOIN", insertText: "JOIN"), + CompletionEntry(label: "LEFT JOIN", insertText: "LEFT JOIN"), + CompletionEntry(label: "UNION ALL", insertText: "UNION ALL"), + CompletionEntry(label: "WITH", insertText: "WITH"), + CompletionEntry(label: "CONNECT BY", insertText: "CONNECT BY"), + CompletionEntry(label: "START WITH", insertText: "START WITH"), + CompletionEntry(label: "PARTITION BY", insertText: "PARTITION BY") + ] let duckdbDialect = SQLDialectDescriptor( identifierQuote: "\"", keywords: [ @@ -532,7 +602,8 @@ extension PluginMetadataRegistry { ] return ( clickhouseDialect, clickhouseColumnTypes, mssqlDialect, mssqlColumnTypes, - oracleDialect, oracleColumnTypes, duckdbDialect, duckdbColumnTypes, + oracleDialect, oracleColumnTypes, damengDialect, damengCompletions, damengColumnTypes, + duckdbDialect, duckdbColumnTypes, cassandraDialect, cassandraColumnTypes, mongoCompletions, mongoColumnTypes, etcdCompletions, redisCompletions, redisColumnTypes, d1Dialect, d1ColumnTypes ) diff --git a/TablePro/Core/Services/Query/QueryPlanParser.swift b/TablePro/Core/Services/Query/QueryPlanParser.swift index d2acc3bfb..4a6a3a6d2 100644 --- a/TablePro/Core/Services/Query/QueryPlanParser.swift +++ b/TablePro/Core/Services/Query/QueryPlanParser.swift @@ -345,6 +345,113 @@ struct IndentedTextPlanParser: QueryPlanParser { } } +// MARK: - Dameng Text Parser + +/// Parses DM8 plans whose hierarchy is encoded by spaces after a numeric line prefix. +struct DamengPlanParser: QueryPlanParser { + private struct ParsedLine { + let indent: Int + let operation: String + let details: String? + } + + func parse(rawText: String) -> QueryPlan? { + let lines = rawText.components(separatedBy: .newlines).compactMap(parseLine) + guard !lines.isEmpty else { return nil } + + func buildNodes(from startIndex: Int, parentIndent: Int) -> (nodes: [QueryPlanNode], nextIndex: Int) { + var nodes: [QueryPlanNode] = [] + var index = startIndex + + while index < lines.count { + let line = lines[index] + if line.indent <= parentIndent && index > startIndex { + break + } + + let children: [QueryPlanNode] + let nextIndex: Int + if index + 1 < lines.count, lines[index + 1].indent > line.indent { + let nested = buildNodes(from: index + 1, parentIndent: line.indent) + children = nested.nodes + nextIndex = nested.nextIndex + } else { + children = [] + nextIndex = index + 1 + } + + nodes.append(QueryPlanNode( + operation: line.operation, + relation: nil, + schema: nil, + alias: nil, + estimatedStartupCost: nil, + estimatedTotalCost: nil, + estimatedRows: nil, + estimatedWidth: nil, + actualStartupTime: nil, + actualTotalTime: nil, + actualRows: nil, + actualLoops: nil, + properties: line.details.map { ["Details": $0] } ?? [:], + children: children + )) + index = nextIndex + } + + return (nodes, index) + } + + let result = buildNodes(from: 0, parentIndent: -1) + let root: QueryPlanNode + if result.nodes.count == 1 { + root = result.nodes[0] + } else { + root = QueryPlanNode( + operation: "Query Plan", + relation: nil, + schema: nil, + alias: nil, + estimatedStartupCost: nil, + estimatedTotalCost: nil, + estimatedRows: nil, + estimatedWidth: nil, + actualStartupTime: nil, + actualTotalTime: nil, + actualRows: nil, + actualLoops: nil, + properties: [:], + children: result.nodes + ) + } + + return QueryPlan(rootNode: root, planningTime: nil, executionTime: nil, rawText: rawText) + } + + private func parseLine(_ line: String) -> ParsedLine? { + let content = line.drop(while: { $0 == " " || $0 == "\t" }) + let numberEnd = content.prefix(while: { $0.isNumber }).endIndex + guard numberEnd != content.startIndex else { return nil } + + let afterNumber = content[numberEnd...] + let indentation = afterNumber.prefix(while: { $0 == " " || $0 == "\t" }) + let text = afterNumber.dropFirst(indentation.count).trimmingCharacters(in: .whitespaces) + guard text.first == "#" else { return nil } + + let planText = text.dropFirst() + let parts = planText.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + let operation = parts[0].trimmingCharacters(in: .whitespaces) + guard !operation.isEmpty else { return nil } + let details = parts.count == 2 ? parts[1].trimmingCharacters(in: .whitespaces) : nil + + return ParsedLine( + indent: indentation.reduce(into: 0) { $0 += $1 == "\t" ? 4 : 1 }, + operation: operation, + details: details?.isEmpty == false ? details : nil + ) + } +} + // MARK: - CockroachDB Parser /// Parses CockroachDB `EXPLAIN` and `EXPLAIN ANALYZE` text output. CockroachDB @@ -497,6 +604,8 @@ enum QueryPlanParserFactory { return SQLitePlanParser() case .clickhouse, .duckdb: return IndentedTextPlanParser() + case .dameng: + return DamengPlanParser() default: return nil } diff --git a/TablePro/Info.plist b/TablePro/Info.plist index 26bb8f3a9..0318102b1 100644 --- a/TablePro/Info.plist +++ b/TablePro/Info.plist @@ -390,6 +390,7 @@ scylladb scylla oracle + dm clickhouse ch etcd diff --git a/TablePro/Models/Connection/DatabaseConnection.swift b/TablePro/Models/Connection/DatabaseConnection.swift index b253a1678..f6840b324 100644 --- a/TablePro/Models/Connection/DatabaseConnection.swift +++ b/TablePro/Models/Connection/DatabaseConnection.swift @@ -36,6 +36,7 @@ extension DatabaseType { static let redis = DatabaseType(rawValue: "Redis") static let mssql = DatabaseType(rawValue: "SQL Server") static let oracle = DatabaseType(rawValue: "Oracle") + static let dameng = DatabaseType(rawValue: "Dameng") static let clickhouse = DatabaseType(rawValue: "ClickHouse") static let duckdb = DatabaseType(rawValue: "DuckDB") static let cassandra = DatabaseType(rawValue: "Cassandra") @@ -200,6 +201,7 @@ extension DatabaseType { case "SQLite": Color(hex: "0F80CC") case "SQL Server": Color(hex: "CC2927") case "Oracle": Color(hex: "C74634") + case "Dameng": Color(hex: "C60018") case "MongoDB": Color(hex: "00684A") case "Redis": Color(hex: "FF4438") case "ClickHouse": Color(hex: "FFCC01") diff --git a/TableProTests/Core/Plugins/ContainerEntityNameTests.swift b/TableProTests/Core/Plugins/ContainerEntityNameTests.swift index d03f8e077..3405568b2 100644 --- a/TableProTests/Core/Plugins/ContainerEntityNameTests.swift +++ b/TableProTests/Core/Plugins/ContainerEntityNameTests.swift @@ -82,6 +82,14 @@ struct ContainerEntityNameTests { #expect(snapshot(forTypeId: "Oracle")?.schema.defaultSchemaName == "") } + @Test("Dameng switches hierarchical schemas") + func damengContainerIsSchema() { + #expect(PluginManager.shared.containerSwitchTarget(for: .dameng) == .schema) + #expect(PluginManager.shared.containerEntityName(for: .dameng) == "Schema") + #expect(PluginManager.shared.databaseGroupingStrategy(for: .dameng) == .hierarchicalSchema) + #expect(PluginManager.shared.supportsDatabaseTree(for: .dameng) == false) + } + @Test("Engines supporting both prefer databases") func dualModeEnginesPreferDatabases() { #expect(PluginManager.shared.containerSwitchTarget(for: .postgresql) == .database) diff --git a/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift b/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift index a2e3271e5..09b68b638 100644 --- a/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift +++ b/TableProTests/Core/Plugins/DriverPluginMetadataTests.swift @@ -308,6 +308,7 @@ struct RegistryAutoLimitStyleTests { func sqlPluginsDeclareStyle() { #expect(defaults["SQL Server"]?.editor.sqlDialect?.autoLimitStyle == .top) #expect(defaults["Oracle"]?.editor.sqlDialect?.autoLimitStyle == .fetchFirst) + #expect(defaults["Dameng"]?.editor.sqlDialect?.autoLimitStyle == .fetchFirst) #expect(defaults["ClickHouse"]?.editor.sqlDialect?.autoLimitStyle == .limit) #expect(defaults["DuckDB"]?.editor.sqlDialect?.autoLimitStyle == .limit) #expect(defaults["Cassandra"]?.editor.sqlDialect?.autoLimitStyle == .limit) diff --git a/TableProTests/Core/Plugins/PluginMetadataRegistrySchemaSwitchingTests.swift b/TableProTests/Core/Plugins/PluginMetadataRegistrySchemaSwitchingTests.swift index 1d28a1fd3..cc4241932 100644 --- a/TableProTests/Core/Plugins/PluginMetadataRegistrySchemaSwitchingTests.swift +++ b/TableProTests/Core/Plugins/PluginMetadataRegistrySchemaSwitchingTests.swift @@ -64,6 +64,31 @@ struct PluginMetadataRegistrySchemaSwitchingTests { #expect(snap.postConnectActions.contains(.selectSchemaFromLastSession)) } + @Test("Dameng supports schema switching without TLS") + func damengSupportsSchemaSwitching() { + guard let snap = snapshot(forTypeId: "Dameng") else { + Issue.record("Registry default for Dameng missing") + return + } + #expect(snap.capabilities.supportsSchemaSwitching == true) + #expect(snap.capabilities.supportsSSL == false) + #expect(snap.postConnectActions.contains(.selectSchemaFromLastSession)) + } + + @Test("Dameng publishes DM8 typing suggestions") + func damengPublishesTypingSuggestions() { + guard let snap = snapshot(forTypeId: "Dameng") else { + Issue.record("Registry default for Dameng missing") + return + } + let labels = Set(snap.editor.statementCompletions.map(\.label)) + #expect(labels.isSuperset(of: ["SET SCHEMA", "CREATE SCHEMA", "EXPLAIN", "CONNECT BY"])) + #expect(snap.editor.sqlDialect?.functions.contains("NVL") == true) + #expect(snap.editor.sqlDialect?.dataTypes.contains("VARCHAR2") == true) + #expect(snap.editor.sqlDialect?.dataTypes.contains("XMLTYPE") == false) + #expect(snap.editor.sqlDialect?.functions.contains("DBMS_RANDOM.VALUE") == false) + } + // MARK: - PostgreSQL (regression for the working reference) @Test("PostgreSQL supports schema switching") @@ -99,7 +124,7 @@ struct PluginMetadataRegistrySchemaSwitchingTests { @Test("Quick Switcher allowlist agrees with registry capability flag") func quickSwitcherAllowlistMatchesRegistry() { - let typesThatShouldSupportSchemas = ["PostgreSQL", "Redshift", "Oracle", "SQL Server"] + let typesThatShouldSupportSchemas = ["PostgreSQL", "Redshift", "Oracle", "Dameng", "SQL Server"] for typeId in typesThatShouldSupportSchemas { guard let snap = snapshot(forTypeId: typeId) else { Issue.record("Registry default for \(typeId) missing") diff --git a/TableProTests/Core/Services/Query/DamengPlanParserTests.swift b/TableProTests/Core/Services/Query/DamengPlanParserTests.swift new file mode 100644 index 000000000..f18e0fc6f --- /dev/null +++ b/TableProTests/Core/Services/Query/DamengPlanParserTests.swift @@ -0,0 +1,39 @@ +// +// DamengPlanParserTests.swift +// TableProTests +// + +@testable import TablePro +import Testing + +@Suite("Dameng Plan Parser") +struct DamengPlanParserTests { + private let parser = DamengPlanParser() + + @Test("Parses numeric prefixes and indentation as a tree") + func parsesHierarchy() throws { + let text = [ + "1 #NSET2: [1, 1, 108]", + "2 #PRJT2: [1, 1, 108]; exp_num(3)", + "3 #CSCN2: [1, 1, 108]; TABLEPRO_TEST", + ].joined(separator: "\n") + + let plan = try #require(parser.parse(rawText: text)) + + #expect(plan.rootNode.operation == "NSET2") + #expect(plan.rootNode.properties["Details"] == "[1, 1, 108]") + #expect(plan.rootNode.children.first?.operation == "PRJT2") + #expect(plan.rootNode.children.first?.children.first?.operation == "CSCN2") + } + + @Test("Ignores malformed non-plan lines") + func ignoresMalformedLines() { + #expect(parser.parse(rawText: "not a DM8 plan") == nil) + #expect(parser.parse(rawText: "1 missing marker") == nil) + } + + @Test("Factory returns the Dameng parser") + func factoryReturnsDamengParser() { + #expect(QueryPlanParserFactory.parser(for: .dameng) is DamengPlanParser) + } +} diff --git a/TableProTests/Models/DatabaseTypeTests.swift b/TableProTests/Models/DatabaseTypeTests.swift index 0e74cf594..a45c49c83 100644 --- a/TableProTests/Models/DatabaseTypeTests.swift +++ b/TableProTests/Models/DatabaseTypeTests.swift @@ -57,6 +57,7 @@ struct DatabaseTypeTests { (DatabaseType.redshift, "Redshift"), (DatabaseType.mssql, "SQL Server"), (DatabaseType.oracle, "Oracle"), + (DatabaseType.dameng, "Dameng"), (DatabaseType.clickhouse, "ClickHouse"), (DatabaseType.duckdb, "DuckDB"), (DatabaseType.cassandra, "Cassandra"), diff --git a/TableProTests/Plugins/DamengParameterBinderTests.swift b/TableProTests/Plugins/DamengParameterBinderTests.swift new file mode 100644 index 000000000..4c4a3f614 --- /dev/null +++ b/TableProTests/Plugins/DamengParameterBinderTests.swift @@ -0,0 +1,58 @@ +import Foundation +import TableProPluginKit +import Testing +@testable import TablePro + +@Suite("Dameng parameter binder") +struct DamengParameterBinderTests { + @Test("binds text, null, and binary values") + func bindsSupportedValues() throws { + let sql = try DamengParameterBinder.bind( + query: "INSERT INTO sample VALUES (?, ?, ?)", + parameters: [.text("达梦 O'Brien"), .null, .bytes(Data([0x00, 0xAB, 0xFF]))] + ) + #expect(sql == "INSERT INTO sample VALUES ('达梦 O''Brien', NULL, HEXTORAW('00ABFF'))") + } + + @Test("does not bind placeholders inside SQL syntax regions") + func preservesSyntaxRegions() throws { + let sql = try DamengParameterBinder.bind( + query: """ + SELECT '?', "?", q'[?]', ? + FROM sample + -- ? + /* outer ? /* inner ? */ */ + """, + parameters: [.text("bound")] + ) + #expect(sql.contains("SELECT '?', \"?\", q'[?]', 'bound'")) + #expect(sql.contains("-- ?")) + #expect(sql.contains("/* outer ? /* inner ? */ */")) + } + + @Test("escapes injection-shaped text as one literal") + func escapesInjectionText() throws { + let sql = try DamengParameterBinder.bind( + query: "SELECT ? FROM DUAL", + parameters: [.text("x'; DROP SCHEMA SYSDBA CASCADE; --")] + ) + #expect(sql == "SELECT 'x''; DROP SCHEMA SYSDBA CASCADE; --' FROM DUAL") + } + + @Test("rejects parameter count mismatches") + func rejectsCountMismatches() { + #expect(throws: DamengParameterBindingError.insufficientParameters) { + try DamengParameterBinder.bind(query: "SELECT ?, ?", parameters: [.text("one")]) + } + #expect(throws: DamengParameterBindingError.unusedParameters) { + try DamengParameterBinder.bind(query: "SELECT ?", parameters: [.text("one"), .text("two")]) + } + } + + @Test("rejects embedded null bytes") + func rejectsEmbeddedNull() { + #expect(throws: DamengParameterBindingError.embeddedNull) { + try DamengParameterBinder.bind(query: "SELECT ?", parameters: [.text("a\0b")]) + } + } +} diff --git a/TableProTests/Plugins/DamengStatementClassifierTests.swift b/TableProTests/Plugins/DamengStatementClassifierTests.swift new file mode 100644 index 000000000..391676c22 --- /dev/null +++ b/TableProTests/Plugins/DamengStatementClassifierTests.swift @@ -0,0 +1,60 @@ +import Testing +@testable import TablePro + +@Suite("Dameng statement classifier") +struct DamengStatementClassifierTests { + @Test("recognizes row-producing statements after comments") + func recognizesRowStatements() { + #expect(DamengStatementClassifier.expectsRows("/* nested /* note */ */ SELECT 1")) + #expect(DamengStatementClassifier.expectsRows("-- note\nWITH value AS (SELECT 1) SELECT * FROM value")) + #expect(DamengStatementClassifier.expectsRows("EXPLAIN SELECT 1")) + #expect(!DamengStatementClassifier.expectsRows("UPDATE sample SET value = 1")) + } + + @Test("caps a single select on the server") + func capsSingleSelect() { + let capped = DamengStatementClassifier.applyingRowCap(50, to: "SELECT * FROM sample;") + #expect(capped.contains("SELECT * FROM (")) + #expect(capped.contains("SELECT * FROM sample")) + #expect(capped.hasSuffix("WHERE ROWNUM <= 51")) + } + + @Test("does not overflow the maximum row cap") + func maximumCapDoesNotOverflow() { + let capped = DamengStatementClassifier.applyingRowCap(Int.max, to: "SELECT 1") + #expect(capped.hasSuffix("WHERE ROWNUM <= \(Int.max)")) + } + + @Test("does not rewrite multiple statements") + func rejectsMultipleStatements() { + let query = "SELECT 1; DROP TABLE sample" + #expect(DamengStatementClassifier.applyingRowCap(10, to: query) == query) + } + + @Test("keeps semicolons in quoted strings") + func allowsQuotedSemicolons() { + let capped = DamengStatementClassifier.applyingRowCap(10, to: "SELECT ';' FROM DUAL;") + #expect(capped.hasSuffix("WHERE ROWNUM <= 11")) + } + + @Test("keeps semicolons in Dameng alternative quotes") + func allowsAlternativeQuotedSemicolons() { + for query in [ + "SELECT q'[;]' FROM DUAL;", + "SELECT Q'(;)' FROM DUAL;", + "SELECT q'{;}' FROM DUAL;", + "SELECT q'<;>' FROM DUAL;", + "SELECT q'!;!' FROM DUAL;" + ] { + let capped = DamengStatementClassifier.applyingRowCap(10, to: query) + #expect(capped.hasSuffix("WHERE ROWNUM <= 11")) + } + } + + @Test("does not rewrite nonpositive caps or writes") + func ignoresUnsupportedCaps() { + #expect(DamengStatementClassifier.applyingRowCap(nil, to: "SELECT 1") == "SELECT 1") + #expect(DamengStatementClassifier.applyingRowCap(0, to: "SELECT 1") == "SELECT 1") + #expect(DamengStatementClassifier.applyingRowCap(10, to: "DELETE FROM sample") == "DELETE FROM sample") + } +} diff --git a/docs/databases/connection-urls.mdx b/docs/databases/connection-urls.mdx index 3f5428548..3a7b2d0e7 100644 --- a/docs/databases/connection-urls.mdx +++ b/docs/databases/connection-urls.mdx @@ -26,6 +26,7 @@ TablePro parses standard database connection URLs for importing connections, ope | `sqlserver://` | Microsoft SQL Server (alias) | | `jdbc:sqlserver://` | Microsoft SQL Server (JDBC) | | `oracle://` | Oracle Database | +| `dm://` | Dameng DM8 | | `jdbc:oracle:thin://` | Oracle Database (JDBC thin) | | `cassandra://` | Cassandra | | `cql://` | Cassandra (alias) | @@ -44,7 +45,7 @@ TablePro parses standard database connection URLs for importing connections, ope | `libsql://` | libSQL / Turso | | `surrealdb://` | SurrealDB | -Every scheme above works in the **Import from URL...** sheet. Only these are registered with macOS, so only these can be launched with `open` or a browser link: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, `surrealdb`. +Every scheme above works in the **Import from URL...** sheet. Only these are registered with macOS, so only these can be launched with `open` or a browser link: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `dm`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, `surrealdb`. Append `+ssh` to any non-file-based scheme (not `sqlite`, `duckdb`, or `beancount`) to use an SSH tunnel: diff --git a/docs/databases/dameng.mdx b/docs/databases/dameng.mdx new file mode 100644 index 000000000..6004c1ddf --- /dev/null +++ b/docs/databases/dameng.mdx @@ -0,0 +1,50 @@ +--- +title: Dameng DM8 +description: Connect to Dameng DM8 with TablePro +--- + +TablePro connects directly to Dameng DM8 over its native wire protocol. The macOS driver is a downloadable plugin and does not require a local DM client installation. + +## Install Plugin + +1. Open **Settings > Plugins > Browse** +2. Find **Dameng Driver** and click **Install** +3. Create a Dameng connection from the connection chooser + +## Connection Settings + +| Field | Default | Notes | +|-------|---------|-------| +| **Host** | `localhost` | DM8 server or tunnel endpoint | +| **Port** | `5236` | Default DM8 listener port | +| **Database** | - | Optional initial schema | +| **Username** | - | DM username, such as `SYSDBA` | +| **Password** | - | Stored in the macOS Keychain | + +The driver supports SSH, SOCKS, and Cloudflare tunnels. Native DM8 TLS is not yet available, so the SSL pane is hidden. Use a trusted tunnel when the database is not on a private network. + +## Connection URL + +```text +dm://user:password@host:5236/schema +``` + +## Features + +- Browse schemas, tables, views, columns, indexes, and foreign keys +- Complete DM8 statements, functions, data types, schemas, tables, views, and columns while typing +- Run SQL with Unicode text, parameterized binary writes, transactions, and row limits +- Switch schemas without reconnecting +- Edit rows with escaped parameter values +- Create and alter tables, indexes, primary keys, and foreign keys +- Inspect table metadata, generated DDL, view definitions, and visual `EXPLAIN` plans + +## Limitations + +- Username and password authentication only +- No native TLS, query cancellation, or trigger editor +- Native binary and off-row LOB reads are not supported by the current transport. Use `RAWTOHEX` for binary + columns and cast `CLOB` values to `VARCHAR` when reading them. +- The Database field selects an initial schema; DM8 database switching is not supported + +If a server value fails to decode, include the DM8 version and column type in a [GitHub issue](https://github.com/TableProApp/TablePro/issues). diff --git a/docs/databases/overview.mdx b/docs/databases/overview.mdx index b484183b6..5f7968963 100644 --- a/docs/databases/overview.mdx +++ b/docs/databases/overview.mdx @@ -3,7 +3,7 @@ title: Managing Connections description: Create, organize, and switch database connections, with health monitoring and startup commands. --- -TablePro connects to 25 databases through its plugin system. This page covers creating and organizing connections. Driver-specific fields and quirks live on each database's own page. +TablePro connects to 26 databases through its plugin system. This page covers creating and organizing connections. Driver-specific fields and quirks live on each database's own page. ## Supported Databases @@ -17,6 +17,7 @@ TablePro connects to 25 databases through its plugin system. This page covers cr | [PGlite](/databases/pglite) | 5432 | No | No | No | No | No | | [Microsoft SQL Server](/databases/mssql) | 1433 | Yes | Yes | Yes | Yes | Yes | | [Oracle](/databases/oracle) | 1521 | Yes | Yes | Yes | No | Yes | +| [Dameng DM8](/databases/dameng) | 5236 | Yes | No | Yes | No | Yes | | [ClickHouse](/databases/clickhouse) | 8123 | Yes | Yes | Yes | No | Yes | | [Teradata](/databases/teradata) | 1025 | Yes | Yes | Yes | No | Yes | | [Trino](/databases/trino) | 8080 | Yes | Yes | Yes | No | Yes | @@ -80,7 +81,7 @@ Opening a database URL from a browser or terminal skips the form: open "postgresql://user:pass@host:5432/dbname" ``` -TablePro registers these URL schemes with macOS: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, and `surrealdb`. +TablePro registers these URL schemes with macOS: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `dm`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, and `surrealdb`. What happens on open: diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index db83c5e00..f2482f9e0 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -53,6 +53,7 @@ actor SSHTunnelManager { | TableProCore | Local package in `Packages/` | Shared SwiftPM modules the macOS and iOS apps both use | | Sparkle | SPM, 2.9.0 | Auto-update with EdDSA signing | | OracleNIO | SPM, TablePro fork pinned by revision | Oracle wire protocol for OracleDriverPlugin | +| rust-dameng | Cargo, vendored pinned snapshot | DM8 wire protocol for DamengDriverPlugin | | swift-certificates | SPM, 1.19.0 | Generates the MCP server's self-signed TLS certificate | | Yams | SPM, 5.4.0 | YAML parsing for project folder import | @@ -83,12 +84,13 @@ Five driver plugins ship inside the app bundle, covering nine database types: The app bundle also carries non-driver plugins: CSVInspectorPlugin, export plugins (CSV, JSON, SQL, XLSX, MQL), and import plugins (CSV, JSON, SQL). -The remaining 16 driver plugins are downloaded on demand from the [plugin registry](/development/plugin-registry): +The remaining 17 driver plugins are downloaded on demand from the [plugin registry](/development/plugin-registry): | Plugin | Database types | Connectivity | |--------|---------------|--------------| | MongoDBDriverPlugin | MongoDB | CLibMongoc | | OracleDriverPlugin | Oracle | OracleNIO (SPM fork) | +| DamengDriverPlugin | Dameng DM8 | Rust native wire bridge | | DuckDBDriverPlugin | DuckDB | CDuckDB | | MSSQLDriverPlugin | SQL Server | CFreeTDS | | CassandraDriverPlugin | Cassandra, ScyllaDB | CCassandra | diff --git a/docs/development/plugin-registry.mdx b/docs/development/plugin-registry.mdx index 46697ba9f..b40c4c465 100644 --- a/docs/development/plugin-registry.mdx +++ b/docs/development/plugin-registry.mdx @@ -106,6 +106,7 @@ The full field list is `RegistryPluginMetadata` in `TablePro/Core/Plugins/Regist |-----------|-----------------| | `plugin-mongodb` | `"MongoDB"` | | `plugin-oracle` | `"Oracle"` | +| `plugin-dameng` | `"Dameng"` | | `plugin-duckdb` | `"DuckDB"` | | `plugin-beancount` | `"Beancount"` | | `plugin-mssql` | `"SQL Server"` | diff --git a/docs/docs.json b/docs/docs.json index 1de1b5d15..eab24b195 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -56,7 +56,8 @@ "databases/cockroachdb", "databases/pglite", "databases/mssql", - "databases/oracle" + "databases/oracle", + "databases/dameng" ] }, { diff --git a/docs/features/plugins.mdx b/docs/features/plugins.mdx index 02fda56a0..4da27a823 100644 --- a/docs/features/plugins.mdx +++ b/docs/features/plugins.mdx @@ -3,7 +3,7 @@ title: Plugins & Themes description: Install database drivers, import and export formats, and themes from the plugin registry, and keep them updated. --- -TablePro loads database drivers, import/export formats, and themes as `.tableplugin` bundles. Five driver plugins ship inside the app and cover 9 databases; 16 more download on demand from the plugin registry. +TablePro loads database drivers, import/export formats, and themes as `.tableplugin` bundles. Five driver plugins ship inside the app and cover 9 databases; 17 more download on demand from the plugin registry. ## Bundled plugins @@ -28,6 +28,7 @@ These install from the registry when you need them: |--------|----------| | MongoDB | MongoDB | | Oracle | Oracle Database | +| Dameng | Dameng DM8 | | Microsoft SQL Server | SQL Server | | DuckDB | DuckDB | | Cassandra | Cassandra, ScyllaDB | diff --git a/docs/index.mdx b/docs/index.mdx index f0404b934..c62101347 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,7 +3,7 @@ title: Introduction description: Native macOS database client for MySQL, PostgreSQL, SQLite, MongoDB, Redis, and 20 more. --- -Native macOS client for 25 databases. Built with SwiftUI and AppKit, no Electron. The download is about 20 MB. +Native macOS client for 26 databases. Built with SwiftUI and AppKit, no Electron. The download is about 20 MB. TablePro main interface @@ -21,7 +21,7 @@ Native macOS client for 25 databases. Built with SwiftUI and AppKit, no Electron **[Safe Mode](/features/safe-mode)**: 6 per-connection protection levels, from no prompt at all to confirmation dialogs, Touch ID, and read-only. **[Import & Export](/features/import-export)**: CSV, JSON, SQL, XLSX, MQL. Streaming export for large datasets. **[CSV Inspector](/features/csv-inspector)**: Open `.csv` and `.tsv` files directly. Edit cells, insert and delete rows and columns, save in the original dialect. -**[Plugin System](/features/plugins)**: 5 bundled drivers covering 9 databases, plus 16 more drivers installable from the plugin registry. +**[Plugin System](/features/plugins)**: 5 bundled drivers covering 9 databases, plus 17 more drivers installable from the plugin registry. **[iCloud Sync](/features/icloud-sync)**: Connections, groups, tags, settings, SSH profiles, saved queries and folders, favorite tables, and custom AI slash commands sync across Macs. **[Themes](/customization/appearance)**: Light, dark, and custom editor themes. Per-connection color labels. @@ -41,6 +41,7 @@ Native macOS client for 25 databases. Built with SwiftUI and AppKit, no Electron | Redis | 6379 | Built-in | | MongoDB | 27017 | Plugin | | Oracle Database | 1521 | Plugin | +| Dameng DM8 | 5236 | Plugin | | DuckDB | N/A (file-based) | Plugin | | Beancount | N/A (file-based) | Plugin | | Cassandra / ScyllaDB | 9042 | Plugin | diff --git a/project.yml b/project.yml index 3ba6c957f..200ab8a54 100644 --- a/project.yml +++ b/project.yml @@ -69,6 +69,7 @@ targetTemplates: - path: Plugins/${folder} excludes: - Info.plist + - README.md dependencies: - target: TableProPluginKit embed: false @@ -316,6 +317,8 @@ targets: - Plugins/ClickHouseDriverPlugin/ClickHouseCredentials.swift - Plugins/ClickHouseDriverPlugin/ClickHouseGeneratedColumnClassification.swift - Plugins/ClickHouseDriverPlugin/ClickHouseTableOperations.swift + - Plugins/DamengDriverPlugin/DamengParameterBinder.swift + - Plugins/DamengDriverPlugin/DamengStatementClassifier.swift - Plugins/DuckDBDriverPlugin/QuackConnectBuilder.swift - Plugins/DynamoDBDriverPlugin/DynamoDBQueryBuilder.swift - Plugins/DynamoDBDriverPlugin/DynamoDBStatementGenerator.swift @@ -691,6 +694,75 @@ targets: folder: DynamoDBDriverPlugin principalClass: DynamoDBPlugin + DamengDriver: + templates: [DriverPlugin] + templateAttributes: + folder: DamengDriverPlugin + principalClass: DamengPlugin + preBuildScripts: + - name: Build Dameng native bridge + script: | + if [[ ! -f "$SRCROOT/Libs/libdameng_bridge.a" ]]; then + "$SRCROOT/scripts/build-dameng.sh" both + fi + basedOnDependencyAnalysis: false + settings: + base: + HEADER_SEARCH_PATHS: + - $(inherited) + - $(SRCROOT)/Plugins/DamengDriverPlugin/CDameng + LIBRARY_SEARCH_PATHS: + - $(inherited) + - $(SRCROOT)/Libs + OTHER_LDFLAGS: + - $(inherited) + - -force_load + - $(SRCROOT)/Libs/libdameng_bridge.a + - -framework + - Security + - -framework + - CoreFoundation + - -liconv + SWIFT_INCLUDE_PATHS: $(SRCROOT)/Plugins/DamengDriverPlugin/CDameng + + DamengDriverTests: + type: bundle.unit-test + platform: macOS + sources: + - path: Plugins/DamengDriverPlugin + excludes: + - Info.plist + - README.md + - Plugins/DamengDriverPluginTests + dependencies: + - target: TableProPluginKit + preBuildScripts: + - name: Build Dameng native bridge + script: | + if [[ ! -f "$SRCROOT/Libs/libdameng_bridge.a" ]]; then + "$SRCROOT/scripts/build-dameng.sh" both + fi + basedOnDependencyAnalysis: false + settings: + base: + GENERATE_INFOPLIST_FILE: YES + HEADER_SEARCH_PATHS: + - $(inherited) + - $(SRCROOT)/Plugins/DamengDriverPlugin/CDameng + LIBRARY_SEARCH_PATHS: + - $(inherited) + - $(SRCROOT)/Libs + OTHER_LDFLAGS: + - $(inherited) + - -force_load + - $(SRCROOT)/Libs/libdameng_bridge.a + - -framework + - Security + - -framework + - CoreFoundation + - -liconv + SWIFT_INCLUDE_PATHS: $(SRCROOT)/Plugins/DamengDriverPlugin/CDameng + ElasticsearchDriverPlugin: templates: [DriverPlugin] templateAttributes: @@ -831,6 +903,7 @@ aggregateTargets: - CassandraDriver - ClickHouseDriver - CloudflareD1DriverPlugin + - DamengDriver - DuckDBDriver - DynamoDBDriverPlugin - ElasticsearchDriverPlugin @@ -856,6 +929,16 @@ aggregateTargets: scheme: {} schemes: + DamengDriverTests: + build: + targets: + DamengDriverTests: test + buildImplicitDependencies: true + test: + config: Debug + targets: + - name: DamengDriverTests + TablePro: build: targets: diff --git a/scripts/build-dameng.sh b/scripts/build-dameng.sh new file mode 100755 index 000000000..5bc277cd6 --- /dev/null +++ b/scripts/build-dameng.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +BRIDGE_DIR="$ROOT_DIR/Native/DamengBridge" +LIBS_DIR="$ROOT_DIR/Libs" +ARCH="${1:-both}" +MACOS_TARGET="14.0" +RUST_TOOLCHAIN="1.91.1" + +if ! rustup toolchain list | grep -q "^${RUST_TOOLCHAIN}"; then + rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal +fi + +build_slice() { + local arch="$1" + local target="$2" + local output="$LIBS_DIR/libdameng_bridge_${arch}.a" + + if ! rustup target list --installed --toolchain "$RUST_TOOLCHAIN" | grep -qx "$target"; then + rustup target add "$target" --toolchain "$RUST_TOOLCHAIN" + fi + + env RUSTUP_TOOLCHAIN="$RUST_TOOLCHAIN" MACOSX_DEPLOYMENT_TARGET="$MACOS_TARGET" \ + cargo build \ + --manifest-path "$BRIDGE_DIR/Cargo.toml" \ + --locked \ + --release \ + --target "$target" + + cp "$BRIDGE_DIR/target/$target/release/libtablepro_dameng_bridge.a" "$output" + nm -gU "$output" | grep 'T _tp_dm_connect' > /dev/null +} + +mkdir -p "$LIBS_DIR" + +case "$ARCH" in + arm64) + build_slice arm64 aarch64-apple-darwin + cp "$LIBS_DIR/libdameng_bridge_arm64.a" "$LIBS_DIR/libdameng_bridge.a" + ;; + x86_64) + build_slice x86_64 x86_64-apple-darwin + cp "$LIBS_DIR/libdameng_bridge_x86_64.a" "$LIBS_DIR/libdameng_bridge.a" + ;; + both|universal) + build_slice arm64 aarch64-apple-darwin + build_slice x86_64 x86_64-apple-darwin + lipo -create \ + "$LIBS_DIR/libdameng_bridge_arm64.a" \ + "$LIBS_DIR/libdameng_bridge_x86_64.a" \ + -output "$LIBS_DIR/libdameng_bridge_universal.a" + cp "$LIBS_DIR/libdameng_bridge_universal.a" "$LIBS_DIR/libdameng_bridge.a" + ;; + *) + echo "Usage: $0 [arm64|x86_64|both]" >&2 + exit 1 + ;; +esac + +file "$LIBS_DIR"/libdameng_bridge*.a From 0aea8dadb0454abb526f99ab06241a06b91e9bee Mon Sep 17 00:00:00 2001 From: sophiathedev Date: Sat, 15 Aug 2026 00:07:16 +0700 Subject: [PATCH 2/4] docs(plugin): add Dameng preview --- docs/images/dameng-external-connection.png | Bin 0 -> 629637 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/dameng-external-connection.png diff --git a/docs/images/dameng-external-connection.png b/docs/images/dameng-external-connection.png new file mode 100644 index 0000000000000000000000000000000000000000..c3636dc4a1d9f84768bf9ef62bca95e7832cfbe6 GIT binary patch literal 629637 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�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%0AwH>C#O}*R8GM;6&<9Hja+|p7B5=#M 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%0AK*9IO0QA8A;0@nIo4_%r%^3 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 z z0LXQ}dV4Xjb^Kj`4vda9j+rIDKDXzg83C)Giyi{U_~}yNF9DE8ytbFSd#G?I1Puwp zg!*%h@e#8f1aAEva2BZi&_E}l$|Hu^*`Ga(_Gah{Tn&f9Gm=Ctr!?f0;;MypqYzmW z%3Arc^pF^mw!CUzCJ~oc_&dnYgrh@-`P9f+vxMV%1PkOgZMt80r*sKm_r$N6ovXIO z@`K^cKC9i%c+mZX#GT)Nsvmy&fM_%eC13vV)*HX(U1dy1Kw#el7uCVCh^HPmx6}8L z)1vklqia>lk(rdB(uu2*&8aP%Ls_#%o++HWf$U!65OGfNEduyPN^t$Cme>>?Ow2?T zL}P!svK}opA^;5Oe%H@XNU z%anz+yt+7;<)Kwx<(aP+WJ?p%kM3rkO4r~N81c)1m*PbPalMaGF?%&izxgSj(ZhoI zz6VKv`MRr#Bc*`P|bLN(CU!_;T}mKVph}J=1v<>j8itM40VfkFnP{j zWK6%Fgw@)tdad<`Sw7wLDSxg-n5?fhU6asmnQENr3mPCF&j9hIe|vJ5$GpdgRSwfu zgED@Er_;XS7+&=$cBn(S%4(9~b&r-tX3t{|*utDsKhHVsb{R}nIny$?M_c@gtYhGW zuEA$OZNi_Y2tcEGIW@D`$Zi6y1)q-ExT0wc-s9MA&-v1!?_*iz#4YakTbf^N=3q>A z?)QtjTG@mrUxCyrehPM4{ot}@AXEus-~0hoN@R)lZhA2Y%^&a}1V78KHbr%lU)%it%QogI@a{7?C5^ zx~m{PXf|xw3znQrOH?So%@0e<#a`?v;N4X|%yLs=&5nY(((arZoF4@nWr0NN{|zfux(>oc`X{|@ew_M*o`0Bd4^V;9*Z zMEDsb|5JM3D9O_@RwH(-*0+B~%T;QLtv=R>IrFNRK`49*ELJOO?YZMi9Nfxb&3|xh zY0lXl6ZS;#zujRHXva%d{c@BX1aal)OW%3>e|J@M`a}+lI^y?5iEu5&9&0z+%pIIy zrR-XF2Vq5WP)v1-K)s}ab#S;=zE=nxJxN{K(3mX&rv#TAKU6Tt-wkmB2gijy-yK>i zATWY+qQul6!XtuxC>^Q|{c0JzUptA{cjw7ANwGi5fPjLqzO!%vEz! z^p43h(%Or;EZe*Qjpes<%knoC4ac&wrdfCvy*tNERSw&E!68j!G zq=qG`>fS_}YO8pgTASXn8aLb>imVAR5lqh2XRI0_>;Y9PH9I!vXFsf*^A+)zmVP~r zvf!LbyJ3FyUsKu(ue|xm;bSgB`2(tnI-jT&MqRo=3o(6P zcrpO5lG>T32_h%@|AVUX)vnQY&fmY=kUwCCkh8L``1;v1iQV)d|4^rpw{bN1FpSGh zyksdLZ@ooB2_xpkr4h^!{gw47Gwl}oDMZnZe(afmh^9+^?t3+Ff%XphtLxzb(K3;F z<_IY(b-`=7qaM5Ds@$8%$=GMCLFZ0t=wR;W7PR;0WNGs(`6iNTPa`Gu3=U>p> z%-#YiPhK?w?)20zk7{>bC4HMriMucqpL8m1)1z!4+4|}KG8%AK`3Wt6=r)15pd6Jo zSR&KT5r2-GFw5&S$>DbX%X|#}c)(p#Rp+K6AY*&W{hlv^1!b`K;pVZ$pc2~K$YB+P z#ls^$G4%L-K@37|hB-?we=X)Wtg=jj`b~TKT@xhBL%O9>u3vKu(@ZD70)aDRpE|{+ z(+H)AYW&q&jn}F!QQ^qhe#EL67J*P|oKq*YX$n3o-)@Fxc{m?y9czdDmB8e4HQr5C z^sIh}XS%g-6f-FvhaF0a6DOw%MIB zPb5q-#lXF;R*e+(4pw6)8ZY|Qr+5!D{;#@;13L77?B?8G{!hB;#Av0scl7)e?<2TB zb?DET?7IZqc$YL_WP1Z{Gh*nusp1TY{N+NmwiJC`*tW*R?t8 ze>;G?zl+tXD_}A56Ei~{W!8!n%5%Zj$3MZiqR&z9yze|?c`2-rnLP17LV2R`;y?r* z#J|V@Lr@=(6pPCz7zC?NytTC-WWNbn%phUwc0`4H2;AC)sT7lk_QHvs4zFVuPGDm; zL+o=mYffO{8QPoZrA@M|9VbR=P7Iv3Hxo6IQfYs_iC#r4my8}-!qM&LP5QQQ9em?@ zcXg=}(it^x33KYPLm3Lu&E;7;g7_RQWu906hS~M>q$fFR^sS-$k@FTnfJrp;V^GS) zP+WH;a7aY5=939sdfNO0Hvzte8br|w<}QP8>{iEl&(AIcb6g}_{IxFVO-U=1r^;Ch zX6ti9p4JQhnsd|@L#Wvjs-fO4*HQy6UKr}7p}-VNQVon^fPQM>ap?dIw|$ro2t>JS6~H`KFV1^ zG`fNq``2*JKkNg=nHv`i81Cjm3>EcOu>{syp+z!>dA8CfE`^UreK^dkn%rR(| z{YUz zNCs8mw}ala??Mz7>UBauH#)dIB!AmW1ouwPf92!>LL{{HFeK zbl5wKKl1Cf4gm$U_6V0+igUKs-eusZvhEC9NfdKSIw_jQ4FjU*yiACpM6^h zC#0lOaJ2h5iGyTV|ADN+e5!uOrZ9Vnbdu*C! z>86_O*qEr13nR!i?>UqB2IETjl6X!$zhJUkoeHZd41vQvkO4J2JoNVaLmZ=is9pK% zOwyK!G*DMz-tsa(^(BX7Ol14(i94VhV}%%fs1{g_z{?q;Q12{L_bjXk+-il3A5y-v z|NdTvWMhiboLdm0)AZC0$RAp{mjaefi_atH5}T847&>L43eE7Vc0PzdR;s*`k-JjT zqr9IlV`*lcPnwlOy}j?kKwg4A_?)XZIf_`v;Eh$4BCK}L5!to9LHJ^%QxM>6P>Qhf z$~!#fWwns9!f=a>lBrClYEnXHpJ(SMETw*LR$o8yl;wgLYQLgKYJEG*Kt4B-^_vNl zDvZ_)3oWfn!>^Z>axH5kp<8m=$I@2YVdnZP;pf9uTQyEw(b}_;_Bd?=Y~Jo9h$#u( z#O>kzz_ynUPKujvu{vOdE9B>+EljyFIGF&Ejt5pCuBGC0Ehj8qqeJHAX0Gk3*>T(^ zJIOpH@HtjGh6zOpYnN##5e<2Q<2?BkU`!KHCoT--$z)H(HBL3<35(v&e}Eaty)Y!u zajniA@znVezHd8tZ~1MsvVM6E8HM;qOD2)CrPH~p(KxN9=P**x7I}_OWnP#wX}hU< z@|H%sVReul_NlPb8vacy-=$sFB(c5#UZN8)G#X;8o@K0#i+{w&YsZ}J4lA0`LyZxx zdP-}VY$c*m8Q^JilnC#%Cq5oCqiLnI{nBIosw^ z?>;H+ud;_5KZ<|L@DV|p2K1gMdd!qSX5tA5^qs50&Ovo2Irl9ZJk=IGm)ikR96}iK zlnX(Av6@8fhqLrS%v^7-(-tIsvwOx)F|EDP9mavlqi1r;Z%z0Gl~B6(Gz#3C7`Fu~ z+9+=rSM7rwMqn6rFFAk(;0rK%VL#KG@R!A-fim#0_!1~)D18+c%C(5uXc+G2p5H)2!W=NLk|Wt|@8BtZqVbWm-$q2iNbHqKXEn6sL={(QHtcUn3%Q2UsE){4_Ii61CRIt~Ntj`!~$@o?YMa)IpK z4lX!JdJlMV^X02x<)-MTU|h$VA5Wgr}EO#k(STdIW+qAEWc^Iz7L?xsnn! zQKPp1VSHfDiAM9;a)`6Rju@pnR?#cyYv*J&$PxlY(45sL_(%%+CHG*4c-gxSp}z=$ zIX`9~Z6%iCs>8R+C{cQExp@q6X%6GcMSL~V>!wGR%wl`GOnF<%e7Kjl=vKSpuy=;F z`x2qR>|(F!LN`~UOjavcJVJe>#3r$(IqS=V4LO4wn*xs+7A(}+Jkg%3FC-nZtbCjz zP>TY<259-jR=>KNB&(|WBVq$ML!|Q-tVWEM4R*No%`qTIaJ1q_{~ z5z7jkXkN5%eeKiyFrw{%>x4Z*(7II|T+ZXCkXHgJN~v9HHQ^gHR@3SS0w2p{MDm~& zHwc`l&<~I^tX8GNVdkBk=KxRQ#@%5Q%?%ahC<;gkV!L z`LE<3fDA<^dJGo7O5s?<3$MRb`sCY|Cp!5?i45{3#>$}B^wVFx3oYsuKF%Pe4 zM_Og1fj3R@Eg(z$O~J>ybr?sG*_cHb?q>n!gBi@+gSlX{I)A^5NzK&fg?Y>~bS1Ez zBEMGZ#R3?ZEdI0Kt;5LG1TA}*3FqA;mS2Q)NC#;%@M67+@|b?jF^Y$`Q>>fx{+TzA z)q#~#n@BLKY*)Ps3vzy|e;I>2%`Vj*T&aJyFo@mK-$cwI6X)*i=uaf|3b}d&&fofE zUl#|IGtlLn??;yc_?|6n7T%XGgkc4P}qMj)hCf|@&hm=|k;l}+I?h=q8Ay0zU)!luomUudIE___KdKoRcRRELj5=Y@5AsgpMNKzNpg8OdOyw(l^{9U1 z6aFU$_KQA@f_wQ@Fi+0;)zUZMU`<6aq{N*=fiA z`;ScOKcoFU->PM@o%T@ya_yyqhi2`cH%eoiBC7Mmj5q_HXfn(@ys|7&YfPK4kjf*q zuOq7!lNX@tZp!<=ou4l&Y|qLJt9A`Dw_`#$b=pdveOx+{aw^Jt4P|k)qPy`SD-7{y zV>5n5NRU7Hd>YWE!7TebSf%;P4exm%=MxkBTFiqrgE6w;;OYWk zX`hjq{Mv+WOz(V(#PR!-Za55)D`s7D@#cq)?BZu?t$QY zPWKHuKmKD$9KE`pX&8GDgfPn+h*8JUOIwzL5b$aPg?PF-?nJHnD7E(`mK0-a-!GK! ztX0|eM2M>>*W-&)ba{(69Jd}QPa`^yo(eGcWQJ{*tOL!xS6gMf*ij?6q=Jl=W)zN` zrW~IYNgr?0Re-DKiZ5b+h7uD?5N+1ZB%LS&?QYixAAzAQBiH<^2VHN?m_5oF0rppI z+s*Z^{j<6MVFxo(b3$U4xZd)K@`mcmOd~Uo2c3^HTxS73DxAppbd04AH8-l(-LlrD zgv|o;$!Vyg<2{n4Dj=r^8w8IuU8EriQE5;dp}65$B$CG9nccRhUfE2}2`sfIli=w4 zWyoJUHZZ~*DFUTXEsHwh(r4v?+jr(9F|T0jnp>JobKS7MUVS=+u(75s z>qMq4J}s~)zF%A#>FI1^_jyu~1~r)CeCM}A3I3CGfnn=VXp`DD=BmNBxnKWLd!^1j zlV|)T(lwEByK5r|zQ@%v7L`oHw!$a zkh9UV_kp6$IVpm2!+d@@U8@JSShTYU|CV0Lq*nSlW=Q%gxrH7s*qyyXL>0oG>DHUI z)oUR>&x+RXVV)ECQwRmcgKjhX*w!9;|5d>+QPT@h9pAFWSdu@ubx4suIcjd5sOBo! zq~WDG|7jIVVDLTFwYc$H&B8;sOH1_=TZn4qE*ov&=10AasPDT`x#jP7XZ<3svh+`b zI}Wha7xi1e84t+3=r!ZlaR400rKF^T?GUD~zf(T^c5jWNjP1A-WXnCcB-FlzX+~Gh zo|hd`yh4nN1%DS-d2Wq~l~v?`SF$j1jJN$r$J%{1E29MUcuMOnEC}GPd;*R`8@q5z zZw=5annTVxxb#X*aA`YgQ!pdYU$eVOZ){1rtOAo3ryT(77;Vk!wR{ZU>Hu(s*AiY1 zbE~bleSRBgGIg0`fRSU0GXC!+^PNc2%wjwML;xo4oHa`-5EH?~ZHZGlkFb6=3>(DZ z2(IM(g~dTu8*$L^?3L}(VY?^43Rmn?R}sMX-ZbyVF-C2J(a2H;GXaJlQ4hK4+w-7U z=6TitvQMj>aagtpsSLXv&;gDk;;WDD7`l3GMFI{!xoX}btYp%SAy)zKs5o+GYU%kKFdPyWm;qK2A1U;a(rMC90@k(+^toT%&TV>LM8 z;aKtXX2=N-1f3pP%IsDyehO5TOG4TfAh}HyEUJ=v<8Ng7BvM=FPj{SuD~+;3f3;8_ zrmc>7%xG0*T?wLuKxp0wvbeV}#du|Xi?P*%T?e}06ggM_Xno$ooR3Tnv2)&**v23u zJ<+)NK=wsg+NkW;bX#V25R77BX^ zkNq)^rQxNadI*xbf%{Wii}G}WZ4>6|S3OnRBO@;D6!)a@p>_bKsa^FMpb&zK7dOQp z#OkA!pX@}=;U{=E4ULhdCpjCvgHxMZDxx>)1SdRiz|`joz{Lc-1?NDAPlcD*H!}|O zs+gZXRsc*k7e=SP8UGvgd75{AiY&j3quLlXL3vkFN_UiM3qa7FY=aK)Sc*%YlzLiK zb0=<%A_^}8^c*)YXpZuN{&x~i5X?o`?+Qkb%2--_?#QtB9&f1|BG(0_URIfGZu@p3 zT3}x)7YqUL8Zp@AV5_tLK%;d|pWc(*W?s+S`ZWj1xE)-}KnM8oJiFJMY5K=Zv4Xb< zm2ref5Evd{y3iR6Aw#VK` z&n)#$8aD4!8M?9)I5H!*oJD{Oe{ua?8iPk{`kX2`;RVY>zpRZM_KQXg4fY~``X&be zQkJ`SQf(!ROM`B-lZY%*g1yhPdo$OI21cg4*!woJ0Dwqi6MJ)9i7S)DyO`OF5O5Ro zj70?CT!~YkYegA1#UvT9@Jq*@7C-=MlDCe#R_EV!bL60rv36r=tHV+NA*nV(gOH3# zPfLMJwgOhnnwhW^ka4WVbWqySlN}X@sNJmTa*Dg3l+D-7y~Q;5?=!kd(lgr#(}WDh$7%_xRDed!Ld=qpXwrq0i)3PgZNr-@zlFT z3fcNp@UR0|Ze%XA-~eKT>ZE%n+);eh8x~cJlG$qj*i(Nx(rHo_OHtX3(Ze|R6=(SkS0qNOuK09Ju!tg ziZ=x`hzHYj(ByFV+8eZuFnYcDiN?=($n5tHGw;A`iK)6C zs1#*mo2e3F>!v(l7iJWaZpyJKmyL82%EmT>Y^Ydf9%Lj0Lb|bI6O4h_#Ks^YB_T$! z83bWQLJAf`Fi0{85Q7GUgaimAgy#7jT(^7Ox4Kt%uhs9ZzWrAH#nO?^*_*x3+57kV ze&6?<@qekk!dbn6tP4kKo}WbP2c3IB?VD9#rMh$#M3r!}j}u0CxkHA$`JjATm7tTE zLODrmYe7-w=alLpzM05jEpWW#V!f(q^P2PMKptM&q6-+dqk5Ft;}&yJb57LAcq}ph zYMK3BKH~UUY7~m#_HY`}TbxDV$sPz?JIaZfI?t*zpbHMuR=<3!NG=NnSusE*_y{mR z3$lv*3ucL%&!{Oj5Y`VDRM@}Jf5Rp;Q14L54S{>s%S#r>iiKW1wOQmOW|e?vP9|bS zEq(Nn&+-<7C3bVhAnVgQQ`!iSG)5f;&0>K2rFk~)g&;}mf-!Ilv4y78>VYn_R@%PT z{+G`H6%GJ6&T$tg>n|ckySEQNE^!o0ALSG&uQkUbE`zU+6ntcRQ`-&I2gKc-O zD>D(8>Fr*awR5jvx$dQo4fKNj*JJ1mx3g1v*D6IW> z^Fc(mzikjt3|{04cg_Uo+|KM9?($WFx4ewNibdS?LN6AUMg&7wN-716?c>F?E%xXlfhT=qdBysb^)el^z+ZiVdcQo*ZfK z5R&yLU_rkZ58vTx-!1otLEF8Pcn*!H4Zme}6rzWlbdkdV55a4X+tMMN`a<2YW9Fo@ zV0p2YZS+@0pH|@p7lX62wtRy@-8!Z7H%{HVEO`XDAq;Xy_5uGthzCTx$Qr7fdoCt) zj`T9ZxzvkA-_zlgV93fD-$=*QR!$%B6{$K3RrvJCQU|IP>=ueM8S|!n6tp?piwa)O zGHN0#|EZ+UP9s+w)@j`ByTKbbqx%Kd!l%TrmGa|w-1|UD7d@%D96@|2coF5p(SF1OF3)~?Kt%3^76nOW&(_&{V zJi4mD?4G+fXBl|b3`)k3(2NEZ->pIPE;|J`) zUH^Aodu~~cvQzA9u5tN<+ZkJdI%}eeYVvbt8+3a4=L>y`Y`uUlbN`jfU$w5{HT;Im zM5U^Lr&v`{q0;BpE{S7?Pv$YFOt+7O((+}EUSZH<+9{_q{?1$H4T+YOXBx*}7fxz7 zBM}=J1cU6B6#Y(>z9!^Y5Hu~aV2%5hHlR%ZOV>%c?Vp@y6iQxD29&bAqhw+UGZ>*W z;x@fkH>Xz3$VaOJ{%Y+$qWT?LGGV)JPLne-;~X3V-TwVTo>*DCtsA}{k_dl{p= zV{G~_7=Hfk{PVD@m-8IgA`1MdJUdEHi?bhg*I5^)x~~*Mf-hI7{<2;)39(hI@&(e5 zj)!))DR>59IT567PeVGzfcJ_vmx z0!Vd+gW1RGW+A;_R?S|{qp98&{Nn=u7=!;8_kr~RUjCss+b#{@m@kZ?rKtWDnksIlhZx!Ay<}k!rgOO-t_{br;NBi)fu$gg#KU+dtYW zw%;O-v0pAtlY99ZuJ%Ny>|WJr5PUD@*F5^_Q@SEVz2PP`0Cf)io$t(~NQ#4qVm!+_S{mNH3li7Xp z`y1aYLyGW+K@PG8_VY+11e{Gt5IGF4hd1AIe>qn+>Uh8Xa2>z>*gtp6r=FqiB^Bid z0r{P(+4|8z=5Bf5Ryv&`^>3crEKp4H1(%C{Rz-yah1!KE-}#S&lFc^=HkVhnRIwot zDMLK`k2-v+JIV{u(fC(Dl?l!j4t^6% zdB+BaVB=KSMqb%dvuke9YvT{o0FePM3OTtl9-!n? zF}-KTi`*GYrI^QSKu`+O)b78f^wg^m4AJ{aK*FK>nDLi861;neRjKe$XtQ?0`aJ>6V9N^X&HPP~^6WLE>apDcl;=q#ys)FDw@-RRYKowBt zn9a(P;6Uy^5O${d9HP8B5=CGG(LC>M)kjArzZ}TDlP^94(BBHAMx{s=)2YNWV{81!|M|{nvY0E%_{qFjso?BR+)cHPy z8uz3Yb~BTBh}YhA*tlT5zu=4@x>|!h0^&9f0AzF?iC-Pssg)8hXC!U;9+Z5SG*W~0 z^P?{=1pE$!?B6&?Puapn(Vw!YFl1XNHyf{HkBv2vSCpbFVL?`D1SL3ltJ0}AL;2PRe-jBD=VE)kvnXjYW-+B57AsA&Nusv#E^qg`NhcI z+%pBqN^^keFeqPd95T$)*q({#Do%V!;L-&aYk!j6fcJd}A_&8Sm)@qO!&@D}X7@CD zrEH^jnG)!LmX9&ql`0@!)cw+fLs6GqO^UV-C}Baks2r#6tY799?%D!AfqaETqwfj2 zoW}p<`SEYxy%PiR4@v+Y5qNoFmqW|MQcH1HYQvB?f$s%t8GVL$xDNvU&+fP~~d(aU5kYEikDSy5=QxY|Jf0`%_J6#o*uE^`Xgq)xe>pay ztQ zI%!PfsBUmBF05O>g0Ecth#_lEht&=*NjpUe4^igj(gGmgl~8ooiwVD6e9fKJr?P!; zB>@1Zbf*&KthcH>cvGCy0{KE=L`7eqA3V+F7auqha}0K-_5g=-l;& zEzZ%ddxrhR`XN-qCfuW<0U3l0Ybx=1=J;KJ83_ZL0$`p^u$@!>DdB#lolcdhwvi-D zol|!9L)g%WQ^?5g9P)K}gZyN+J~qu6?rx^ zJh>;{f8(BqoF0(HYYH&L`BnlJd@|>Q*36_rF_qAqnxyg>ag=+HLmlt^j>szky!+JD zTh0ubiK&^OeuopAk9wVlP}4bGs#JPS(LEDfLW|fOjzqB43P)si(pCgDx)6?r@8^;4 zQoj^jR?%Gd5EqJ7M1rP&L4i4yYkba@dBpH( zU;FzxT4_JAcjrQGG{EhOC2~LwtnEU)=>4SEs@a*_)oykgbVWPW7sVhHGBNk?6s1KQFVj-^+d|oRJ4Ez5eNp^9rZ?=XAC1-Q1gt-?^G( z@rq8P<;(C>IiC<`Hji!17yhG}|Kfx8l#mCC@<0LQA~vxLdbK@LwIEW~ycV7p??+PrBDr4o1RfA60EcUZZ7j2rcJ(iHyi`K^+U zZq&1l13>Ao7dE*3Pde2})$4pV8vQZfZ_mbhjn=LPDLb;D(omn7K1I9hx@%-4POtl| z%AqlehO5+v_+~}be7kenIZ}_{*A}>=B0c#om45Y*2~Csm$ow!7Wv(}=g9|{b3B{kd zV@MQeVo_Igo=6eVV|7_0p1CoG)tAeugjR)aJQXRT>GclN6jcY%$0iv+3IO=f*}qK( z2i(Z?j3pPqs7%^qOtRw~{!Sj-UEk|=;X+Z_m`9Or17SPPTmy=^McdU{H^g8+4O zD5F49>ue{oul2+NS5ZO6jsx@|)jg4vO)Zio>NFxLjDHw7WVZ_$i{jZOr_`G$DMsx^0WOI zd?OTgIYS3o_{A^&^%v(p{^+lA0nr5HLEWdT{SgUmxe-CW&^d%@$^!(f%aOilh0JD# zHT46QZu-^wvDB)L`tiJ@wcW&S3l&t{CAP|>?< z-+-?0Tx(SYG*>X7vwTq|eV0uwxwMN&0kqr^iLIrLUhLY+1wy?4(dva%q(55HlsaTc zr6JdRoi*8x2!!}53*h_;`WDwdR%Jr>B(Cv{vQm9-S0PUotfQ~RczD_E12*ujfa6&X zjWufkPIKlqct8?F7BNE)yc>>MoDX2dRxAP&to_P4aHeRzPY>i}yFUZbfYLxeQBN4h z{yE@yUu+_l04P6B&yB7E0YAX({~vh#rU_lG?4L`+wiqR&hC;Xysap-DQ0DZ#z^eE_ zT-_bUN?Ie2e(t>K(tVD7AzK>O9`LC=J9v&fd#RgESh}YlRv#$OWNLGYG-Ucd)dLDF zqNL^>LLh6xiVi{@c`q&K1V@p?9K}R_*ewcT+zN z1L62Dvzx49s>PAjQz|oo>jAEb)0~J7I!H1fC2zE3?g#QOjU7vLM`fR8BLK%mz($mv z?qp>&Y0sNMz&lG^4r2V=#bgX1GK%t9EvjN_=Q)-G=>*`*Hxmc%2^!D>)uDRq*A5mj zYZPRH7b5pBXlr+K09M7{AI4Av0IRtjQma#qTE<>^e_Y%9@zdE^hhHi13K-R@>NQAt z%Jq8~zvR&52Q+(HR?nW6{bWQQzd`y5tSyB5ttZXXrMOp*UE-w1>l>aeSV-Q4LLfEX zzv~HLP`X{$@rXlYVFtU4vK`}pWQT0Byf*^1C}Z$<6P)Er52+Gy9C0w0bkdpYeq2!4 zO8jiL?76D-HzDUXOP}4N-U`>PM&^G< zC9Mx~(@Rzgz~s~7ouLT5HDn)@j8W4(5Egq*<7zJ+w(<(Mq~@7N z9LTj7uI?6*O&apy%XC{ zQ^it@Gup)UCDz`QQ``-vb>YD^AgwvXFl1#F;h$pNm=si+d2~+^a#Y6vxMwb8AL`~aHsum zC4h*+A_it_Zc-GZo$^2P-*rYx>}nb8p+p26qFM03T(&1h$eVAMXP0a&{iqrtg{DuY~aY+3QULf zr_I=~C!){88mjmEM%O(R)W*6_z(j;B{}TxXBXjlEwBwcC+Mm*S|JMCPI%Pn=&YzOr z8c+^+LRDdLbl{%0l3{lD<^JO5#BqnuS$$a{CG(R)QnP=eaO?RUyX<@EenPAA6jVOso4PFRHfnI42LoH3b#%6O~X2REeMKqf=xc_F}4gV%* zWOi0TabZ$=ZLsc&JXB&$C7=tKXQ0`UQjjl-W4LiD&@#nM;x7h=kUTZOa*#7#11YeJ zto+czumyM_s*dkR8%Z}U#!71yz~p_KiA1RM5m@pb@%jL4tys8iF49$O)I8$*dDN32 z1>xJi9vo^e@6S^5+@0=`&~(mKftCD_64CBKwat9M1{TsxdtoG%-z&eha>LX0iI%|v zc+wpkjo50uCVW3|T~2OSV)lO51N2#&a@?N@>AK=GQ_E95FaUkCOm!c&0`qorK`5}| zd49ChVMUvjg;FDsr2(DmXDHAk1Hg6tE7~^-Ih!bQ@MIXuw7Aj6oo}7F1x0<2;;j$R zSRsH%2_Ot&QFtn}Cfu)Y?hbLv-lBDFcWzC5hzj0w7PxK>IaBC!4<;u+WC7jJuFRad z*V&+t2!x<-ctVZM=LkSrKW{SLf>O~VtFO8Kj(Q2!>3-?zMs28Ebx}#ISoJ|I(~Md2 z$OeS&SyW!`szB|Y_RF#nmtS{QGM(d|XRDaKEsKXy)bKg7JW`V zj}Cbvk_EmzW1EwvNlQZ?Cbe|u=1!tl5PH_qGd>*x*IS+PiPMhl8UgG3{b{O1DuwU) zdy&Eg>e)EWxIVGGIq(Tg%=bJee&{9#@CGfD9f&-OB8HhS){hKc)BQYAa_FT26jrc@ zV)+I}TpOx42V6=i97ug+{pg&bu4UA|nGUzDc~PVFfWjOwA5nRz0qYO*^@Rt3MMxS~-I(T@FQV!41)DelU;($ktYCY?R zZg3Z11fpkg3k91j;IAMb{ER=7cJ` z1R!aX;-YH7>X>*B5q?o1;OYVV8xY5TT;6 zd)q^0Z>16tlrFk`^fc@Zi;MD^{B07z;^s&k>krwz???GZJZDr}oh2VTD<&pB;LCiX zaKcr7y|mzj6c)Y*(H62bdQn|Aut39!IddnHN)5^C28>*=I#DJFgptSw^vcl81aw^G zMenp({J<(d7nx6wz<1B`zYS<2x(3*_(s_ln4q>i5v(q}V%l+NMcgU2HKh4P-!CfO$ zA#^mMG_N?{{K&i-(Jlm@Qz82EPdNrJ^lfl>d47;paZ0FPa)_;tapO_1xUvFD;3q8O z;U@>#ffDjt)LcQR>IhY%PjklmXKG=hYkSy(zU_lvU9wY-h4`R8oTN~;R%U3=in7)b zC3T61D5fv#gWw6r0DKcDp>K%9?cb{O&EEG|e+H|Es{s24EGn*)23cAu0~!NhQzsDv z#+ox$ta$a^Z-XXzeL4l;Eg_z}xRhEMZd)3h1`t}HvM=U|AwGprY2o#ivX1T60JjM& z=5fJ%Uc8XAJ5Z$5ff#^ zEa^>BWPGssnUMca=kXlUNZ4gn_(q^N=N!wAJg1GsvD}Wg((JllAEx@pXUmrWVk+4* zZ2hREQhBT5w@2M%(;QQIi&u>}l?U0+uJGx$-pF5v20;rg@d2T|u>z(hUqU7x;5fAcZFA}Y331J8HJ-1cK9z&1 zdGj+d>m?J^fPxRf*%eHCp6+{u)SP{sU%?iP?A1D)s%odm31h4>Ce3gcxHv(#4ZPTn z8|wPDb$5ATU3M9TOdyhN%)u(C6Nu%UV~`c#$A8)W@k#%>ZWX{Cb}a+jHwSXgQ3q`) zn4|##0|3={6vyQul~&-;emW25@kTZwy0by(0hMmIjj$QsfHUTAw13fzMQtCFPe*94 zxed-TRE2vVz$vsVN7Xf(R(MsI9k;{nycE>NCd%XeTeRPK+S3S@o;uEo^?td0EheD2 z)>50#^eL({O z$Dox-w<4Y|Bd41o%Td2o)$k8iI#Rqr1a&QyNlo;}gI85h)zdiwu#Km-JcaYu$uD|a zD<~U0A_ix6_m&kS9x=`*x3bW6v}%FK!)gVp8>oP7i6l~T*C9Xda5Ug0&j>VIotoSQ zoH?O*iUZiEx(ua0wK_SnU(b79yHgD?V>0A`g2f)B{gW%#+XgwON&M{{BUp$ljNopU zQtC%f>H+TQ(%iU8aD3=ip{g!6#Crx{?92@Vb8Y^6TZW=XBE|O- zRf-Z|o({Fw@M($nR}DNcE$w)d9N7HJi`*_N5aty{Xl7%Jq4um-9AmrX1a(*Rl(tYQ zGdSjiJ1JFHU2p9V&|7PzxWD0VgK1Wl-atG5{m}Fs`6&t>8fS*Y!_pnyDZEe^o)2H3 zsNJmJENuZ|x5f4Z{IiSiz|-oo=!!Ixssbf^9%g0hVN4Nrbg*^vwd&-pXOc#xd2{PI!G#gERt~ zAhumgD+b(cf7{SW*zXj2BE>1O0T#`?APE}~DXC<*-gcCi0d63Nf zS3ar-&^GtDlo276cf&sMr<13OZ(eIhNTbMb3d2yFJ^Im|`@T|-| zv!I;3;2M348mI0H(<#l^dLXk|F7=`1CIJ{vFy!W8$-xLXjGgp8eR$E!e#983m*e_; zh@VLs=2(FjM7K_mV+R3|R|HEQTWsQa9iM#ebV%OMD%$GRXR1muAGkzLV}J32-LL|9 zasca#2Nm_z;{1QOc(B}>^8PnqkK}*1_%K&)L`_r-QW?~K z09?RIVc6y0iT66EM$y~2-2lz6G-pCGwY{=Z8F}|L_X%m{6&CS-^Aa)bVE-q_5D?$W zwz-@Bx9F_H7ZM!<=ce$5)V&wxEmE7PV67(p+9mNEUym>0-G)n_tPkLTqsVnsr`-tf(8`Hm~xmtq@n+T{P>BLng-x z4$BMLpSk5;)h)Z?JWqP(zK}VYPsp=wDMiV+z*!jrW%KCLgE0*`M647 z{|Th9&Ax!ASd!II9$9PS*eS}fC}?>I0_Hz3oxDk&vgJWn0r!D=Z-g@%P$`M&b^Hh2 z)>|81gtY^?J}_qjXZboInb&lurn0KgO)(aLf~d`4~=Uij;fG0s^E%a$qyeIL7#SyN>sI`x`ysc>v%V zVS0~ZdQ>L?UO{v!7=Ix)ISp3-Wyf+qAz&A!FaxEI5Q51hk z>;D_g>@4c4N>uy}6;UjGgw3P}yME)+=@O$TKoULkV;MQqJz&grL`61Ka{?p$b|LAy zYtQxk1!DK^AgFaNCaG|OltS3V6}&y7Ry|bnBlW87WQXUe9vv2-NGw!6k?ke)0?M|K^@rElH_@y|(9n|f`fBBU>1IU_zQ5DHGV%8#I@(;wS2fz5;Kfm+afBKLP0b{S;;(WUo zRv1yvorMUViZ*lmFe4=;BthZESZC-_4EE-V*BGK4FB}`oZz857`UC(*Vv8rNvt$MO zk@k)>6j#U<%i1wBj!)Y@a(wFD+4FQd|KP0^hxJVUI5IW$K%GPW_;g%TXaRggr>F9` zJ?Q8Bfz+bUTKJ1_J0qP_ZuHF6!6syCbyh}3oH1*kBI*r^{VVxj3Q6dOfNI)ArX9(zd>FVP8sVOdgWJ5rD!W$d{@6HdF z=GqMn&w_Jx@YCXPtO(klx_TjjDfqWAy&v!*%BzuH-r9eh^1tMWHBX34xhbRoqAO6+ zRCGVJq&rd^WUW|0PVcbKxf=Uc^7*E!u}@Tl!8nsK`@m9H+UkM`tD7|iIAD0TNnwhdw>U~CZ$I5QpUr)or4H$KUQBFliE*4KCrr`v63T)` zf6`>Mh&MT;pr@?vna>+z$V@@_QNXHUiZk*oz(0U?lF&3iOxdKl#KBj2gQ^gWOVbD4 zkRI&a;j|UjwyP@xdCB6zl>x?}TK+K;y6qc@&tt&!X8aBrHvmv2-1Y-^_Y>QJf z${qs=qun4Rf;7*nWRU{hcWVY8d589ES8@i!3nj7a@Kd&803tFF3 za5f$@pJo=%zCh$1NeBSA9;g1^`Ds*%`ZU#_+`@c1r;6yrC z`O_80Y#pH|q7ko7Xi~?bIih?BUlk8)=op0^icql4s?ZlOEt=`xRl#J~%s_c@WY3W#)%?iawwzE)R7-3OIuc;#`J;9p3t3W*4s}ud?vC&x z@nqiEP$bQrK73=c3x!boAco{WhwXCP;QU3BZJv);vU8dq3_BaN+j=6_4ZFk%rJoGX z5W@7tNXf?JK7FK(m3fDuv_z!MUK5`d zS^aiY=Jyd#GAiXo1bCfgW2@A>Gj zX?oaCFM&Jy7^7BvnjaI=n@@eJ`r>?w$&;opXxnpP`-xOGq<@1uZ~ik zcDsphKAA~`fsy5o2Y2|eg_k$VuVNlcr{S6hsl!Sbnr%B6t9IWjBVt#U){_g45@+7V zvKEjVRe%#MdJtbc7x29lmID?#m#|SGL+mjp`x615tzMY^7kFmC)gwOst~Gy5NHeTB zBuDq^o>vq~f<#tEX)14L<1aL4e@!^MQRZ%1(xw=>`h~vVBEQ;?8XiC&CJuY6okQPt zspE>Lx`yi8*1{s5m*{8>PyLI-&quSYI<%r?#r9VrZni>dbX=)tJ06hOeNcQ-09^{w?6XDgl^x&dNM);_(8 zQG&C0paS#~xS50Eq_XAOr9>ZTsvpz`ZucX4Yv(O+_2yj8yw<`f5DsInmhkTh2qV%V zMkq@Vi4P^4z^>zxPF>xMtXPCmO|8o*S7?)qub2m3VvVxV&fS!&(OKReBUi5EV?*<)4rewXolTLowcKI9n)sBD zX|^dW!Jh2>LJ<5YGRarn)XEz|CQ4gcatWuF%n#EStd=}HP8j&4q&*Q9QL3BugNevI zeTrY?QAc%a?+?fg!#DHTx>lCSKgt*3cSL*sjFS`sw}#sc_1H*u0q^vy_;a*Vry6-j3E9h}5;L zENxR<$M(nNad*gA0i(CSJ!zuC{cHcn!EWJE`G(TOD?9^sfeQZ0b0FIvbdwgq>b)Bl z>f$F6dLs9alK)B3fInO~NR6^~74K%aWc0$<$mLH11K6G{$@={u+Pytd zy5rrG>8`)L)$kLG6lAMaix0woUsya*dx;Vw*4zzCH{N9C zFO`u5`CCBnr34muqt8RtaQg)R-yDPrMb7fzRy&*m;Y)EYo*qkKGd+B@J5H+o0xO0d zdxb@}SPMq$oM3x3Bxn8J(+$XN*{jm!l;x$d_Wx0b?Eo2=g#}5AsA4JcMo`}R=-&S1 zaQiue30^Lq3Yt}f(9oMRHNvU{YKtfQv|FJ)rD&LzYp(o-8@0-KF(9C&xMDL45(ALF zB)N;eVsm^zo9PsMvQX&Y4I#J!2(SekE>y6WK-GG&>(n%2w{>v?6#|bAP?337CysO5MG*C=6_FCVkh=7!$R*`#RPJDg zt>~kr9ztPHCaJzfzrip1T?q$uy{%b&bu$@#*nby@P*E^{anHUY8?rrkXL~Q{JbFnr z-dhHQJnMP5j&PFJtjd5xCfwiUfjIA5W4+;2e$=g2Ic zvWqSMI2!2-xlx=P;k@&_j62Z#UED4d$!9!&kpHcab=5bbV9DgA>KQ&#m?|H}xHZkt zeK6MPqL~k*y%#glH{30m{=SDRxL1q=z5BIhdbY3xltslHC5J+J+OGqI^&S^?m$#&D zC`U#(bJL6`Ev4?&3015Mp|_nSTvcTM%es19)?ggZXEHi+uBx6l=%184+eGQd=cBLl3FSdP+9+$qM49J3`r`x^d zYE>*N#3|E9*NDw)-h?SQpv<~wfd$qv6fg9E z3ht%azQp=Vamy{9QAB#9lDhMX)r}VHv|Yp;liA@U*O7 z70%#TqN?5Oh*D#9Sw6;@`h~YN74T)eTl~^f)dDNOY|HeaCY&UUIA$-prwpr4C9g2M z1L!G)^%ulV&4?AxcM9-G#j1`1TqX zR#{P`WJ`#_P&<+0-j02#+Y8&h!HqHE2*s?$xm#S0&*;f-@{S)pD#U*lu^d<+{UDq9 zI5*zOmO|p%RL&v^d+x1~tPiq~E-;Y7@a<+MkCK>GLKiX3{<Um; zvcaX8qxro1f!;@8MeiUXZfs|4$}{%T7u_!bowS>3>+K;f<%$_WNi6L#(W14o1b9V8 zLuQ?2tky#w!JgVIVW;1#07xWw>!@!dr+RTY@-jekMo+&k!w^*dCJbo zd1E%S_-ebf^04t+VpGyNex&eFSkXHr6Yqks*jgbW?b6kO^;JMuH8J`L@r*VXy%0w` z{DdN6!pV<$O^La2`Lc*uxdEwNNvl9(YEt>xwoySTH8`YCnT(&_Ppd8rqzpi6jWlJPuc3S&<TkH-pacoPsYyNIX@|RYIYVnJqxr??DH%xFSiWIf4GSXhrdDdpbQgmytL}VE&3T~b z%6)3Gl{A=6Tkjjs0{$Ht;`p(I+;shh;r?6ceuOnJTTh~?rvBW-ia6mIbZkZfpS@wI zG~f}xlH_IUiyd&US$T(a73tsu!y~Ed!H1FN#+Fsn|?o6*1uR(Uk zwdBtJCH?N*H@T+iT7Dt5kpJ>=x^LIy)$20qn^@y=orET7&sjcL(%fqN(vPgLwCR6-r@EvqZz**-B@VUa1bH87_cnU-V9C{pKQBcJcImGiqo^A`CS-DbP?8{ zjCBVTKS$PS_@3FO&tvd=u~Z)2acy&!b|>W?*Bz{7=Pzho14BAh2sfNeE{#q}E@A7h*I{7O0%`kHA65#V+`Ae98EgyBOUwOV?aPjsq{_YC4LhXL z>n&TYD=*5ppC?WT5xD%N8}JLhqY$J)u}-4v4F76G2{y8 z9ou(*iWkSWGM)t@O75Ncih433F}X9ZHEEZl3rtXpjau6rT8O$Rn0q zWl}vBA~UYmF1EWLOt%=I)AOmW1J3%oV0tzHdkm1o}-#XX5+;` zsg!tPzAd6rJ>0vWUR#0f4Bw);?GL!7argY!?L_Bb0?Y5=m7&q*8?>{oD54hq$1%Ml zk@VyId%-`RZ=5B_nld`JV?ecE`+1d>L3v6kNB#Ujv!uAxnWo_D%}`J~#E#8;l~ywT z^YP|n``z{*C!hd&O2>{L2X3*#VxiJ9*r+F75oV(eICejE?&o z-n>_5?dwt)z+a1y*<19^mlo3~uc@i9E1l-gd-WoI`L=kO!EeHH2S_6$0O9Q0=}TFt z+$t($`a(tebxt~4Z2_N;&prvoKa-MaQ5r?FEe4(zw?F2|rTE^up%^HE_d@v3j9DQN z!SxawsXOf;|F^_*B7vYE2ykF*Mcr^q5IJdGoLb!3yTkpBM|zUsAT~h2W5vD|P^p}% zM$z`*Q2++xUTm~e9f@o|iMix0bouJTVBIyU$$f=FdCqt8VJDUk`X*qOmb$7IdM#VF zk;uGKxvGc%xPQNpb`?{-vjwE-ki9*2#W=K$3Oo=m-=@0MHOBQN71_$B9ME5<|2mO+ z0{9`plnrU8Bs$1%kk+9N;k^VZRk{Ax^`>{nWG`tk66+~%7tTe0&aEk}DyO0;ZabvE z!SML4$`B!Cb+Cw+*H+$L;kO5*5+wAbp~|27AIHxt>JVrd)g_!#jt`Cw^CP5y(UT@3 zG1m6#^N}U<1&{>&q%4z}zn&8wK~=uI2u>d~Sj6Sa?xiquis6Eft@DJ7gypi?dLR{|SKc2YxeK+6`IJESurIQK^#nrC+BcNf8h zUnfzqy{(F1A%GzAdA5I3Z|mqz);xPJth4z;UjI5zAvZeR8N`F9h`6G#AK3CG z>JC#dE+j(}=!$AR`f-r3NiCYXJhV2KV|b~Z{nJu+CNgtf9FiZCjx7!^#uPSBJ043D z3aN%{xbRG+@&&S$r>70Yr!MkX$<~V-dty10X@orZ7Wb3tDB{-i%W3*izYsygVOr5d z?;nvc#^EcD$1#%pVPxG!aUyq>7&GwzqbH%Sa-p*=t_rFMIC3)#cvDL{)TFr2B50K6 z$GG>A||+B;2vk2hqIb7hnc?QE5cko;djTNwbG)VZ(RQl zcc=dm1OUIm+fVBMllK1BiQv{m$~aR?AlEHdPulc5 zo->ejchfn?=qLOxjF53pQ#p1|%6eMizjS#&SWWVQS^Y}R3cpZ~N7Z%p2rwJ)_J}po z!hTIh3-Hbn1qd~qSS5{JHvf3Ncd;0DVJMLd~uretn&r(`r0G4Ye4nu89PV{!9q245GPWw z4OHF6nt&`z<<#3Uw-VMRz2$ProZeL&1Jopt?TdBW>B5_wfrm|T^Vad&ufRnFVv?XH z7_R(}q=0Y7vV1UF(J(UmNk3M?rq7KZ0HGypqRSo@ywN{K#uhCkxH=f~E6PRW#1TqQ z6bWCEK14Mo_p5o4A7{Y41{3*n3k;4B3cnPgt(;&U0!PZ+Dknh zo-xD8O^GXBj>GpRx_Maluw2`b=;?a376Za0FC|hRYl7}H?=X)gBqaXXQyAidpO7Rl z|6O)}A?~XLHzMh91O0eHXPqZ2vaxUT<_`l4Mv^B%Z|#`Jv=ns(KqY(=HNROpzrlcC zUb<2p4iv9cgZxhdS+D7yk(4M>U(I=@&BYc_p$m{c)mmTdTheLS&jNCOuw?oj?P={E z&S=Pr7Zh(RRi$RBE?EW#Y3>>#W}wZxbfG$%F-G4kE#2N)#*T-Z-p{pfWn!&+p(k~3 zg2Q)4l7KXewQ-#N5_EWbKM7(i&XxiBTDN#P4!F{r7|`3d=wVN>wTt2SXX;bSn}qjv zO3-h+gQS>hq|D3fP4t+UvwmjDw7j6%I)YDL+FnzC+*#58XAwZmLab@RN4a$u)(s)X zd{CvZAf4-YV7xpxnkgRDrhiSkJW||m5wzIMQgj&Gn1E_^+07bym-^aKzua{NV|Y40 zAC_-fdfY%D#u1&is|lbBA|XVbvq+V|TreKb$2B7UDchS|wE3({n{*J(K7l;Z*pMPp zSZ*DR1vei|mz!a^;`tPZz+IcfpUmJn zfbj)3VS2$xtMfrqD?$%oI59xnmoZ@nUcCuGd@4WP{8whb&&^<(lM%I{chpSUGxz}V z2jl;oep-TkTf$GUV{!8;Yw$Dk+?%=*<)QE%Q~N6>wb!KG>fq~9f6=zT0Su!8>AP{# zcMD+X4e4^Kdzvp5r3lUKPH}8}ZMn{flC|GZqMBNc!8 zqW&h95c;s@^--STM`7Z?ZdtHM>?78LQ`)+hDCp?IB&_ac9Tc^RfbD( z)?=S!?G~NaxI5H&tr@Iu*((X{)PFz~a8Z@p{%erpw< z#hefxLs6qlmfwAMuQPkS^@#`9!%F%8JHSN^&9FY6-f9;Lxe!(}{)R9J`OE*pM^?JSPV z9*;fK`YhuI(q%KQgv5H?*JX2dtKI`K4rFx|v`f2jUVqAlx~y%};3zO62sSgZHgT$rOCovATjO zbhx><=+c#q@rn_sE^c{aX)YFwC9OkGJ4HARi3eVhW`mB`s!gw&6Tzxau~kDwS*h)? z>HD(W~ORCi-my*&`|rUMa$2hi@##Khpt zp}ax|a-T4=ujp)?%k=$l)xu(bgi?J`x%BCj75<^HQ`e>pCcuMW?IW7ss{eZyo#oD6 zv{^QsWUTHk&x5+uqE$0UXhxkNb-ph8%N${9x1q^; z|3T)WW*Io?V+SPo$tyXX(I-hH(ER8z!QNgQpQaoK6I+sS9_5re#Cwov-yNT$KBPu&q7uQbpRW3cHSdfq+9?%0ycqE@ zQR2w(sCT3Iroe?}X4JLfh8h73_u2n=&=S6jLKMkoNIoyXTEGwBOL%^Pm^_PAaE8s+ z%Gnb~eA@6I@f%{Y7l%JN4aQj;e6&62@zEQgZ07M!@{v!3gs{*$fqJO2vP!M<15HTj z+Upn&xt?*WVW7Ft;aC7|CiSbvMIYzzg79R!O!YjTVeB!6UXG3&_38%`P+0GCEsn=I z^xPW0d}C#pP?kT_MZR=?zi|ZF$-X^+COs@HF$ua;BMjZFa1Wn$P2aJ)UREU^LvE+s zMl_{$Mp)zL!SFo9#P{%^Yto@zGhii+EG?Tt$RD}4=i=jxDN#chvbr(A>lif*2lXWq zgsKsjglnTmTLtDLW|rh*@@Q)e=t6e0^5o>^MJoJ~Wp_9)Le=!)pyQ-ilwXZ!#?Y!g zEaN12PSjV7b$lZ|4-c1g>}INzct7Wvv2UfBoxIc`2H%=~y0v);1qiiR6ug`|5zm8y zB=R6{-0FUx^1Rbs=r-1aZgP8C+?Yu)-o0eKQj_I2@K$oDBX1`u|}$iC#gh zcV%_05KI^164hAXRK8Z==}E`;$B=C8%41>%`d;*bEzYH~rLOK(Q=!S2LSZ`t0*0CT zIo@4-odG}F4qCGT4VStVzr_QKX|W4eJL=vRs?#abG64Vsx@71!v;5(qQ|i<$`HV`Cj_lR@iyND_Z0&Xx58Qp~vgywnkDI(k0h3r)N43nw;E$ z!us3l!nW~*uqD@sf-J=cx|HuNDyY#_MRL)9p49EgmT#T*b#% zclSoh&`;dRo?Kev(==h`h0f?cbdU{vm5wS9g{nx{>1k+$O-q*dZai_OP(=K6iVjI` zI*Mi+dq#eaK}lu0q1@ZR#1|m4s`@)R+h`^AvlK9?nx+ACZ1nK;c&!4Aj59gOjX6d% z+8p+a!$?6R&9a>JYNcd~UFrkE9=B}(5u%6G`{N*pZuT9}`xn=j0cZruQH7{PbJ79I zK}xg|lchpLW8v5IW;lnVA9_YkmZ!a~w-9e2+bjBc3clV9;UKmAk!_Pf!fdG?4Tj#d zbajAH(C}@9+L*fu33SZzcphjf|zd3&r;8ElTmcwpaz9T8qzmv1X53!BWdcy z=;Qi}bYq|55H_@;Uws)A%r4gVT6E}8#-j^@kJl!u!9;F?wT_jevrgWWU_Q@MQ#R5w z+8jj?-t<)gy7B#%#e^T?FKe$IT+XXGS`6I!3<929_Hx5l_S!Xq#$}Ha6)i(H3}CCT z0n#Y{t4N8Qq#cbp3etW>XTMh*x4+uqH6a&VTvQsrDiKUZU$I*>{8M%yZ*XxvR<&rf z^w1_3#vPlF$pSW8Z3s%&pg`?Ew*Ex?^5Lv^%fl(nM`-iMxSYC;m0I2^U|^hbQGwuu zsVi=#LF9RTCePB9<}N7Bat&Wj8-EpPEU6~IL8t<>WOcmtMsT}R%TvDP+Pe|_3679= z%^#=et(msIG!O3D_nDxdJ4SNK$>_8GZfj4FZh~`eSqd>~C)>|q<&^ZDE@Tl^o9YU4 z)_Pb~q^Jt>FS{7*vSlViV{ZXP8V?~&)#Nb0MVz-1Q6PMiSqkP*3bt^pY43y7!)X5lYxZhoIVsx3G1ecMgFFi^XKmhPSW;E3bzVTUXG3DV8%r+ zg^rPbr@iwFI%C8E(*3kJCE>)TKX z)JCW)gaJiCv5{^xM%jc5;&|N!crCM#e}+jkbe{so>6PJ}jVo&yY_c0Ch6P5C0K-@S z0m+2gHn-a6--4X)8ijH1SXZKb3`a!=v>LvQXA((Trabu5?%?bu7HDE7A)q5pfh$)H znCxRjvDp-J7OisP98>~Yz=-iUL}^6idU;&^b{Vho8xkx^q-URn-f~Px62I2R#8+S9u?6Fp$#${G($e)U>Qi+P!o=X1!g5ak|MqL z7<*#(cA0?2(lj1un>0>~38y&ylfF}z$$gg9K3+_SI)Oo$QY^CqmfKaZp<& zW>%232BBX6l4YGg!ex_0PTagKq$p5?pjY4~j zU#@G<>GXy80{q9akI#k|M1jUGX=&-}<%h*z11qM=4|=q33=X^cSkWUZmRy~W(WR3m z=e5}|#M4$6+K5KIue!Cp`6!u7D|2FL!lr(rmE}s{#1|1583VmIKU8FjzD*7SwxnQm zQv3|mG9pVi46i7@JQ|w{uq@HQ$+01Gq`MBy{@L>F@!d6}cZ1XY^Z~UMVlIBr{%A3F z^f{UhEB9v`OG9(htZ2crlPlEuAPOS0z$r zTiq%hCMMsn%X z)J)NrDY3L)hG91P^9N<%2K29Kn$}ESUEET4kJBybL=65xSZ$enI0G?@*>61+<}dKU z_myBlvhWOYWQgj7H;>5uIvsJL#E7Qx4HjzJzJ`zv=C(1Hw{`%G7gvw0 zn{|xBaBd?bs~QLnJ*^9#4VxJ;ec2cL2^l=|@f$u5-ZGqHmwXa6R!uty@HFbV?(wYMV&~Pp5w_wmqza>w%wY1URp%OWhHJpK*AW5;*ujuk>b^MFqO!_ zH6S@mdcd4PtFfFgD>4^5zCYZ9PwUI+V^BF`Pj3w!VmG=Wj%l_)L`~x<(XZc8^)x-! zCXLH@#{c#E9V1n@eEa~YP3~0RF;BtAomhVYRYV>`S3kaE$+cod27`*FXySm&!K`ht zy5=%ob_Y~Pp{#|T9qih3XITN}9_=In8%($;+p(c#&osD!nyEr|$0m7z6(c>kmFe;E^j0k8Fl(&Z);a$oT{#x$OkTD+8^>G1tf(gpGj;)pBqJz$*|UYln}zTm4oT(gIYKy6$Q@l=YnZ=YK)&#itG!C0ICa{fYuEq@YhE;>tO z7h075<_PGPmxxY?s=$GB9q=q^+=gZ-XetYwgJdG@DnuX$pj^~fK}SZwmObvPB!wyo z6MX5(9h(wZa8$Im^SYuZ>E zZKJjfEZ44G*3Ab-#I>2V!$od9MbFaPC7F{AtfFa?io5229u2Y`rbOxwTGhSXVq!Zj zF*%GS?f6+*bTs@}ZK|%3HXa+AQ*2TBLtQ9r#K}B|ypphj7}obhScVQrk9iZ&Iq1dv z+Y-)R{bOhqCxtX6ZgDxj_bf-H#FF)5=_`>*mt5I|De;qRV)B+vD^ZE6G&YG$8NfUU z1f8-$n`=LshT|=d;;+tllEOc0Ou%(G&ziA_27(Oc^<}H5a}E`=u4`M9M~7CLum4B~ zccQ23S+RG0&NeP;iqJ6rDll0@EeWCMIX+d`t3k)WQ<5t!ox0<>-Ba$=;3m;$!B4_% z8?QF_$8)fE96-*D$~0lyf?tM_ne?|+0~2u1fuX|;u^qKhuom8yzuMyyz`tN!2s)!g zA$&JdK}dhXzLG!$9W>~WzFAuYk=KpXU{eL?O|;g_N*^4^8%;7|3LUm*KMj5*D;Q*W zWQLxiRm^x6O;^lEljb4NI{B@s2Z&hQR@m~!xByU7)SU~G2iHvfi4#jW|M&>GRW-u0 zVmiB)GX-bGnq}TFA z-N=2>ggECkUGG+_T1R&+eTw9Fa(bhC;v$aM)`dkxn!q*F=u@^j-)6ZW5~u7pMO@pz z5RrO5)f4wRLjTvSqX&O@3lOb`83D9zs9oxT7bdJdT6JFMcls5JOsjm-X7#!)m=7Ms zdzUE>P%bR8%$L45N*4`FD~%Wz5MmWil1wHPSh^<@P)4Xz1?6@}siKXTDR@!ZmQMdJ z9%N|HW}8xuy3gMDu^i<0(S46oa`W)^YfJe%#;5p0bqbA*h|GG+ert**ydQ?bc`O-v6J)C${vf7(b5+=moJ4A` zc!buNCldPG@ac*OT^rgQtO~C2KrNo=LIGgNgxYP5{wjTZ_Q->Y+$8&)Tecu_*jO>N z2EK>M{xFsGvM2N;NDfH+749`l^W^`O0d(^FptshwzaNzK`sMdL1<|lOkXWr`uyNa1 ziBqDk7~wDdz3Mg>52WUqA*xys4r>hSJU>n({@&5P$s<7w$?qz0*h@G=$|R3HFq|;!nl1!&B}=$h2aaYg*nR6B!*IYj`xn3|dm@;GgL_42Wb{cQiP zFIbZ!sSIM-iL_fiX=!7(?FJiGK*CI7Z&68DOsfdu=Vm8exz}bcP}`hw)2z-h(kU_I zWIQB2mM6D&u(9F9Tz>@zwiV~}AUwKE05Ofv>2k4wlBLnNr~w7{K|+Wjw_He)jO@n2 z&%3re+RJXS*|ZR0r-puzA^^N2J#WO^t_@q**^3cbsC8ElAJ%2?+=?53iC0V2jM6KS z)qOM)H#!@9K=6-wu0~R8D-Kij<yG;|hu78Y6#Sve@~ROE+HTuTJ0U zjND9C`OnEfhbz9h#_?|2)DqS5Nu7SZl_A|b=9;e33DN=y`c+>hq+1G(gWe)Al1({7-7Qx7dgqU*+uT30=PI2fP#WBT!w=C|56|^G+!b=hzLd_Qfzv zOUhNQx3~JYBc`p9_yrws|XXQMjh-gTQQJ}cQcbZap ztCkFkSm&vb!fIF zX3{1AB+PVN94}l-aM5}8gKQ!l!?}>S!0&uO@!yzmNeDH8P}IP4gD7XhXap)SHgbLO z-1aBa7AGnO(GUgFc{l4$wqFI2g@Z`e&)j%fJ)N+Ycro-U2>1iJ2!C~9HGoB`d#>Eb zo%jWZL6r4dXZ=LC&H)*fzqq}(8XTaqcWC~O#l;UmTpbt*^^HO!%*deBs*{>4;fx^N7S9StO!vDAa6clz`Cu3c#$Un84? ze7bNQz^zWic`ZeOyuU@2)jU^{_N{XeyZ1&~JGCNJ0}vb-t&?LECU+c!k9~L6Q#Xi5 zQL8df&M;P|x0F+s3+76eA>r^3tbQ0a%Z&o9Xm+b?EBc|zB=iA4l#fS^u+r%fY_Ll!D5 z)8*S#5)XbqolbLOWApNh9cM1MgEpFNST-81I3S8|82+}B_0ucc05OlOjn&nzg{k8{ zp~)%1?cffF5Fu5Y+DEt+Tz`5uZj-XrjfJ*u_B_{qU>5*qJ54OaPQ_Zll@9`N-Dofm5E@qYWCsK$ z2j*bgRv2xOc2%$9{{0m&dw|yI5l%MvfvINoD&a3LwQ;rUXT>)fg$0&LB3m#Vq@F>f zHxv*bq=|!*qQw*H-z(g;o_dz0PBQ!D@>XionM8$kMaYy|$wDaQT31L}14_szNH!vs z3uqo~n3MHBhuMl#0&AAllV}kzXs%h*e_*Of(Damh7PWuxKhg?KZH;=xC17|NzBQTZrJ+>v8o7Z0E-PG?=9 zb;eXaj1mW_qy52f@wnchn_v(I=HYA35O@tsPthwRsn^0A*GuJ5)85J~-(WR}YwXSR z(KKUk6hzq@Kg&ML)UScq^;}5^6?}BA6qVkJj6ZhH44QWrh%Hp18*VvGwKsrwEL+`( zVxDyLru5jj224Aq%fP@EpNQ`X8av8Gr2r{BSnNGTU5)R@*mCf=3!u?0uofE`kyR|d zUe$p1ZrdtjS_Y9t>D1*?5EP9l4e=BAKOLc&pT9MWm1cCE3Xb>XBrXk}x`>o++4p<` z>IjL+|KeuM=OqeDG84#C0Kw}V1LKOt=`u-D`kg1XG*ms`#1vaA!lH4THYB&1r6}NX z<|FP$?g6m63S(8awo<1ptad7>j89$)&ZHPwMiHh^YT%Ds{segXnr(e2agSF%Kla%r zNKhco(5?J=5p4|K5*`sMN06XzGJJ$7e+Q!F7Gjdh?NF%5^@s_nEebd0-HKaVvIRo4 zw33jf(Z&TRtp=(!hC_sW0myH+)hdyy1JGTg0 zVqRrNhQ}4bK_hzk4DZ4B6JTryaJwk(7zY&{Phj>|j2i>ZdP%z70^d^Fdu^yD?i6TG z;76qW39B6po6zFf@Ne@T+LGxHt+k+eKAu&2jS7kKHMdI2ossxfh0 zo)oJ6L#OnOntZjEQ)C-nFW&up4+%`yutxeux(ns`&c$Ji=ziAYI@@?urr(JMO9Kcw zuXDi&{mccJ04*(b9-1uc?Q0mi5y5DuX%uCP0OF>;kJjCZev;f(X?zB21aTD*Kd7Cf zb4^PFG@=j#TwwN3DlA(o3dWVxuKJd0VNJ7xG@O(Q}ma#C1w zp~c~l8{G5KGNL=py!v=}p)sHMB8Hm)_dn4R<62yyuMtf!`r3Q^x!ChANXZsQys_6+^Ha{79=Wf+4*7(^O?hFPWy23N^2m)IKP;(e#gNX7G&CG zTF=%rZ8jul-hm1UmZevO=7wZf0B?8<_J-Obaa% zHnZ%@;?Us20H3$oAp#?f|6*wyWFns&sue*^J147O8HdT@tW>RgX(wZ5)O7s>0z6dh zoUC@PsI|Po`*%I=D{a2E z^>_b_KIt4Y8}cbT?2^8kv*48ml9Nl#YMIrC`4xU?S3Uc0HP(a*f4EIa=MF!2Idzr7 z>#6F?T^IN#IzpOhORm1&bzJ&=XN*8EN9sM}-Cc63!Z&U#V+z|cY^CXjH4y&)2Hd6b zh8y@4gNwusWngL$0T*J#2YxS!^od*k6*rnQTJWUIlUM;Nd_^nz2Z5LLJGvh0vP%&3 zr5N5;61eaGy~Z}fU-l*O;`;A*n&OMRbE$#v0Z4n**vbP%$LQSio=8{UahWJdM+Dr4u0NURUx02lpdu3L<-kYh zB;!Lb?@KR2Y`8r5i-#K-+gvKj$@wd(zdQI4qDJ9PZIfPK*Fln%u4ASp%1Etc>h}YR z=%99JypE-sF^X1Y8`Uo1k)|8;*>`PvC`t@-X;8*%@!`bi8)#cL{B|j{`3>zS4#!Bm z+R|}U_SjEg_()8-OWyMx`fOW(Fy`my#vj42`w}R<(O{^7^c?e>g*MG@ zd5@3|oy5*a;DVzKVv5Hl!gl7 zl)cATFq(2Vyc-Xw_7v6ZwgenIZl2+e?DOENnXKH*(bcPj)jEAfo*=f)9oj`5NmhQ#Q@lMrrnVH2+#; zMvBfD9)g&ekR=spAkRxri%O+eqN5m_ylOMhUq%e`oANX!Rd%xjthcsE3$ZzK_BnEjFVT1p9JpXNHJ~R=aa_gi$q~rA6{x1CiIvBM7=I;I?lpb`T)b7DPe~uKBr7-ZUmlllmf>?$dKmR zm=D>IwWo_{){6&@C!M_AYJeuU_!F>V{2*V#Z}Pmdz&ftq!3|Jkff^{T}U zwJCxa9@khh1Mr&Qk}xBvtS?^y74THxl!IgtP@AqD)kGP@qb==;hjyHEY2Tr_JBlLb zNT=^(k9DnOBRJ#G)Fg(B1do|aeDCb%$jMfZxSfm9MYwHCi`1#XccM_Ues<~t7@(_V za%qPaml+@n)Kn6UFVgPkG}P7kKz70%%)XvgWWFw9H({2q#KzPF^1#jUztgN`z$kH^ z>_|vU1?J1UJAwN_k!UtshBque_M&DkH0pA3)6xS(^Jl(s4^CY0vK91!QGVh^UpbrK zA?_8;084qn#i-a#NF#77oVIO6b_CF6e<8n4{&tBRBrZG_L$)!0v*Gz!#pvmyTFSg& zwQL6d)f7-q-_0=!@`vu(My#mYA1MfgR1mdm80t8TraqvU3d(zEEqa=6jr071Z(S+j zd%FUISm6~kJ#tGy4#fvg8P=Izczd__Ce)6yA;i^Zr5Q?N$m(Y3{Z+Vd{ig_i9UXl%3v$({k<*E zeNo79Q%;2T{`6XH%Ero_i8;Io1bM^*TmMKlaH6hg8h$yoYYq7u)Mr_Xg=}DxK+&SB z5{HDx+?NcnUk%?X%Rttfpf1J~-Q5ukpN0%4lQ!eO#+V^5%_?5MSgZZmgy`N8;%e6b z>F*RUBWuiaGlBqQx-7LW3XBP}CQ5FBEphaqHJwxbkW=S98Bd|g_EJrJvl-Ce(Rr1h@0VK z=$xzaVpyshWuWIq`Q*ynQQmj(_JQt^hJj@;)`pPQG&X_%gRc+QyB~O2YGC)T2Jk;A zx@ZGQjor}kmaMqfE=4$n*Aim7uh*|(_eF$+);KyH4A@_BAM}e~yHS^%a?#N{F$e@) z-hbHq)9{s~ktZRb<2|@|?A?HzzE5^xmw&7D|E?gsal_B<6{cP`)DPgDNSDHVLd}KP zwmJcuaBTm<+XCTT&pP|*C1acLvgtsuI}E;{yER-#EzB&RlDilJvsQ}oc8o%D4A25 zAwqSsa=c>jZ7`CW0*r$iS)P$Y*+!-yWgo`!CO9$H;$-j(xErjc6u(2BD>2s}X@G#N zc=2WT;4RBUIm_AlQ}}8h)H(Rd<0W~ruuA=pX_QF zG_M8~KY8K!xwr*QWrNX(H_RK28RI-2X@2#v69RV7nM`E|!N!-?_H)s-QNoBo$rY~U z1KS4?;}Ij`XNYr8v~z!aPkW;2{v~)=a)Vo-f4bko_*fiiaVSBgW~L~E{B>(dYHT(- zUNTabUO%ym(V}Xa^5HkXEbZt`&KG>dwu!GF;O<3ac=mVo&@CV1>7J?B&YBq0mT$h) z2vY$a!lPbV={vKd-Xsm*3j|Klb^?p(!`apB*cu9JkqTY{_@UBtyu7;gIkM}SjUnT<=uWZglL7K=cVEE0Pt@jdSX{f;KC!CS5v`?jk$^5{X$>5Zy=#txOk(pYB&? z4}aB1wZQX*)@7p==*BFbxSX)4+f3f5_8=P~a22YPt>ZJb_FS8&s`~M2QFC;e$Wk!n zhd2(=ttPxt;_V{ZuJ-qqP-eyjfl0Id@y0+mnq1T-1Df%>L*pGGkIevwogAu$$_RD#x}&RcWtnsg39Y zpDW+KG{3UPPG(^|>?d)9!&SP~Te~?D&s^72F6o?%<}aWm#@zPW(eDOL*h z?kDlFZ=gR%5Q5<@tnEuVm9Am+Mpm)C(J3NhXpwGbZ=xk4XcWHVmZCdMj|uH5pCC(6@5Ap6gfOK$H*2X2l%9WM8K zkh+P$7*!tSUvSzuvTx!{>wC6#32a^!UD9b#In8|M%v(KJa{-qt!7;B}V$&7hs94ss z&3I?86R8C=Tzkp5^g%~rh%UR8-}OPH)16Kb+_uu*e74-EAC*I zc4|Tat=9va#)7}b5`tkzgflmEvI#wh-c5Lr`iHBFtcAnva#RnN&jaB5#XYr^uQ`Yg z@SmmP?GagN7R8MdE>TXm#&wMJGYpnsW1>+-MrZ3!NdMg1kMHwLo}t|4Qz^jz;h}*M zuqy83^dQ-mocr|NlWBlwiD1F0@VZ4 zd{{PiLHSD7a_zAgTWyM2fhGrWlhQ1lQMCBH=z@6@3Asu59g8H|vtic#k%BmG^Tr{Y zlfRc_8v2WtVJA(e3Fdm&KOpMcO>Q%!fisFetZWv{rrKzTaA-v-MmCLqGj{g!rf7uF z>UjdMtZjtKD|Btv{RI)c6)p&|E%7%DXS3&XhriTSVlt{4kMhiKV3Wib6mS4euQ?pp zNNr2GO64=4Ko^-`o|xL&=20rRkL`%DEh$>j!cr9?(7G4c)^TaNq>F4cVP@{FVmya?9=2I)8dwAQ*F|W;jH?u9m87TQdpeKfBF?g%~$wm$iA^Gw=XKdpT zUDb1KX0-Aq{|*zW@T$nN;CQ80B7g^EK;?29`)9;cvp00$jeuF*1|}s6De}oQ9C>{X z_hkfm_J*HUKPBRHZro=mQF#U+=vw++g(Gp1;=NTsr>h-IV`QK z{UI_Fx_>(@uem(y+ve>VXOS6&iXs;xu;DR4_*CRf13E{wNH7bhh>`$|=s)Hx3UWj} zI*KJ7V%51&iLT+dCcK!FX9i9tb0%Ki{6KC{xh87=5^hc-Xgj^qS7xMtsKk5FCWy+( z(*$+MkMD6^yd>k!3yPKWZp3rA+3L@xujB_P`uS$mzW&8=Z*rerPA&tefW^vB>~n=o z2Gjkh8A-832}i}B@vCEl-W&F8ITbCw!8_S#-O3jUA*agwso*?e;-nxKLG0)`l?ayM z-J9^zm^2n5V4*E=Q*ZkZkAI|9_M@Ys% zO2i-}s%3t==8?u5GW|M;&Ic8W_n>P>6)>YQiZbuAnrojk91$Iyg>S z79}D$yZp=y+Z?2>(JI;eU-L;Ij3;O?LjXZ8h zDI{9NNY4j$4@xz%2N_-q6mpf4lLyi4(}I(#oO)Qk_kqJGJ_qY=uN>J657mwv!F{X4 za~gUA|D(bhUtqlDI-ZA^iQHb6U1W0;2(c)Y94#QFY4lnA{E&0_cihF}p3~cNtP0vg z{2eaGAWxLEMWlb(wIYCT&s?L&3XiQXh&gIYYd4(|Y>S?rgui}=~`?%R8m;is+ z@{40nOS?a-Oz!{8PQ*Z_zeH#!Z!qg!Ft&5w9Y}SU)wesqbT__oiFMa+ zy?6p{$mYAs&yz?^#|+tG=iTPN`TUcV^4#+K8Q=FMx*_Npe4V;Cgh0h=EH@Iyc}Zoi zQKa?vI8veaQm~(TS|!qsHd3S117S>Ej5Mc;aiV^dNLwEyIYlr^gtS)aVp2bOe3Ay<*u{CP5=cd^$hrh7AwFluw z^#}+Lm)lZc4O!o(Vsyu{^n-GBN*6iJWldCo-*WP}fHyDbQG88uzZ011*Yoo;6Tv`@ zZ<9_TK5S;?@t++&Pj1?xZYiiYz_nZPjwe-|*P;>`eP?WtXi@&o)`Y)?TM94 zPv@S~?pqiVR7DqH)tA!EioH<3j;_m!48Z5?4|6VS`6blCqq*J&%Qw zMXufZuGTF!Hi!75(P&9<>IU8d%{TN>o}lJ{qu=B#E5Z?iBm|e9VKm^c&tz~Ztl*}P za!$L_TQX8~8i=d)0v-(k*`efr#Zhyb%Iz04X*oo&K?j!mIhyBAp*iU@){(GN^^p$fL zZ1eLu!d+ysKdGQQ1Tx-qrY!Z`u=YX?zaK` zuYSJ&*b+kS%oy^0Ghm~?z-t#vPdLJw6F1(3Pf02cDL#wgkru`%5q%W_g{*0~ zOqA|gZ>uXoJ+bME1Ufd}43%tLPJz8?e|U5Z=ByN&U!6C8lJepn@4>Z_|5&Y+2Ia5$4*?E?WeY%Kdizx z$zQQjILf~oc{!wd)u}G4yBnGV{iFALLD8ANRr^gO{ppYAphpYk`xD+uV~RubsMlss zhWaTsYBfFCBX4sy%kbw4(J|}bRNc7tQm~yg??w?a?Ik_Q7(ua=MudgM0`q9pjy}+wbh+r+%9Cu-jp^WOAdcimJh^ZmentGO{c@k@im3Gx&ma z5)I{_sxy)(o9BTdw#4&0qNSb17J-T@TaoEJ?Myuqb%-tby>CWc>I{b3QCp1__Swa6 zhDtkFu+)(Ad`4aZpN3tY*qWAaj$B(e(Qqq9_2*1U&-6m2CVp0V38_Y|W+7}R^$K$q zf=*%^GNct#J-FU|J$RS4ftWh!cD+D2%blHABn%bKELT4-%S^)%vMew+ew8Gxgzg?A z0V+<5n66oH7x6)tX1uCEWT*x{0xdQ_^T?pN1ai%KdLJ?Y<{@6lrthxEobP&Zl+D+< zf%*a2%Gj6|C*?WeXT;E(lqU%%Zo+=xOs(-pAT;eApa7x%BIaC#A9sE*;CWdaebe-j z6C3?np;5r?s`D*8;{->(2iy9GA>3^5gbQWA&8$-S`LJjfESDSaev)(q_VpR-4%db* zWy8+{@0t3q>(HIXe+)Qo--)}bb!`D{S!Wgo(Bz-P{nozE=*hmm^tY@xrW{=MQXG2| z?%oD^SvIL7-*-b@)5uqi^D8&rqMEj>)^An1m-+tI@c;9oz{@;;G{3BDLv~GzGF3|> z!z$Ze)eSPQ>>Jf3)nUA(NijlKvu2C%?c1882ldJ7cb2?D;79W)wsS!+QU;=~KK4|M zt8HzpG}t6FB`vsccZaKEdg^Zf73Y!nBQMc+!*0+F9X8nPm91U#*@=H^FJh{o*#E;9Mi5tMmD&gx3(iw^aJs`}?Os^C;d}Z_WrWnN@6ZAMxX5S>dX6=RFY^FNTu59i8t*d{B0=(Q> zJdOQZ&rOkXxH4w?d8jIQa%r*FZKyMHjZh4^D)c*J#cXn;cI+HuBK&T0jNQNi$&C6h zF-tWuY#iYXED;YWyXU9w4ZhBIl{Ayr_F_H4D&fGAp1@F$oY(I5%3l$FZAFDWdwh@w zX}C8Y-gXAIvGiVXb0RM{zgP3rd*P<5&EwizI8^LgEq098ZD1MSTsvNYyLzOdD=|NP zZXo+u%zTBdL+<*mnBHAz=g&j-N-D-bjP(dR;+($u=JQZLN$;7fS@ZVFCq-5vV^1Nj zK5tCz{jJOYK!UxFUN{Eh%MjeD@#eZBvbIwrpev!0#aC8S@Ap_KeHLzVeww$#zqF$X zoV-c8q5O`KzSZxd#ydgAC1K01Wa-kI#DVwH7k?8&34x$62m5oVKIbhv&TBf(Kxel7(0XdXVj00T&Sslhy zkK~O21HO(gC9S`0HHB11Gh!~`XTy_E%pQ{z(AKmmT50&wT;Y}?lqNJ!p0#cvuZ@NY zTjZTxOP4!CcuvSs#j0G`^{m@vRn&}N8k8{smAv~_M6Ztrm zi)TaYvi$JpY;Xm`-RNcw1P@bFwg#-MI(SJbk^*M5D_Z;A!Ib<(MoiobSG3X3?zjK{ zgUVsumgv9BIwgWj+iDe^?D{WfgErv_OQaWV7}pi`w&QzDXKZ!6y%L@=_&U4_(bK+o zo}y;?8{cRZwnwhb{Uv>oa@)pcoUl1I7&tkyi*YaWwVC$CG1ofylI^E93rX@e^+z!_ z`maW^(<`9y#21sN5r0&_=`1O@L~GJ~gZ>cyHTSpT|JEfVx}pVt$Sl*3X={t@0(9z_ zVoi3|Zxu<__6Z>^xMAYewRKp4&n z?W{dgRZLiWJ$?ZLf@atDY13kXUeINuxCT1$U(xQCv2Z1={Z8fSUK>mLCDl2wKbQne zJ-F z+?O9X>5fF|f6)B2OO&pKY%Hug&ZaNlOh}32rq4rje8V&5957QS!*#s*{9tF@8DZL8 zJ2ozT@xB#%xY=p(mg2YK|2`%29tdy{DPJr;65pZAZA}N@2#5r)Gzm!}(KWddlJ*jL zzjq3GWf{l z0UK*_;q1Svi+>)E=_A{5AX$kvv}hXT|BA8wbH)GaM%v200D|8vV49vQ{&j84744CU zAmmdI)qihi`DI=9%blmrATt=zCEH5g|5{GQJrx#<`Uep{A{CTv8|4YG>y7s(kLB2W TaO)qyzmGmZz0dgT Date: Sat, 15 Aug 2026 00:19:28 +0700 Subject: [PATCH 3/4] fix(ci): build Dameng bridge before Xcode --- .github/workflows/build-plugin.yml | 4 ++++ .github/workflows/macos-tests.yml | 5 +++++ project.yml | 14 -------------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index 9521018f8..7bfba1fa7 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -284,6 +284,10 @@ jobs: if: ${{ contains(matrix.tag, 'plugin-cassandra-') }} run: ./scripts/build-cassandra.sh both + - name: Build Dameng native bridge + if: ${{ contains(matrix.tag, 'plugin-dameng-') }} + run: ./scripts/build-dameng.sh both + - name: Build plugin binaries env: TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml index 96219f3a3..775242cae 100644 --- a/.github/workflows/macos-tests.yml +++ b/.github/workflows/macos-tests.yml @@ -100,6 +100,11 @@ jobs: GH_TOKEN: ${{ github.token }} run: scripts/download-libs.sh + # Cargo and rustup need access outside Xcode's user-script sandbox. Build the + # native bridge explicitly before compiling the driver target. + - name: Build Dameng native bridge + run: scripts/build-dameng.sh arm64 + # Secrets.xcconfig is gitignored. Tests do not need analytics keys, so an empty # value is enough for the project to resolve $(ANALYTICS_HMAC_SECRET). - name: Create Secrets.xcconfig diff --git a/project.yml b/project.yml index 200ab8a54..b4e056a8e 100644 --- a/project.yml +++ b/project.yml @@ -699,13 +699,6 @@ targets: templateAttributes: folder: DamengDriverPlugin principalClass: DamengPlugin - preBuildScripts: - - name: Build Dameng native bridge - script: | - if [[ ! -f "$SRCROOT/Libs/libdameng_bridge.a" ]]; then - "$SRCROOT/scripts/build-dameng.sh" both - fi - basedOnDependencyAnalysis: false settings: base: HEADER_SEARCH_PATHS: @@ -736,13 +729,6 @@ targets: - Plugins/DamengDriverPluginTests dependencies: - target: TableProPluginKit - preBuildScripts: - - name: Build Dameng native bridge - script: | - if [[ ! -f "$SRCROOT/Libs/libdameng_bridge.a" ]]; then - "$SRCROOT/scripts/build-dameng.sh" both - fi - basedOnDependencyAnalysis: false settings: base: GENERATE_INFOPLIST_FILE: YES From d54170017539d1746f487ad80f82b7a9fcfd89bf Mon Sep 17 00:00:00 2001 From: sophiathedev Date: Sat, 15 Aug 2026 12:25:42 +0700 Subject: [PATCH 4/4] fix(plugin): clear Dameng build warnings --- .../DamengDriverPlugin/DamengConnection.swift | 26 ++++++++++++++----- .../DamengPluginDriverTests.swift | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Plugins/DamengDriverPlugin/DamengConnection.swift b/Plugins/DamengDriverPlugin/DamengConnection.swift index 8d6d09d5d..8538262b6 100644 --- a/Plugins/DamengDriverPlugin/DamengConnection.swift +++ b/Plugins/DamengDriverPlugin/DamengConnection.swift @@ -12,6 +12,12 @@ struct DamengRawResult: Sendable { } final class DamengConnection: @unchecked Sendable { + private enum TransactionCommand: Sendable { + case begin + case commit + case rollback + } + private let queue = DispatchQueue(label: "com.TablePro.dameng.connection") private var rawConnection: OpaquePointer? @@ -92,24 +98,30 @@ final class DamengConnection: @unchecked Sendable { } func beginTransaction() async throws { - try await transactionOperation(tp_dm_begin) + try await transactionOperation(.begin) } func commitTransaction() async throws { - try await transactionOperation(tp_dm_commit) + try await transactionOperation(.commit) } func rollbackTransaction() async throws { - try await transactionOperation(tp_dm_rollback) + try await transactionOperation(.rollback) } - private func transactionOperation( - _ operation: @escaping @Sendable (OpaquePointer?, UnsafeMutablePointer?) -> Bool - ) async throws { + private func transactionOperation(_ command: TransactionCommand) async throws { try await run { let connection = try self.connectedPointer() var rawError: OpaquePointer? - guard operation(connection, &rawError) else { + let succeeded = switch command { + case .begin: + tp_dm_begin(connection, &rawError) + case .commit: + tp_dm_commit(connection, &rawError) + case .rollback: + tp_dm_rollback(connection, &rawError) + } + guard succeeded else { throw Self.error(from: rawError) } } diff --git a/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift b/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift index 3969f2951..cc064b03a 100644 --- a/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift +++ b/Plugins/DamengDriverPluginTests/DamengPluginDriverTests.swift @@ -109,7 +109,7 @@ final class DamengPluginDriverTests: XCTestCase { try await driver.dropSchema(name: schema) } catch { try? await driver.switchSchema(to: "SYSDBA") - try? await driver.execute(query: "DROP SCHEMA \(driver.quoteIdentifier(schema)) CASCADE") + _ = try? await driver.execute(query: "DROP SCHEMA \(driver.quoteIdentifier(schema)) CASCADE") throw error } }