From d37d40a975052cc84acaaaa33c2d204d97e2a156 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Wed, 25 Mar 2026 17:19:54 +0300 Subject: [PATCH] refactor: make registry postgres-first --- .github/workflows/ci.yml | 16 + Cargo.lock | 374 +++++++++++++--- Cargo.toml | 2 +- crates/mcpaas-registry/src/lib.rs | 4 +- crates/mcpaas-registry/src/migrations.rs | 70 ++- .../src/{sqlite.rs => postgres.rs} | 398 +++++++++++------- docker-compose.yml | 2 + docs/architecture.md | 2 +- docs/database-schema.md | 12 +- docs/diagrams.md | 2 +- docs/module-decomposition.md | 1 - docs/runtime-config.md | 4 +- 12 files changed, 622 insertions(+), 265 deletions(-) rename crates/mcpaas-registry/src/{sqlite.rs => postgres.rs} (76%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56e9c44..0117204 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,22 @@ jobs: rust: name: Rust Checks runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: rmcp_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d rmcp_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + TEST_DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/rmcp_test steps: - name: Checkout diff --git a/Cargo.lock b/Cargo.lock index 97441a0..85ab3c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ dependencies = [ "generic-array", ] +[[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" @@ -210,6 +216,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -244,6 +251,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -261,17 +279,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "foldhash" version = "0.1.5" @@ -303,17 +310,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-intrusive" version = "0.5.0" @@ -417,6 +413,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.4.0" @@ -629,14 +652,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] -name = "libsqlite3-sys" -version = "0.30.1" +name = "libredox" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "cc", - "pkg-config", - "vcpkg", + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -788,6 +812,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -859,7 +893,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -883,10 +917,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.32" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "potential_utf" @@ -897,6 +931,15 @@ dependencies = [ "zerovec", ] +[[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-macro2" version = "1.0.106" @@ -915,6 +958,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +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", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -924,6 +997,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1117,6 +1199,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1128,15 +1213,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - [[package]] name = "sqlx" version = "0.8.6" @@ -1145,7 +1221,7 @@ checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", - "sqlx-sqlite", + "sqlx-postgres", ] [[package]] @@ -1173,6 +1249,7 @@ dependencies = [ "percent-encoding", "rustls", "serde", + "serde_json", "sha2", "smallvec", "thiserror", @@ -1213,34 +1290,47 @@ dependencies = [ "serde_json", "sha2", "sqlx-core", - "sqlx-sqlite", + "sqlx-postgres", "syn", "tokio", "url", ] [[package]] -name = "sqlx-sqlite" +name = "sqlx-postgres" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "flume", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", "futures-channel", "futures-core", - "futures-executor", - "futures-intrusive", "futures-util", - "libsqlite3-sys", + "hex", + "hkdf", + "hmac", + "home", + "itoa", "log", - "percent-encoding", + "md-5", + "memchr", + "once_cell", + "rand", "serde", - "serde_urlencoded", + "serde_json", + "sha2", + "smallvec", "sqlx-core", + "stringprep", "thiserror", "tracing", - "url", + "whoami", ] [[package]] @@ -1249,6 +1339,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1322,6 +1423,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "tokio" version = "1.50.0" @@ -1455,12 +1571,33 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1497,12 +1634,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[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" @@ -1515,6 +1646,12 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "webpki-roots" version = "0.26.11" @@ -1533,19 +1670,47 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1557,34 +1722,67 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[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_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1597,24 +1795,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1650,6 +1872,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index eb807a3..ac4f785 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ axum = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" -sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "sqlite"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tracing = "0.1" diff --git a/crates/mcpaas-registry/src/lib.rs b/crates/mcpaas-registry/src/lib.rs index a912c2e..ed2c39c 100644 --- a/crates/mcpaas-registry/src/lib.rs +++ b/crates/mcpaas-registry/src/lib.rs @@ -1,7 +1,7 @@ mod error; mod migrations; mod model; -mod sqlite; +mod postgres; pub use error::RegistryError; pub use model::{ @@ -11,4 +11,4 @@ pub use model::{ SaveSampleMetadataRequest, YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus, }; -pub use sqlite::SqliteRegistry; +pub use postgres::PostgresRegistry; diff --git a/crates/mcpaas-registry/src/migrations.rs b/crates/mcpaas-registry/src/migrations.rs index 19a07da..101712f 100644 --- a/crates/mcpaas-registry/src/migrations.rs +++ b/crates/mcpaas-registry/src/migrations.rs @@ -1,6 +1,6 @@ -use sqlx::{SqlitePool, query}; +use sqlx::{PgPool, query}; -pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { +pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> { query( "create table if not exists operations ( id text primary key, @@ -10,9 +10,9 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { status text not null, current_draft_version integer not null default 1, latest_published_version integer null, - created_at text not null, - updated_at text not null, - published_at text null + created_at timestamptz not null, + updated_at timestamptz not null, + published_at timestamptz null )", ) .execute(pool) @@ -20,24 +20,23 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { query( "create table if not exists operation_versions ( - operation_id text not null, + operation_id text not null references operations(id) on delete cascade, version integer not null, status text not null, - target_json text not null, - input_schema_json text not null, - output_schema_json text not null, - input_mapping_json text not null, - output_mapping_json text not null, - execution_config_json text not null, - tool_description_json text not null, - samples_json text null, - generated_draft_json text null, - config_export_json text null, + target_json jsonb not null, + input_schema_json jsonb not null, + output_schema_json jsonb not null, + input_mapping_json jsonb not null, + output_mapping_json jsonb not null, + execution_config_json jsonb not null, + tool_description_json jsonb not null, + samples_json jsonb null, + generated_draft_json jsonb null, + config_export_json jsonb null, change_note text null, - created_at text not null, + created_at timestamptz not null, created_by text null, - primary key (operation_id, version), - foreign key (operation_id) references operations(id) on delete cascade + primary key (operation_id, version) )", ) .execute(pool) @@ -45,11 +44,10 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { query( "create table if not exists published_operations ( - operation_id text primary key, + operation_id text primary key references operations(id) on delete cascade, version integer not null, - published_at text not null, + published_at timestamptz not null, published_by text null, - foreign key (operation_id) references operations(id) on delete cascade, foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade )", ) @@ -59,14 +57,13 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { query( "create table if not exists operation_samples ( id text primary key, - operation_id text not null, + operation_id text not null references operations(id) on delete cascade, version integer not null, sample_kind text not null, storage_ref text not null, content_type text not null, file_name text null, - created_at text not null, - foreign key (operation_id) references operations(id) on delete cascade, + created_at timestamptz not null, foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade )", ) @@ -76,14 +73,13 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { query( "create table if not exists descriptors ( id text primary key, - operation_id text null, + operation_id text null references operations(id) on delete cascade, version integer null, descriptor_kind text not null, storage_ref text not null, source_name text null, - package_index_json text null, - created_at text not null, - foreign key (operation_id) references operations(id) on delete cascade, + package_index_json jsonb null, + created_at timestamptz not null, foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade )", ) @@ -95,9 +91,9 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { id text primary key, name text not null unique, kind text not null, - config_json text not null, - created_at text not null, - updated_at text not null + config_json jsonb not null, + created_at timestamptz not null, + updated_at timestamptz not null )", ) .execute(pool) @@ -106,17 +102,15 @@ pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { query( "create table if not exists yaml_import_jobs ( id text primary key, - source_sample_id text null, + source_sample_id text null references operation_samples(id) on delete set null, status text not null, format_version text not null, mode text not null, - result_operation_id text null, + result_operation_id text null references operations(id) on delete set null, result_version integer null, error_text text null, - created_at text not null, - finished_at text null, - foreign key (source_sample_id) references operation_samples(id) on delete set null, - foreign key (result_operation_id) references operations(id) on delete set null + created_at timestamptz not null, + finished_at timestamptz null )", ) .execute(pool) diff --git a/crates/mcpaas-registry/src/sqlite.rs b/crates/mcpaas-registry/src/postgres.rs similarity index 76% rename from crates/mcpaas-registry/src/sqlite.rs rename to crates/mcpaas-registry/src/postgres.rs index f784acc..3bd71bf 100644 --- a/crates/mcpaas-registry/src/sqlite.rs +++ b/crates/mcpaas-registry/src/postgres.rs @@ -1,7 +1,11 @@ use mcpaas_core::{AuthProfile, OperationId, OperationStatus}; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; -use sqlx::{Row, SqlitePool, sqlite::SqlitePoolOptions}; +use sqlx::{ + PgPool, Postgres, Row, Transaction, + postgres::{PgPoolOptions, PgRow}, + types::Json, +}; use crate::{ error::RegistryError, @@ -16,35 +20,44 @@ use crate::{ }; #[derive(Clone, Debug)] -pub struct SqliteRegistry { - pool: SqlitePool, +pub struct PostgresRegistry { + pool: PgPool, } -impl SqliteRegistry { +impl PostgresRegistry { pub async fn connect(database_url: &str) -> Result { - let pool = SqlitePoolOptions::new() - .max_connections(1) - .connect(database_url) - .await?; - - sqlx::query("pragma foreign_keys = on") - .execute(&pool) - .await?; - - let registry = Self { pool }; - registry.migrate().await?; - Ok(registry) + Self::connect_in_schema(database_url, None).await } - pub fn pool(&self) -> &SqlitePool { + pub fn pool(&self) -> &PgPool { &self.pool } pub async fn migrate(&self) -> Result<(), RegistryError> { - migrations::apply_sqlite(&self.pool).await?; + migrations::apply_postgres(&self.pool).await?; Ok(()) } + async fn connect_in_schema( + database_url: &str, + schema: Option<&str>, + ) -> Result { + let pool = PgPoolOptions::new() + .max_connections(1) + .connect(database_url) + .await?; + + if let Some(schema) = schema { + sqlx::query(&format!("set search_path to {schema}")) + .execute(&pool) + .await?; + } + + let registry = Self { pool }; + registry.migrate().await?; + Ok(registry) + } + pub async fn create_operation( &self, snapshot: &RegistryOperation, @@ -77,7 +90,12 @@ impl SqliteRegistry { created_at, updated_at, published_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) values ( + $1, $2, $3, $4, $5, $6, $7, + $8::timestamptz, + $9::timestamptz, + $10::timestamptz + )", ) .bind(snapshot.id.as_str()) .bind(&snapshot.name) @@ -136,10 +154,10 @@ impl SqliteRegistry { sqlx::query( "update operations - set status = ?, - current_draft_version = ?, - updated_at = ? - where id = ?", + set status = $1, + current_draft_version = $2, + updated_at = $3::timestamptz + where id = $4", ) .bind(serialize_enum_text(&request.snapshot.status, "status")?) .bind(to_db_version(request.snapshot.version)) @@ -162,9 +180,9 @@ impl SqliteRegistry { status, current_draft_version, latest_published_version, - created_at, - updated_at, - published_at + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, + to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at from operations order by name asc", ) @@ -187,11 +205,11 @@ impl SqliteRegistry { status, current_draft_version, latest_published_version, - created_at, - updated_at, - published_at + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at, + to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at from operations - where id = ?", + where id = $1", ) .bind(operation_id.as_str()) .fetch_optional(&self.pool) @@ -211,9 +229,9 @@ impl SqliteRegistry { o.name, o.display_name, o.protocol, - o.created_at as operation_created_at, - o.updated_at as operation_updated_at, - o.published_at as operation_published_at, + to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at, + to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at, + to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at, ov.version, ov.status, ov.target_json, @@ -227,11 +245,11 @@ impl SqliteRegistry { ov.generated_draft_json, ov.config_export_json, ov.change_note, - ov.created_at, + to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, ov.created_by from operation_versions ov join operations o on o.id = ov.operation_id - where ov.operation_id = ? and ov.version = ?", + where ov.operation_id = $1 and ov.version = $2", ) .bind(operation_id.as_str()) .bind(to_db_version(version)) @@ -251,9 +269,9 @@ impl SqliteRegistry { o.name, o.display_name, o.protocol, - o.created_at as operation_created_at, - o.updated_at as operation_updated_at, - o.published_at as operation_published_at, + to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at, + to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at, + to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at, ov.version, ov.status, ov.target_json, @@ -267,11 +285,11 @@ impl SqliteRegistry { ov.generated_draft_json, ov.config_export_json, ov.change_note, - ov.created_at, + to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, ov.created_by from operation_versions ov join operations o on o.id = ov.operation_id - where ov.operation_id = ? + where ov.operation_id = $1 order by ov.version asc", ) .bind(operation_id.as_str()) @@ -304,7 +322,7 @@ impl SqliteRegistry { version, published_at, published_by - ) values (?, ?, ?, ?) + ) values ($1, $2, $3::timestamptz, $4) on conflict(operation_id) do update set version = excluded.version, published_at = excluded.published_at, @@ -319,8 +337,8 @@ impl SqliteRegistry { sqlx::query( "update operation_versions - set status = ? - where operation_id = ? and version = ?", + set status = $1 + where operation_id = $2 and version = $3", ) .bind(serialize_enum_text(&OperationStatus::Published, "status")?) .bind(request.operation_id.as_str()) @@ -330,11 +348,11 @@ impl SqliteRegistry { sqlx::query( "update operations - set status = ?, - latest_published_version = ?, - published_at = ?, - updated_at = ? - where id = ?", + set status = $1, + latest_published_version = $2, + published_at = $3::timestamptz, + updated_at = $4::timestamptz + where id = $5", ) .bind(serialize_enum_text(&OperationStatus::Published, "status")?) .bind(to_db_version(request.version)) @@ -358,9 +376,9 @@ impl SqliteRegistry { o.name, o.display_name, o.protocol, - o.created_at as operation_created_at, - o.updated_at as operation_updated_at, - o.published_at as operation_published_at, + to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at, + to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at, + to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at, ov.version, ov.status, ov.target_json, @@ -374,13 +392,13 @@ impl SqliteRegistry { ov.generated_draft_json, ov.config_export_json, ov.change_note, - ov.created_at, + to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, ov.created_by from published_operations po join operation_versions ov on ov.operation_id = po.operation_id and ov.version = po.version join operations o on o.id = po.operation_id - where po.operation_id = ?", + where po.operation_id = $1", ) .bind(operation_id.as_str()) .fetch_optional(&self.pool) @@ -398,9 +416,9 @@ impl SqliteRegistry { o.name, o.display_name, o.protocol, - o.created_at as operation_created_at, - o.updated_at as operation_updated_at, - o.published_at as operation_published_at, + to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at, + to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at, + to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at, ov.version, ov.status, ov.target_json, @@ -414,7 +432,7 @@ impl SqliteRegistry { ov.generated_draft_json, ov.config_export_json, ov.change_note, - ov.created_at, + to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, ov.created_by from published_operations po join operation_versions ov @@ -442,7 +460,7 @@ impl SqliteRegistry { config_json, created_at, updated_at - ) values (?, ?, ?, ?, ?, ?) + ) values ($1, $2, $3, $4, $5::timestamptz, $6::timestamptz) on conflict(id) do update set name = excluded.name, kind = excluded.kind, @@ -452,7 +470,7 @@ impl SqliteRegistry { .bind(request.profile.id.as_str()) .bind(&request.profile.name) .bind(serialize_enum_text(&request.profile.kind, "kind")?) - .bind(serialize_json(&request.profile.config)?) + .bind(Json(serialize_json_value(&request.profile.config)?)) .bind(&request.profile.created_at) .bind(&request.profile.updated_at) .execute(&self.pool) @@ -466,9 +484,15 @@ impl SqliteRegistry { auth_profile_id: &mcpaas_core::AuthProfileId, ) -> Result, RegistryError> { let row = sqlx::query( - "select id, name, kind, config_json, created_at, updated_at + "select + id, + name, + kind, + config_json, + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at from auth_profiles - where id = ?", + where id = $1", ) .bind(auth_profile_id.as_str()) .fetch_optional(&self.pool) @@ -479,7 +503,13 @@ impl SqliteRegistry { pub async fn list_auth_profiles(&self) -> Result, RegistryError> { let rows = sqlx::query( - "select id, name, kind, config_json, created_at, updated_at + "select + id, + name, + kind, + config_json, + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at from auth_profiles order by name asc", ) @@ -503,7 +533,7 @@ impl SqliteRegistry { content_type, file_name, created_at - ) values (?, ?, ?, ?, ?, ?, ?, ?) + ) values ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz) on conflict(id) do update set operation_id = excluded.operation_id, version = excluded.version, @@ -544,9 +574,9 @@ impl SqliteRegistry { storage_ref, content_type, file_name, - created_at + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at from operation_samples - where operation_id = ? and version = ? + where operation_id = $1 and version = $2 order by created_at asc", ) .bind(operation_id.as_str()) @@ -571,7 +601,7 @@ impl SqliteRegistry { source_name, package_index_json, created_at - ) values (?, ?, ?, ?, ?, ?, ?, ?) + ) values ($1, $2, $3, $4, $5, $6, $7, $8::timestamptz) on conflict(id) do update set operation_id = excluded.operation_id, version = excluded.version, @@ -596,7 +626,7 @@ impl SqliteRegistry { )?) .bind(&request.descriptor.storage_ref) .bind(request.descriptor.source_name.as_deref()) - .bind(serialize_option_json(&request.descriptor.package_index)?) + .bind(serialize_option_json_value(&request.descriptor.package_index)?.map(Json)) .bind(&request.descriptor.created_at) .execute(&self.pool) .await?; @@ -618,9 +648,9 @@ impl SqliteRegistry { storage_ref, source_name, package_index_json, - created_at + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at from descriptors - where operation_id = ? and version = ? + where operation_id = $1 and version = $2 order by created_at asc", ) .bind(operation_id.as_str()) @@ -647,7 +677,7 @@ impl SqliteRegistry { error_text, created_at, finished_at - ) values (?, ?, ?, ?, ?, null, null, null, ?, null)", + ) values ($1, $2, $3, $4, $5, null, null, null, $6::timestamptz, null)", ) .bind(request.id.as_str()) .bind(request.source_sample_id.map(|value| value.as_str())) @@ -671,12 +701,12 @@ impl SqliteRegistry { ) -> Result<(), RegistryError> { let result = sqlx::query( "update yaml_import_jobs - set status = ?, - result_operation_id = ?, - result_version = ?, - error_text = ?, - finished_at = ? - where id = ?", + set status = $1, + result_operation_id = $2, + result_version = $3, + error_text = $4, + finished_at = $5::timestamptz + where id = $6", ) .bind(serialize_enum_text(&completion.status, "status")?) .bind( @@ -715,10 +745,10 @@ impl SqliteRegistry { result_operation_id, result_version, error_text, - created_at, - finished_at + to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at, + to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at from yaml_import_jobs - where id = ?", + where id = $1", ) .bind(job_id.as_str()) .fetch_optional(&self.pool) @@ -729,7 +759,7 @@ impl SqliteRegistry { } async fn insert_version_row( - tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + tx: &mut Transaction<'_, Postgres>, snapshot: &RegistryOperation, change_note: Option<&str>, created_by: Option<&str>, @@ -752,21 +782,24 @@ async fn insert_version_row( change_note, created_at, created_by - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) values ( + $1, $2, $3, $4, $5, $6, $7, $8, + $9, $10, $11, $12, $13, $14, $15::timestamptz, $16 + )", ) .bind(snapshot.id.as_str()) .bind(to_db_version(snapshot.version)) .bind(serialize_enum_text(&snapshot.status, "status")?) - .bind(serialize_json(&snapshot.target)?) - .bind(serialize_json(&snapshot.input_schema)?) - .bind(serialize_json(&snapshot.output_schema)?) - .bind(serialize_json(&snapshot.input_mapping)?) - .bind(serialize_json(&snapshot.output_mapping)?) - .bind(serialize_json(&snapshot.execution_config)?) - .bind(serialize_json(&snapshot.tool_description)?) - .bind(serialize_option_json(&snapshot.samples)?) - .bind(serialize_option_json(&snapshot.generated_draft)?) - .bind(serialize_option_json(&snapshot.config_export)?) + .bind(Json(serialize_json_value(&snapshot.target)?)) + .bind(Json(serialize_json_value(&snapshot.input_schema)?)) + .bind(Json(serialize_json_value(&snapshot.output_schema)?)) + .bind(Json(serialize_json_value(&snapshot.input_mapping)?)) + .bind(Json(serialize_json_value(&snapshot.output_mapping)?)) + .bind(Json(serialize_json_value(&snapshot.execution_config)?)) + .bind(Json(serialize_json_value(&snapshot.tool_description)?)) + .bind(serialize_option_json_value(&snapshot.samples)?.map(Json)) + .bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json)) + .bind(serialize_option_json_value(&snapshot.config_export)?.map(Json)) .bind(change_note) .bind(&snapshot.updated_at) .bind(created_by) @@ -804,7 +837,7 @@ fn assert_immutable_fields( Ok(()) } -fn map_operation_summary(row: &sqlx::sqlite::SqliteRow) -> Result { +fn map_operation_summary(row: &PgRow) -> Result { Ok(OperationSummary { id: OperationId::new(row.try_get::("id")?), name: row.try_get("name")?, @@ -816,7 +849,7 @@ fn map_operation_summary(row: &sqlx::sqlite::SqliteRow) -> Result, _>("latest_published_version")? + .try_get::, _>("latest_published_version")? .map(|value| from_db_version(value, "latest_published_version")) .transpose()?, created_at: row.try_get("created_at")?, @@ -825,9 +858,7 @@ fn map_operation_summary(row: &sqlx::sqlite::SqliteRow) -> Result Result { +fn map_operation_version_record(row: &PgRow) -> Result { let operation_id = OperationId::new(row.try_get::("id")?); let version = from_db_version(row.try_get("version")?, "version")?; let status = deserialize_enum_text(&row.try_get::("status")?, "status")?; @@ -846,31 +877,36 @@ fn map_operation_version_record( protocol: deserialize_enum_text(&row.try_get::("protocol")?, "protocol")?, status, version, - target: deserialize_json(&row.try_get::("target_json")?)?, - input_schema: deserialize_json(&row.try_get::("input_schema_json")?)?, - output_schema: deserialize_json(&row.try_get::("output_schema_json")?)?, - input_mapping: deserialize_json(&row.try_get::("input_mapping_json")?)?, - output_mapping: deserialize_json(&row.try_get::("output_mapping_json")?)?, - execution_config: deserialize_json( - &row.try_get::("execution_config_json")?, + target: deserialize_json_value(row.try_get::, _>("target_json")?.0)?, + input_schema: deserialize_json_value( + row.try_get::, _>("input_schema_json")?.0, )?, - tool_description: deserialize_json( - &row.try_get::("tool_description_json")?, + output_schema: deserialize_json_value( + row.try_get::, _>("output_schema_json")?.0, + )?, + input_mapping: deserialize_json_value( + row.try_get::, _>("input_mapping_json")?.0, + )?, + output_mapping: deserialize_json_value( + row.try_get::, _>("output_mapping_json")?.0, + )?, + execution_config: deserialize_json_value( + row.try_get::, _>("execution_config_json")?.0, + )?, + tool_description: deserialize_json_value( + row.try_get::, _>("tool_description_json")?.0, )?, samples: row - .try_get::, _>("samples_json")? - .as_deref() - .map(deserialize_json) + .try_get::>, _>("samples_json")? + .map(|value| deserialize_json_value(value.0)) .transpose()?, generated_draft: row - .try_get::, _>("generated_draft_json")? - .as_deref() - .map(deserialize_json) + .try_get::>, _>("generated_draft_json")? + .map(|value| deserialize_json_value(value.0)) .transpose()?, config_export: row - .try_get::, _>("config_export_json")? - .as_deref() - .map(deserialize_json) + .try_get::>, _>("config_export_json")? + .map(|value| deserialize_json_value(value.0)) .transpose()?, created_at: row.try_get("operation_created_at")?, updated_at: row.try_get("operation_updated_at")?, @@ -879,20 +915,18 @@ fn map_operation_version_record( }) } -fn map_auth_profile(row: &sqlx::sqlite::SqliteRow) -> Result { +fn map_auth_profile(row: &PgRow) -> Result { Ok(AuthProfile { id: mcpaas_core::AuthProfileId::new(row.try_get::("id")?), name: row.try_get("name")?, kind: deserialize_enum_text(&row.try_get::("kind")?, "kind")?, - config: deserialize_json(&row.try_get::("config_json")?)?, + config: deserialize_json_value(row.try_get::, _>("config_json")?.0)?, created_at: row.try_get("created_at")?, updated_at: row.try_get("updated_at")?, }) } -fn map_sample_metadata( - row: &sqlx::sqlite::SqliteRow, -) -> Result { +fn map_sample_metadata(row: &PgRow) -> Result { Ok(OperationSampleMetadata { id: mcpaas_core::SampleId::new(row.try_get::("id")?), operation_id: OperationId::new(row.try_get::("operation_id")?), @@ -908,16 +942,14 @@ fn map_sample_metadata( }) } -fn map_descriptor_metadata( - row: &sqlx::sqlite::SqliteRow, -) -> Result { +fn map_descriptor_metadata(row: &PgRow) -> Result { Ok(DescriptorMetadata { id: mcpaas_core::DescriptorId::new(row.try_get::("id")?), operation_id: row .try_get::, _>("operation_id")? .map(OperationId::new), version: row - .try_get::, _>("version")? + .try_get::, _>("version")? .map(|value| from_db_version(value, "version")) .transpose()?, descriptor_kind: deserialize_enum_text( @@ -927,15 +959,13 @@ fn map_descriptor_metadata( storage_ref: row.try_get("storage_ref")?, source_name: row.try_get("source_name")?, package_index: row - .try_get::, _>("package_index_json")? - .as_deref() - .map(deserialize_json) - .transpose()?, + .try_get::>, _>("package_index_json")? + .map(|value| value.0), created_at: row.try_get("created_at")?, }) } -fn map_yaml_import_job(row: &sqlx::sqlite::SqliteRow) -> Result { +fn map_yaml_import_job(row: &PgRow) -> Result { Ok(YamlImportJob { id: YamlImportJobId::new(row.try_get::("id")?), source_sample_id: row @@ -948,7 +978,7 @@ fn map_yaml_import_job(row: &sqlx::sqlite::SqliteRow) -> Result, _>("result_operation_id")? .map(OperationId::new), result_version: row - .try_get::, _>("result_version")? + .try_get::, _>("result_version")? .map(|value| from_db_version(value, "result_version")) .transpose()?, error_text: row.try_get("error_text")?, @@ -957,16 +987,18 @@ fn map_yaml_import_job(row: &sqlx::sqlite::SqliteRow) -> Result(value: &T) -> Result { - Ok(serde_json::to_string(value)?) +fn serialize_json_value(value: &T) -> Result { + Ok(serde_json::to_value(value)?) } -fn serialize_option_json(value: &Option) -> Result, RegistryError> { - value.as_ref().map(serialize_json).transpose() +fn serialize_option_json_value( + value: &Option, +) -> Result, RegistryError> { + value.as_ref().map(serialize_json_value).transpose() } -fn deserialize_json(value: &str) -> Result { - Ok(serde_json::from_str(value)?) +fn deserialize_json_value(value: Value) -> Result { + Ok(serde_json::from_value(value)?) } fn serialize_enum_text( @@ -987,17 +1019,24 @@ fn deserialize_enum_text( .map_err(|_| RegistryError::InvalidEnumRepresentation { field }) } -fn to_db_version(value: u32) -> i64 { - i64::from(value) +fn to_db_version(value: u32) -> i32 { + value as i32 } -fn from_db_version(value: i64, field: &'static str) -> Result { - u32::try_from(value).map_err(|_| RegistryError::InvalidNumericValue { field, value }) +fn from_db_version(value: i32, field: &'static str) -> Result { + u32::try_from(value).map_err(|_| RegistryError::InvalidNumericValue { + field, + value: i64::from(value), + }) } #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::{ + collections::BTreeMap, + env, + time::{SystemTime, UNIX_EPOCH}, + }; use mcpaas_core::{ ApiKeyHeaderAuthConfig, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig, @@ -1008,10 +1047,10 @@ mod tests { use mcpaas_mapping::{MappingRule, MappingSet}; use mcpaas_schema::{Schema, SchemaKind}; use serde_json::json; + use sqlx::{Executor, PgPool, postgres::PgPoolOptions}; use crate::{ - SqliteRegistry, - error::RegistryError, + PostgresRegistry, RegistryError, model::{ CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata, OperationSampleMetadata, PublishRequest, RegistryOperation, SampleKind, @@ -1022,7 +1061,8 @@ mod tests { #[tokio::test] async fn stores_versions_and_published_operations() { - let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let database = TestDatabase::new().await; + let registry = database.registry().await; let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft); registry @@ -1076,11 +1116,14 @@ mod tests { ); assert_eq!(published.version, 2); assert!(published.is_published()); + + database.cleanup().await; } #[tokio::test] async fn rejects_out_of_order_versions() { - let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let database = TestDatabase::new().await; + let registry = database.registry().await; let operation = test_operation("op_rest_02", 1, OperationStatus::Draft); registry.create_operation(&operation, None).await.unwrap(); @@ -1103,11 +1146,14 @@ mod tests { .. } )); + + database.cleanup().await; } #[tokio::test] async fn stores_auth_profiles_and_artifact_metadata() { - let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let database = TestDatabase::new().await; + let registry = database.registry().await; let operation = test_operation("op_rest_03", 1, OperationStatus::Draft); registry.create_operation(&operation, None).await.unwrap(); @@ -1178,11 +1224,14 @@ mod tests { assert_eq!(auth_profiles, vec![auth_profile]); assert_eq!(samples, vec![input_sample]); assert_eq!(descriptors, vec![descriptor]); + + database.cleanup().await; } #[tokio::test] async fn stores_and_finishes_yaml_import_jobs() { - let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let database = TestDatabase::new().await; + let registry = database.registry().await; let job_id = YamlImportJobId::new("job_yaml_01"); let operation = test_operation("op_rest_04", 1, OperationStatus::Draft); @@ -1222,6 +1271,61 @@ mod tests { assert_eq!(job.status, YamlImportJobStatus::Completed); assert_eq!(job.result_version, Some(2)); assert_eq!(job.mode, ExportMode::Portable); + + database.cleanup().await; + } + + struct TestDatabase { + admin_pool: PgPool, + database_url: String, + schema: String, + } + + impl TestDatabase { + async fn new() -> Self { + let database_url = env::var("TEST_DATABASE_URL") + .expect("TEST_DATABASE_URL must point to a reachable PostgreSQL database"); + let admin_pool = PgPoolOptions::new() + .max_connections(1) + .connect(&database_url) + .await + .unwrap(); + let schema = format!( + "test_registry_{}_{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + + admin_pool + .execute(sqlx::query(&format!("create schema {schema}"))) + .await + .unwrap(); + + Self { + admin_pool, + database_url, + schema, + } + } + + async fn registry(&self) -> PostgresRegistry { + PostgresRegistry::connect_in_schema(&self.database_url, Some(&self.schema)) + .await + .unwrap() + } + + async fn cleanup(&self) { + self.admin_pool + .execute(sqlx::query(&format!( + "drop schema if exists {} cascade", + self.schema + ))) + .await + .unwrap(); + } } fn test_operation(id: &str, version: u32, status: OperationStatus) -> RegistryOperation { @@ -1344,7 +1448,7 @@ mod tests { export_mode: ExportMode::Portable, }), created_at: "2026-03-25T11:58:00Z".to_owned(), - updated_at: format!("2026-03-25T12:{:02}:00Z", version), + updated_at: format!("2026-03-25T12:{version:02}:00Z"), published_at: None, } } diff --git a/docker-compose.yml b/docker-compose.yml index b377b3e..ff2dedf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rmcp} volumes: - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rmcp} -d ${POSTGRES_DB:-rmcp}"] interval: 10s diff --git a/docs/architecture.md b/docs/architecture.md index c8c2d5e..f1f6b2c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -434,7 +434,7 @@ mcpaas/ - `tokio` как async runtime - `axum` для HTTP API - `serde` и `serde_json` -- `sqlx` для PostgreSQL или SQLite +- `sqlx` для PostgreSQL - `reqwest` для REST и GraphQL транспорта - `tonic` и `prost` для работы с gRPC - `tower` для middleware diff --git a/docs/database-schema.md b/docs/database-schema.md index 717f0c0..83275c2 100644 --- a/docs/database-schema.md +++ b/docs/database-schema.md @@ -4,7 +4,7 @@ Этот документ фиксирует структуру хранения конфигураций, версий операций, загруженных артефактов и published runtime-view. Его цель - дать основу для SQL-миграций и для реализации `mcpaas-registry`. -В документе предполагается реляционная модель, ориентированная на `PostgreSQL`. Для MVP допускается адаптация под `SQLite`, но канонической считается схема, совместимая с `PostgreSQL`. +В документе предполагается реляционная модель, ориентированная на `PostgreSQL`. Канонической считается схема, совместимая с `PostgreSQL`. ## 2. Общие принципы хранения @@ -30,13 +30,13 @@ Для MVP рекомендуется отдельная таблица `auth_profiles`, где metadata и secret refs отделены от operation versions. -### 2.5. SQLite-адаптация допустима +### 2.5. Тестовая изоляция -Канонической схемой остается PostgreSQL-совместимая модель, но MVP-реализация registry может использовать SQLite. В таком случае: +Integration tests для registry должны выполняться на реальной `PostgreSQL`, но без влияния на runtime-данные. Предпочтительный способ: -- `jsonb` хранится как `text` с JSON-сериализацией; -- `timestamptz` хранится как `text`; -- внешние ключи и version snapshot semantics сохраняются без изменения. +- отдельная test database; +- либо отдельная временная schema на время теста; +- обязательная очистка после завершения тестов. ## 3. Основные таблицы diff --git a/docs/diagrams.md b/docs/diagrams.md index 4d3a301..59745f8 100644 --- a/docs/diagrams.md +++ b/docs/diagrams.md @@ -27,7 +27,7 @@ flowchart LR REST[adapter-rest] GQL[adapter-graphql] GRPC[adapter-grpc] - DB[(PostgreSQL/SQLite)] + DB[(PostgreSQL)] STORE[(Artifact Storage)] UI --> ADMIN diff --git a/docs/module-decomposition.md b/docs/module-decomposition.md index acdd833..77f2841 100644 --- a/docs/module-decomposition.md +++ b/docs/module-decomposition.md @@ -311,7 +311,6 @@ mcpaas-registry/ get_runtime_view.rs storage/ mod.rs - sqlite.rs postgres.rs cache/ mod.rs diff --git a/docs/runtime-config.md b/docs/runtime-config.md index cd2a83e..cff4e8f 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -9,7 +9,7 @@ ## 2. Базовые решения для MVP - каноническая БД: `PostgreSQL` -- допустимый упрощенный режим разработки: `SQLite` +- локальная разработка и тесты используют ту же `PostgreSQL`-модель хранения - artifact storage: локальная файловая система - MCP transport: `Streamable HTTP` - admin API и mcp-server запускаются как отдельные приложения @@ -100,7 +100,7 @@ var/mcpaas/ Local development: -- `SQLite` допустим; +- локальное окружение должно иметь доступ к `PostgreSQL`; - локальный storage; - упрощенная auth-модель admin-api.