[F14] mxaccess-galaxy: tiberius-backed SQL Resolver + UserResolver
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-galaxy/src/sql_resolver.rs (~480 LoC) gated behind the existing galaxy-resolver Cargo feature. Adds SqlTagResolver + SqlUserResolver, both constructed via from_ado_string(&str) accepting the same connection-string shape the .NET reference uses by default (Server=localhost;Database=ZB;Integrated Security=True; Encrypt=False;TrustServerCertificate=True). Integrated Security=True resolves to Windows auth via tiberius's winauth feature. Each top-level call (resolve / browse / resolve_by_guid / resolve_by_name) opens a fresh Client<Compat<TcpStream>> and drops it on return — matches the .NET `await using` lifecycle at GalaxyRepositoryTagResolver.cs:93-95. tiberius's Client::query only accepts positional @P1..@PN placeholders (delegates to sp_executesql); the canonical RESOLVE_SQL / BROWSE_SQL / USER_BY_GUID_SQL / USER_BY_NAME_SQL constants are rewritten once-per-process via OnceLock<String> (@objectTagName → @P1, etc.). The unrewritten constants stay byte-identical with the .NET reference for ad-hoc diagnostic copy/paste. read_metadata mirrors ReadMetadata (cs:149-165) byte-by-byte: signed smallint → i16 widened to u16 for platform/engine/object IDs (matches the .NET checked((ushort)reader.GetInt16(N)) shape), int → i32 checked-cast to i16 for property_id, nullable nvarchar for primitive_name. read_user_profile mirrors ReadProfile (cs:76-85) including the roles_text blob → parse_role_blob round-trip. Deps added (gated): tiberius 0.12 (default-features = false; tds73 + rustls + winauth — no chrono / rust_decimal pulled), tokio-util's compat feature for the futures-rs ↔ tokio AsyncRead bridge, futures-util for TryStreamExt::try_next. Default-feature build still pulls only mxaccess-codec + async-trait + thiserror + uuid (slim foot-print preserved per the design doc's intent). New `live` feature on this crate (`live = ["galaxy-resolver"]`) for parity with the workspace pattern. 11 offline unit tests pin: SQL named→positional rewriting (no @named left, @P1/@P2/@P3 present), line-count preserved, ado-string acceptance (default Galaxy shape parses, garbage rejected), input validation (max_rows=0 rejected, empty LIKE rejected, empty user_name rejected, all checked before connect attempt). Two #[cfg(feature = "live")] #[ignore]'d tests round-trip against a real Galaxy DB (gated on MX_LIVE + MX_GALAXY_DB env vars per tools/Setup-LiveProbeEnv.ps1). Live verification on this host: live_resolve_test_child_object_test_int and live_browse_test_child_object both pass against the local AVEVA install — TestChildObject.TestInt resolves with mx_data_type=2 (Int32), is_array=false. Closes F14 in design/followups.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-7
@@ -236,15 +236,11 @@ The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/M
|
||||
**Why deferred:** Wave-2 `Session::recover_connection` validates the policy and emits `RecoveryEvent::Started` + `RecoveryEvent::Recovered` on each call but does **NOT** actually tear down + re-establish the NMX transport / re-advise active subscriptions. The .NET reference's `RecoverConnectionCore` (`MxNativeSession.cs:442-474`) does all three: builds a replacement `ManagedNmxService2Client` via `CreateRegisteredService`, re-`Connect`s every `_publisherEndpoints` entry, re-`AdviseSupervisory`s every entry in `_subscriptions`, then atomically swaps the old service for the new one. Porting this to Rust requires (a) tracking the active subscriptions inside `SessionInner` (currently they're owned by the consumer's `Subscription` handles, with no central registry); (b) the long-lived connection task per R15 in `design/70-risks-and-open-questions.md` so swap-in-place is safe under concurrent operations; (c) a way to re-create the `CallbackExporter` (or keep the existing one bound while the underlying transport is replaced — needs design work).
|
||||
**Resolves when:** R15's long-lived connection task lands and `SessionInner` gains a subscription registry. At that point the recover loop becomes ~50 lines: for `attempt in 1..=max_attempts`, emit Started → drop+rebuild NmxClient → `register_engine_2` with the existing OBJREF → re-advise every registered correlation_id → emit Recovered (or Failed + sleep delay + continue, mirroring the `cs:407-440` shape exactly).
|
||||
|
||||
### F14 — `tiberius`-backed SQL implementation of `Resolver` + `UserResolver`
|
||||
**Severity:** P2
|
||||
**Source:** M3 stream A, `crates/mxaccess-galaxy/src/sql.rs` (constants present, no client wiring yet)
|
||||
**Why deferred:** `tiberius` is the recommended Rust SQL Server client; pulling it as a non-default dep means the `mxaccess-galaxy` crate keeps a slim default footprint (consumers can plug their own `Resolver` / `UserResolver` impl without dragging in TDS / native-tls / winauth). The actual `GalaxyRepositoryTagResolver` and `GalaxyRepositoryUserResolver` impls are short — they just bind the canonical SQL constants in `crate::sql` (`RESOLVE_SQL`, `BROWSE_SQL`, `USER_BY_GUID_SQL`, `USER_BY_NAME_SQL`) and translate `tiberius::Row` → typed `GalaxyTagMetadata` / `GalaxyUserProfile`.
|
||||
**Resolves when:** A `tiberius`-backed module lands behind the existing `galaxy-resolver` Cargo feature flag in `mxaccess-galaxy/Cargo.toml`. Live-probe gating: needs a Galaxy DB to verify against (`MX_GALAXY_DB` env var, populated by `tools/Setup-LiveProbeEnv.ps1`). The pure-Rust foundation (data types, parser, trait, SQL strings) is already in place — this is "fill in the backend" rather than "design the surface."
|
||||
|
||||
|
||||
## Resolved
|
||||
|
||||
### F14 — `tiberius`-backed SQL implementation of `Resolver` + `UserResolver`
|
||||
**Resolved:** 2026-05-05 (commit `<this commit>`). New module `crates/mxaccess-galaxy/src/sql_resolver.rs` (~480 LoC) gated behind the existing `galaxy-resolver` Cargo feature; adds `SqlTagResolver` + `SqlUserResolver`, both constructed via `from_ado_string(&str)` accepting the same shape the .NET reference uses by default (`Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True`). `Integrated Security=True` resolves to Windows authentication via tiberius's `winauth` feature. Each top-level call opens a fresh `Client<Compat<TcpStream>>` and drops it on return — matches the .NET `await using` shape. `tiberius`'s `Client::query` only accepts positional `@P1..@PN` placeholders (delegates to `sp_executesql`); the canonical `RESOLVE_SQL` / `BROWSE_SQL` / `USER_BY_GUID_SQL` / `USER_BY_NAME_SQL` constants are rewritten once-per-process via `OnceLock<String>` (`@objectTagName` → `@P1`, etc.). `read_metadata` mirrors `ReadMetadata` (`cs:149-165`) byte-by-byte: signed `smallint` → `i16` widened to `u16` for platform/engine/object IDs (matches the .NET `checked((ushort)...)`), `int` → `i32` checked-cast to `i16` for `property_id`, nullable `nvarchar` for `primitive_name`. `read_user_profile` mirrors `ReadProfile` (`cs:76-85`) including the `roles_text` blob → `parse_role_blob` round-trip. New deps: `tiberius 0.12` (`tds73`/`rustls`/`winauth` features, no `chrono` / `rust_decimal`), `tokio-util` `compat` feature for the futures-rs ↔ tokio AsyncRead bridge, `futures-util` for `TryStreamExt::try_next`. New `live` feature in the crate for parity with the workspace pattern (`live = ["galaxy-resolver"]`). 11 offline unit tests pin: SQL named→positional rewriting (no `@named` left, `@P1`/`@P2`/`@P3` present), line-count preserved by rewriting, ado-string acceptance (default Galaxy shape parses; garbage rejected), input validation (`max_rows=0` rejected, empty `LIKE` rejected, empty user_name rejected). Two `#[cfg(feature = "live")]` `#[ignore]`'d tests round-trip against a real Galaxy DB (gated on `MX_LIVE` + `MX_GALAXY_DB` env vars per `tools/Setup-LiveProbeEnv.ps1`): `live_resolve_test_child_object_test_int` (TestChildObject.TestInt → mx_data_type=2 Int32, is_array=false) and `live_browse_test_child_object` (browse returns ≥1 attribute on TestChildObject). Both pass against the local AVEVA install.
|
||||
|
||||
### F4 + F5 — BindAck body parser + captured-bytes round-trip
|
||||
**Resolved:** 2026-05-05 (commit `<this commit>`). Single change closes both: new `BindAckPdu` struct + `BindAckResult` per-result type + `decode`/`encode` impl in `crates/mxaccess-rpc/src/pdu.rs`. Body layout per `[C706]` §12.6.3.4: `port_any_t` secondary address (u16-length + bytes including NUL) + alignment to 4-byte boundary + `n_results` u8 + 3 reserved + array of `p_result_t` (u16 result + u16 reason + 20-byte SyntaxId). Accepts both `PacketType::BindAck` and `PacketType::AlterContextResponse` (same body shape). New regression test `bind_ack_round_trips_live_capture` decodes the first 84 bytes of `captures/013-loopback-subscribe-scalars/tcp-stream-__1_49704-to-__1_55690.bin` (the server's response to the client's first Bind), asserts the shape (sec_addr=`"49704\0"`, n_results=2, NDR accepted + DCOM negotiate_ack reason 3), then re-encodes and asserts byte-identical against the original frame. Stronger live-wire parity than the prior synthetic-frame tests. F4 + F5 collapsed into one commit because they share scope (parser + round-trip-test).
|
||||
|
||||
|
||||
Generated
+506
-23
@@ -30,12 +30,43 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asynchronous-codec"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -69,6 +100,12 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
@@ -84,6 +121,16 @@ dependencies = [
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -111,6 +158,28 @@ dependencies = [
|
||||
"inout 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "connection-string"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
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 = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -159,6 +228,41 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[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 = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -175,6 +279,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
@@ -205,8 +315,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
@@ -221,6 +334,17 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -229,7 +353,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -293,6 +417,12 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -312,6 +442,18 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e6bcd6433cff03a4bfc3d9834d504467db1f1cf6d0ea765d37d330249ed629d"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -329,8 +471,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -346,8 +488,8 @@ dependencies = [
|
||||
"mxaccess-galaxy",
|
||||
"mxaccess-nmx",
|
||||
"mxaccess-rpc",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"rand 0.8.6",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@@ -359,8 +501,8 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"mxaccess-asb-nettcp",
|
||||
"mxaccess-codec",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"rand 0.8.6",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -380,10 +522,10 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pbkdf2",
|
||||
"rand",
|
||||
"rand 0.8.6",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -394,7 +536,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"mxaccess-codec",
|
||||
"mxaccess-rpc",
|
||||
"rand",
|
||||
"rand 0.8.6",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -403,7 +545,7 @@ dependencies = [
|
||||
name = "mxaccess-codec"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -418,9 +560,12 @@ name = "mxaccess-galaxy"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures-util",
|
||||
"mxaccess-codec",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tiberius",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -432,8 +577,8 @@ dependencies = [
|
||||
"mxaccess-codec",
|
||||
"mxaccess-galaxy",
|
||||
"mxaccess-rpc",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"rand 0.8.6",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
@@ -445,9 +590,9 @@ dependencies = [
|
||||
"hmac",
|
||||
"md-5",
|
||||
"md4",
|
||||
"rand",
|
||||
"rand 0.8.6",
|
||||
"rc4",
|
||||
"thiserror",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -485,6 +630,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -510,6 +661,12 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty-hex"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -528,6 +685,19 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
@@ -535,8 +705,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -546,7 +726,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -555,7 +744,16 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -567,12 +765,111 @@ dependencies = [
|
||||
"cipher 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"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 = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -595,6 +892,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
@@ -614,7 +917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -634,13 +937,33 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -654,6 +977,34 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiberius"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"asynchronous-codec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"connection-string",
|
||||
"encoding_rs",
|
||||
"enumflags2",
|
||||
"futures-util",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pretty-hex",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"winauth",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.2"
|
||||
@@ -666,7 +1017,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -680,6 +1031,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
@@ -700,6 +1061,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -711,6 +1073,7 @@ version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -748,6 +1111,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.23.1"
|
||||
@@ -764,6 +1133,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -815,12 +1190,56 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "winauth"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f820cd208ce9c6b050812dc2d724ba98c6c1e9db5ce9b3f58d925ae5723a5e6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
"md5",
|
||||
"rand 0.7.3",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -830,6 +1249,70 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
|
||||
@@ -14,6 +14,17 @@ async-trait = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = "1"
|
||||
|
||||
# F14 — tiberius-backed SQL resolver. Optional, gated by `galaxy-resolver`
|
||||
# so the default footprint stays slim (no TDS / TLS / winauth pulled when
|
||||
# consumers plug their own Resolver impl). tokio-util's `compat` feature
|
||||
# bridges `tokio::net::TcpStream` to the futures-rs AsyncRead/Write that
|
||||
# tiberius expects. `futures-util::TryStreamExt` is needed to drain the
|
||||
# QueryStream rows.
|
||||
tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "winauth"], optional = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
tokio-util = { version = "0.7", features = ["compat"], optional = true }
|
||||
futures-util = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -22,7 +33,11 @@ default = []
|
||||
# `galaxy-resolver` (off by default in M0) pulls `tiberius`. Consumers using a
|
||||
# custom `Resolver` impl leave it off and avoid pulling TDS / native-tls /
|
||||
# winauth. Per design/30-crate-topology.md `mxaccess-galaxy` section.
|
||||
galaxy-resolver = []
|
||||
galaxy-resolver = ["dep:tiberius", "dep:tokio", "dep:tokio-util", "dep:futures-util"]
|
||||
# Enables live integration tests that hit a real SQL Server (typically the
|
||||
# Galaxy DB on the local AVEVA install). Driven by `MX_LIVE` + `MX_GALAXY_DB`
|
||||
# env vars, populated by `tools/Setup-LiveProbeEnv.ps1`.
|
||||
live = ["galaxy-resolver"]
|
||||
auth-windows = []
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -31,10 +31,14 @@ pub mod parser;
|
||||
pub mod resolver;
|
||||
pub mod role_blob;
|
||||
pub mod sql;
|
||||
#[cfg(feature = "galaxy-resolver")]
|
||||
pub mod sql_resolver;
|
||||
pub mod user;
|
||||
|
||||
pub use metadata::{GalaxyTagMetadata, UnsupportedDataType};
|
||||
pub use parser::{ParseError, ParsedTagReference};
|
||||
pub use resolver::{Resolver, ResolverError};
|
||||
pub use role_blob::parse_role_blob;
|
||||
#[cfg(feature = "galaxy-resolver")]
|
||||
pub use sql_resolver::{SqlTagResolver, SqlUserResolver};
|
||||
pub use user::{GalaxyUserProfile, UserResolver, UserResolverError};
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
//! `tiberius`-backed implementations of [`crate::Resolver`] and
|
||||
//! [`crate::UserResolver`]. Gated by the `galaxy-resolver` Cargo feature.
|
||||
//!
|
||||
//! Direct port of `GalaxyRepositoryTagResolver.cs` and
|
||||
//! `GalaxyRepositoryUserResolver.cs`. The pure-Rust foundation
|
||||
//! (parser, metadata, SQL constants) was already in place; this module
|
||||
//! is the "fill in the backend" piece tracked as F14 in
|
||||
//! `design/followups.md`.
|
||||
//!
|
||||
//! ## Connection-string parsing
|
||||
//!
|
||||
//! Both resolvers accept an `ADO.NET`-style connection string via
|
||||
//! [`SqlTagResolver::from_ado_string`] / [`SqlUserResolver::from_ado_string`].
|
||||
//! The string is parsed by `tiberius::Config::from_ado_string`, which
|
||||
//! accepts the same shape the .NET reference uses by default
|
||||
//! (`Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True`).
|
||||
//! `Integrated Security=True` resolves to Windows authentication on
|
||||
//! Windows hosts via the `winauth` feature.
|
||||
//!
|
||||
//! ## Named-parameter rewriting
|
||||
//!
|
||||
//! `tiberius` only accepts positional `@P1..@PN` placeholders (it
|
||||
//! delegates to `sp_executesql` internally). The canonical SQL constants
|
||||
//! in [`crate::sql`] use named parameters (`@objectTagName`,
|
||||
//! `@attributeName`, `@primitiveName`, `@objectTagLike`, `@attributeLike`,
|
||||
//! `@maxRows`, `@userGuid`, `@userName`) to stay byte-identical with the
|
||||
//! .NET reference. Each query string is rewritten once at module-init
|
||||
//! time via [`std::sync::OnceLock`].
|
||||
//!
|
||||
//! ## Connection lifetime
|
||||
//!
|
||||
//! Each top-level call (`resolve`, `browse`, `resolve_by_guid`,
|
||||
//! `resolve_by_name`) opens a fresh `tiberius::Client` and drops it on
|
||||
//! return. This matches the `await using` pattern in the .NET reference
|
||||
//! (`GalaxyRepositoryTagResolver.cs:93-95`). The Galaxy DB is not
|
||||
//! request-pooled in the .NET shape either — tag resolution happens once
|
||||
//! per session bring-up, not on the data-plane hot path.
|
||||
|
||||
#![cfg(feature = "galaxy-resolver")]
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use tiberius::{Client, Config, QueryItem, Row};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::metadata::GalaxyTagMetadata;
|
||||
use crate::parser::ParsedTagReference;
|
||||
use crate::resolver::{Resolver, ResolverError};
|
||||
use crate::sql;
|
||||
use crate::user::{GalaxyUserProfile, UserResolver, UserResolverError};
|
||||
|
||||
/// Shorthand for the tiberius client we hold per call.
|
||||
type SqlClient = Client<Compat<TcpStream>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tag resolver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Tiberius-backed [`Resolver`] hitting a Galaxy Repository SQL Server.
|
||||
///
|
||||
/// Construct with [`SqlTagResolver::from_ado_string`]. The same
|
||||
/// connection string the .NET reference uses works verbatim
|
||||
/// (`Server=...;Database=...;Integrated Security=True;Encrypt=False;TrustServerCertificate=True`).
|
||||
#[derive(Debug)]
|
||||
pub struct SqlTagResolver {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl SqlTagResolver {
|
||||
/// Build a resolver from an ADO.NET connection string. Mirrors
|
||||
/// the .NET reference's default-construction path
|
||||
/// (`GalaxyRepositoryTagResolver.cs:77-86`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`ResolverError::Backend`] when `tiberius::Config::from_ado_string`
|
||||
/// rejects the string (unparseable key/value, unsupported auth,
|
||||
/// etc.).
|
||||
pub fn from_ado_string(connection_string: &str) -> Result<Self, ResolverError> {
|
||||
let config = Config::from_ado_string(connection_string).map_err(|e| {
|
||||
ResolverError::Backend {
|
||||
message: format!("invalid ADO.NET connection string: {e}"),
|
||||
}
|
||||
})?;
|
||||
Ok(Self { config })
|
||||
}
|
||||
|
||||
async fn open(&self) -> Result<SqlClient, ResolverError> {
|
||||
open_client(&self.config)
|
||||
.await
|
||||
.map_err(|message| ResolverError::Backend { message })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Resolver for SqlTagResolver {
|
||||
async fn resolve(&self, tag_reference: &str) -> Result<GalaxyTagMetadata, ResolverError> {
|
||||
let candidates = ParsedTagReference::parse_candidates(tag_reference)?;
|
||||
let mut client = self.open().await?;
|
||||
|
||||
for parsed in &candidates {
|
||||
let primitive = parsed.primitive_name.as_deref();
|
||||
let object_tag = parsed.object_tag_name.as_str();
|
||||
let attribute = parsed.attribute_name.as_str();
|
||||
|
||||
let mut stream = client
|
||||
.query(
|
||||
resolve_sql_pos(),
|
||||
&[&object_tag, &attribute, &primitive],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ResolverError::Backend {
|
||||
message: format!("RESOLVE_SQL execute: {e}"),
|
||||
})?;
|
||||
|
||||
while let Some(item) = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ResolverError::Backend {
|
||||
message: format!("RESOLVE_SQL fetch: {e}"),
|
||||
})?
|
||||
{
|
||||
if let QueryItem::Row(row) = item {
|
||||
let metadata = read_metadata(&row).map_err(|e| ResolverError::Backend {
|
||||
message: format!("RESOLVE_SQL row decode: {e}"),
|
||||
})?;
|
||||
return Ok(parsed.apply_overrides(metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(ResolverError::NotFound {
|
||||
tag_reference: tag_reference.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn browse(
|
||||
&self,
|
||||
object_tag_like: &str,
|
||||
attribute_like: &str,
|
||||
max_rows: usize,
|
||||
) -> Result<Vec<GalaxyTagMetadata>, ResolverError> {
|
||||
if object_tag_like.trim().is_empty() {
|
||||
return Err(ResolverError::Backend {
|
||||
message: "object_tag_like must not be empty".to_string(),
|
||||
});
|
||||
}
|
||||
if attribute_like.trim().is_empty() {
|
||||
return Err(ResolverError::Backend {
|
||||
message: "attribute_like must not be empty".to_string(),
|
||||
});
|
||||
}
|
||||
if max_rows == 0 {
|
||||
return Err(ResolverError::Backend {
|
||||
message: "max_rows must be positive".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Mirror the .NET clamp at GalaxyRepositoryTagResolver.cs:137.
|
||||
let clamped = i32::try_from(max_rows.min(1000)).unwrap_or(1000);
|
||||
|
||||
let mut client = self.open().await?;
|
||||
let mut stream = client
|
||||
.query(
|
||||
browse_sql_pos(),
|
||||
&[&object_tag_like, &attribute_like, &clamped],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ResolverError::Backend {
|
||||
message: format!("BROWSE_SQL execute: {e}"),
|
||||
})?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
while let Some(item) = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ResolverError::Backend {
|
||||
message: format!("BROWSE_SQL fetch: {e}"),
|
||||
})?
|
||||
{
|
||||
if let QueryItem::Row(row) = item {
|
||||
out.push(read_metadata(&row).map_err(|e| ResolverError::Backend {
|
||||
message: format!("BROWSE_SQL row decode: {e}"),
|
||||
})?);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User resolver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Tiberius-backed [`UserResolver`].
|
||||
///
|
||||
/// Mirrors `GalaxyRepositoryUserResolver` (`cs:13-149`).
|
||||
#[derive(Debug)]
|
||||
pub struct SqlUserResolver {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl SqlUserResolver {
|
||||
/// Build a user resolver from an ADO.NET connection string.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`UserResolverError::Backend`] when the connection string is
|
||||
/// rejected by `tiberius::Config::from_ado_string`.
|
||||
pub fn from_ado_string(connection_string: &str) -> Result<Self, UserResolverError> {
|
||||
let config = Config::from_ado_string(connection_string).map_err(|e| {
|
||||
UserResolverError::Backend {
|
||||
message: format!("invalid ADO.NET connection string: {e}"),
|
||||
}
|
||||
})?;
|
||||
Ok(Self { config })
|
||||
}
|
||||
|
||||
async fn open(&self) -> Result<SqlClient, UserResolverError> {
|
||||
open_client(&self.config)
|
||||
.await
|
||||
.map_err(|message| UserResolverError::Backend { message })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserResolver for SqlUserResolver {
|
||||
async fn resolve_by_guid(
|
||||
&self,
|
||||
user_guid: Uuid,
|
||||
) -> Result<GalaxyUserProfile, UserResolverError> {
|
||||
let mut client = self.open().await?;
|
||||
// tiberius's `Uuid` parameter binds to `uniqueidentifier` directly.
|
||||
let mut stream = client
|
||||
.query(user_by_guid_sql_pos(), &[&user_guid])
|
||||
.await
|
||||
.map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_GUID_SQL execute: {e}"),
|
||||
})?;
|
||||
|
||||
while let Some(item) = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_GUID_SQL fetch: {e}"),
|
||||
})?
|
||||
{
|
||||
if let QueryItem::Row(row) = item {
|
||||
return read_user_profile(&row).map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_GUID_SQL row decode: {e}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(UserResolverError::NotFound {
|
||||
key: user_guid.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_by_name(
|
||||
&self,
|
||||
user_name: &str,
|
||||
) -> Result<GalaxyUserProfile, UserResolverError> {
|
||||
if user_name.trim().is_empty() {
|
||||
return Err(UserResolverError::Backend {
|
||||
message: "user_name must not be empty".to_string(),
|
||||
});
|
||||
}
|
||||
let mut client = self.open().await?;
|
||||
let mut stream = client
|
||||
.query(user_by_name_sql_pos(), &[&user_name])
|
||||
.await
|
||||
.map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_NAME_SQL execute: {e}"),
|
||||
})?;
|
||||
|
||||
while let Some(item) = stream
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_NAME_SQL fetch: {e}"),
|
||||
})?
|
||||
{
|
||||
if let QueryItem::Row(row) = item {
|
||||
return read_user_profile(&row).map_err(|e| UserResolverError::Backend {
|
||||
message: format!("USER_BY_NAME_SQL row decode: {e}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(UserResolverError::NotFound {
|
||||
key: user_name.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Open a fresh tiberius client to the configured server. Returns a
|
||||
/// `String` error so each caller can wrap into its preferred error
|
||||
/// taxonomy.
|
||||
async fn open_client(config: &Config) -> Result<SqlClient, String> {
|
||||
let stream = TcpStream::connect(config.get_addr())
|
||||
.await
|
||||
.map_err(|e| format!("TCP connect to {}: {e}", config.get_addr()))?;
|
||||
// NODELAY mirrors what tiberius's own examples set; latency-sensitive
|
||||
// for short query/response cycles.
|
||||
let _ = stream.set_nodelay(true);
|
||||
Client::connect(config.clone(), stream.compat_write())
|
||||
.await
|
||||
.map_err(|e| format!("tiberius connect: {e}"))
|
||||
}
|
||||
|
||||
/// Decode one resolver row per `ReadMetadata` (`cs:149-165`).
|
||||
///
|
||||
/// SQL Server smallint → tiberius `i16`; bit → `bool`; nvarchar →
|
||||
/// `&str`. The platform/engine/object IDs are signed `smallint` on the
|
||||
/// wire but the .NET reference checked-casts to `ushort`; we widen to
|
||||
/// `u16` the same way.
|
||||
fn read_metadata(row: &Row) -> Result<GalaxyTagMetadata, String> {
|
||||
let object_tag_name: &str = row
|
||||
.try_get::<&str, _>(0)
|
||||
.map_err(|e| format!("col 0 object_tag_name: {e}"))?
|
||||
.ok_or("col 0 object_tag_name: NULL")?;
|
||||
let attribute_name: &str = row
|
||||
.try_get::<&str, _>(1)
|
||||
.map_err(|e| format!("col 1 attribute_name: {e}"))?
|
||||
.ok_or("col 1 attribute_name: NULL")?;
|
||||
let primitive_name: Option<&str> = row
|
||||
.try_get::<&str, _>(2)
|
||||
.map_err(|e| format!("col 2 primitive_name: {e}"))?;
|
||||
let platform_id_i16: i16 = row
|
||||
.try_get::<i16, _>(3)
|
||||
.map_err(|e| format!("col 3 mx_platform_id: {e}"))?
|
||||
.ok_or("col 3 mx_platform_id: NULL")?;
|
||||
let engine_id_i16: i16 = row
|
||||
.try_get::<i16, _>(4)
|
||||
.map_err(|e| format!("col 4 mx_engine_id: {e}"))?
|
||||
.ok_or("col 4 mx_engine_id: NULL")?;
|
||||
let object_id_i16: i16 = row
|
||||
.try_get::<i16, _>(5)
|
||||
.map_err(|e| format!("col 5 mx_object_id: {e}"))?
|
||||
.ok_or("col 5 mx_object_id: NULL")?;
|
||||
let primitive_id: i16 = row
|
||||
.try_get::<i16, _>(6)
|
||||
.map_err(|e| format!("col 6 mx_primitive_id: {e}"))?
|
||||
.ok_or("col 6 mx_primitive_id: NULL")?;
|
||||
let attribute_id: i16 = row
|
||||
.try_get::<i16, _>(7)
|
||||
.map_err(|e| format!("col 7 mx_attribute_id: {e}"))?
|
||||
.ok_or("col 7 mx_attribute_id: NULL")?;
|
||||
let property_id_i32: i32 = row
|
||||
.try_get::<i32, _>(8)
|
||||
.map_err(|e| format!("col 8 property_id: {e}"))?
|
||||
.ok_or("col 8 property_id: NULL")?;
|
||||
let mx_data_type: i16 = row
|
||||
.try_get::<i16, _>(9)
|
||||
.map_err(|e| format!("col 9 mx_data_type: {e}"))?
|
||||
.ok_or("col 9 mx_data_type: NULL")?;
|
||||
let is_array: bool = row
|
||||
.try_get::<bool, _>(10)
|
||||
.map_err(|e| format!("col 10 is_array: {e}"))?
|
||||
.ok_or("col 10 is_array: NULL")?;
|
||||
let security_classification: i16 = row
|
||||
.try_get::<i16, _>(11)
|
||||
.map_err(|e| format!("col 11 security_classification: {e}"))?
|
||||
.ok_or("col 11 security_classification: NULL")?;
|
||||
let attribute_source: &str = row
|
||||
.try_get::<&str, _>(12)
|
||||
.map_err(|e| format!("col 12 attribute_source: {e}"))?
|
||||
.ok_or("col 12 attribute_source: NULL")?;
|
||||
|
||||
let property_id = i16::try_from(property_id_i32)
|
||||
.map_err(|_| format!("property_id {property_id_i32} out of i16 range"))?;
|
||||
|
||||
Ok(GalaxyTagMetadata {
|
||||
object_tag_name: object_tag_name.to_string(),
|
||||
attribute_name: attribute_name.to_string(),
|
||||
primitive_name: primitive_name.map(str::to_string),
|
||||
platform_id: u16::try_from(platform_id_i16)
|
||||
.map_err(|_| format!("platform_id {platform_id_i16} negative"))?,
|
||||
engine_id: u16::try_from(engine_id_i16)
|
||||
.map_err(|_| format!("engine_id {engine_id_i16} negative"))?,
|
||||
object_id: u16::try_from(object_id_i16)
|
||||
.map_err(|_| format!("object_id {object_id_i16} negative"))?,
|
||||
primitive_id,
|
||||
attribute_id,
|
||||
property_id,
|
||||
mx_data_type,
|
||||
is_array,
|
||||
security_classification,
|
||||
attribute_source: attribute_source.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode one user-profile row per `ReadProfile` (`cs:76-85`).
|
||||
fn read_user_profile(row: &Row) -> Result<GalaxyUserProfile, String> {
|
||||
let user_profile_id: i32 = row
|
||||
.try_get::<i32, _>(0)
|
||||
.map_err(|e| format!("col 0 user_profile_id: {e}"))?
|
||||
.ok_or("col 0 user_profile_id: NULL")?;
|
||||
let user_profile_name: &str = row
|
||||
.try_get::<&str, _>(1)
|
||||
.map_err(|e| format!("col 1 user_profile_name: {e}"))?
|
||||
.ok_or("col 1 user_profile_name: NULL")?;
|
||||
let user_guid: Uuid = row
|
||||
.try_get::<Uuid, _>(2)
|
||||
.map_err(|e| format!("col 2 user_guid: {e}"))?
|
||||
.ok_or("col 2 user_guid: NULL")?;
|
||||
let default_security_group: &str = row
|
||||
.try_get::<&str, _>(3)
|
||||
.map_err(|e| format!("col 3 default_security_group: {e}"))?
|
||||
.ok_or("col 3 default_security_group: NULL")?;
|
||||
let intouch_access_level: Option<i32> = row
|
||||
.try_get::<i32, _>(4)
|
||||
.map_err(|e| format!("col 4 intouch_access_level: {e}"))?;
|
||||
let roles_text: Option<&str> = row
|
||||
.try_get::<&str, _>(5)
|
||||
.map_err(|e| format!("col 5 roles_text: {e}"))?;
|
||||
|
||||
Ok(GalaxyUserProfile::from_columns(
|
||||
user_profile_id,
|
||||
user_profile_name.to_string(),
|
||||
user_guid,
|
||||
default_security_group.to_string(),
|
||||
intouch_access_level,
|
||||
roles_text,
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Named-parameter rewriting
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// tiberius accepts only positional `@P1..@PN` placeholders. Each canonical
|
||||
// SQL constant in `crate::sql` uses named parameters (matching the .NET
|
||||
// reference verbatim); rewrite to `@PN` once per process start.
|
||||
|
||||
fn resolve_sql_pos() -> &'static str {
|
||||
static CACHE: OnceLock<String> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
sql::RESOLVE_SQL
|
||||
.replace("@objectTagName", "@P1")
|
||||
.replace("@attributeName", "@P2")
|
||||
.replace("@primitiveName", "@P3")
|
||||
})
|
||||
}
|
||||
|
||||
fn browse_sql_pos() -> &'static str {
|
||||
static CACHE: OnceLock<String> = OnceLock::new();
|
||||
CACHE.get_or_init(|| {
|
||||
sql::BROWSE_SQL
|
||||
.replace("@objectTagLike", "@P1")
|
||||
.replace("@attributeLike", "@P2")
|
||||
.replace("@maxRows", "@P3")
|
||||
})
|
||||
}
|
||||
|
||||
fn user_by_guid_sql_pos() -> &'static str {
|
||||
static CACHE: OnceLock<String> = OnceLock::new();
|
||||
CACHE.get_or_init(|| sql::USER_BY_GUID_SQL.replace("@userGuid", "@P1"))
|
||||
}
|
||||
|
||||
fn user_by_name_sql_pos() -> &'static str {
|
||||
static CACHE: OnceLock<String> = OnceLock::new();
|
||||
CACHE.get_or_init(|| sql::USER_BY_NAME_SQL.replace("@userName", "@P1"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ----- offline tests (no SQL Server required) -------------------------
|
||||
|
||||
#[test]
|
||||
fn resolve_sql_rewrites_three_named_to_positional() {
|
||||
let sql = resolve_sql_pos();
|
||||
assert!(sql.contains("@P1"));
|
||||
assert!(sql.contains("@P2"));
|
||||
assert!(sql.contains("@P3"));
|
||||
assert!(!sql.contains("@objectTagName"));
|
||||
assert!(!sql.contains("@attributeName"));
|
||||
assert!(!sql.contains("@primitiveName"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_sql_rewrites_three_named_to_positional() {
|
||||
let sql = browse_sql_pos();
|
||||
assert!(sql.contains("@P1"));
|
||||
assert!(sql.contains("@P2"));
|
||||
assert!(sql.contains("@P3"));
|
||||
assert!(!sql.contains("@objectTagLike"));
|
||||
assert!(!sql.contains("@attributeLike"));
|
||||
assert!(!sql.contains("@maxRows"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_by_guid_rewrites_named_to_positional() {
|
||||
let sql = user_by_guid_sql_pos();
|
||||
assert!(sql.contains("@P1"));
|
||||
assert!(!sql.contains("@userGuid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_by_name_rewrites_named_to_positional() {
|
||||
let sql = user_by_name_sql_pos();
|
||||
assert!(sql.contains("@P1"));
|
||||
assert!(!sql.contains("@userName"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewriting_preserves_line_count() {
|
||||
// Sanity — replacing named params shouldn't add or remove lines.
|
||||
assert_eq!(
|
||||
sql::RESOLVE_SQL.lines().count(),
|
||||
resolve_sql_pos().lines().count()
|
||||
);
|
||||
assert_eq!(
|
||||
sql::BROWSE_SQL.lines().count(),
|
||||
browse_sql_pos().lines().count()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ado_string_rejects_garbage() {
|
||||
let err = SqlTagResolver::from_ado_string("this is not a valid ADO string").unwrap_err();
|
||||
assert!(matches!(err, ResolverError::Backend { .. }));
|
||||
|
||||
let err = SqlUserResolver::from_ado_string("=;=;=").unwrap_err();
|
||||
assert!(matches!(err, UserResolverError::Backend { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ado_string_accepts_default_galaxy_shape() {
|
||||
// The default shape used by the .NET reference at
|
||||
// GalaxyRepositoryTagResolver.cs:78.
|
||||
let s = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
|
||||
SqlTagResolver::from_ado_string(s).expect("default Galaxy connection shape parses");
|
||||
SqlUserResolver::from_ado_string(s)
|
||||
.expect("default Galaxy connection shape parses for user resolver");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_rejects_zero_max_rows() {
|
||||
// We can exercise the input-validation arm without a live DB —
|
||||
// it's checked before the connect attempt.
|
||||
let resolver = SqlTagResolver::from_ado_string(
|
||||
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True",
|
||||
)
|
||||
.unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let err = rt
|
||||
.block_on(resolver.browse("%", "%", 0))
|
||||
.expect_err("max_rows=0 must be rejected");
|
||||
match err {
|
||||
ResolverError::Backend { message } => assert!(message.contains("max_rows")),
|
||||
other => panic!("expected Backend error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_rejects_empty_like_patterns() {
|
||||
let resolver = SqlTagResolver::from_ado_string(
|
||||
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True",
|
||||
)
|
||||
.unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let err = rt
|
||||
.block_on(resolver.browse(" ", "%", 10))
|
||||
.expect_err("empty object_tag_like must be rejected");
|
||||
assert!(matches!(err, ResolverError::Backend { .. }));
|
||||
let err = rt
|
||||
.block_on(resolver.browse("%", "", 10))
|
||||
.expect_err("empty attribute_like must be rejected");
|
||||
assert!(matches!(err, ResolverError::Backend { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_by_name_rejects_empty_user_name() {
|
||||
let resolver = SqlUserResolver::from_ado_string(
|
||||
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True",
|
||||
)
|
||||
.unwrap();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let err = rt
|
||||
.block_on(resolver.resolve_by_name(" "))
|
||||
.expect_err("empty user_name must be rejected");
|
||||
assert!(matches!(err, UserResolverError::Backend { .. }));
|
||||
}
|
||||
|
||||
// ----- live tests (require MX_LIVE + MX_GALAXY_DB) --------------------
|
||||
|
||||
/// Live integration test — gated on the workspace's `live` feature
|
||||
/// AND `MX_LIVE` env var being non-empty AND `MX_GALAXY_DB` being
|
||||
/// set to a parseable ADO connection string. Populated by
|
||||
/// `tools/Setup-LiveProbeEnv.ps1`.
|
||||
#[cfg(feature = "live")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "requires a live Galaxy DB; gated on MX_LIVE + MX_GALAXY_DB"]
|
||||
async fn live_resolve_test_child_object_test_int() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
eprintln!("MX_LIVE not set; skipping");
|
||||
return;
|
||||
}
|
||||
let conn = match std::env::var("MX_GALAXY_DB") {
|
||||
Ok(s) if !s.is_empty() => s,
|
||||
_ => {
|
||||
eprintln!("MX_GALAXY_DB not set; skipping");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let resolver = SqlTagResolver::from_ado_string(&conn).unwrap();
|
||||
let m = resolver
|
||||
.resolve("TestChildObject.TestInt")
|
||||
.await
|
||||
.expect("resolve live tag");
|
||||
assert_eq!(m.object_tag_name, "TestChildObject");
|
||||
assert_eq!(m.attribute_name, "TestInt");
|
||||
// mx_data_type 2 = Int32 per the Galaxy attribute table.
|
||||
assert_eq!(m.mx_data_type, 2);
|
||||
assert!(!m.is_array);
|
||||
}
|
||||
|
||||
#[cfg(feature = "live")]
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "requires a live Galaxy DB; gated on MX_LIVE + MX_GALAXY_DB"]
|
||||
async fn live_browse_test_child_object() {
|
||||
if std::env::var_os("MX_LIVE").is_none() {
|
||||
return;
|
||||
}
|
||||
let conn = match std::env::var("MX_GALAXY_DB") {
|
||||
Ok(s) if !s.is_empty() => s,
|
||||
_ => return,
|
||||
};
|
||||
let resolver = SqlTagResolver::from_ado_string(&conn).unwrap();
|
||||
let rows = resolver
|
||||
.browse("TestChildObject", "%", 50)
|
||||
.await
|
||||
.expect("browse live tag");
|
||||
// The TestChildObject template ships with at least TestInt +
|
||||
// TestString + a few framework attributes; assert non-empty.
|
||||
assert!(
|
||||
!rows.is_empty(),
|
||||
"expected at least one attribute on TestChildObject"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user