diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 45a0cedbe89b..76b72a687917 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ /integration/ @FerretDB/leads /internal/ @FerretDB/leads +/internal/backends/ @AlekSi /internal/handlers/hana/ @AlekSi # @AlekSi for engineering, @ptrfarkas for marketing. diff --git a/.github/RELEASE_CHECKLIST.md b/.github/RELEASE_CHECKLIST.md index 49b0dbee968e..777a1c174f3c 100644 --- a/.github/RELEASE_CHECKLIST.md +++ b/.github/RELEASE_CHECKLIST.md @@ -44,4 +44,5 @@ 1. Bump the latest version on [Beacon](https://beacon.ferretdb.io). 2. Publish and announce blog post. 3. Tweet, toot. -4. Update NixOS package. +4. Update NixOS package: . +5. Update Civo package: . diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 000000000000..0025a4fd09f5 --- /dev/null +++ b/.github/workflows/clean.yml @@ -0,0 +1,26 @@ +name: clean old packages +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + scan-old-package: + runs-on: ubuntu-latest + + # disable for now + if: false + + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.20" + - name: Install dependencies + run: cd tools; go get ./cleantool + - name: Scan old package versions and store the version id to env + env: + ROBOT_TOKEN: ${{ secrets.ROBOT_TOKEN }} + run: | + cd tools;go run ./cleantool/cleantool.go diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2023d6d14a6a..bea21baba052 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -43,6 +43,7 @@ jobs: include: - { name: "MongoDB", task: "mongodb" } - { name: "PostgreSQL", task: "pg" } + - { name: "SQLite", task: "sqlite" } # - { name: "Tigris", task: "tigris" } # - { name: "Tigris main", task: "tigris", tigris_dockerfile: "tigris_main" } diff --git a/.gitignore b/.gitignore index 2c2be7644e47..ac5f3af0231f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ bin/ vendor/ # development and build artifacts, temporary files, etc -*.txt *.out +*.sqlite +*.txt **/testdata/fuzz/ state.json tmp/ diff --git a/.golangci.yml b/.golangci.yml index 6ed0f86f90ee..eb19dc86ecb1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -167,10 +167,13 @@ issues: path: cmd/envtool text: pgdb - # only `pg` handler can import `sjson` package, no other handler can do that + # only `pg` handler and `sqlite` backend can import `sjson` package, no other handlers or backends can do that - linters: [depguard] path: internal/handlers/pg text: sjson + - linters: [depguard] + path: internal/backends/sqlite + test: sjson # only `tigris` handler can import `tigrisdb` package, no other handler can do that - linters: [depguard] diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eeb6580f33e..b0be72149933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,78 @@ # Changelog +## [v1.2.0](https://github.com/FerretDB/FerretDB/releases/tag/v1.2.0) (2023-05-22) + +### What's Changed + +This release includes a highly experimental and unsupported SQLite backend. +It will be improved in future releases. + +### Fixed Bugs πŸ› +* Fix compatibility with C# driver by @b1ron in https://github.com/FerretDB/FerretDB/pull/2613 +* Fix a bug with unset field sorting by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2638 +* Return `int64` values for `dbStats` and `collStats` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2642 +* Return command error from `findAndModify` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2646 +* Fix index creation on nested fields by @wqhhust in https://github.com/FerretDB/FerretDB/pull/2637 + +### Enhancements πŸ›  +* Perform `insertMany` in a single transaction by @raeidish in https://github.com/FerretDB/FerretDB/pull/2532 +* Relax PostgreSQL connection checks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2602 +* Cleanup `insert` command by @noisersup in https://github.com/FerretDB/FerretDB/pull/2609 +* Support dot notation in projection by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2536 + +### Documentation πŸ“„ +* Add FerretDB v1.1.0 release blog post by @Fashander in https://github.com/FerretDB/FerretDB/pull/2594 +* Update blog post image for 1.1.0 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2601 +* Add documentation for `.rpm` packages by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2604 +* Fix a typo in a blog post by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2611 +* Fix typo on RPM package file name by @christiano in https://github.com/FerretDB/FerretDB/pull/2628 +* Update documentation formatting by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2640 +* Add blog post on "Meteor and FerretDB" by @Fashander in https://github.com/FerretDB/FerretDB/pull/2654 + +### Other Changes πŸ€– +* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2592 +* Remove `TODO` comment for closed issue by @adetunjii in https://github.com/FerretDB/FerretDB/pull/2573 +* Add experimental integration test flag for pushdown sorting by @noisersup in https://github.com/FerretDB/FerretDB/pull/2595 +* Extract handler parameters from corresponding structure by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2513 +* Add `shell` subcommands (`mkdir`, `rmdir`) in `envtool` by @kropidlowsky in https://github.com/FerretDB/FerretDB/pull/2596 +* Add basic postcondition checker for errors by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2607 +* Add `sqlite` handler stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2608 +* Make protocol-level crashes easier to understand by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2610 +* Simplify `envtool shell` subcommands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2614 +* Cleanup old Docker images by @wqhhust in https://github.com/FerretDB/FerretDB/pull/2533 +* Fix exponential backoff minimum duration by @noisersup in https://github.com/FerretDB/FerretDB/pull/2578 +* Fix `count`'s `query` parameter by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2622 +* Add a README.md file for assertions by @b1ron in https://github.com/FerretDB/FerretDB/pull/2569 +* Use `ExtractParameters` in handlers by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2620 +* Verify `OP_MSG` message checksum by @adetunjii in https://github.com/FerretDB/FerretDB/pull/2540 +* Separate codebase for aggregation `$project` and query `projection` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2631 +* Implement `envtool shell read` subcommand by @wqhhust in https://github.com/FerretDB/FerretDB/pull/2626 +* Cleanup projection by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2641 +* Add common backend interface prototype by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2619 +* Add SQLite handler flags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2651 +* Add tests for aggregation expressions with dots in `$group` aggregation stage by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2636 +* Implement some SQLite backend commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2655 +* Fix tests to assert correct error by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2546 +* Aggregation expression refactor by @noisersup in https://github.com/FerretDB/FerretDB/pull/2644 +* Move common commands to `commoncommands` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2660 +* Add basic observability into backend interfaces by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2661 +* Implement metadata storage by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2656 +* Add `Query` to the common backend interface by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2662 +* Implement query request for SQLite backend by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2665 +* Add test case for read in envtools by @wqhhust in https://github.com/FerretDB/FerretDB/pull/2657 +* Run integration tests for `sqlite` handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2666 +* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2671 +* Create SQLite directory if needed by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2673 +* Implement SQLite `update` and `delete` commands by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2674 + +### New Contributors +* @adetunjii made their first contribution in https://github.com/FerretDB/FerretDB/pull/2573 +* @christiano made their first contribution in https://github.com/FerretDB/FerretDB/pull/2628 + +[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/41?closed=1). +[All commits](https://github.com/FerretDB/FerretDB/compare/v1.1.0...v1.2.0). + + ## [v1.1.0](https://github.com/FerretDB/FerretDB/releases/tag/v1.1.0) (2023-05-09) ### New Features πŸŽ‰ @@ -107,7 +180,7 @@ * Small refactoring by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2575 * Merge `no ci` label into `not ready` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2580 -## New Contributors +### New Contributors * @cooljeanius made their first contribution in https://github.com/FerretDB/FerretDB/pull/2420 * @j0holo made their first contribution in https://github.com/FerretDB/FerretDB/pull/2419 * @AuruTus made their first contribution in https://github.com/FerretDB/FerretDB/pull/2435 @@ -165,1521 +238,6 @@ We are delighted to announce the release of FerretDB 1.0 GA! [All commits](https://github.com/FerretDB/FerretDB/compare/v0.9.4...v1.0.0). -## [v0.9.4](https://github.com/FerretDB/FerretDB/releases/tag/v0.9.4) (2023-03-27) - -### New Features πŸŽ‰ -* Support dot notation in sorting by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2156 -* Add support for `$each` modifier to `$addToSet` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2187 -* Add support for `$each` modifier to `$push` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2202 -* Add support for `$pull` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2222 - -### Fixed Bugs πŸ› -* Fix `hana` and `tigris` handlers initialization by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2234 -* Fix long overflow detection on `$mul` by @noisersup in https://github.com/FerretDB/FerretDB/pull/2232 -* Fix pushdown for large double values by @noisersup in https://github.com/FerretDB/FerretDB/pull/2163 - -### Enhancements πŸ›  -* Support `skip` argument for `find` and `count` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2193 -* Support query pushdown for `$match` aggregation stage by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2199 -* Rework integration test setup for validator by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2225 -* Change collection names mangling for PostgreSQL by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2239 -* Implement `nameOnly` for `listCollections` command by @kropidlowsky in https://github.com/FerretDB/FerretDB/pull/2247 -* Extract dollar path field expression to types package by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2263 - -### Documentation πŸ“„ -* Prevent confusing messages for embedded FerretDB by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2209 -* Add new FerretDB v0.9.3 release blogpost by @Fashander in https://github.com/FerretDB/FerretDB/pull/2201 -* Add documentation for `all-in-one` Docker image by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2215 -* Tweak Docker docs a bit by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2228 -* Add section about field update operators by @Fashander in https://github.com/FerretDB/FerretDB/pull/2180 -* Update telemetry information by @Fashander in https://github.com/FerretDB/FerretDB/pull/2243 -* Add basic security policy by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2242 -* Add new blog post - MongoDB vs PostgreSQL by @Fashander in https://github.com/FerretDB/FerretDB/pull/2146 -* Document array update operator by @Fashander in https://github.com/FerretDB/FerretDB/pull/2237 - -### Other Changes πŸ€– -* Merge and speedup packages building by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2195 -* Provide "all-in-one" Docker image by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2183 -* Improve logging of retries by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2192 -* Split `TestQueryCompatRunner` back into separate tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2189 -* Fix telemetry tag and improve unimplemented error in aggregation by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2198 -* Improve build cache usage by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2197 -* Add CI configuration for Hana by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2194 -* Use `runit` for all-in-one Docker image by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2203 -* Pass `struct`s by pointers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2221 -* Skip a `$mod` test for arm64 by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2223 -* Fix `env-data` Taskfile by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2211 -* Make `listIndexes` use data from the database by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2214 -* Tweak dependabot config by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2233 -* Filter documents with iterators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2245 -* Add a few query tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2260 -* Fix traffic recording by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2267 -* Cleanup unimplemented and ignored parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2258 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2264 -* Collection deletion and metadata refactoring by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2224 -* Update diff.md for database name differences by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2274 -* Remove indexes from `DocumentsIterator` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2278 -* Bump Tigris to 1.0.0-beta.51 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2277 - -## New Contributors -* @kropidlowsky made their first contribution in https://github.com/FerretDB/FerretDB/pull/2247 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/38?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.9.3...v0.9.4). - - -## [v0.9.3](https://github.com/FerretDB/FerretDB/releases/tag/v0.9.3) (2023-03-13) - -### New Features πŸŽ‰ -* Implement `$sort` aggregation pipeline stage by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2093 -* Support aggregation pipeline for Tigris by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2085 -* Pushdown `$eq` query operator by @noisersup in https://github.com/FerretDB/FerretDB/pull/1880 -* Pushdown `$ne` query operator for PostgreSQL by @noisersup in https://github.com/FerretDB/FerretDB/pull/2145 -* Create unqiue index for `_id` automatically by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2048 -* Support `$group` aggregation stage by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2151 - -### Fixed Bugs πŸ› -* Add dot notation support for `$max` and `$min` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2072 -* Fix `$mul` operator handling dot notation by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2094 -* Add lowercase variant of `dbstats` by @b1ron in https://github.com/FerretDB/FerretDB/pull/2119 -* Fix querying an embedded field in array by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2030 -* Fix dot notation errors by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2122 -* Make `$pullAll` remove all instances from the existing array by @b1ron in https://github.com/FerretDB/FerretDB/pull/2066 -* Fix `saslStart` for particular clients by @yu-re-ka in https://github.com/FerretDB/FerretDB/pull/2164 - -### Enhancements πŸ›  -* Relax validation rules a bit by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2009 -* Allow empty strings pushdown for Tigris by @noisersup in https://github.com/FerretDB/FerretDB/pull/2110 -* Pushdown `bool` values by @noisersup in https://github.com/FerretDB/FerretDB/pull/2143 -* Pushdown `date` values by @noisersup in https://github.com/FerretDB/FerretDB/pull/2162 - -### Documentation πŸ“„ -* Add blog post on FerretDB v0.92 by @Fashander in https://github.com/FerretDB/FerretDB/pull/2067 -* Restructure documentation by @Fashander in https://github.com/FerretDB/FerretDB/pull/2073 -* Improve social preview for Security page by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2091 -* Update aggregation in supported command document by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2154 -* Document explicit telemetry sharing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2169 -* Update supported command document for `$group` aggregation operator by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2188 - -### Other Changes πŸ€– -* Fix `SkipForTigrisWithReason` helper by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2061 -* Add dot notation test cases for update operators by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2047 -* Push Docker images to Docker Hub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2059 -* Temporarily remove pushdown for dot notation by @noisersup in https://github.com/FerretDB/FerretDB/pull/2068 -* Set `retention-days` for all CI artifacts by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2087 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2041 -* Use Go 1.20 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2098 -* Small cleanups by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2107 -* Update CODEOWNERS for the Hana handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2096 -* Initial setup of `hana` handler by @lucboj in https://github.com/FerretDB/FerretDB/pull/2071 -* Checkout named branch by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2112 -* Skip docs build if no docs were changed by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2095 -* Remove CI optimization that does not work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2113 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2148 -* Add aggregation pipeline compat tests and fix errors by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2099 -* Test that version is set correctly by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2152 -* Add connection to HANA by @lucboj in https://github.com/FerretDB/FerretDB/pull/2120 -* Bump deps, minor cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2176 -* Refactor common `saslStart` code by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2181 -* Use `stringer` for "enums" by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2144 -* Rework build by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2177 -* Disable race detector on arm64 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2182 - -## New Contributors -* @lucboj made their first contribution in https://github.com/FerretDB/FerretDB/pull/2071 -* @yu-re-ka made their first contribution in https://github.com/FerretDB/FerretDB/pull/2164 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/37?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.9.2...v0.9.3). - - -## [v0.9.2](https://github.com/FerretDB/FerretDB/releases/tag/v0.9.2) (2023-02-27) - -### New Features πŸŽ‰ -* Support `listIndexes` command by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1960 -* Add support for `$addToSet` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2004 -* Add `$pullAll` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/2032 -* Add Tigris authentication support by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2040 - -### Fixed Bugs πŸ› -* Add support for `create` with `capped: false` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1978 -* Do not log the latest version if it's unknown by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1967 - -### Enhancements πŸ›  -* Add a flag for disabling query pushdown by @noisersup in https://github.com/FerretDB/FerretDB/pull/1963 -* Disable query pushdown if the corresponding flag is set by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2003 - -### Documentation πŸ“„ -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1976 -* Add blog post on FerretDB v0.9.1 by @Fashander in https://github.com/FerretDB/FerretDB/pull/1965 -* Set up analytics for documentation and blog by @Fashander in https://github.com/FerretDB/FerretDB/pull/2010 -* Add section for evaluation query operators - `$mod` and `$regex` by @Fashander in https://github.com/FerretDB/FerretDB/pull/1941 -* Embedded FerretDB documentation - examples improvements by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2017 -* Do not specify Docker Compose version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2023 -* Document bitwise query operators by @Fashander in https://github.com/FerretDB/FerretDB/pull/2042 -* Update documentation definitions for operators by @Fashander in https://github.com/FerretDB/FerretDB/pull/2016 - -### Other Changes πŸ€– -* Build multiple Docker images for git tags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1991 -* Enable array tests for Tigris by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1966 -* Store cursors in `ConnInfo` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1998 -* Bump Go to 1.19.6 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1999 -* Bump more dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1981 -* Bump more deps, update Dependabot config by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2002 -* Simplify pushdown disabling logic by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2008 -* Extract more `find` command parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2006 -* Bump Tigris version to `1.0.0-beta.38` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2011 -* Refactor `types.NewPathFromString` to return `(Path, error)` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1994 -* Bump `golang.org/x/net` version to 0.7.0 in `tools/go.mod` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/2018 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2022 -* Refactor integration tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1925 -* Simplify fields sorting, add TODO by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2039 -* Run `task testjs` on CI by @AlekSi in https://github.com/FerretDB/FerretDB/pull/2031 -* Return a proper error when a document with duplicate `_id` is sent for `insert` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/2024 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/31?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.9.1...v0.9.2). - - -## [v0.9.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.9.1) (2023-02-13) - -### New Features πŸŽ‰ -* Support Tigris pushdowns for numbers by @noisersup in https://github.com/FerretDB/FerretDB/pull/1842 -* Pushdown Tigris queries with dot notation by @noisersup in https://github.com/FerretDB/FerretDB/pull/1908 - -### Fixed Bugs πŸ› -* Fix `$pop` operator error handling of non-existent path by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1907 -* Fix SASL response for `PLAIN` authentication by @b1ron in https://github.com/FerretDB/FerretDB/pull/1942 -* Fix key ordering on document replacing by @noisersup in https://github.com/FerretDB/FerretDB/pull/1946 - -### Documentation πŸ“„ -* Add blog post on new FerretDB release by @Fashander in https://github.com/FerretDB/FerretDB/pull/1893 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1902 -* Fix broken link by @Fashander in https://github.com/FerretDB/FerretDB/pull/1918 -* Add blog post on "MongoDB Alternatives: 5 Database Alternatives to MongoDB for 2023" by @Fashander in https://github.com/FerretDB/FerretDB/pull/1911 - -### Other Changes πŸ€– -* Use multiple Tigris instances to run tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1878 -* Assorted internal tweaks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1909 -* Bump Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1916 -* Add simple `otel` tracing to collect data from tests by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1863 -* Remove unused parameter by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1919 -* Rework on integration test setup by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1857 -* Add `iterator.WithClose` helper by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1947 -* Tweak CI settings by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1948 -* Rename function, add TODO by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1955 -* Remove `skipTigrisPushdown` from tests by @noisersup in https://github.com/FerretDB/FerretDB/pull/1957 -* Implement Tigris query iterator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1924 - - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/30?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.9.0...v0.9.1). - - -## [v0.9.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.9.0) (2023-01-31) - -## What's Changed - -We are pleased to announce our first Developer Preview release! - -This release adds an initial implementation of aggregation pipelines. -For now, only the `$match` and `$count` stages are implemented. -Additional stages will be implemented in future releases. - -This release also pushes more filtering queries to the backend, significantly improving their speed. -Again, more will be implemented in future releases. - -### New Features πŸŽ‰ -* Support `$mul` field update operator by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1760 -* Support `$push` array update operator by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1819 -* Support PostgreSQL pushdowns for numbers by @noisersup in https://github.com/FerretDB/FerretDB/pull/1809 -* Pushdown SQL queries with dot notation by @noisersup in https://github.com/FerretDB/FerretDB/pull/1864 -* Initial support for aggregation pipelines by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1860 - -### Fixed Bugs πŸ› -* Fix error types and array handling when dot notation is used with `$set` operator by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1795 -* Fix `$inc` operator panics for non-existing array index by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1787 -* Fix `$set` operator to apply correct comparison by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1814 - -### Enhancements πŸ›  -* Set default `pool_max_conns` to 20 for PostgreSQL by @jkoenig134 in https://github.com/FerretDB/FerretDB/pull/1852 - -### Documentation πŸ“„ -* Blog and documentation configuration improvements by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1799 -* Fix internal links and images by @Fashander in https://github.com/FerretDB/FerretDB/pull/1796 -* Bump ferretdb/docusaurus-docs from 2.2.0-2 to 2.2.0-3 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/1823 -* Tweak documentation about flags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1820 -* Add blog post about FerretDB v0.8.1 release by @Fashander in https://github.com/FerretDB/FerretDB/pull/1813 -* Add blog post on FerretDB beta release 0.8.0 by @Fashander in https://github.com/FerretDB/FerretDB/pull/1815 -* Add basic documentation writing guide by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1826 -* Update docker deployment documentation by @noisersup in https://github.com/FerretDB/FerretDB/pull/1817 -* Reformat Markdown tables by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1847 -* Reformat blog and documentation setup by @Fashander in https://github.com/FerretDB/FerretDB/pull/1839 -* Clarify what command statistics we gather by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1861 -* Add blog post on "How FerretDB fetches data (About query pushdown)" by @Fashander in https://github.com/FerretDB/FerretDB/pull/1853 -* Add content creation process for documentation and blog by @Fashander in https://github.com/FerretDB/FerretDB/pull/1859 -* Mention that there is no configuration file by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1879 -* Truncate blog post display by @Fashander in https://github.com/FerretDB/FerretDB/pull/1874 - -### Other Changes πŸ€– -* Simplify `types.Array` `Append` signature by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1793 -* Integrate `explain` into tests by @noisersup in https://github.com/FerretDB/FerretDB/pull/1790 -* Add `package.txt` stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1806 -* Remove lazy connection pools by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1812 -* Add Go execution tracing for tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1804 -* Retry transaction more times and log retries by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1818 -* Migrate to Tigris `beta.27` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1810 -* Remove `ListenerOpts` from another `ListenerOpts` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1837 -* Update CONTRIBUTING.md for test data tip by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1832 -* Tiny cleanups by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1849 -* Update development documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1851 -* Make proper port visible on blog development server by @noisersup in https://github.com/FerretDB/FerretDB/pull/1862 -* Ensure that we don't import extra dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1856 -* Extract handler errors into own package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1872 -* Run query compat tests on same collection by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1870 -* Fix `InTransaction` helper's edge case by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1881 -* Improve insert in Tigris by using single query for multiple documents by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1871 -* Unify reply code in handlers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1883 -* Make iterator interface more strict by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1882 - -## New Contributors -* @jkoenig134 made their first contribution in https://github.com/FerretDB/FerretDB/pull/1852 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/13?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.8.1...v0.9.0). - - -## [v0.8.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.8.1) (2023-01-16) - -### New Features πŸŽ‰ -* Report availability of newer versions in mongosh by @noisersup in https://github.com/FerretDB/FerretDB/pull/1738 -* Implement `distinct` command by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1739 -* Validate client's TLS certificate when root CA certificate is provided by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1740 -* Support `$rename` field update operator by @noisersup in https://github.com/FerretDB/FerretDB/pull/1753 -* Enable support for arrays in Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1778 - -### Fixed Bugs πŸ› -* Fix filterering in `distinct` for Tigris handler by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1766 - -### Enhancements πŸ›  -* Add `findandmodify` to the `common.Commands` map by @b1ron in https://github.com/FerretDB/FerretDB/pull/1730 - -### Documentation πŸ“„ -* Add documentation about CLI flags and environment variables by @noisersup in https://github.com/FerretDB/FerretDB/pull/1737 -* Prepare configuration and data for docusaurus-based blog by @Fashander in https://github.com/FerretDB/FerretDB/pull/1756 -* Fix links from docs to blog by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1794 -* Reformat documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1792 -* Add comments about Git LFS and `lfs-warning` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1798 - -### Other Changes πŸ€– -* Support arrays in `tjson` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1721 -* Move $mod to compat test by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1748 -* Skip incompatible tests on `arm64` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1770 -* Minor cleanups by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1786 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1785 -* Build Docker images for tags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1788 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/29?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.8.0...v0.8.1). - - -## [v0.8.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.8.0) (2023-01-02) - -### What's Changed - -We are pleased to announce our first Beta release! - -#### Storage changes for PostgreSQL - -We made a few _backward-incompatible_ changes in the way we store data in PostgreSQL to improve FerretDB performance. -In the future, those changes will allow us to use indexes and query collections faster. - -To keep your data: - -* backup FerretDB databases using `mongodump` or `mongoexport`; -* backup PostgreSQL database using `pg_dump` or other tool (just in case); -* stop FerretDB; -* drop PostgreSQL views for FerretDB databases; -* start FerretDB 0.8; -* restore databases using `mongorestore` or `mongoimport`. - -#### Authentication - -It is now possible to use the backend's authentication mechanisms in FerretDB. -See [documentation](https://docs.ferretdb.io/security/). - -### New Features πŸŽ‰ -* Support `$min` field update operator by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1652 -* Support `ordered` argument for `insert` command by @noisersup in https://github.com/FerretDB/FerretDB/pull/1673 -* Implement authentication for PostgreSQL by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1725 - -### Fixed Bugs πŸ› -* Fix unset document being updated by invalid value of `$inc` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1685 - -### Enhancements πŸ›  -* Update building documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1713 - -### Documentation πŸ“„ -* Add section for comparison and logical query operators by @Fashander in https://github.com/FerretDB/FerretDB/pull/1647 -* Add documentation for element query operators by @Fashander in https://github.com/FerretDB/FerretDB/pull/1675 -* Add documentation for array query operator by @Fashander in https://github.com/FerretDB/FerretDB/pull/1695 -* Enable blog post section by @Fashander in https://github.com/FerretDB/FerretDB/pull/1700 - -### Other Changes πŸ€– -* Simplify release procedure by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1657 -* Modify `pjson` format by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1620 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1664 -* Remove leading space from `SELECT` queries by @noisersup in https://github.com/FerretDB/FerretDB/pull/1665 -* Add `InTransactionRetry` helper by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1670 -* Add `mongo` test script example by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1600 -* Use faster runner instances by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1678 -* Update issue templates by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1671 -* Bump Tigris version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1680 -* Move update tests to compat tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1659 -* Add TODO comments by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1687 -* Add `saslStart` stub by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1649 -* Improve the way of storing data about collections by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1650 -* Implement `iterator.Interface` for `types.Document` and `types.Array` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1683 -* Improve issue template by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1692 -* Remove `$elemMatch` and `$slice` projection operators by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1698 -* Add `currentOp` stub by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1708 -* Add basic benchmark for query pushdowns by @noisersup in https://github.com/FerretDB/FerretDB/pull/1689 -* Enable authentication in PostgreSQL by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1716 -* Fix Docker build by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1715 -* Minor refactorings of iterators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1718 -* Test `ordered` argument validation by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1719 -* Add stub for getting client-specific connection by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1723 -* Add compat tests for `InsertOne` in addition to `InsertMany` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1726 -* Run `govulncheck` on CI by @noisersup in https://github.com/FerretDB/FerretDB/pull/1729 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/28?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.7.1...v0.8.0). - - -## [v0.7.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.7.1) (2022-12-19) - -### New Features πŸŽ‰ -* Add basic TLS support by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1586 -* Add `validate` command stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1645 - -### Fixed Bugs πŸ› -* Fix parsing of `OP_MSG` packets with multiple sections by @b1ron in https://github.com/FerretDB/FerretDB/pull/1611 -* Fix parsing of `OP_MSG` packets with multiple sections by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1633 -* Fix comparison with unset fields by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1634 - -### Enhancements πŸ›  -* Compare documents by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1597 - -### Documentation πŸ“„ -* Infinity values are not allowed in documents by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1622 - -### Other Changes πŸ€– -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1609 -* Update release checklist by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1621 -* Compare unit tests for edge cases by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1624 -* Bump Go and other deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1629 -* Refactor integration tests setup functions by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1625 -* Fix `.deb`/`.rpm` package testing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1631 -* Bump `golang.org/x/net` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1640 -* Introduce schema for `pjson` format by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1635 -* Use TLS for MongoDB in integration tests by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1623 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1644 -* Bump Tigris deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1651 -* Remove Incomparable by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1646 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/27?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.7.0...v0.7.1). - - -## [v0.7.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.7.0) (2022-12-05) - -### New Features πŸŽ‰ -* Add `msg_explain` implementation for Tigris by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1574 -* Add `filter` support to `listCollections` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1567 - -### Fixed Bugs πŸ› -* Fix parallel collection inserts for PostgreSQL by @noisersup in https://github.com/FerretDB/FerretDB/pull/1513 -* Fix validation for documents with duplicate keys by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1602 -* Fix greater and less operators on array value comparison by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1585 - -### Enhancements πŸ›  -* Downgrade min wire protocol version to 13 / 5.0 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1571 -* Make default telemetry state a bit more clear by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1561 -* Add documents validation to `wire` package by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1401 -* Allow `-` in database names by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1582 -* Support more default `find` parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1588 - -### Documentation πŸ“„ -* Add authentication and role management commands to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1527 -* Add section for diagnostic command `buildInfo` and `collStats` by @Fashander in https://github.com/FerretDB/FerretDB/pull/1480 -* Add session and free monitoring commands to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1546 -* Add Mastodon links by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1555 -* Add glossary section to documentation by @Fashander in https://github.com/FerretDB/FerretDB/pull/1583 - -### Other Changes πŸ€– -* Simplify array comparison, remove `[]CompareResult` by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1499 -* Disable CockroachDB on GitHub Actions by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1524 -* Add placeholder for testing scripts by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1525 -* Add path to record loading error message by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1543 -* Move array query tests to compat tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1526 -* Move `_id` field check to separate function by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1544 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1562 -* Implement `listIndexes` command stub by @noisersup in https://github.com/FerretDB/FerretDB/pull/1565 -* Do not record partial files by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1566 -* Sync label descriptions and issue templates by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1573 -* Auto-merge is required now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1563 -* Move more query tests to compat and change filter bad value check order by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1560 -* Move more tests to compat query tests by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1570 -* Fix test names to make them more consistent by @noisersup in https://github.com/FerretDB/FerretDB/pull/1575 -* Update documentation for types, add aliases by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1580 -* Simplify documents fetching for the `count` operator by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1487 -* Simplify documents fetching for `delete` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1584 -* Fix `listIndexes` stub by @noisersup in https://github.com/FerretDB/FerretDB/pull/1591 -* Move query tests to compat by comparing the error code instead of the error message by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1579 -* Add TODOs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1595 -* Simplify documents fetching for `find`, `findAndModify`, and `update` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1538 -* Improve `iterator.Interface` implementation in `pgdb.queryIterator` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1592 -* Refactor `pg`'s `MsgListDatabases` and `pgdb` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1596 -* Fix `Conform PR` workflow by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1599 -* Improve `envtool` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1598 -* Replace `docker-compose` with `docker compose` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1606 -* Add test certificates by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1612 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/26?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.6.2...v0.7.0). - - -## [v0.6.2](https://github.com/FerretDB/FerretDB/releases/tag/v0.6.2) (2022-11-21) - -### New Features πŸŽ‰ -* Provide builds for `linux/arm/v7` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1377 -* Implement `enableFreeMonitoring`, `disableFreeMonitoring` and `getFreeMonitoringStatus` commands by @noisersup in https://github.com/FerretDB/FerretDB/pull/1380 - -### Fixed Bugs πŸ› -* Fix `SchemaStats` to return empty stats by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1359 -* Fix issues for the Unix listener by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1397 - -### Enhancements πŸ›  -* Tweak supported wire protocol versions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1261 -* Add more `NewCommandErrorMsgWithArgument` calls by @noisersup in https://github.com/FerretDB/FerretDB/pull/1358 -* Use environment variables for configuration by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1383 -* Add missing locks when update settings table by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1381 - -### Documentation πŸ“„ -* Make telemetry page visible in documentation sidebar by @Fashander in https://github.com/FerretDB/FerretDB/pull/1393 -* Add documentation for dot notation in arrays, objects, and embedded documents by @Fashander in https://github.com/FerretDB/FerretDB/pull/1382 -* Start supported commands table by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1406 -* Add aggregation section (collection stages) to the docs by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1411 -* Add query and write operation commands to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1409 -* Add aggregation section (database stages, operators) to the docs by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1448 -* Use colored emoji by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1485 -* Add update operators to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1481 -* Reorganize a list of supported commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1490 -* Add changelog draft by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1479 -* Add user management commands to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1489 -* Fix information about `delete`'s `comment` argument inside "supported commands" reference by @noisersup in https://github.com/FerretDB/FerretDB/pull/1498 -* Add query plan cache commands to the docs by @b1ron in https://github.com/FerretDB/FerretDB/pull/1501 -* Add documentation for embedded/nested documents query by @Fashander in https://github.com/FerretDB/FerretDB/pull/1478 - -### Other Changes πŸ€– -* Do not cancel in-progress CI runs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1378 -* Sync and update golangci-lint configurations by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1205 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1400 -* Restructure text, add unestimated tasks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1374 -* Ignore website problems for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1404 -* Use lowercase directory names by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1408 -* Minor telemetry cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1446 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1477 -* Allow duplicates in `bson` documents by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1391 -* Implement `debugError` command by @noisersup in https://github.com/FerretDB/FerretDB/pull/1402 -* Update some TODOs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1452 -* Record integration tests connections by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1482 -* Enable more `textlint` rules by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1484 -* Use `primitive.Regex` to test that regex `_id` is not allowed by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1486 -* Bump Tigris version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1379 -* Use `-` in test collection names by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1325 - -## New Contributors -* @b1ron made their first contribution in https://github.com/FerretDB/FerretDB/pull/1409 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/25?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.6.1...v0.6.2). - - -## [v0.6.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.6.1) (2022-11-07) - -### Enhancements πŸ›  -* Deprecate dotted fields in data documents by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1313 -* Forbid regex types and arrays in document's `_id` field by @noisersup in https://github.com/FerretDB/FerretDB/pull/1326 -* Make users know about telemetry via `startupWarnings` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1336 -* Deprecate nested arrays by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1334 - -### Documentation πŸ“„ -* Fix syntax highlighting by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1340 -* Add text for empty pages by @Fashander in https://github.com/FerretDB/FerretDB/pull/1338 -* Update dark and light theme logo for documentation by @Fashander in https://github.com/FerretDB/FerretDB/pull/1368 -* Add documentation for configuring telemetry service by @Fashander in https://github.com/FerretDB/FerretDB/pull/1342 - -### Other Changes πŸ€– -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1337 -* Switch from `markdownlint-cli` to `markdownlint-cli2` by @codingmickey in https://github.com/FerretDB/FerretDB/pull/1319 -* A minor clarification about diff tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1339 -* Add stress tests for `SchemaStats` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1318 -* Do not compare error strings by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1344 -* Add stress tests for settings table and fix simple issues with transactions by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1316 -* Cleanup compat tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1345 -* Fix ignore patterns for tools by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1349 -* Use pre-built textlint image by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1366 -* Use pre-built Docusaurus image by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1365 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1367 -* Add "_id" string to linter exceptions by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1364 -* Remove extra `nolint` directives by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1348 -* Setup `lfs-warning` GitHub Action check by @ndkhangvl in https://github.com/FerretDB/FerretDB/pull/1371 -* Bump Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1372 -* Remove unsupported NaN and Inf from pjson package documentation by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1373 - -## New Contributors -* @codingmickey made their first contribution in https://github.com/FerretDB/FerretDB/pull/1319 -* @ndkhangvl made their first contribution in https://github.com/FerretDB/FerretDB/pull/1371 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/24?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.6.0...v0.6.1). - - -## [v0.6.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.6.0) (2022-10-27) - -### What's Changed -We are pleased to announce our first Alpha release! - -### New Features πŸŽ‰ -* Support `$max` field update operator by @noisersup in https://github.com/FerretDB/FerretDB/pull/1124 -* Migrate FerretDB to Kong by @noisersup in https://github.com/FerretDB/FerretDB/pull/1184 -* Make embedded FerretDB's address configurable by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1199 -* `tjson`: Support `null` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1005 -* Add simple query pushdown for PostgreSQL by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1207 -* Run tests on CockroachDB by @noisersup in https://github.com/FerretDB/FerretDB/pull/1260 -* Add support for Unix domain sockets by @zhiburt in https://github.com/FerretDB/FerretDB/pull/1214 -* Add basic telemetry by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1299 -* Deprecate infinity values in data documents by @noisersup in https://github.com/FerretDB/FerretDB/pull/1296 -* Explicitly disallow duplicate keys in data documents by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1293 - -### Fixed Bugs πŸ› -* Allow empty document field names by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1196 -* Fix test helpers for the `nil` case by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1241 -* Fix error messages for invalid `$and`/`$or`/`$nor` arguments by @ronaudinho in https://github.com/FerretDB/FerretDB/pull/1234 -* Fix `explain` command by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1294 -* Fix `tjson` schema unmarshalling by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1304 - -### Enhancements πŸ›  -* Add support for Tigris auth parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1177 -* Use single transaction per `msg_insert` request by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1213 -* Improve `buildInfo` and `serverStatus` commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1197 -* Add UUID to log messages by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1208 -* Add update operators data document fields order test by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1238 -* Add UUID to Prometheus metrics if requested by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1240 -* Add simplest validation to check data documents by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1246 -* Add β€œmetrics” section to `serverStatus` response by @noisersup in https://github.com/FerretDB/FerretDB/pull/1231 -* Call data document validation when insert or update documents in Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1290 -* Add support for empty command documents by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1277 -* Make `_id` field required in data documents by @noisersup in https://github.com/FerretDB/FerretDB/pull/1278 -* Add more ways to disable telemetry by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1311 -* Allow dashes (`-`) in collection names by @noisersup in https://github.com/FerretDB/FerretDB/pull/1312 -* Collect command metrics in telemetry by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1327 -* Include info about unimplemented arguments by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1330 - -### Documentation πŸ“„ -* Update introduction documentation by @Fashander in https://github.com/FerretDB/FerretDB/pull/1174 -* Add local search plugin by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1178 -* Setup documentation search by @Fashander in https://github.com/FerretDB/FerretDB/pull/1180 -* DRY known differences documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1181 -* Documentation website tweaks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1183 -* Documentation for contributors by @Fashander in https://github.com/FerretDB/FerretDB/pull/1194 -* Add "CRUD operations" and "Understanding FerretDB" sections by @Fashander in https://github.com/FerretDB/FerretDB/pull/1232 -* Add documentation for the `.deb` package usage by @Fashander in https://github.com/FerretDB/FerretDB/pull/1267 - -### Other Changes πŸ€– -* Use transactions in more `pgdb` functions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1157 -* Add `task` targets for offline work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1171 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1175 -* Fuzz `wire` package with recorded data by @noisersup in https://github.com/FerretDB/FerretDB/pull/1168 -* Fix fluky test, refactor it by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1185 -* Simplify / unify similar cases by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1187 -* Setup Tigris test cases with explicit schemas by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1167 -* Migrate envtool to Kong by @noisersup in https://github.com/FerretDB/FerretDB/pull/1190 -* Replace `pgxtype.Querier` with `pgx.Tx` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1188 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1195 -* Cleanup `pgdb` SQL statements by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1193 -* Run linters on integration tests folder by @ravilushqa in https://github.com/FerretDB/FerretDB/pull/1200 -* Use codecov upload token by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1204 -* Add security scan by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1142 -* Use single transaction per `msg_update` request by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1212 -* Use Go 1.19.2 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1211 -* Fix running `pg` and `tigris` tests in parallel by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1218 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1230 -* Use single transaction per `msg_findandmodify` request by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1217 -* Improve `task env-data` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1220 -* Split `fjson` into `pjson` and `types/fjson` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1219 -* Use single transaction for `listDatabases` command by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1237 -* Cleanup old validation by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1179 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1245 -* Update internal process docs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1249 -* Fix flag name by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1255 -* Fix CLI flags for Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1256 -* Remove forked `golangci-lint` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1258 -* Cleanup types/fjson package by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1254 -* Minor handlers refactoring by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1264 -* `fjson` and fuzzing cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1262 -* Skip `pjson` fuzzing of invalid documents for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1274 -* Add schema-related test cases to `tjson` package by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1247 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1275 -* Update docs for the `dummy` handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1276 -* Fix documentation for linking PRs and issues by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1268 -* Add experimental mergify configuration by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1281 -* Improve tests cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1287 -* Remove implicit mergify rules by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1288 -* Run CockroachDB tests on CI by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1289 -* Bump cockroachdb/cockroach from v22.1.8 to v22.1.9 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/1285 -* Migrate to a newer Tigris version and fix relevant tests by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1239 -* Add ability to subscribe to state changes by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1265 -* Move tjson into internal/handlers/tigris/tjson by @chilagrow in https://github.com/FerretDB/FerretDB/pull/1291 -* Fix a typo in the `types` package docs by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1297 -* Disallow usage of old context package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1292 -* Disable Unix sockets in tests for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1298 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1309 -* Expand `debugError` stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1303 -* Add comment about `diff` tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1302 -* Refactor handler errors by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1322 - -## New Contributors -* @chilagrow made their first contribution in https://github.com/FerretDB/FerretDB/pull/1254 -* @ronaudinho made their first contribution in https://github.com/FerretDB/FerretDB/pull/1234 -* @zhiburt made their first contribution in https://github.com/FerretDB/FerretDB/pull/1214 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/22?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.5.4...v0.6.0). - - -## [v0.5.4](https://github.com/FerretDB/FerretDB/releases/tag/v0.5.4) (2022-09-22) - -### Fixed Bugs πŸ› -* Add missing `$k` to the schema when creating collection in Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1136 - -### Documentation πŸ“„ -* Remove docusaurus references and update documentation by @Fashander in https://github.com/FerretDB/FerretDB/pull/1130 -* Deploy documentation PRs to Vercel by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1131 - -### Other Changes πŸ€– -* Add transaction to `msg_drop` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1129 -* Add transaction to `pg`'s `msg_listcollections` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1135 -* Fix tests for Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1134 -* Use fixed `-test-record` directory in Task targets by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1139 -* Fix a typo in Readme by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1141 -* Use transaction in more `pgdb` functions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1143 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1158 -* Use transaction in more `pgdb` functions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1144 -* Refactor `msg_delete` handlers by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1152 -* Improve contributing guidelines by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1146 -* Update process documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1153 -* Update issues and PR templates by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1155 -* Fix typo by @si3nloong in https://github.com/FerretDB/FerretDB/pull/1165 -* Migrate fuzztool to Kong by @noisersup in https://github.com/FerretDB/FerretDB/pull/1159 - -## New Contributors -* @si3nloong made their first contribution in https://github.com/FerretDB/FerretDB/pull/1165 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/23?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.5.3...v0.5.4). - - -## [v0.5.3](https://github.com/FerretDB/FerretDB/releases/tag/v0.5.3) (2022-09-08) - -### New Features πŸŽ‰ -* Add support for updates with replacement objects by @fcoury in https://github.com/FerretDB/FerretDB/pull/791 -* Add support for `$update`'s `$set` and `$setOnInsert` operators dot notation by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1008 -* Support `$pop` array update operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1020 -* Add support for `$update`'s `$unset` operators dot notation by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1028 -* `tjson`: Implement `regex` by @noisersup in https://github.com/FerretDB/FerretDB/pull/1050 -* Implement `MsgDataSize` for Tigris by @polldo in https://github.com/FerretDB/FerretDB/pull/1060 -* Support `ordered` argument for `delete` command by @noisersup in https://github.com/FerretDB/FerretDB/pull/1004 -* Implement simple query pushdown for Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1091 -* Implement `MsgFindAndModify` for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1065 -* implement `timestamp` type for tigris by @noisersup in https://github.com/FerretDB/FerretDB/pull/1117 - -### Fixed Bugs πŸ› -* Improve `TestCommandsAdministrationServerStatus` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1062 -* Fix `ModifiedCount` for updates with an empty replacement document by @nicolascb in https://github.com/FerretDB/FerretDB/pull/1067 -* Fix `$inc` `update` operator int64-max issue by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1071 -* Handle `findAndModify` and `update` correctly when collection doesn't exist by @noisersup in https://github.com/FerretDB/FerretDB/pull/1087 -* Require `limit` parameter in `delete` command by @noisersup in https://github.com/FerretDB/FerretDB/pull/1066 - -### Enhancements πŸ›  -* Fix `update` operation for Tigris handler by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1041 -* Collect sizes in `MsgListDatabases` for Tigris by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1043 - -### Documentation πŸ“„ -* Add GitHub Pages with documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1100 -* Improve contribution guidelines and documentation website by @Fashander in https://github.com/FerretDB/FerretDB/pull/1114 -* Fix macOS spelling by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1127 - -### Other Changes πŸ€– -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1014 -* Fix `Run linters` job in Taskfile.yml by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1022 -* Improve and document integration tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1021 -* Add missing `MaxTimeMS` support for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1026 -* Expose `delete` problem by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1030 -* Bump Tigris version to 1.0.0-alpha.27 by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1032 -* Bump postgres from 14.4 to 14.5 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/1033 -* `tjson`: Implement `datetime` by @noisersup in https://github.com/FerretDB/FerretDB/pull/1027 -* `tjson`: Add package documentation for types mapping by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1031 -* Rework database and collection creation for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1038 -* Add a few more tests for logical query operators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1049 -* Ensure that database and collection names are unique by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1046 -* Implement `MsgDBStats` for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1047 -* Bump Tigris version to 1.0.0-alpha.29 by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1054 -* Add tests for update with replacement by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1044 -* Small `pgdb` cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1055 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1059 -* Less strict delta for `dataSize` in tests by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1053 -* Add `collMod` command stub by @ravilushqa in https://github.com/FerretDB/FerretDB/pull/1037 -* Add a linter for Semantic Line Breaks in Markdown files by @GrandShow in https://github.com/FerretDB/FerretDB/pull/998 -* Fix data race in test by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1072 -* Use `CODECOV_TOKEN` if available by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1073 -* Bump dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1084 -* Add integration tests for logical operators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1085 -* Implement `MsgCollStats` for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1063 -* Implement `MsgCreate` for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/1048 -* Use npm lock files for tools by @folex in https://github.com/FerretDB/FerretDB/pull/1093 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1099 -* Simplify/sync `delete` a bit by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1104 -* Enable `errorlint` for new code by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1105 -* Add missing TODO by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1108 -* Migrate to MongoDB 6 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1074 -* Switch to Go 1.19, bump dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1123 -* Record incoming data for fuzzing by @noisersup in https://github.com/FerretDB/FerretDB/pull/1107 -* Add transaction to `msg_drop_database` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/1126 - -## New Contributors -* @polldo made their first contribution in https://github.com/FerretDB/FerretDB/pull/1060 -* @ravilushqa made their first contribution in https://github.com/FerretDB/FerretDB/pull/1037 -* @GrandShow made their first contribution in https://github.com/FerretDB/FerretDB/pull/998 -* @nicolascb made their first contribution in https://github.com/FerretDB/FerretDB/pull/1067 -* @folex made their first contribution in https://github.com/FerretDB/FerretDB/pull/1093 -* @Fashander made their first contribution in https://github.com/FerretDB/FerretDB/pull/1114 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/21?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.5.2...v0.5.3). - - -## [v0.5.2](https://github.com/FerretDB/FerretDB/releases/tag/v0.5.2) (2022-08-09) - -### New Features πŸŽ‰ -* Support `comment` and `$comment` `update`'s arguments by @noisersup in https://github.com/FerretDB/FerretDB/pull/937 -* Support `multi` `update`'s argument by @fcoury in https://github.com/FerretDB/FerretDB/pull/790 -* Support `comment` and `$comment` `findAndModify`'s argument by @noisersup in https://github.com/FerretDB/FerretDB/pull/958 -* Support `comment` and `$comment` `delete`'s arguments by @noisersup in https://github.com/FerretDB/FerretDB/pull/954 -* Support `maxTimeMS` argument for `find` and `findAndModify` methods by @DoodgeMatvey in https://github.com/FerretDB/FerretDB/pull/608 -* Add support for `update`'s `$inc` operator dot notation by @w84thesun in https://github.com/FerretDB/FerretDB/pull/915 - -### Fixed Bugs πŸ› -* Fix `nModified` count for `update`'s `$set` operator with the same value by @w84thesun in https://github.com/FerretDB/FerretDB/pull/949 - -### Other Changes πŸ€– -* `tjson`: Fix schema comparison by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/944 -* Make compat error messages better by @AlekSi in https://github.com/FerretDB/FerretDB/pull/950 -* Enable `wsl` linter for new and changed code by @AlekSi in https://github.com/FerretDB/FerretDB/pull/856 -* Fix some collection names breaking `listDatabases` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/953 -* Remove some tests to make next PRs smaller by @AlekSi in https://github.com/FerretDB/FerretDB/pull/959 -* Add `SkipForTigris` helper, use it by @AlekSi in https://github.com/FerretDB/FerretDB/pull/960 -* Add setup for compatibility tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/961 -* Add compatibility tests for `$and` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/963 -* Bump igorshubovych/markdownlint-cli from v0.32.0 to v0.32.1 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/955 -* `tjson`: Check how we support `binary` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/967 -* Move logic operators tests to compatibility tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/965 -* Add compatibility tests for `$inc` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/964 -* Add test case for an empty update path by @AlekSi in https://github.com/FerretDB/FerretDB/pull/976 -* Bump golang from 1.18.4 to 1.18.5 by @dependabot in https://github.com/FerretDB/FerretDB/pull/977 -* Improve Document's Path API by @w84thesun in https://github.com/FerretDB/FerretDB/pull/973 -* `tjson`: Add unit tests for `ObjectID` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/971 -* Make linter to enforce our preferred types order in type switch by @fenogentov in https://github.com/FerretDB/FerretDB/pull/654 -* Add back `task env-data` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/983 -* Insert test data in random order by @AlekSi in https://github.com/FerretDB/FerretDB/pull/862 -* `tjson`: Improve ObjectID test by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/992 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/995 -* Bump Tigris Docker image to alpha.26 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/997 -* Tigris: simplify `ObjectID` and filter usage by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/968 -* Add more scalar values to tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/984 -* Implement `aggregate` command stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/981 -* Reformat with Go 1.19 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1003 -* `tjson`: Cover `document` (`object`) type with tests by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/957 -* Add compatibility `delete` test for Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/1002 - -## New Contributors -* @fcoury made their first contribution in https://github.com/FerretDB/FerretDB/pull/790 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/20?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.5.1...v0.5.2). - - -## [v0.5.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.5.1) (2022-07-26) - -### New Features πŸŽ‰ -* Validate database names by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/913 -* Support `$all` array query operator by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/724 -* Support `getLog` diagnostic command by @fenogentov in https://github.com/FerretDB/FerretDB/pull/711 -* Implement `MsgCount` for Tigris by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/928 -* Support `explain` diagnostic command by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/909 - -### Fixed Bugs πŸ› -* Fix edge cases in `drop` and `dropDatabase` handlers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/891 -* Fix `ModifyCount` for update operators by @w84thesun in https://github.com/FerretDB/FerretDB/pull/939 - -### Enhancements πŸ›  -* Support `gt` `lt` operator comparison for Array type by @ribaraka in https://github.com/FerretDB/FerretDB/pull/819 -* Optimize documents fetching / filtering by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/808 -* Add test for a database name border case by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/921 - -### Documentation πŸ“„ -* Add a tip to limit concurrent tasks by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/883 - -### Other Changes πŸ€– -* Add a few testing helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/874 -* Add support for `no ci` label by @AlekSi in https://github.com/FerretDB/FerretDB/pull/876 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/880 -* Bump golang from 1.18.3 to 1.18.4 by @dependabot in https://github.com/FerretDB/FerretDB/pull/881 -* Extract two more helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/875 -* Set pprof label for client connections by @AlekSi in https://github.com/FerretDB/FerretDB/pull/885 -* Cancel request's context when request processed by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/884 -* Simplify `dbStats` tests a bit, add TODOs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/886 -* Disable logs during test setup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/888 -* Use `InsertMany` instead of `InsertOne` in tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/882 -* Restart development containers faster by @AlekSi in https://github.com/FerretDB/FerretDB/pull/889 -* Cover more logic in transactions by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/887 -* Disconnect client in embedded tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/890 -* Stop tests on the first data race by @AlekSi in https://github.com/FerretDB/FerretDB/pull/893 -* Wait for `Tigris` backend to be ready by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/894 -* Handle `42P07` PostgreSQL error to fix the tests by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/895 -* Build .rpm and .deb packages by @fenogentov in https://github.com/FerretDB/FerretDB/pull/739 -* Add setup for compatibility tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/901 -* Extract parameter list into one variable in `QueryDocuments` by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/910 -* Add first compatibility tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/863 -* Use `v` instead of `value` in tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/916 -* Tweak codecov settings by @AlekSi in https://github.com/FerretDB/FerretDB/pull/920 -* Remove deprecated functions from `pgdb.Pool` by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/922 -* Extract integration tests setup to own package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/923 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/927 -* comment `url.Values` to prevent test failing by @noisersup in https://github.com/FerretDB/FerretDB/pull/930 -* Add a comment to the setup function about database and collection creation when provider list is empty by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/929 -* Bump `golangci-lint`, remove old hacks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/932 -* Fix tests for `$all` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/934 -* Add Path tests by @w84thesun in https://github.com/FerretDB/FerretDB/pull/936 -* Build packages on CI by @AlekSi in https://github.com/FerretDB/FerretDB/pull/938 -* Tweak linter settings by @AlekSi in https://github.com/FerretDB/FerretDB/pull/942 -* Port and sync unit testing approach from `fjson` to `tjson` by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/935 -* Ensure that update operators are in sync by @AlekSi in https://github.com/FerretDB/FerretDB/pull/946 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/19?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.5.0...v0.5.1). - - -## [v0.5.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.5.0) (2022-07-11) - -### What's Changed -This release enables the usage of FerretDB as a Go library. -See [this blog post](https://www.ferretdb.io/0-5-0-release-is-out-embedding-ferretdb-into-go-programs/). - -### New Features πŸŽ‰ -* Support embedded use-case by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/754 -* Validate collection names by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/844 - -### Fixed Bugs πŸ› -* Fix embedded usage by @AlekSi in https://github.com/FerretDB/FerretDB/pull/798 -* Fix `whatsmyuri` command by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/796 -* Handle `null` value for `nameOnly` in `listDatabases` handler by @DoodgeMatvey in https://github.com/FerretDB/FerretDB/pull/738 -* pgdb: cover transactions with `inTransaction` function to simplify error handling by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/833 - -### Enhancements πŸ›  -* Support all valid collection names by @w84thesun in https://github.com/FerretDB/FerretDB/pull/778 -* Remove MongoDB driver "dependency" by @AlekSi in https://github.com/FerretDB/FerretDB/pull/853 - -### Documentation πŸ“„ -* Update contributing docs and PR template according to our best practices by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/779 -* Update contributing documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/843 -* Document that NUL (`\0`) strings is not supported by @w84thesun in https://github.com/FerretDB/FerretDB/pull/865 - -### Other Changes πŸ€– -* Tweak schedule of daily builds by @AlekSi in https://github.com/FerretDB/FerretDB/pull/794 -* Do not import `pg` handler explicitly by @AlekSi in https://github.com/FerretDB/FerretDB/pull/799 -* Add TODO item by @AlekSi in https://github.com/FerretDB/FerretDB/pull/804 -* Fix `task env-pull` target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/810 -* Improve contributing documentation for Windows development by @w84thesun in https://github.com/FerretDB/FerretDB/pull/795 -* Fix Docker image build by @AlekSi in https://github.com/FerretDB/FerretDB/pull/805 -* Make it easier to trigger rebuilds by @AlekSi in https://github.com/FerretDB/FerretDB/pull/815 -* Use `github.head_ref` instead of `github.ref` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/814 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/817 -* Install QEMU for building Docker images by @AlekSi in https://github.com/FerretDB/FerretDB/pull/820 -* Use multi-stage docker build by @AlphaB in https://github.com/FerretDB/FerretDB/pull/605 -* Unify `update` tests by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/809 -* Add TODOs for all update operators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/832 -* Add `env-data` Taskfile target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/834 -* Tweak tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/837 -* Export integration tests helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/838 -* Remove old style of `+build` tags where possible by @AlekSi in https://github.com/FerretDB/FerretDB/pull/839 -* Export fields by @AlekSi in https://github.com/FerretDB/FerretDB/pull/840 -* Update Tigris version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/841 -* Create Tigris databases by @AlekSi in https://github.com/FerretDB/FerretDB/pull/842 -* Add basic CI for Tigris by @AlekSi in https://github.com/FerretDB/FerretDB/pull/784 -* Test with the `main` version of Tigris too by @AlekSi in https://github.com/FerretDB/FerretDB/pull/846 -* Wait for Tigris to be fully up by @AlekSi in https://github.com/FerretDB/FerretDB/pull/854 -* Fill MongoDB on `task env-data` too by @AlekSi in https://github.com/FerretDB/FerretDB/pull/860 -* Add CI job for short tests without environment by @AlekSi in https://github.com/FerretDB/FerretDB/pull/855 -* Add TODOs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/852 -* Fix `task run` on Windows by @AlekSi in https://github.com/FerretDB/FerretDB/pull/867 -* Fix invalid variable names by @AlekSi in https://github.com/FerretDB/FerretDB/pull/868 -* Add `ferretdb_` prefix to our custom build tags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/869 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/18?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.4.0...v0.5.0). - - -## [v0.4.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.4.0) (2022-06-27) - -### What's Changed -This release adds preliminary support for the [Tigris](https://www.tigrisdata.com) backend. -We plan to reach parity with our PostgreSQL backend in the next release. - -### New Features πŸŽ‰ -* Support `$setOnInsert` field update operator by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/644 -* Support `$unset` field update operator by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/691 -* Support `$currentDate` field update operator by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/662 -* Support array querying by @w84thesun in https://github.com/FerretDB/FerretDB/pull/618 -* Support `$elemMatch` array query operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/707 -* Implement `getFreeMonitoringStatus` stub by @noisersup in https://github.com/FerretDB/FerretDB/pull/751 -* Implement `setFreeMonitoring` stub by @noisersup in https://github.com/FerretDB/FerretDB/pull/759 -* Implement `tigris` handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/690 - -### Fixed Bugs πŸ› -* Handle both `buildinfo` and `buildInfo` commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/688 -* Fix a bug with proxy response logs by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/705 -* Handle `find`, `count` and `delete` correctly when collection doesn't exist by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/710 -* Fix default values for flags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/743 -* Fix embedded array query bug by @ribaraka in https://github.com/FerretDB/FerretDB/pull/736 - -### Enhancements πŸ›  -* Array comparison substitution by @ribaraka in https://github.com/FerretDB/FerretDB/pull/676 -* Build `tigris` handler only if tag is present by @AlekSi in https://github.com/FerretDB/FerretDB/pull/681 -* Support getParameter's showDetails, allParameters by @fenogentov in https://github.com/FerretDB/FerretDB/pull/606 -* Make log level configurable by @fenogentov in https://github.com/FerretDB/FerretDB/pull/687 -* `$currentDate` Timestamp fix `DateTime` seconds and milliseconds bug by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/701 - -### Documentation πŸ“„ -* Be explicit about MongoDB version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/679 -* Fix pull request template by @AlekSi in https://github.com/FerretDB/FerretDB/pull/746 - -### Other Changes πŸ€– -* Use `"` instead of `'` in all .yml files by @AlekSi in https://github.com/FerretDB/FerretDB/pull/675 -* Add empty Tigris handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/671 -* Do not test a global list of databases in parallel by @AlekSi in https://github.com/FerretDB/FerretDB/pull/678 -* Enable `revive` linter by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/672 -* More tests for dot notation support by @w84thesun in https://github.com/FerretDB/FerretDB/pull/660 -* Use circular buffer for zap logs by @fenogentov in https://github.com/FerretDB/FerretDB/pull/585 -* Fix build by @AlekSi in https://github.com/FerretDB/FerretDB/pull/703 -* Add `tjson` package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/682 -* Improve function comment by @AlekSi in https://github.com/FerretDB/FerretDB/pull/712 -* Use separate encodings for ObjectID and binary by @AlekSi in https://github.com/FerretDB/FerretDB/pull/713 -* Add the default Task target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/716 -* Add workaround for Dependabot by @AlekSi in https://github.com/FerretDB/FerretDB/pull/717 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/723 -* Always install Go and skip test cache by @AlekSi in https://github.com/FerretDB/FerretDB/pull/718 -* Bump mongo from 5.0.8 to 5.0.9 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/719 -* Better dummy handler errors by @AlekSi in https://github.com/FerretDB/FerretDB/pull/715 -* Add `task run-proxy` command by @AlekSi in https://github.com/FerretDB/FerretDB/pull/725 -* Add `Min` and `Max` methods to `types.Array` by @ribaraka in https://github.com/FerretDB/FerretDB/pull/726 -* Add arrays with `NaN`, `double` and nested empty array to tests' shared data by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/728 -* Bump github.com/go-task/task/v3 from 3.12.1 to 3.13.0 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/741 -* Disable "free monitoring" to simplify tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/748 -* Re-enable `TestStatisticsCommands` tests by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/704 -* Fix `lint-golangci-lint` task for Windows systems by @w84thesun in https://github.com/FerretDB/FerretDB/pull/752 -* Remove outdated comment by @AlekSi in https://github.com/FerretDB/FerretDB/pull/755 -* Skip `-race` flag on Windows by @w84thesun in https://github.com/FerretDB/FerretDB/pull/753 -* Fix fluky test by @AlekSi in https://github.com/FerretDB/FerretDB/pull/757 -* `tjson` improvements by @AlekSi in https://github.com/FerretDB/FerretDB/pull/760 -* Unify similar code in `pg` handler by @AlekSi in https://github.com/FerretDB/FerretDB/pull/762 -* Add Tigris environment by @AlekSi in https://github.com/FerretDB/FerretDB/pull/761 -* Bump postgres from 14.3 to 14.4 in /build/deps by @dependabot in https://github.com/FerretDB/FerretDB/pull/768 -* Use forked `golangci-lint` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/758 -* Update `conform-pr` action by @AlekSi in https://github.com/FerretDB/FerretDB/pull/783 -* Drop `test_db` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/788 -* Add `task init-clean` target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/756 -* Add `godoc` to tools by @AlekSi in https://github.com/FerretDB/FerretDB/pull/789 - -## New Contributors -* @noisersup made their first contribution in https://github.com/FerretDB/FerretDB/pull/751 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/17?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.3.0...v0.4.0). - - -## [v0.3.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.3.0) (2022-06-01) - -### New Features πŸŽ‰ -* Support `findAndModify` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/548 -* Support `$inc` field `update` operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/596 -* Support `$set` field update operator by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/634 - -### Fixed Bugs πŸ› -* Improve negative zero handling by @AlekSi in https://github.com/FerretDB/FerretDB/pull/613 - -### Enhancements πŸ›  -* Added support for sorting scalar data types by @ribaraka in https://github.com/FerretDB/FerretDB/pull/607 - -### Other Changes πŸ€– -* Better `-0` handling in tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/616 -* Bump github.com/golangci/golangci-lint from 1.46.1 to 1.46.2 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/617 -* Bump PostgreSQL and MongoDB versions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/599 -* Rename `OP_*` constants to `OpCode*` constants by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/620 -* Bump gopkg.in/yaml.v3 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/646 -* Bump gopkg.in/yaml.v3 in tools by @AlekSi in https://github.com/FerretDB/FerretDB/pull/648 -* Make `Path` type by @w84thesun in https://github.com/FerretDB/FerretDB/pull/635 -* Fix incorrect test for `$mod` operator by @fenogentov in https://github.com/FerretDB/FerretDB/pull/645 -* Skip test on all ARM64 OSes by @AlekSi in https://github.com/FerretDB/FerretDB/pull/652 -* Add more visibility for the router/proxy error log levels by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/650 -* Update CODEOWNERS by @AlekSi in https://github.com/FerretDB/FerretDB/pull/655 -* Sync dummy and pg handlers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/641 -* Panic on unexpected order values by @AlekSi in https://github.com/FerretDB/FerretDB/pull/668 -* Add some comments to the functions and variables by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/619 -* Remove dead code by @AlekSi in https://github.com/FerretDB/FerretDB/pull/669 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/15?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.2.1...v0.3.0). - - -## [v0.2.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.2.1) (2022-05-17) - -### New Features πŸŽ‰ -* Support `$slice` projection query operator by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/518 -* Support `$comment` query operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/563 -* Support basic `connectionStatus` diagnostic command by @fenogentov in https://github.com/FerretDB/FerretDB/pull/572 -* Support `$regex` evaluation query operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/588 - -### Enhancements πŸ›  -* Support querying documents by @w84thesun in https://github.com/FerretDB/FerretDB/pull/573 -* Improve comparison of arrays and documents by @ribaraka in https://github.com/FerretDB/FerretDB/pull/589 -* Support `getParameter`'s parameters by @fenogentov in https://github.com/FerretDB/FerretDB/pull/535 -* Add stubs to make VSCode plugin work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/603 - -### Documentation πŸ“„ -* Add conform CI workflow, improve docs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/566 -* Update CONTRIBUTING.md with typo fix and a tiny correction by @rumyantseva in https://github.com/FerretDB/FerretDB/pull/574 -* Add note about forks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/575 - -### Other Changes πŸ€– -* Bump go.mongodb.org/mongo-driver from 1.9.0 to 1.9.1 in /integration by @dependabot in https://github.com/FerretDB/FerretDB/pull/555 -* Add missing `//nolint` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/556 -* Set the handler to use via a command-line flag and remove debug handlers from interface by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/534 -* Add tests for `RemoveByPath` by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/549 -* Add `altMessage` to `AssertEqualError` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/550 -* Add documentation for values comparision by @AlekSi in https://github.com/FerretDB/FerretDB/pull/559 -* Add `debug` and `panic` msg handlers to `Command` map by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/561 -* Add `RemoveByPath` for `Array` and `CompositeTypeInterface` by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/560 -* Bump docker/login-action from 1 to 2 by @dependabot in https://github.com/FerretDB/FerretDB/pull/565 -* Bump pgx version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/570 -* Use `float64(x)` for `ok` everywhere by @AlekSi in https://github.com/FerretDB/FerretDB/pull/577 -* Improve `AssertEqualAltError` documentation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/578 -* Remove `types.MustNewDocument` in some places by @AlekSi in https://github.com/FerretDB/FerretDB/pull/579 -* Remove `MustNewDocument` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/581 -* Remove `MustNewArray` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/582 -* Remove `MustConvertDocument` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/583 -* Enable `staticcheck` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/580 -* Enable `gosimple` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/584 -* Change the way linters work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/586 -* Merge `BigNumbersData` into `Scalars` by @AlphaB in https://github.com/FerretDB/FerretDB/pull/595 -* Set `GOLANGCI_LINT_CACHE` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/597 -* Increase `golangci-lint` timeout by @AlekSi in https://github.com/FerretDB/FerretDB/pull/598 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/604 - -## New Contributors -* @rumyantseva made their first contribution in https://github.com/FerretDB/FerretDB/pull/574 -* @AlphaB made their first contribution in https://github.com/FerretDB/FerretDB/pull/595 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/16?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.2.0...v0.2.1). - - -## [v0.2.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.2.0) (2022-05-04) - -### What's Changed -This release implements all required functionality to support [CLA Assistant](https://github.com/cla-assistant/cla-assistant). -More details will be available shortly in [our blog](https://www.ferretdb.io/blog/). - -### New Features πŸŽ‰ -* Add support for `$nin` operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/459 -* Support querying with dot notation for documents by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/483 -* Add support for `$ne` operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/464 -* Add basic `findAndModify` implementation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/501 -* Add support for `$in` operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/499 - -### Fixed Bugs πŸ› -* Fix large numbers comparision by @DoodgeMatvey in https://github.com/FerretDB/FerretDB/pull/466 -* Fix panic on receiving a filter query with unknown operator by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/517 -* Fix bitwise operators by @w84thesun in https://github.com/FerretDB/FerretDB/pull/488 - -### Enhancements πŸ›  -* Return better errors for unimplemented operations by @AlekSi in https://github.com/FerretDB/FerretDB/pull/504 -* Implement `nameOnly` for `listDatabases` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/524 -* Improve `hostInfo` command's `os` response by @DoodgeMatvey in https://github.com/FerretDB/FerretDB/pull/509 - -### Documentation πŸ“„ -* Mention force pushes by @AlekSi in https://github.com/FerretDB/FerretDB/pull/500 -* Update guidelines by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/496 -* Document `task env-pull` target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/528 - -### Other Changes πŸ€– -* Stabilize tests by always sorting results by @AlekSi in https://github.com/FerretDB/FerretDB/pull/490 -* Skip one test for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/494 -* Bump MongoDB version by @AlekSi in https://github.com/FerretDB/FerretDB/pull/495 -* Use `goimports` to group imports on `task fmt` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/498 -* Make default Docker arguments a bit more useful by @AlekSi in https://github.com/FerretDB/FerretDB/pull/502 -* Export helpers that will be used in other package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/505 -* Correctly override `FUZZTIME` on CI by @AlekSi in https://github.com/FerretDB/FerretDB/pull/506 -* Pass context to PostgreSQL pool by @AlekSi in https://github.com/FerretDB/FerretDB/pull/507 -* Bump dependencies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/514 -* Remove `Array.Subslice` method by @AlekSi in https://github.com/FerretDB/FerretDB/pull/515 -* Remove `types.CString` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/529 -* Make test helpers harder to misuse by @AlekSi in https://github.com/FerretDB/FerretDB/pull/530 -* Move existing comparision code to `types` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/531 -* Extract common interface for handlers by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/521 -* Move all handler test to integration tests by @w84thesun in https://github.com/FerretDB/FerretDB/pull/523 -* Use `nil` errors instead of empty values by @fenogentov in https://github.com/FerretDB/FerretDB/pull/542 -* Delete old tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/543 -* Add tests for `sort` and `find` parameters type by @w84thesun in https://github.com/FerretDB/FerretDB/pull/544 - -## New Contributors -* @DoodgeMatvey made their first contribution in https://github.com/FerretDB/FerretDB/pull/466 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/14?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.1.1...v0.2.0). - - -## [v0.1.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.1.1) (2022-04-19) - -### New Features πŸŽ‰ -* Support `$gt` comparision operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/330 -* Support `$exists` element query operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/446 -* Add basic `upsert` support by @AlekSi in https://github.com/FerretDB/FerretDB/pull/473 -* Add `$type` operator by @w84thesun in https://github.com/FerretDB/FerretDB/pull/453 -* Support `$mod` evaluation query operator by @fenogentov in https://github.com/FerretDB/FerretDB/pull/440 -* Support logical operators by @w84thesun in https://github.com/FerretDB/FerretDB/pull/465 - -### Enhancements πŸ›  -* Ping database in some commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/435 -* Ensure that `_id` fields are always the first by @AlekSi in https://github.com/FerretDB/FerretDB/pull/476 - -### Documentation πŸ“„ -* Improve contributing guidelines by @AlekSi in https://github.com/FerretDB/FerretDB/pull/480 - -### Other Changes πŸ€– -* Integration tests improvements by @AlekSi in https://github.com/FerretDB/FerretDB/pull/441 -* Add test stub for bitwise operators by @AlekSi in https://github.com/FerretDB/FerretDB/pull/443 -* Add tests for collections `create` and `drop` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/444 -* Add tests for more diagnostic commands by @AlekSi in https://github.com/FerretDB/FerretDB/pull/448 -* Transfer existing comparison tests by @ribaraka in https://github.com/FerretDB/FerretDB/pull/445 -* Move `getParameter` tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/450 -* Improve `envtool` diagnostics by @w84thesun in https://github.com/FerretDB/FerretDB/pull/426 -* Fix Postgres port check for `envtool` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/451 -* Add support for `$gte`, `$lt`, `$lte` operators by @ribaraka in https://github.com/FerretDB/FerretDB/pull/452 -* Add tests for `null` values by @w84thesun in https://github.com/FerretDB/FerretDB/pull/458 -* Bump actions/upload-artifact from 2 to 3 by @dependabot in https://github.com/FerretDB/FerretDB/pull/460 -* Update tests for the latest mongo-driver by @AlekSi in https://github.com/FerretDB/FerretDB/pull/463 -* Rearrange tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/467 -* Do not invoke Dance tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/468 -* Minor unification of tests style by @AlekSi in https://github.com/FerretDB/FerretDB/pull/469 -* Add helpers that generate ObjectID by @AlekSi in https://github.com/FerretDB/FerretDB/pull/474 -* Add deep copy helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/475 -* Allow the usage of proxy/diff mode in tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/477 -* Bump codecov/codecov-action from 2 to 3 by @dependabot in https://github.com/FerretDB/FerretDB/pull/461 -* Composite data type find handling by @ribaraka in https://github.com/FerretDB/FerretDB/pull/471 -* Fix failing tests by @w84thesun in https://github.com/FerretDB/FerretDB/pull/482 -* Rename `q` to `filter` in tests by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/484 -* Supress linter warning by @AlekSi in https://github.com/FerretDB/FerretDB/pull/485 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/11?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.1.0...v0.1.1). - - -## [v0.1.0](https://github.com/FerretDB/FerretDB/releases/tag/v0.1.0) (2022-04-04) - -### What's Changed -In this release, we made a big change in the way FerretDB fetches data from PostgreSQL. - -Previously, we generated a single SQL query that extensively used json/jsonb PostgreSQL functions for each incoming MongoDB request, then converted fetched data. -All the filtering was performed by PostgreSQL. -Unfortunately, the semantics of those functions do not match MongoDB behavior in edge cases like comparison or sorting of different types. -That resulted in a difference in behavior between FerretDB and MongoDB, and that is a problem we wanted to fix. - -So starting from this release we fetch more data from PostgreSQL and perform filtering on the FerretDB side. -This allows us to match MongoDB behavior in all cases. -Of course, that also greatly reduces performance. -We plan to address it in future releases by pushing down parts of filtering queries that can be made fully compatible with MongoDB. -For example, a simple query like `db.collection.find({_id: 'some-id-value'})` can be converted to SQL `WHERE` condition relatively easy and be compatible even with weird values like IEEE 754 NaNs, infinities, etc. - -In short, we want FerretDB to be compatible with MongoDB first and fast second, and we are still working towards the first goal. - -### New Features πŸŽ‰ -* Implement `$bitsAllClear` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/394 -* Support `$elemMatch` projection query operator by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/383 -* Support all bitwise query operators by @w84thesun in https://github.com/FerretDB/FerretDB/pull/400 -* Support `$eq` comparision operator by @ribaraka in https://github.com/FerretDB/FerretDB/pull/309 -### Fixed Bugs πŸ› -* Fix a few issues found by fuzzing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/345 -* More fixes for bugs found by fuzzing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/346 -* Commands are case-sensitive by @AlekSi in https://github.com/FerretDB/FerretDB/pull/369 -* Make updates work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/385 -* Handle any number type for `limit` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/399 -* Fix numbers comparision by @ribaraka in https://github.com/FerretDB/FerretDB/pull/356 -* Fix finding zero documents with `FindOne` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/409 -* Fix `sort` for arrays and documents by @AlekSi in https://github.com/FerretDB/FerretDB/pull/424 -* Fix pgdb helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/425 -### Enhancements πŸ›  -* Update `insert` command's help by @narqo in https://github.com/FerretDB/FerretDB/pull/321 -* Return correct error codes for projections by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/384 -* Add SortDocuments by @w84thesun in https://github.com/FerretDB/FerretDB/pull/378 -### Documentation πŸ“„ -* Add Docker badge by @AlekSi in https://github.com/FerretDB/FerretDB/pull/305 -* Tweak Markdown linter a bit by @AlekSi in https://github.com/FerretDB/FerretDB/pull/393 -### Other Changes πŸ€– -* Remove Docker volumes on `make env-down` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/315 -* Update deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/320 -* Build static binaries by @AlekSi in https://github.com/FerretDB/FerretDB/pull/322 -* Integrate with dance PRs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/324 -* Bump Docker images by @AlekSi in https://github.com/FerretDB/FerretDB/pull/325 -* Bump github.com/golangci/golangci-lint from 1.44.0 to 1.44.2 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/327 -* Bump mvdan.cc/gofumpt from 0.2.1 to 0.3.0 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/329 -* Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/FerretDB/FerretDB/pull/333 -* Various small cleanups by @AlekSi in https://github.com/FerretDB/FerretDB/pull/334 -* Rewrite `generate.sh` in Go by @w84thesun in https://github.com/FerretDB/FerretDB/pull/338 -* Add helper for getting required parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/339 -* Use safe type assertions for inputs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/341 -* Fix seed fuzz corpus collection by @AlekSi in https://github.com/FerretDB/FerretDB/pull/340 -* Add fuzzing tests for handlers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/328 -* Add `bin/task` to tools by @AlekSi in https://github.com/FerretDB/FerretDB/pull/349 -* Add more checks for Go versions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/350 -* Improve Windows tooling by @w84thesun in https://github.com/FerretDB/FerretDB/pull/348 -* Add assertions for BSON values comparision by @AlekSi in https://github.com/FerretDB/FerretDB/pull/352 -* Replace `Makefile` with `Taskfile` by @w84thesun in https://github.com/FerretDB/FerretDB/pull/358 -* Fix Taskfile by @w84thesun in https://github.com/FerretDB/FerretDB/pull/365 -* Remove OS-specific Taskfiles, cleanup by @AlekSi in https://github.com/FerretDB/FerretDB/pull/366 -* Remove SQL storage by @w84thesun in https://github.com/FerretDB/FerretDB/pull/367 -* Use square brackets for nicer logs by @AlekSi in https://github.com/FerretDB/FerretDB/pull/373 -* Fix build tags by @AlekSi in https://github.com/FerretDB/FerretDB/pull/374 -* Add converter from types.Regex to regexp.Regexp by @AlekSi in https://github.com/FerretDB/FerretDB/pull/375 -* Log test failures for updates and deletes by @AlekSi in https://github.com/FerretDB/FerretDB/pull/376 -* Filter documents using Go code by @AlekSi in https://github.com/FerretDB/FerretDB/pull/370 -* Projection: `: <1 or true>` and `: <0 or false>` by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/377 -* Fix small issues after rewrite by @AlekSi in https://github.com/FerretDB/FerretDB/pull/380 -* Projection: `: <1 or true>` and `: <0 or false>`: error messages formatting by @seeforschauer in https://github.com/FerretDB/FerretDB/pull/382 -* Bump dependecnies by @AlekSi in https://github.com/FerretDB/FerretDB/pull/387 -* Fix some fluky tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/351 -* Minor CI and build tweaks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/390 -* Add Markdown linter by @fenogentov in https://github.com/FerretDB/FerretDB/pull/386 -* Do not cache modules by @AlekSi in https://github.com/FerretDB/FerretDB/pull/392 -* Fix more fluky tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/391 -* Fix the last fluky test by @AlekSi in https://github.com/FerretDB/FerretDB/pull/395 -* Allow access to actual listener's address by @AlekSi in https://github.com/FerretDB/FerretDB/pull/397 -* Add a new way to write integration tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/389 -* Move `internal/pg` to `internal/handlers/pg/pgdb` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/401 -* Move `handlers/jsonb1` to `handlers/pg` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/402 -* Move handler to `pg` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/403 -* Move tests back for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/404 -* Use `testutil.AssertEqual` helper by @AlekSi in https://github.com/FerretDB/FerretDB/pull/407 -* Move `$size` tests to integration tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/410 -* Improve logging in integration tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/412 -* Tweak `hello`/`ismaster`/`isMaster` responses by @AlekSi in https://github.com/FerretDB/FerretDB/pull/418 -* Fix named loggers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/427 -* Add tests for `getLog` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/421 -* Bump deps by @AlekSi in https://github.com/FerretDB/FerretDB/pull/430 - -## New Contributors -* @narqo made their first contribution in https://github.com/FerretDB/FerretDB/pull/321 -* @w84thesun made their first contribution in https://github.com/FerretDB/FerretDB/pull/338 -* @seeforschauer made their first contribution in https://github.com/FerretDB/FerretDB/pull/377 -* @fenogentov made their first contribution in https://github.com/FerretDB/FerretDB/pull/386 -* @ribaraka made their first contribution in https://github.com/FerretDB/FerretDB/pull/356 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/12?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.6...v0.1.0). - - -## [v0.0.6](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.6) (2022-02-10) - -### New Features πŸŽ‰ -* Support projections by @ekalinin in https://github.com/FerretDB/FerretDB/pull/212 -* Support `dbStats` by @ekalinin in https://github.com/FerretDB/FerretDB/pull/232 -* Support `dataSize` by @ekalinin in https://github.com/FerretDB/FerretDB/pull/246 -* Implement `listCommands` by @OpenSauce in https://github.com/FerretDB/FerretDB/pull/203 -* Support `serverStatus` by @ekalinin in https://github.com/FerretDB/FerretDB/pull/289 -* Add more metrics by @AlekSi in https://github.com/FerretDB/FerretDB/pull/298 -* Implement `$size` query operator by @taaraora in https://github.com/FerretDB/FerretDB/pull/296 -### Fixed Bugs πŸ› -* Forbid short document keys like `$k` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/234 -* Fix benchmarks by @AlekSi in https://github.com/FerretDB/FerretDB/pull/236 -* Move handler tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/239 -* Fix and enable fuzzing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/240 -* Make `db.collection.stats()` & `.dataSize()` work from `mongosh` by @ekalinin in https://github.com/FerretDB/FerretDB/pull/243 -* fix: remove amd-v2 limit by @Junnplus in https://github.com/FerretDB/FerretDB/pull/282 -* Catch concurrent schema/table creation by @AlekSi in https://github.com/FerretDB/FerretDB/pull/283 -* Ignore some parameters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/310 -### Enhancements πŸ›  -* Add `buildEnvironment` and `debug` to `buildInfo` command by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/218 -* Add helper for checking for unimplemented fields by @AlekSi in https://github.com/FerretDB/FerretDB/pull/267 -* Ignore `authorizedXXX` parameters for now by @AlekSi in https://github.com/FerretDB/FerretDB/pull/311 -### Documentation πŸ“„ -* Update documentation about `fjson` package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/262 -* Update tutorial ,add depends_on in docker-compose by @muyouming in https://github.com/FerretDB/FerretDB/pull/275 -### Other Changes πŸ€– -* Bump github.com/reviewdog/reviewdog from 0.13.0 to 0.13.1 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/222 -* Fix Docker workflow by @AlekSi in https://github.com/FerretDB/FerretDB/pull/225 -* Extract `fjson` package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/207 -* Fix test for `collstats` by @ekalinin in https://github.com/FerretDB/FerretDB/pull/233 -* Bump go.uber.org/zap from 1.19.1 to 1.20.0 by @dependabot in https://github.com/FerretDB/FerretDB/pull/241 -* Use generics for CompositeType by @AlekSi in https://github.com/FerretDB/FerretDB/pull/245 -* Enable go-consistent by @AlekSi in https://github.com/FerretDB/FerretDB/pull/248 -* Unexport `fjson` types by @AlekSi in https://github.com/FerretDB/FerretDB/pull/231 -* Remove JSON methods from bson package by @AlekSi in https://github.com/FerretDB/FerretDB/pull/259 -* Fix `make gen` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/264 -* Add fuzztool by @ferretdb-bot in https://github.com/FerretDB/FerretDB/pull/56 -* Use FerretDB/github-actions/linters by @AlekSi in https://github.com/FerretDB/FerretDB/pull/265 -* Make PRs from forks work by @AlekSi in https://github.com/FerretDB/FerretDB/pull/266 -* Use `types.Null` instead of `nil` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/268 -* Add `make fuzz-corpus` target by @AlekSi in https://github.com/FerretDB/FerretDB/pull/279 -* Pass Documents by pointer by @AlekSi in https://github.com/FerretDB/FerretDB/pull/272 -* Unexport some `bson` types by @AlekSi in https://github.com/FerretDB/FerretDB/pull/280 -* Rename receivers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/284 -* Bump github.com/prometheus/client_golang from 1.11.0 to 1.12.0 by @dependabot in https://github.com/FerretDB/FerretDB/pull/285 -* Introduce generics for types by @AlekSi in https://github.com/FerretDB/FerretDB/pull/287 -* Fix some typos and style by @ekalinin in https://github.com/FerretDB/FerretDB/pull/286 -* Add Docker workflow stub by @AlekSi in https://github.com/FerretDB/FerretDB/pull/288 -* Split Docker Build and Push by @AlekSi in https://github.com/FerretDB/FerretDB/pull/290 -* Securely build and push Docker images by @AlekSi in https://github.com/FerretDB/FerretDB/pull/292 -* Update golangci-lint by @AlekSi in https://github.com/FerretDB/FerretDB/pull/294 -* Make fuzztool less verbose by @AlekSi in https://github.com/FerretDB/FerretDB/pull/295 -* Fix compilation with the latest go tip by @AlekSi in https://github.com/FerretDB/FerretDB/pull/300 -* Use `values` MongoDB database by @AlekSi in https://github.com/FerretDB/FerretDB/pull/299 -* Spend less time fuzzing pull requests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/302 -* Detect matching PR by @AlekSi in https://github.com/FerretDB/FerretDB/pull/303 -* Add detection action by @AlekSi in https://github.com/FerretDB/FerretDB/pull/304 -* Remove extra allocation by @peakle in https://github.com/FerretDB/FerretDB/pull/307 -* Micro fixes: type assert order, strings.Split -> strings.Cut by @peakle in https://github.com/FerretDB/FerretDB/pull/308 -* Bump github.com/prometheus/client_golang from 1.12.0 to 1.12.1 by @dependabot in https://github.com/FerretDB/FerretDB/pull/306 - -## New Contributors -* @Junnplus made their first contribution in https://github.com/FerretDB/FerretDB/pull/282 -* @muyouming made their first contribution in https://github.com/FerretDB/FerretDB/pull/275 -* @peakle made their first contribution in https://github.com/FerretDB/FerretDB/pull/307 -* @taaraora made their first contribution in https://github.com/FerretDB/FerretDB/pull/296 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/10?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.5...v0.0.6). - - -## [v0.0.5](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.5) (2022-01-04) - -### New Features πŸŽ‰ -* Add basic metrics by @AlekSi in https://github.com/FerretDB/FerretDB/pull/108 -* Implement `serverStatus` command by @jyz0309 in https://github.com/FerretDB/FerretDB/pull/116 -* Implement `dropDatabase` command by @radmirnovii in https://github.com/FerretDB/FerretDB/pull/117 -* Support count function by @thuan1412 in https://github.com/FerretDB/FerretDB/pull/97 -* Implement `getParameter` command by @jyz0309 in https://github.com/FerretDB/FerretDB/pull/142 -* Support `limit` parameter in delete by @OpenSauce in https://github.com/FerretDB/FerretDB/pull/141 -* Implement basic `create` command by @ekalinin in https://github.com/FerretDB/FerretDB/pull/184 -* Build Docker image with GitHub Actions by @pboros in https://github.com/FerretDB/FerretDB/pull/189 -* Automatically create databases by @AlekSi in https://github.com/FerretDB/FerretDB/pull/185 -* Support `hello` command by @AlekSi in https://github.com/FerretDB/FerretDB/pull/195 -* Add stub for `createindexes` command by @AlekSi in https://github.com/FerretDB/FerretDB/pull/196 -* Support basic `hostInfo` command by @ekalinin in https://github.com/FerretDB/FerretDB/pull/188 -* Support `collStats` command by @ekalinin in https://github.com/FerretDB/FerretDB/pull/206 -### Fixed Bugs πŸ› -* Accept $ and `.` in object field names by @AlekSi in https://github.com/FerretDB/FerretDB/pull/127 -* Make `checkConnection` less strict for common UTF8 localizations by @klokar in https://github.com/FerretDB/FerretDB/pull/135 -* Wait for PostgreSQL on `make env-up` by @agneum in https://github.com/FerretDB/FerretDB/pull/149 -* Fix build info parsing by @AlekSi in https://github.com/FerretDB/FerretDB/pull/205 -* Fix GetLog & add missed test for it by @ekalinin in https://github.com/FerretDB/FerretDB/pull/211 -### Enhancements πŸ›  -* Return version in `serverStatus` command by @AlekSi in https://github.com/FerretDB/FerretDB/pull/121 -* Improve output of buildInfo command by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/204 -### Documentation πŸ“„ -* CONTRIBUTING.md: fix typo & add clonning section by @ekalinin in https://github.com/FerretDB/FerretDB/pull/114 -* CONTRIBUTING.md: fix "/user/.../" -> "/usr/.../" by @GinGin3203 in https://github.com/FerretDB/FerretDB/pull/137 -* Add community links by @AlekSi in https://github.com/FerretDB/FerretDB/pull/180 -### Other Changes πŸ€– -* Add convention for Decimal128 by @AlekSi in https://github.com/FerretDB/FerretDB/pull/103 -* Bump github.com/jackc/pgx/v4 from 4.14.0 to 4.14.1 by @dependabot in https://github.com/FerretDB/FerretDB/pull/99 -* Build multi-arch Docker images by @AlekSi in https://github.com/FerretDB/FerretDB/pull/107 -* Verify modules on `make init` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/123 -* Enable go-consistent linter by @AlekSi in https://github.com/FerretDB/FerretDB/pull/124 -* Use composite GitHub Action for Go setup in https://github.com/FerretDB/FerretDB/pull/126 -* Use shared setup-go action by @AlekSi in https://github.com/FerretDB/FerretDB/pull/131 -* Add an option to use read-only user in tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/132 -* Refactor handler tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/136 -* Bump MongoDB and test_db versions by @AlekSi in https://github.com/FerretDB/FerretDB/pull/139 -* Remove old hack by @AlekSi in https://github.com/FerretDB/FerretDB/pull/144 -* Enable goheader linter by @AlekSi in https://github.com/FerretDB/FerretDB/pull/145 -* Cleanups and fixes by @AlekSi in https://github.com/FerretDB/FerretDB/pull/146 -* Use `any` instead of `interface{}` by @AlekSi in https://github.com/FerretDB/FerretDB/pull/147 -* Tweak storage by @AlekSi in https://github.com/FerretDB/FerretDB/pull/148 -* Add helpers for accessing objects by paths by @AlekSi in https://github.com/FerretDB/FerretDB/pull/140 -* Bump mvdan.cc/gofumpt from 0.2.0 to 0.2.1 in /tools by @dependabot in https://github.com/FerretDB/FerretDB/pull/186 -* Add and use schema and table helpers by @AlekSi in https://github.com/FerretDB/FerretDB/pull/191 -* Refactor / cleanup tests by @AlekSi in https://github.com/FerretDB/FerretDB/pull/192 -* Add missed test for `buildInfo` command by @ekalinin in https://github.com/FerretDB/FerretDB/pull/187 -* Refactor slice / `types.Array` type by @AlekSi in https://github.com/FerretDB/FerretDB/pull/202 -* Bump golang.org/x/text from 0.3.6 to 0.3.7 by @dependabot in https://github.com/FerretDB/FerretDB/pull/208 -* Setup changelog generation by @ekalinin in https://github.com/FerretDB/FerretDB/pull/209 -* Build containers for branches as well by @pboros in https://github.com/FerretDB/FerretDB/pull/213 -* Container builds for PRs and tags by @pboros in https://github.com/FerretDB/FerretDB/pull/215 -* Use our own action for extracting Docker tag by @AlekSi in https://github.com/FerretDB/FerretDB/pull/219 - -## New Contributors -* @AlekSi made their first contribution in https://github.com/FerretDB/FerretDB/pull/103 -* @ekalinin made their first contribution in https://github.com/FerretDB/FerretDB/pull/114 -* @jyz0309 made their first contribution in https://github.com/FerretDB/FerretDB/pull/116 -* @radmirnovii made their first contribution in https://github.com/FerretDB/FerretDB/pull/117 -* @klokar made their first contribution in https://github.com/FerretDB/FerretDB/pull/126 -* @GinGin3203 made their first contribution in https://github.com/FerretDB/FerretDB/pull/137 -* @agneum made their first contribution in https://github.com/FerretDB/FerretDB/pull/149 -* @pboros made their first contribution in https://github.com/FerretDB/FerretDB/pull/189 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/4?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.4...v0.0.5). - - -## [v0.0.4](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.4) (2021-12-01) - -* A new name in https://github.com/FerretDB/FerretDB/discussions/100 -* Added support for databases sizes in `listDatabases` command ([#61](https://github.com/FerretDB/FerretDB/issues/61), thanks to [Leigh](https://github.com/OpenSauce)). - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/3?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.3...v0.0.4). - - -## [v0.0.3](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.3) (2021-11-19) - -* Added support for `$regex` evaluation query operator in https://github.com/FerretDB/FerretDB/issues/28 -* Fixed handling of Infinity, -Infinity, NaN BSON Double values in https://github.com/FerretDB/FerretDB/issues/29 -* Improved documentation for contributors (thanks to [Leigh](https://github.com/OpenSauce)). - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/2?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.2...v0.0.3). - - -## [v0.0.2](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.2) (2021-11-13) - -* Added support for comparison query operators: `$eq`,`$gt`,`$gte`,`$in`,`$lt`,`$lte`,`$ne`,`$nin` in https://github.com/FerretDB/FerretDB/issues/26 -* Added support for logical query operators: `$and`, `$not`, `$nor`, `$or` in https://github.com/FerretDB/FerretDB/issues/27 - -[All closed issues and pull requests](https://github.com/FerretDB/FerretDB/milestone/1?closed=1). -[All commits](https://github.com/FerretDB/FerretDB/compare/v0.0.1...v0.0.2). - - -## [v0.0.1](https://github.com/FerretDB/FerretDB/releases/tag/v0.0.1) (2021-11-01) +## Older Releases -* Initial public release! +See . diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0bf434bf82c5..988a4f86b2d7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -55,7 +55,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -conduct@ferretdb.io. +. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33b325d940f3..0aa8f13cf737 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,8 +117,6 @@ The `internal` subpackages contain most of the FerretDB code: * `types` package provides Go types matching BSON types that don't have built-in Go equivalents: we use `int32` for BSON's int32, but `types.ObjectID` for BSON's ObjectId. * `types/fjson` provides converters from/to FJSON for built-in and `types` types. - FJSON adds some extensions to JSON for keeping object keys in order, - preserving BSON type information in the values themselves, etc. It is used for logging of BSON values and wire protocol messages. * `bson` package provides converters from/to BSON for built-in and `types` types. * `wire` package provides wire protocol implementation. @@ -128,13 +126,13 @@ The `internal` subpackages contain most of the FerretDB code: * `handlers` contains a common interface for backend handlers that they should implement. Handlers use `types` and `wire` packages, but `bson` package details are hidden. * `handlers/common` contains code shared by different handlers. -* `handlers/dummy` contains a stub implementation of that interface that could be copied into a new package - as a starting point for the new handlers. -* `handlers/pg` contains the implementation of the PostgreSQL handler. -* `handlers/pg/pjson` provides converters from/to PJSON for built-in and `types` types. - PJSON adds some extensions to JSON for keeping object keys in order, +* `handlers/sjson` provides converters from/to SJSON for built-in and `types` types. + SJSON adds some extensions to JSON for keeping object keys in order, preserving BSON type information in the values themselves, etc. - It is used by `pg` handler. + It is used by `sqlite` and `pg` handlers. +* `handlers/sqlite` contains the implementation of the SQLite handler. + It is being converted into universal handler for all backends. +* `handlers/pg` contains the implementation of the PostgreSQL handler. * `handlers/tigris` contains the implementation of the Tigris handler. * `handlers/tigris/tjson` provides converters from/to TJSON with JSON Schema for built-in and `types` types. BSON type information is preserved either in the schema (where possible) or in the values themselves. @@ -272,7 +270,9 @@ Before submitting a pull request, please make sure that: so there is **no need** to squash them manually, amend them, and/or do force pushes. But notice that the autogenerated GitHub's squash commit's body **should be** manually replaced by "Closes #{issue_number}.". -5. Please don't forget to click "re-request review" buttons once PR is ready for re-review. +5. Please don't forget to click + ["re-request review" buttons](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) + once PR is ready for re-review. If you have interest in becoming or are a long-term contributor, please read [PROCESS.md](.github/PROCESS.md) for more details. @@ -284,7 +284,9 @@ To help us accurately identify the cause, we encourage you to include a pull request with test script. Please write the test script in [build/legacy-mongo-shell/test.js](build/legacy-mongo-shell/test.js). -You can find an example of how to prepare a test script in +You can find an overview of the available assertions [here](build/legacy-mongo-shell/README.md). +Use these assertions to validate your test's assumptions and invariants. +You can also find an example of how to prepare a test script in [build/legacy-mongo-shell/test.example.js](build/legacy-mongo-shell/test.example.js). Test your script using following steps: diff --git a/SECURITY.md b/SECURITY.md index 077073326e41..73270b85590e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,6 +8,6 @@ We support all FerretDB versions since 1.0.0. Please do not use public issues for security problems. Instead, report them [there](https://github.com/FerretDB/FerretDB/security/advisories/new). -You can also reach us by email at security@ferretdb.io. +You can also reach us by email at . We should respond to you within seven days. diff --git a/Taskfile.yml b/Taskfile.yml index 58ecc48138bc..ea44e6e23446 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -66,7 +66,7 @@ tasks: - docker system prune --all --volumes - bin/golangci-lint{{exeExt}} cache clean - go clean -cache -testcache -modcache -fuzzcache - - rm -fr .cache .task tools/.task integration/.task tmp + - go run ./cmd/envtool shell rmdir .cache .task tools/.task integration/.task tmp - task: env-pull - task: init @@ -176,10 +176,11 @@ tasks: - bin/task{{exeExt}} -d tools tools-test test-integration: - desc: "Run integration tests for `pg` and `tigris` in parallel" + desc: "Run integration tests for several backends in parallel" deps: - test-integration-pg - test-integration-mongodb + # no test-integration-sqlite yet # disable for now - test-integration-tigris # no test-integration-hana @@ -195,6 +196,17 @@ tasks: -postgresql-url=postgres://username@127.0.0.1:5432/ferretdb -compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem' + test-integration-sqlite: + desc: "Run integration tests for `sqlite` handler" + dir: integration + cmds: + - > + go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + -coverprofile=integration-sqlite.txt . + -target-backend=ferretdb-sqlite + -target-tls + -compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem' + test-integration-tigris: desc: "Run integration tests for `tigris` handler" dir: integration @@ -284,6 +296,19 @@ tasks: --postgresql-url=postgres://username@127.0.0.1:5432/ferretdb --test-records-dir=tmp/records + run-sqlite: + desc: "Run FerretDB with `sqlite` handler" + deps: [build-host] + cmds: + - > + bin/ferretdb{{exeExt}} -test.coverprofile=cover.txt -- + --listen-addr=:27017 + --proxy-addr=127.0.0.1:47017 + --mode=diff-normal + --handler=sqlite + --sqlite-uri=tmp/sqlite + --test-records-dir=tmp/records + run-tigris: desc: "Run FerretDB with `tigris` handler" deps: [build-host] @@ -406,6 +431,11 @@ tasks: cmds: - docker compose exec -e PGPASSWORD=password postgres psql -U username -d ferretdb + sqlite3: + desc: "Run sqlite3" + cmds: + - sqlite3 *.sqlite + mongosh: desc: "Run MongoDB shell (`mongosh`)" cmds: @@ -451,9 +481,9 @@ tasks: . vars: VERSION: - sh: cat build/version/version.txt + sh: go run ./cmd/envtool shell read build/version/version.txt COMMIT: - sh: cat build/version/commit.txt + sh: go run ./cmd/envtool shell read build/version/commit.txt # do not change that target much; see "Building and packaging" in README.md build-release: @@ -517,7 +547,7 @@ tasks: packages-deb: cmds: - echo 'deb' > build/version/package.txt - - mkdir -p tmp/debs + - go run ./cmd/envtool shell mkdir tmp/debs - task: docker-build vars: FILE: production @@ -528,13 +558,14 @@ tasks: env: ARCH: amd64 VERSION: - sh: cat build/version/version.txt + sh: go run ./cmd/envtool shell read build/version/version.txt + POSTGRES_PACKAGE: postgresql - docker compose run --rm ubuntu /bin/sh -c 'dpkg -i /debs/ferretdb.deb && ferretdb --version' packages-rpm: cmds: - echo 'rpm' > build/version/package.txt - - mkdir -p tmp/rpms + - go run ./cmd/envtool shell mkdir tmp/rpms - task: docker-build vars: FILE: production @@ -545,7 +576,8 @@ tasks: env: ARCH: amd64 VERSION: - sh: cat build/version/version.txt + sh: go run ./cmd/envtool shell read build/version/version.txt + POSTGRES_PACKAGE: postgresql - docker compose run --rm ubi /bin/sh -c 'rpm -i /rpms/ferretdb.rpm && ferretdb --version' fmt-yaml: diff --git a/build/deps/docusaurus-docs.Dockerfile b/build/deps/docusaurus-docs.Dockerfile index 1a088fe92f6d..3cea6488c1f9 100644 --- a/build/deps/docusaurus-docs.Dockerfile +++ b/build/deps/docusaurus-docs.Dockerfile @@ -1 +1 @@ -FROM ghcr.io/ferretdb/docusaurus-docs:2.4.0-2 +FROM ghcr.io/ferretdb/docusaurus-docs:2.4.1-1 diff --git a/build/deps/ferretdb-wrangler.Dockerfile b/build/deps/ferretdb-wrangler.Dockerfile index aba6583e38c7..48646ebe07ae 100644 --- a/build/deps/ferretdb-wrangler.Dockerfile +++ b/build/deps/ferretdb-wrangler.Dockerfile @@ -1 +1 @@ -FROM ghcr.io/ferretdb/ferretdb-wrangler:2.16.0-1 +FROM ghcr.io/ferretdb/ferretdb-wrangler:2.20.0-1 diff --git a/build/deps/jaeger.Dockerfile b/build/deps/jaeger.Dockerfile index de0b71854cc1..f68cfed2f171 100644 --- a/build/deps/jaeger.Dockerfile +++ b/build/deps/jaeger.Dockerfile @@ -1 +1 @@ -FROM jaegertracing/all-in-one:1.44.0 +FROM jaegertracing/all-in-one:1.45.0 diff --git a/build/deps/markdownlint.Dockerfile b/build/deps/markdownlint.Dockerfile index 1c49c9883afa..15c039a93158 100644 --- a/build/deps/markdownlint.Dockerfile +++ b/build/deps/markdownlint.Dockerfile @@ -1 +1 @@ -FROM davidanson/markdownlint-cli2:v0.6.0 +FROM davidanson/markdownlint-cli2:v0.7.1 diff --git a/build/deps/postgres.Dockerfile b/build/deps/postgres.Dockerfile index 92c5da897aef..43c80f9ce24d 100644 --- a/build/deps/postgres.Dockerfile +++ b/build/deps/postgres.Dockerfile @@ -1 +1 @@ -FROM postgres:15.2 +FROM postgres:15.3 diff --git a/build/deps/trivy.Dockerfile b/build/deps/trivy.Dockerfile index 9444b85e6ed5..89501bf727c0 100644 --- a/build/deps/trivy.Dockerfile +++ b/build/deps/trivy.Dockerfile @@ -1,2 +1,2 @@ -FROM aquasec/trivy:0.40.0 +FROM aquasec/trivy:0.41.0 WORKDIR /workdir diff --git a/build/deps/ubi9.Dockerfile b/build/deps/ubi9.Dockerfile index 76899320c447..546dacca3a13 100644 --- a/build/deps/ubi9.Dockerfile +++ b/build/deps/ubi9.Dockerfile @@ -1 +1 @@ -FROM redhat/ubi9-minimal:9.1.0 +FROM redhat/ubi9-minimal:9.2 diff --git a/build/docker/all-in-one.Dockerfile b/build/docker/all-in-one.Dockerfile index bf1ef9dd34bd..7545d7570260 100644 --- a/build/docker/all-in-one.Dockerfile +++ b/build/docker/all-in-one.Dockerfile @@ -12,7 +12,7 @@ ARG LABEL_COMMIT # build stage -FROM ghcr.io/ferretdb/golang:1.20.3-2 AS all-in-one-build +FROM ghcr.io/ferretdb/golang:1.20.4-1 AS all-in-one-build ARG LABEL_VERSION ARG LABEL_COMMIT @@ -42,7 +42,8 @@ ENV GOCOVERDIR=cover ENV GORACE=halt_on_error=1,history_size=2 ENV GOARM=7 -# do not raise it without providing a v1 build because v2+ is problematic for some virtualization platforms +# do not raise it without providing a v1 build because v2+ is problematic +# for some virtualization platforms and older hardware ENV GOAMD64=v1 # TODO https://github.com/FerretDB/FerretDB/issues/2170 @@ -58,6 +59,8 @@ RUN --mount=type=cache,target=/cache \ RUN --mount=type=cache,target=/cache < const res = db.runCommand({findAndModify: "foo", query: {}, update: {$set: {_id: 1}}}); +> assert.commandFailedWithCode(res, ErrorCodes.ImmutableField); +``` + +It is not always necessary use the `db.runCommand()` helper as some write methods wrap a `writeError` which the helpers can parse. +For example, `insert`, `update`, and `remove` will all return a `WriteResult` so the function can parse the result and look for a `writeError`. + +See the `WriteResult` object below: + +```js +// WriteResult object +> assert.commandWorked(db.foo.insert({a: 1})); +WriteResult({ "nInserted" : 1 }) +> const res = db.foo.insert({a: 1}); +> res.hasWriteError(); +false +> Object.keys(res); +[ + "ok", + "nInserted", + "nUpserted", + "nMatched", + "nModified", + "nRemoved", + "getUpsertedId", + "getRawResponse", + "getWriteError", + "hasWriteError", + "getWriteConcernError", + "hasWriteConcernError", + "tojson", + "toString", + "shellPrint" +] +> res instanceof WriteResult +true +> +``` + + The `find()` method will return an object, the properties of which will not be parsed by the various assert functions: + +```js +> const res = db.foo.find({a: 1}); +> res._filter +{ "a" : 1 } +> res._db +test +> res._batchSize +0 +// all properties are private but the shell will iterate the cursor object when called +> Object.keys(res); +[ + "_mongo", + "_db", + "_collection", + "_ns", + "_filter", + "_projection", + "_limit", + "_skip", + "_batchSize", + "_options", + "_additionalCmdParams", + "_cursor", + "_numReturned" +] +> +``` + +But a `runCommand()` result will return the correct object that can be parsed: + +```js +> // use a runCommand instead +> const res = db.runCommand({find: "foo", filter: {a: 1}}); +> res.ok +1 +> res._commandObj +{ + "find" : "foo", + "filter" : { + "a" : 1 + }, + "lsid" : { + "id" : UUID("cb64879f-6f69-4656-a38e-ce6dbe3ccebc") + } +} +> assert.commandFailed(res); +uncaught exception: Error: command worked when it should have failed: { + "cursor" : { + "firstBatch" : [ + { + "_id" : ObjectId("6458ec7adb858f891c0b8c68"), + "a" : 1 + }, + { + "_id" : ObjectId("6458ec9ddb858f891c0b8c6a"), + "a" : 1 + } + ], + "id" : NumberLong(0), + "ns" : "test.foo" + }, + "ok" : 1 +} : +_getErrorWithCode@src/mongo/shell/utils.js:24:13 +doassert@src/mongo/shell/assert.js:18:14 +_assertCommandFailed@src/mongo/shell/assert.js:819:25 +assert.commandFailed@src/mongo/shell/assert.js:877:16 +@(shell):1:8 +``` + +## Some useful functions + +`assert.eq(a, b, msg)` + +throws if two values are not equal (tested without strict equality). + +`assert.isnull(what, msg)` + +throws if `what` is not null. + +`assert.commandFailed(res, msg)` + +throws if the command did not fail. + +`assert.commandWorked(res)` + +throws if the result contained an error. + +`assert.commandFailedWithCode(res, expectedCode, msg)` + +throws if the command did not fail with the expected code. + +`assert.sameMembers(aArr, bArr, msg, compareFn = _isDocEq)` + +throws if the two arrays do not have the same members, in any order. +By default, nested arrays must have the same order to be considered equal. +Optionally accepts a `compareFn` to compare values instead of using `docEq`. + +`assert.writeOK(res, msg)` + +throws if write result contained an error. + +`assert.docEq(expectedDoc, actualDoc, msg)` + +throws if `actualDoc` object is not equal to `expectedDoc` object. +The order of fields +(properties) within objects is disregarded. +Throws if object representation in BSON exceeds 16793600 bytes. + +`assert.retry(func, msg, num_attempts, intervalMS)` + +calls the given function `func` repeatedly at time intervals specified by +`intervalMS` (milliseconds) until either `func()` returns true or the number of +attempted function calls is equal to `num_attempts`. +Throws an exception with +message `msg` after all attempts are used up. +If no `intervalMS` argument is passed, it defaults to 0. + +## All assert functions + +```js +assert.adminCommandWorkedAllowingNetworkError +assert.between +assert.betweenEx +assert.betweenIn +assert.bind +assert.call +assert.close +assert.closeWithinMS +assert.commandFailed +assert.commandFailedWithCode +assert.commandWorked +assert.commandWorkedIgnoringWriteConcernErrors +assert.commandWorkedIgnoringWriteErrors +assert.commandWorkedIgnoringWriteErrorsAndWriteConcernErrors +assert.commandWorkedOrFailedWithCode +assert.contains +assert.containsPrefix +assert.docEq +assert.doesNotThrow +assert.dropExceptionsWithCode +assert.eq +assert.gt +assert.gte +assert.hasFields +assert.includes +assert.isnull +assert.lt +assert.lte +assert.neq +assert.noAPIParams +assert.retry +assert.retryNoExcept +assert.sameMembers +assert.setEq +assert.soon +assert.soonNoExcept +assert.throws +assert.throwsWithCode +assert.time +assert.toLocaleString +assert.toString +assert.valueOf +assert.writeError +assert.writeErrorWithCode +assert.writeOK +``` + +## Error Codes + +`ErrorCodes` is an object that is generated from various source files. +It provides error names that correspond to their respective codes. + +```js +// tab in the shell after typing the below to display all 443 error codes +> ErrorCodes. +Display all 443 possibilities? (y or n) +ErrorCodes.APIDeprecationError ErrorCodes.NetworkInterfaceExceededTimeLimit +ErrorCodes.APIMismatchError ErrorCodes.NetworkTimeout +ErrorCodes.APIStrictError ErrorCodes.NewReplicaSetConfigurationIncompatible +ErrorCodes.APIVersionError ErrorCodes.NoConfigPrimary +ErrorCodes.AlarmAlreadyFulfilled ErrorCodes.NoMatchParseContext +ErrorCodes.AlreadyInitialized ErrorCodes.NoMatchingDocument +ErrorCodes.AmbiguousIndexKeyPattern ErrorCodes.NoProgressMade +ErrorCodes.AtomicityFailure ErrorCodes.NoProjectionFound +ErrorCodes.AuditingNotEnabled ErrorCodes.NoQueryExecutionPlans +ErrorCodes.AuthSchemaIncompatible ErrorCodes.NoReplicationEnabled +ErrorCodes.AuthenticationAbandoned ErrorCodes.NoShardingEnabled +ErrorCodes.AuthenticationFailed ErrorCodes.NoSuchKey +ErrorCodes.AuthenticationRestrictionUnmet ErrorCodes.NoSuchReshardCollection +ErrorCodes.BSONObjectTooLarge ErrorCodes.NoSuchSession +ErrorCodes.BackgroundOperationInProgressForDatabase ErrorCodes.NoSuchTenantMigration +ErrorCodes.BackgroundOperationInProgressForNamespace ErrorCodes.NoSuchTransaction +ErrorCodes.BackupCursorOpenConflictWithCheckpoint ErrorCodes.NodeNotElectable +ErrorCodes.BadPerfCounterPath ErrorCodes.NodeNotFound +ErrorCodes.BadValue ErrorCodes.NonConformantBSON +ErrorCodes.BalancerInterrupted ErrorCodes.NonExistentPath +ErrorCodes.BrokenPromise ErrorCodes.NonRetryableTenantMigrationConflict +ErrorCodes.CallbackCanceled ErrorCodes.NotAReplicaSet +ErrorCodes.CanRepairToDowngrade ErrorCodes.NotARetryableWriteCommand +ErrorCodes.CannotApplyOplogWhilePrimary ErrorCodes.NotExactValueField +ErrorCodes.CannotBackfillArray ErrorCodes.NotImplemented +ErrorCodes.CannotBackup ErrorCodes.NotPrimaryNoSecondaryOk +ErrorCodes.CannotBuildIndexKeys ErrorCodes.NotPrimaryOrSecondary +ErrorCodes.CannotConvertIndexToUnique ErrorCodes.NotSecondary +ErrorCodes.CannotCreateCollection ErrorCodes.NotSingleValueField +ErrorCodes.CannotCreateIndex ErrorCodes.NotWritablePrimary +ErrorCodes.CannotDowngrade ErrorCodes.NotYetInitialized +ErrorCodes.CannotDropShardKeyIndex ErrorCodes.OBSOLETE_BalancerLostDistributedLock +``` + +`ErrorCodeStrings` is an object with the reverse mapping of the above. + +```js +> ErrorCodeStrings[0] +OK +> ErrorCodeStrings[2] +BadValue +> ErrorCodeStrings[66] +ImmutableField +``` diff --git a/build/nfpm.yml b/build/nfpm.yml index a17b2283b34b..7fbfe349b2d9 100644 --- a/build/nfpm.yml +++ b/build/nfpm.yml @@ -10,6 +10,8 @@ maintainer: packages@ferretdb.io description: A truly Open Source MongoDB alternative homepage: https://ferretdb.io license: Apache License 2.0 +suggests: # https://www.debian.org/doc/debian-policy/ch-relationships.html#binary-dependencies-depends-recommends-suggests-enhances-pre-depends + - ${POSTGRES_PACKAGE} contents: - src: tmp/build/ferretdb dst: /usr/bin/ferretdb diff --git a/cmd/envtool/envtool.go b/cmd/envtool/envtool.go index 6b23bbbd60dc..27f1627cbbf3 100644 --- a/cmd/envtool/envtool.go +++ b/cmd/envtool/envtool.go @@ -321,11 +321,62 @@ func printDiagnosticData(setupError error, logger *zap.SugaredLogger) { }) } +// mkdir creates all directories from given paths. +func mkdir(paths ...string) error { + var errs error + + for _, path := range paths { + if err := os.MkdirAll(path, 0o777); err != nil { + errs = errors.Join(errs, err) + } + } + + return errs +} + +// rmdir removes all directories from given paths. +func rmdir(paths ...string) error { + var errs error + + for _, path := range paths { + if err := os.RemoveAll(path); err != nil { + errs = errors.Join(errs, err) + } + } + + return errs +} + +// read will show the content of a file. +func read(w io.Writer, paths ...string) error { + for _, path := range paths { + b, err := os.ReadFile(path) + if err != nil { + return err + } + + fmt.Fprint(w, string(b)) + } + + return nil +} + // cli struct represents all command-line commands, fields and flags. // It's used for parsing the user input. var cli struct { Debug bool `help:"Enable debug mode."` - Setup struct{} `cmd:""` + Setup struct{} `cmd:"" help:"Setup development environment."` + Shell struct { + Mkdir struct { + Paths []string `arg:"" name:"path" help:"Paths to create." type:"path"` + } `cmd:"" help:"Create directories if they do not already exist."` + Rmdir struct { + Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` + } `cmd:"" help:"Remove directories."` + Read struct { + Paths []string `arg:"" name:"path" help:"Paths to read." type:"path"` + } `cmd:"" help:"read files"` + } `cmd:""` } func main() { @@ -347,11 +398,23 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - switch kongCtx.Command() { + var err error + + switch cmd := kongCtx.Command(); cmd { case "setup": - if err := setup(ctx, logger); err != nil { - printDiagnosticData(err, logger) - os.Exit(1) - } + err = setup(ctx, logger) + case "shell mkdir ": + err = mkdir(cli.Shell.Mkdir.Paths...) + case "shell rmdir ": + err = rmdir(cli.Shell.Rmdir.Paths...) + case "shell read ": + err = read(os.Stdout, cli.Shell.Read.Paths...) + default: + err = fmt.Errorf("unknown command: %s", cmd) + } + + if err != nil { + printDiagnosticData(err, logger) + os.Exit(1) } } diff --git a/cmd/envtool/envtool_test.go b/cmd/envtool/envtool_test.go index c4e6099ce880..63b6e1441cc2 100644 --- a/cmd/envtool/envtool_test.go +++ b/cmd/envtool/envtool_test.go @@ -15,6 +15,8 @@ package main import ( + "bytes" + "os" "testing" "github.com/stretchr/testify/assert" @@ -31,3 +33,46 @@ func TestPrintDiagnosticData(t *testing.T) { printDiagnosticData(nil, l.Sugar()) }) } + +func TestRmdirAbsentDir(t *testing.T) { + t.Parallel() + + err := rmdir("absent") + assert.NoError(t, err) +} + +func TestMkdirAndRmdir(t *testing.T) { + t.Parallel() + + paths := []string{"ab/c", "ab"} + + err := mkdir(paths...) + assert.NoError(t, err) + + for _, path := range paths { + assert.DirExists(t, path) + } + + err = rmdir(paths...) + assert.NoError(t, err) + + for _, path := range paths { + assert.NoDirExists(t, path) + } +} + +func TestRead(t *testing.T) { + t.Parallel() + + f, err := os.CreateTemp("", "test_read") + assert.NoError(t, err) + + s := "test string in a file" + _, err = f.Write([]byte(s)) + assert.NoError(t, err) + + var output bytes.Buffer + err = read(&output, f.Name()) + assert.NoError(t, err) + assert.Equal(t, s, output.String()) +} diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index 231c7e4088bc..dc3ad9081090 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -30,8 +30,6 @@ import ( "github.com/prometheus/common/expfmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/build/version" "github.com/FerretDB/FerretDB/internal/clientconn" @@ -65,8 +63,6 @@ var cli struct { ProxyAddr string `default:"" help:"Proxy address."` DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."` - PostgreSQLURL string `name:"postgresql-url" default:"${default_postgresql_url}" help:"PostgreSQL URL for 'pg' handler."` - // see setCLIPlugins kong.Plugins @@ -82,7 +78,7 @@ var cli struct { Test struct { RecordsDir string `default:"" help:"Experimental: directory for record files."` DisableFilterPushdown bool `default:"false" help:"Experimental: disable filter pushdown."` - EnableSortingPushdown bool `default:"false" help:"Experimental: enable sorting pushdown."` + EnableSortPushdown bool `default:"false" help:"Experimental: enable sort pushdown."` EnableCursors bool `default:"false" help:"Experimental: enable cursors."` //nolint:lll // for readability @@ -96,7 +92,21 @@ var cli struct { } `embed:"" prefix:"test-"` } -// The tigrisFlags struct represents flags that are used specifically by "tigris" handler. +// The pgFlags struct represents flags that are used by the "pg" handler. +// +// See main_pg.go. +var pgFlags struct { + PostgreSQLURL string `name:"postgresql-url" default:"${default_postgresql_url}" help:"PostgreSQL URL for 'pg' handler."` +} + +// The sqliteFlags struct represents flags that are used by the "sqlite" handler. +// +// See main_sqlite.go. +var sqliteFlags struct { + SQLiteURI string `name:"sqlite-uri" default:"." help:"Directory path or 'file' URI for 'sqlite' handler."` +} + +// The tigrisFlags struct represents flags that are used by the "tigris" handler. // // See main_tigris.go. var tigrisFlags struct { @@ -105,7 +115,7 @@ var tigrisFlags struct { TigrisClientSecret string `default:"" help:"Tigris Client secret."` } -// The hanaFlags struct represents flags that are used specifically by "hana" handler. +// The hanaFlags struct represents flags that are used by the "hana" handler. // // See main_hana.go. var hanaFlags struct { @@ -115,13 +125,21 @@ var hanaFlags struct { // handlerFlags is a map of handler names to their flags. var handlerFlags = map[string]any{} -// setCLIPlugins adds Kong flags for handlers in the stable order. +// setCLIPlugins adds Kong flags for handlers in the right order. func setCLIPlugins() { - handlers := maps.Keys(handlerFlags) - slices.Sort(handlers) + handlers := registry.Handlers() + + if len(handlers) != len(handlerFlags) { + panic("handlers and handlerFlags are not in sync") + } for _, h := range handlers { - cli.Plugins = append(cli.Plugins, handlerFlags[h]) + f := handlerFlags[h] + if f == nil { + panic(fmt.Sprintf("handler %q has no flags", h)) + } + + cli.Plugins = append(cli.Plugins, f) } } @@ -330,7 +348,9 @@ func run() { Metrics: metrics.ConnMetrics, StateProvider: stateProvider, - PostgreSQLURL: cli.PostgreSQLURL, + PostgreSQLURL: pgFlags.PostgreSQLURL, + + SQLiteURI: sqliteFlags.SQLiteURI, TigrisURL: tigrisFlags.TigrisURL, TigrisClientID: tigrisFlags.TigrisClientID, @@ -340,7 +360,7 @@ func run() { TestOpts: registry.TestOpts{ DisableFilterPushdown: cli.Test.DisableFilterPushdown, - EnableSortPushdown: cli.Test.EnableSortingPushdown, + EnableSortPushdown: cli.Test.EnableSortPushdown, EnableCursors: cli.Test.EnableCursors, }, }) diff --git a/cmd/ferretdb/main_pg.go b/cmd/ferretdb/main_pg.go new file mode 100644 index 000000000000..9b47c5519764 --- /dev/null +++ b/cmd/ferretdb/main_pg.go @@ -0,0 +1,20 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +// init adds "pg" handler flags. +func init() { + handlerFlags["pg"] = &pgFlags +} diff --git a/cmd/ferretdb/main_sqlite.go b/cmd/ferretdb/main_sqlite.go new file mode 100644 index 000000000000..62b328228076 --- /dev/null +++ b/cmd/ferretdb/main_sqlite.go @@ -0,0 +1,20 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +// init adds "sqlite" handler flags. +func init() { + handlerFlags["sqlite"] = &sqliteFlags +} diff --git a/ferretdb/ferretdb.go b/ferretdb/ferretdb.go index 1ca4c1cbe6ed..88b332bae62a 100644 --- a/ferretdb/ferretdb.go +++ b/ferretdb/ferretdb.go @@ -40,16 +40,22 @@ import ( type Config struct { Listener ListenerConfig - // Handler to use; one of `pg` or `tigris` (if enabled at compile-time). + // Handler to use; one of `pg`, `sqlite`, or `tigris` (if enabled at compile-time). Handler string // PostgreSQL connection string for `pg` handler. // See: - // * https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#ParseConfig - // * https://pkg.go.dev/github.com/jackc/pgx/v5#ParseConfig - // * https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#ParseConfig + // - https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#ParseConfig + // - https://pkg.go.dev/github.com/jackc/pgx/v5#ParseConfig + // - https://pkg.go.dev/github.com/jackc/pgx/v5/pgconn#ParseConfig PostgreSQLURL string // For example: `postgres://hostname:5432/ferretdb`. + // SQLite directory path or `file:` URI for `sqlite` handler. + // See: + // - https://www.sqlite.org/c3ref/open.html + // - https://www.sqlite.org/uri.html + SQLiteURI string + // Tigris parameters for `tigris` handler. // See https://www.tigrisdata.com/docs/sdkstools/golang/getting-started/ TigrisURL string @@ -122,6 +128,8 @@ func New(config *Config) (*FerretDB, error) { PostgreSQLURL: config.PostgreSQLURL, + SQLiteURI: config.SQLiteURI, + TigrisURL: config.TigrisURL, TigrisClientID: config.TigrisClientID, TigrisClientSecret: config.TigrisClientSecret, diff --git a/go.mod b/go.mod index 0140b5ec361f..bc949cfb8f22 100644 --- a/go.mod +++ b/go.mod @@ -4,29 +4,30 @@ go 1.20 require ( github.com/AlekSi/pointer v1.2.0 - github.com/SAP/go-hdb v1.2.1 + github.com/SAP/go-hdb v1.3.0 github.com/alecthomas/kong v0.7.1 github.com/google/uuid v1.3.0 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx-zap v0.0.0-20221202020421-94b1cb2f889f github.com/jackc/pgx/v5 v5.3.1 github.com/pmezard/go-difflib v1.0.0 - github.com/prometheus/client_golang v1.15.0 - github.com/prometheus/client_model v0.3.0 - github.com/prometheus/common v0.42.0 - github.com/stretchr/testify v1.8.2 - github.com/tigrisdata/tigris-client-go v1.0.0-beta.29 - go.opentelemetry.io/otel v1.14.0 - go.opentelemetry.io/otel/trace v1.14.0 + github.com/prometheus/client_golang v1.15.1 + github.com/prometheus/client_model v0.4.0 + github.com/prometheus/common v0.43.0 + github.com/stretchr/testify v1.8.3 + github.com/tigrisdata/tigris-client-go v1.0.0 + go.opentelemetry.io/otel v1.15.1 + go.opentelemetry.io/otel/trace v1.15.1 go.uber.org/zap v1.24.0 - golang.org/x/crypto v0.8.0 // indirect; always use @latest - golang.org/x/exp v0.0.0-20230418202329-0354be287a23 - golang.org/x/net v0.9.0 - golang.org/x/sys v0.7.0 + golang.org/x/crypto v0.9.0 // indirect; always use @latest + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc + golang.org/x/net v0.10.0 + golang.org/x/sys v0.8.0 + modernc.org/sqlite v1.22.1 ) require ( - cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute v1.19.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/benbjohnson/clock v1.1.0 // indirect @@ -34,39 +35,44 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect - github.com/getkin/kin-openapi v0.114.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/golang/glog v1.0.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect - google.golang.org/grpc v1.53.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index bf6f94b9565e..3fb944df163a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -9,8 +9,8 @@ github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tS github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/SAP/go-hdb v1.2.1 h1:Bd9jS47v10rVSCFSeMLjXIJUKDbLzlfwZAKjsKb4Fg8= -github.com/SAP/go-hdb v1.2.1/go.mod h1:eWjj7L4p1HWEDbYjWasS0xp+jxSGaAwkZPK2KMyCfk0= +github.com/SAP/go-hdb v1.3.0 h1:ldNnAfI1dOHARYxagtvvJj5y6jK+joHUZ5DXRr2QgXM= +github.com/SAP/go-hdb v1.3.0/go.mod h1:Aqn8GpDM15AOEV83O9CA0Q28MOKszDCCo83FvJx5cl4= github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= @@ -23,6 +23,7 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -32,13 +33,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -47,30 +49,19 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas= -github.com/getkin/kin-openapi v0.114.0 h1:ar7QiJpDdlR+zSyPjrLf8mNnpoFP/lI90XcywMCFNe8= -github.com/getkin/kin-openapi v0.114.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -78,7 +69,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -101,19 +91,16 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 h1:I6ITHEanAwjB0FvaxmGm8pKqmCLR7QIe05ZmO4QAXMw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1/go.mod h1:gYC+WX4YJFarA2ie73G2epzt7TBWpo9pzcBnK1g0MSw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -126,28 +113,24 @@ github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk= github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jhump/protoreflect v1.14.1 h1:N88q7JkxTHWFEqReuTsYH1dPIwXxA0ITNQp7avLY10s= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -155,87 +138,78 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= -github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= -github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= +github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tigrisdata/tigris-client-go v1.0.0-beta.29 h1:Acv8E3y+Y8yg2gznUnn7Q6ykBm2jEwYUY3wOyThdqFM= -github.com/tigrisdata/tigris-client-go v1.0.0-beta.29/go.mod h1:+0L8NNdNpaw9vWwGKc+S3P75f+Sru3KktpW0oUtZSJw= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tigrisdata/tigris-client-go v1.0.0 h1:07Qw8Tm0qL15WiadP0hp4iBiRzfNSJ+GH4/ozO0nNs0= +github.com/tigrisdata/tigris-client-go v1.0.0/go.mod h1:2n6TQUdoTbzuTtakHT/ZNuK5X+I/i57BqqCcYAzG7y4= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230418202329-0354be287a23 h1:4NKENAGIctmZYLK9W+X1kDK8ObBFqOSCJM6WE7CvkJY= -golang.org/x/exp v0.0.0-20230418202329-0354be287a23/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -249,12 +223,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -274,8 +248,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -289,10 +265,13 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -307,8 +286,8 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec h1:6rwgChOSUfpzJF2/KnLgo+gMaxGpujStSkPWrbhXArU= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -317,8 +296,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -337,14 +316,37 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= +modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= diff --git a/integration/Taskfile.yml b/integration/Taskfile.yml index 85b65a8b6618..c8805ca6d9ad 100644 --- a/integration/Taskfile.yml +++ b/integration/Taskfile.yml @@ -21,6 +21,7 @@ tasks: -tags=ferretdb_testenvdata . -target-backend=ferretdb-pg -postgresql-url=postgres://username:password@127.0.0.1:5433/ferretdb?pool_min_conns=1 + # no sqlite yet - > go test -count=1 {{.RACEFLAG}} -run=TestEnvData -tags=ferretdb_testenvdata,ferretdb_tigris . diff --git a/integration/aggregate_documents_compat_test.go b/integration/aggregate_documents_compat_test.go index 80615ca43141..21d9f387564f 100644 --- a/integration/aggregate_documents_compat_test.go +++ b/integration/aggregate_documents_compat_test.go @@ -127,15 +127,16 @@ func testAggregateStagesCompatWithProviders(t *testing.T, providers shareddata.P if targetErr != nil { t.Logf("Target error: %v", targetErr) t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return } require.NoError(t, compatErr, "compat error; target returned no error") - var targetRes, compatRes []bson.D - require.NoError(t, targetCursor.All(ctx, &targetRes)) - require.NoError(t, compatCursor.All(ctx, &compatRes)) + targetRes := FetchAll(t, ctx, targetCursor) + compatRes := FetchAll(t, ctx, compatCursor) AssertEqualDocumentsSlice(t, compatRes, targetRes) @@ -205,6 +206,8 @@ func testAggregateCommandCompat(t *testing.T, testCases map[string]aggregateComm if targetErr != nil { t.Logf("Target error: %v", targetErr) t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return @@ -521,12 +524,6 @@ func TestAggregateCompatGroup(t *testing.T) { {"_id", "$add"}, }}}}, }, - "DotNotationID": { - pipeline: bson.A{bson.D{{"$group", bson.D{ - {"_id", "$v.foo"}, - }}}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2166", - }, "EmptyPath": { pipeline: bson.A{bson.D{{"$group", bson.D{ {"_id", "$"}, @@ -612,76 +609,47 @@ func TestAggregateCompatGroup(t *testing.T) { testAggregateStagesCompat(t, testCases) } -func TestAggregateCompatGroupDotNotation(t *testing.T) { +func TestAggregateCompatGroupExpressionDottedFields(t *testing.T) { t.Parallel() - // Providers Composites, ArrayAndDocuments and Mixed + // TODO Use all providers after fixing $sort problem: https://github.com/FerretDB/FerretDB/issues/2276. + // + // Currently, providers Composites, DocumentsDeeplyNested and Mixed // cannot be used due to sorting difference. // FerretDB always sorts empty array is less than null. // In compat, for `.sort()` an empty array is less than null. // In compat, for aggregation `$sort` null is less than an empty array. - // https://github.com/FerretDB/FerretDB/issues/2276 - - providers := []shareddata.Provider{ - shareddata.Scalars, - - shareddata.Doubles, - shareddata.OverflowVergeDoubles, - shareddata.SmallDoubles, - shareddata.Strings, - shareddata.Binaries, - shareddata.ObjectIDs, - shareddata.Bools, - shareddata.DateTimes, - shareddata.Nulls, - shareddata.Regexes, - shareddata.Int32s, - shareddata.Timestamps, - shareddata.Int64s, - shareddata.Unsets, - shareddata.ObjectIDKeys, - - // shareddata.Composites, - shareddata.PostgresEdgeCases, - - shareddata.DocumentsDoubles, - shareddata.DocumentsStrings, - shareddata.DocumentsDocuments, - - shareddata.ArrayStrings, - shareddata.ArrayDoubles, - shareddata.ArrayInt32s, - shareddata.ArrayRegexes, - shareddata.ArrayDocuments, - - // shareddata.Mixed, - // shareddata.ArrayAndDocuments, - } + providers := shareddata.AllProviders().Remove("Mixed", "Composites", "DocumentsDeeplyNested") testCases := map[string]aggregateStagesCompatTestCase{ - "DocDotNotation": { + "NestedInDocument": { pipeline: bson.A{bson.D{{"$group", bson.D{ {"_id", "$v.foo"}, }}}}, }, - "ArrayDotNotation": { + "DeeplyNested": { // Expect non-empty results for DocumentsDeeplyNested provider + pipeline: bson.A{bson.D{{"$group", bson.D{ + {"_id", "$v.a.b.c"}, + }}}}, + }, + "ArrayIndex": { pipeline: bson.A{bson.D{{"$group", bson.D{ {"_id", "$v.0"}, }}}}, }, - "ArrayDocDotNotation": { + "NestedInArray": { pipeline: bson.A{bson.D{{"$group", bson.D{ {"_id", "$v.0.foo"}, }}}}, }, - "NestedDotNotation": { + "NonExistentParent": { pipeline: bson.A{bson.D{{"$group", bson.D{ - {"_id", "$v.0.foo.0.bar"}, + {"_id", "$non.existent"}, }}}}, }, - "NonExistentDotNotation": { + "NonExistentChild": { pipeline: bson.A{bson.D{{"$group", bson.D{ - {"_id", "$non.existent"}, + {"_id", "$v.non.existent"}, }}}}, }, } @@ -689,56 +657,25 @@ func TestAggregateCompatGroupDotNotation(t *testing.T) { testAggregateStagesCompatWithProviders(t, providers, testCases) } -func TestAggregateCompatGroupDocDotNotation(t *testing.T) { +func TestAggregateCompatGroupExpressionDottedFieldsDocs(t *testing.T) { t.Parallel() - // Providers Composites and Mixed cannot be used due to sorting difference. - // FerretDB always sorts empty array is less than null. - // In compat, for `.sort()` an empty array is less than null. - // In compat, for aggregation `$sort` null is less than an empty array. + // TODO Merge the current function with TestAggregateCompatGroupExpressionDottedFields + // and use all providers when $sort problem is fixed: // https://github.com/FerretDB/FerretDB/issues/2276 providers := []shareddata.Provider{ - shareddata.Scalars, - - shareddata.Doubles, - shareddata.OverflowVergeDoubles, - shareddata.SmallDoubles, - shareddata.Strings, - shareddata.Binaries, - shareddata.ObjectIDs, - shareddata.Bools, - shareddata.DateTimes, - shareddata.Nulls, - shareddata.Regexes, - shareddata.Int32s, - shareddata.Timestamps, - shareddata.Int64s, - shareddata.Unsets, - shareddata.ObjectIDKeys, - - // shareddata.Composites, - shareddata.PostgresEdgeCases, - - shareddata.DocumentsDoubles, - shareddata.DocumentsStrings, - shareddata.DocumentsDocuments, - - shareddata.ArrayStrings, - shareddata.ArrayDoubles, - shareddata.ArrayInt32s, - shareddata.ArrayRegexes, - shareddata.ArrayDocuments, - - // shareddata.Mixed, - shareddata.ArrayAndDocuments, + shareddata.DocumentsDeeplyNested, } testCases := map[string]aggregateStagesCompatTestCase{ - "DocDotNotation": { + "DeeplyNested": { // Expect non-empty results for DocumentsDeeplyNested provider pipeline: bson.A{bson.D{{"$group", bson.D{ - {"_id", "$v.foo"}, + {"_id", "$v.a.b.c"}, }}}}, + skip: "https://github.com/FerretDB/FerretDB/issues/2276", + // compat results [ { _id: null }, { _id: 12 }, { _id: { d: 123 } }, { _id: [ 1, 2 ] } ] + // target results [ { _id: null }, { _id: [ 1, 2 ] }, { _id: 12 }, { _id: { d: 123 } } ] }, } @@ -925,7 +862,9 @@ func TestAggregateCompatGroupSum(t *testing.T) { Remove("ArrayAndDocuments"). // TODO: handle $sum of doubles near max precision. // https://github.com/FerretDB/FerretDB/issues/2300 - Remove("Doubles") + Remove("Doubles"). + // TODO: https://github.com/FerretDB/FerretDB/issues/2616 + Remove("ArrayDocuments") testCases := map[string]aggregateStagesCompatTestCase{ "GroupNullID": { @@ -1188,13 +1127,19 @@ func TestAggregateCompatSort(t *testing.T) { {"_id", 1}, // sort by _id when v is the same. }}}}, }, - - "DotNotation": { + "AscendingValueDescendingID": { pipeline: bson.A{bson.D{{"$sort", bson.D{ - {"v.foo", 1}, - {"_id", 1}, // sort by _id when v is the same. + {"v", 1}, + {"_id", -1}, + }}}}, + }, + "DescendingValueDescendingID": { + pipeline: bson.A{bson.D{{"$sort", bson.D{ + {"v", -1}, + {"_id", -1}, }}}}, }, + "DotNotationIndex": { pipeline: bson.A{bson.D{{"$sort", bson.D{ {"v.0", 1}, @@ -1235,6 +1180,25 @@ func TestAggregateCompatSort(t *testing.T) { testAggregateStagesCompat(t, testCases) } +func TestAggregateCompatSortDotNotation(t *testing.T) { + t.Parallel() + + providers := shareddata.AllProviders(). + // TODO: https://github.com/FerretDB/FerretDB/issues/2617 + Remove("ArrayDocuments") + + testCases := map[string]aggregateStagesCompatTestCase{ + "DotNotation": { + pipeline: bson.A{bson.D{{"$sort", bson.D{ + {"v.foo", 1}, + {"_id", 1}, // sort by _id when v is the same. + }}}}, + }, + } + + testAggregateStagesCompatWithProviders(t, providers, testCases) +} + func TestAggregateCompatUnwind(t *testing.T) { t.Parallel() @@ -1542,6 +1506,58 @@ func TestAggregateCompatProject(t *testing.T) { bson.D{{"$project", bson.D{{"_id", false}, {"foo", primitive.Regex{Pattern: "^fo"}}}}}, }, }, + "DotNotationInclude": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.foo", true}, + }}}, + }, + }, + "DotNotationIncludeTwo": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.foo", true}, + {"v.array", true}, + }}}, + }, + }, + "DotNotationExclude": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.foo", false}, + }}}, + }, + }, + "DotNotationExcludeTwo": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.foo", false}, + {"v.array", false}, + }}}, + }, + }, + "DotNotationExcludeSecondLevel": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.array.42", false}, + }}}, + }, + }, + "DotNotationIncludeExclude": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{ + {"_id", true}, + {"v.foo", true}, + {"v.array.42", false}, + }}}, + }, + resultType: emptyResult, + }, } testAggregateStagesCompat(t, testCases) diff --git a/integration/aggregate_documents_test.go b/integration/aggregate_documents_test.go new file mode 100644 index 000000000000..d5a55f7ccbdc --- /dev/null +++ b/integration/aggregate_documents_test.go @@ -0,0 +1,76 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + + "github.com/FerretDB/FerretDB/integration/setup" +) + +func TestAggregateProjectErrors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + expectedErr *mongo.CommandError + altMessage string + skip string + pipeline bson.A + }{ + "EmptyPipeline": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{}}}, + }, + expectedErr: &mongo.CommandError{ + Code: 51272, + Name: "Location51272", + Message: "Invalid $project :: caused by :: projection specification must have at least one field", + }, + }, + "EmptyProjectionField": { + pipeline: bson.A{ + bson.D{{"$project", bson.D{{"v", bson.D{}}}}}, + }, + expectedErr: &mongo.CommandError{ + Code: 51270, + Name: "Location51270", + Message: "Invalid $project :: caused by :: An empty sub-projection is not a valid value. Found empty object at path", + }, + skip: "https://github.com/FerretDB/FerretDB/issues/2633", + }, + } { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + if tc.skip != "" { + t.Skip(tc.skip) + } + + t.Parallel() + ctx, collection := setup.Setup(t) + + _, err := collection.Aggregate(ctx, tc.pipeline) + + if tc.expectedErr != nil { + AssertEqualAltCommandError(t, *tc.expectedErr, tc.altMessage, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/integration/aggregate_stats_compat_test.go b/integration/aggregate_stats_compat_test.go index 7cee60a72cab..dae55a1975ca 100644 --- a/integration/aggregate_stats_compat_test.go +++ b/integration/aggregate_stats_compat_test.go @@ -103,15 +103,16 @@ func TestAggregateCompatCollStats(t *testing.T) { if targetErr != nil { t.Logf("Target error: %v", targetErr) t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return } require.NoError(t, compatErr, "compat error; target returned no error") - var targetRes, compatRes []bson.D - require.NoError(t, targetCursor.All(ctx, &targetRes)) - require.NoError(t, compatCursor.All(ctx, &compatRes)) + targetRes := FetchAll(t, ctx, targetCursor) + compatRes := FetchAll(t, ctx, compatCursor) // $collStats returns one document per shard. require.Equal(t, 1, len(compatRes)) diff --git a/integration/commands_administration_compat_test.go b/integration/commands_administration_compat_test.go index 0e59de177ec2..dcc7fbbaa11f 100644 --- a/integration/commands_administration_compat_test.go +++ b/integration/commands_administration_compat_test.go @@ -21,7 +21,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" "github.com/FerretDB/FerretDB/integration/setup" "github.com/FerretDB/FerretDB/integration/shareddata" @@ -41,7 +40,6 @@ func TestCommandsAdministrationCompatCollStatsWithScale(t *testing.T) { for name, tc := range map[string]struct { //nolint:vet // for readability scale any resultType compatTestCaseResultType - altMessage string }{ "scaleOne": {scale: int32(1)}, "scaleBig": {scale: int64(1000)}, @@ -55,12 +53,10 @@ func TestCommandsAdministrationCompatCollStatsWithScale(t *testing.T) { "scaleString": { scale: "1", resultType: emptyResult, - altMessage: `BSON field 'collStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double]'`, }, "scaleObject": { scale: bson.D{{"a", 1}}, resultType: emptyResult, - altMessage: `BSON field 'collStats.scale' is the wrong type 'object', expected types '[long, int, decimal, double]'`, }, "scaleNull": {scale: nil}, } { @@ -79,26 +75,16 @@ func TestCommandsAdministrationCompatCollStatsWithScale(t *testing.T) { compatCommand := bson.D{{"collStats", compatCollection.Name()}, {"scale", tc.scale}} compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes) - if tc.resultType == emptyResult { - require.Error(t, compatErr) + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altMessage != "" { - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr) - } else { - assert.Equal(t, compatErr, targetErr) - } + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } - - require.NoError(t, compatErr) - require.NoError(t, targetErr) + require.NoError(t, compatErr, "compat error; target returned no error") targetDoc := ConvertDocument(t, targetRes) compatDoc := ConvertDocument(t, compatRes) @@ -124,7 +110,6 @@ func TestCommandsAdministrationCompatDBStatsWithScale(t *testing.T) { for name, tc := range map[string]struct { //nolint:vet // for readability scale any resultType compatTestCaseResultType - altMessage string }{ "scaleOne": {scale: int32(1)}, "scaleBig": {scale: int64(1000)}, @@ -146,26 +131,16 @@ func TestCommandsAdministrationCompatDBStatsWithScale(t *testing.T) { compatCommand := bson.D{{"dbStats", int32(1)}, {"scale", tc.scale}} compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes) - if tc.resultType == emptyResult { - require.Error(t, compatErr) + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altMessage != "" { - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr) - } else { - assert.Equal(t, compatErr, targetErr) - } + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } - - require.NoError(t, compatErr) - require.NoError(t, targetErr) + require.NoError(t, compatErr, "compat error; target returned no error") targetDoc := ConvertDocument(t, targetRes) compatDoc := ConvertDocument(t, compatRes) diff --git a/integration/commands_administration_test.go b/integration/commands_administration_test.go index d9d5c16dec06..e856a20d9f7a 100644 --- a/integration/commands_administration_test.go +++ b/integration/commands_administration_test.go @@ -586,12 +586,12 @@ func TestCommandsAdministrationCollStatsEmpty(t *testing.T) { // Expected result is to have empty statistics (neither the database nor the collection exists) doc := ConvertDocument(t, actual) assert.Equal(t, collection.Database().Name()+"."+collection.Name(), must.NotFail(doc.Get("ns"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("size"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("count"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("storageSize"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("nindexes"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("totalIndexSize"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("totalSize"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("size"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("count"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("storageSize"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("nindexes"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("totalIndexSize"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("totalSize"))) assert.Equal(t, int32(1), must.NotFail(doc.Get("scaleFactor"))) assert.Equal(t, float64(1), must.NotFail(doc.Get("ok"))) } @@ -607,9 +607,11 @@ func TestCommandsAdministrationCollStats(t *testing.T) { doc := ConvertDocument(t, actual) + // Values are returned as "numbers" that could be int32 or int64. + // FerretDB always returns int64 for simplicity. // TODO Set better expected results https://github.com/FerretDB/FerretDB/issues/1771 assert.Equal(t, collection.Database().Name()+"."+collection.Name(), must.NotFail(doc.Get("ns"))) - assert.Equal(t, int32(6), must.NotFail(doc.Get("count"))) // // Number of documents in DocumentsStrings + assert.EqualValues(t, 6, must.NotFail(doc.Get("count"))) // // Number of documents in DocumentsStrings assert.Equal(t, int32(1), must.NotFail(doc.Get("scaleFactor"))) assert.Equal(t, float64(1), must.NotFail(doc.Get("ok"))) @@ -625,12 +627,14 @@ func TestCommandsAdministrationCollStats(t *testing.T) { return } - assert.InDelta(t, int32(40_000), must.NotFail(doc.Get("size")), 39_900) - assert.InDelta(t, int32(2_400), must.NotFail(doc.Get("avgObjSize")), 2_370) - assert.InDelta(t, int32(40_000), must.NotFail(doc.Get("storageSize")), 39_900) - assert.Equal(t, int32(1), must.NotFail(doc.Get("nindexes"))) - assert.InDelta(t, int32(12_000), must.NotFail(doc.Get("totalIndexSize")), 11_000) - assert.InDelta(t, int32(32_000), must.NotFail(doc.Get("totalSize")), 30_000) + // Values are returned as "numbers" that could be int32 or int64. + // FerretDB always returns int64 for simplicity. + assert.InDelta(t, 40_000, must.NotFail(doc.Get("size")), 39_900) + assert.InDelta(t, 2_400, must.NotFail(doc.Get("avgObjSize")), 2_370) + assert.InDelta(t, 40_000, must.NotFail(doc.Get("storageSize")), 39_900) + assert.EqualValues(t, 1, must.NotFail(doc.Get("nindexes"))) + assert.InDelta(t, 12_000, must.NotFail(doc.Get("totalIndexSize")), 11_000) + assert.InDelta(t, 32_000, must.NotFail(doc.Get("totalSize")), 30_000) } func TestCommandsAdministrationCollStatsWithScale(t *testing.T) { @@ -645,7 +649,7 @@ func TestCommandsAdministrationCollStatsWithScale(t *testing.T) { // TODO Set better expected results https://github.com/FerretDB/FerretDB/issues/1771 doc := ConvertDocument(t, actual) assert.Equal(t, collection.Database().Name()+"."+collection.Name(), must.NotFail(doc.Get("ns"))) - assert.Equal(t, int32(6), must.NotFail(doc.Get("count"))) // Number of documents in DocumentsStrings + assert.EqualValues(t, 6, must.NotFail(doc.Get("count"))) // Number of documents in DocumentsStrings assert.Equal(t, int32(1000), must.NotFail(doc.Get("scaleFactor"))) assert.Equal(t, float64(1), must.NotFail(doc.Get("ok"))) @@ -661,12 +665,12 @@ func TestCommandsAdministrationCollStatsWithScale(t *testing.T) { return } - assert.InDelta(t, int32(16), must.NotFail(doc.Get("size")), 16) - assert.InDelta(t, int32(2_400), must.NotFail(doc.Get("avgObjSize")), 2_370) - assert.InDelta(t, int32(24), must.NotFail(doc.Get("storageSize")), 24) - assert.Equal(t, int32(1), must.NotFail(doc.Get("nindexes"))) - assert.InDelta(t, int32(8), must.NotFail(doc.Get("totalIndexSize")), 8) - assert.InDelta(t, int32(24), must.NotFail(doc.Get("totalSize")), 24) + assert.InDelta(t, 16, must.NotFail(doc.Get("size")), 16) + assert.InDelta(t, 2_400, must.NotFail(doc.Get("avgObjSize")), 2_370) + assert.InDelta(t, 24, must.NotFail(doc.Get("storageSize")), 24) + assert.EqualValues(t, 1, must.NotFail(doc.Get("nindexes"))) + assert.InDelta(t, 8, must.NotFail(doc.Get("totalIndexSize")), 8) + assert.InDelta(t, 24, must.NotFail(doc.Get("totalSize")), 24) } func TestCommandsAdministrationDataSize(t *testing.T) { @@ -683,9 +687,9 @@ func TestCommandsAdministrationDataSize(t *testing.T) { doc := ConvertDocument(t, actual) assert.Equal(t, float64(1), must.NotFail(doc.Get("ok"))) - assert.InDelta(t, float64(24_576), must.NotFail(doc.Get("size")), 24_576) - assert.InDelta(t, float64(4), must.NotFail(doc.Get("numObjects")), 4) // TODO https://github.com/FerretDB/FerretDB/issues/727 - assert.InDelta(t, float64(200), must.NotFail(doc.Get("millis")), 200) + assert.InDelta(t, 24_576, must.NotFail(doc.Get("size")), 24_576) + assert.InDelta(t, 4, must.NotFail(doc.Get("numObjects")), 4) // TODO https://github.com/FerretDB/FerretDB/issues/727 + assert.InDelta(t, 200, must.NotFail(doc.Get("millis")), 200) }) t.Run("NonExistent", func(t *testing.T) { @@ -699,9 +703,9 @@ func TestCommandsAdministrationDataSize(t *testing.T) { doc := ConvertDocument(t, actual) assert.Equal(t, float64(1), must.NotFail(doc.Get("ok"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("size"))) - assert.Equal(t, int32(0), must.NotFail(doc.Get("numObjects"))) - assert.InDelta(t, float64(159), must.NotFail(doc.Get("millis")), 159) + assert.EqualValues(t, 0, must.NotFail(doc.Get("size"))) + assert.EqualValues(t, 0, must.NotFail(doc.Get("numObjects"))) + assert.InDelta(t, 159, must.NotFail(doc.Get("millis")), 159) }) } @@ -714,11 +718,13 @@ func TestCommandsAdministrationDBStats(t *testing.T) { err := collection.Database().RunCommand(ctx, command).Decode(&actual) require.NoError(t, err) + // Values are returned as "numbers" that could be int32 or int64. + // FerretDB always returns int64 for simplicity. // TODO Set better expected results https://github.com/FerretDB/FerretDB/issues/1771 doc := ConvertDocument(t, actual) assert.Equal(t, collection.Database().Name(), doc.Remove("db")) - assert.Equal(t, int32(1), doc.Remove("collections")) - assert.Equal(t, int32(6), doc.Remove("objects")) // Number of documents in DocumentsStrings + assert.EqualValues(t, 1, doc.Remove("collections")) + assert.EqualValues(t, 6, doc.Remove("objects")) // Number of documents in DocumentsStrings assert.Equal(t, float64(1), doc.Remove("scaleFactor")) assert.Equal(t, float64(1), doc.Remove("ok")) @@ -732,10 +738,10 @@ func TestCommandsAdministrationDBStats(t *testing.T) { return } - assert.InDelta(t, int32(37_500), doc.Remove("avgObjSize"), 37_460) - assert.InDelta(t, int32(37_500), doc.Remove("dataSize"), 37_450) - assert.InDelta(t, int32(37_500), doc.Remove("storageSize"), 37_450) - assert.InDelta(t, int32(49_152), doc.Remove("totalSize"), 49_100) + assert.InDelta(t, 37_500, doc.Remove("avgObjSize"), 37_460) + assert.InDelta(t, 37_500, doc.Remove("dataSize"), 37_450) + assert.InDelta(t, 37_500, doc.Remove("storageSize"), 37_450) + assert.InDelta(t, 49_152, doc.Remove("totalSize"), 49_100) // TODO assert.Empty(t, doc.Keys()) // https://github.com/FerretDB/FerretDB/issues/727 @@ -756,9 +762,9 @@ func TestCommandsAdministrationDBStatsEmpty(t *testing.T) { assert.Equal(t, collection.Database().Name(), doc.Remove("db")) assert.EqualValues(t, float64(1), doc.Remove("scaleFactor")) // TODO use assert.Equal https://github.com/FerretDB/FerretDB/issues/727 - assert.InDelta(t, int32(1), doc.Remove("collections"), 1) - assert.InDelta(t, float64(35500), doc.Remove("dataSize"), 35500) - assert.InDelta(t, float64(16384), doc.Remove("totalSize"), 16384) + assert.InDelta(t, 1, doc.Remove("collections"), 1) + assert.InDelta(t, 35500, doc.Remove("dataSize"), 35500) + assert.InDelta(t, 16384, doc.Remove("totalSize"), 16384) // TODO assert.Empty(t, doc.Keys()) // https://github.com/FerretDB/FerretDB/issues/727 @@ -779,9 +785,9 @@ func TestCommandsAdministrationDBStatsWithScale(t *testing.T) { assert.Equal(t, collection.Database().Name(), doc.Remove("db")) assert.Equal(t, float64(1000), doc.Remove("scaleFactor")) - assert.InDelta(t, int32(1), doc.Remove("collections"), 1) - assert.InDelta(t, float64(35500), doc.Remove("dataSize"), 35500) - assert.InDelta(t, float64(16384), doc.Remove("totalSize"), 16384) + assert.InDelta(t, 1, doc.Remove("collections"), 1) + assert.InDelta(t, 35500, doc.Remove("dataSize"), 35500) + assert.InDelta(t, 16384, doc.Remove("totalSize"), 16384) // TODO assert.Empty(t, doc.Keys()) // https://github.com/FerretDB/FerretDB/issues/727 @@ -802,9 +808,9 @@ func TestCommandsAdministrationDBStatsEmptyWithScale(t *testing.T) { assert.Equal(t, collection.Database().Name(), doc.Remove("db")) assert.EqualValues(t, float64(1000), doc.Remove("scaleFactor")) // TODO use assert.Equal https://github.com/FerretDB/FerretDB/issues/727 - assert.InDelta(t, int32(1), doc.Remove("collections"), 1) - assert.InDelta(t, float64(35500), doc.Remove("dataSize"), 35500) - assert.InDelta(t, float64(16384), doc.Remove("totalSize"), 16384) + assert.InDelta(t, 1, doc.Remove("collections"), 1) + assert.InDelta(t, 35500, doc.Remove("dataSize"), 35500) + assert.InDelta(t, 16384, doc.Remove("totalSize"), 16384) // TODO assert.Empty(t, doc.Keys()) // https://github.com/FerretDB/FerretDB/issues/727 @@ -844,8 +850,8 @@ func TestCommandsAdministrationServerStatus(t *testing.T) { assert.True(t, ok) // catalogStats is calculated across all the databases, so there could be quite a lot of collections here. - assert.InDelta(t, float64(632), must.NotFail(catalogStats.Get("collections")), 632) - assert.InDelta(t, float64(3), must.NotFail(catalogStats.Get("internalCollections")), 3) + assert.InDelta(t, 632, must.NotFail(catalogStats.Get("collections")), 632) + assert.InDelta(t, 3, must.NotFail(catalogStats.Get("internalCollections")), 3) assert.Equal(t, int32(0), must.NotFail(catalogStats.Get("capped"))) assert.Equal(t, int32(0), must.NotFail(catalogStats.Get("timeseries"))) diff --git a/integration/commands_diagnostic_test.go b/integration/commands_diagnostic_test.go index ecaea501a875..a46f4cc3176b 100644 --- a/integration/commands_diagnostic_test.go +++ b/integration/commands_diagnostic_test.go @@ -185,8 +185,8 @@ func TestCommandsDiagnosticGetLog(t *testing.T) { var actual bson.D err := collection.Database().RunCommand(ctx, tc.command).Decode(&actual) - if err != nil { - AssertEqualAltError(t, *tc.err, tc.alt, err) + if tc.err != nil { + AssertEqualAltCommandError(t, *tc.err, tc.alt, err) return } require.NoError(t, err) diff --git a/integration/count_command_compat_test.go b/integration/count_command_compat_test.go new file mode 100644 index 000000000000..3673d3bda5a2 --- /dev/null +++ b/integration/count_command_compat_test.go @@ -0,0 +1,211 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/FerretDB/FerretDB/integration/setup" +) + +type countCompatCommandTestCase struct { + skipForTigris string + skip string + collectionName any + command bson.D +} + +// testQueryCompat tests query compatibility test cases. +func testCountCompatCommand(t *testing.T, testCases map[string]countCompatCommandTestCase) { + t.Helper() + + // Use shared setup because count queries can't modify data. + // TODO Use read-only user. https://github.com/FerretDB/FerretDB/issues/1025 + ctx, targetCollections, compatCollections := setup.SetupCompat(t) + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Helper() + + if tc.skipForTigris != "" { + setup.SkipForTigrisWithReason(t, tc.skipForTigris) + } + + if tc.skip != "" { + t.Skip(tc.skip) + } + + t.Parallel() + + for i := range targetCollections { + targetCollection := targetCollections[i] + compatCollection := compatCollections[i] + t.Run(targetCollection.Name(), func(t *testing.T) { + t.Helper() + + targetCollectionName := tc.collectionName + compatCollectionName := tc.collectionName + if tc.collectionName == nil { + targetCollectionName = targetCollection.Name() + compatCollectionName = compatCollection.Name() + } + + targetCommand := append( + bson.D{ + {"count", targetCollectionName}, + }, + tc.command..., + ) + compatCommand := append( + bson.D{ + {"count", compatCollectionName}, + }, + tc.command..., + ) + + targetResult := targetCollection.Database().RunCommand(ctx, targetCommand) + compatResult := compatCollection.Database().RunCommand(ctx, compatCommand) + + targetErr := targetResult.Err() + compatErr := compatResult.Err() + + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) + + return + } + require.NoError(t, compatErr, "compat error; target returned no error") + + var targetRes, compatRes bson.D + require.NoError(t, targetResult.Decode(&targetRes)) + require.NoError(t, compatResult.Decode(&compatRes)) + + AssertEqualDocuments(t, compatRes, targetRes) + + targetCount := targetRes.Map()["n"].(int32) + compatCount := compatRes.Map()["n"].(int32) + + require.Equal(t, compatCount, targetCount) + }) + } + }) + } +} + +func TestCountCompatCommandErrors(t *testing.T) { + t.Parallel() + + testCases := map[string]countCompatCommandTestCase{ + "Pass": { + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionDocument": { + collectionName: bson.D{}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionArray": { + collectionName: primitive.A{}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionDouble": { + collectionName: 3.14, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionBinary": { + collectionName: primitive.Binary{}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionObjectID": { + collectionName: primitive.ObjectID{}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionBool": { + collectionName: true, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionDate": { + collectionName: time.Now(), + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionNull": { + collectionName: nil, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionRegex": { + collectionName: primitive.Regex{Pattern: "/foo/"}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionInt": { + collectionName: int32(42), + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionTimestamp": { + collectionName: primitive.Timestamp{}, + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "CollectionLong": { + collectionName: int64(42), + command: bson.D{ + {"query", bson.D{}}, + }, + }, + "QueryArray": { + command: bson.D{ + {"query", bson.A{}}, + }, + }, + "QueryInt": { + command: bson.D{ + {"query", int32(42)}, + }, + }, + } + + testCountCompatCommand(t, testCases) +} diff --git a/integration/count_compat_test.go b/integration/count_compat_test.go index d828bd50a254..90ddaf69c1d9 100644 --- a/integration/count_compat_test.go +++ b/integration/count_compat_test.go @@ -22,7 +22,6 @@ import ( "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" "github.com/FerretDB/FerretDB/integration/setup" "github.com/FerretDB/FerretDB/integration/shareddata" @@ -37,7 +36,6 @@ type countCompatTestCase struct { optSkip any // optional, skip option for the query, defaults to nil limit int64 // optional, limit option for the query, defaults to 0 - altMessage string // optional, alternative error message to use in the assertion resultType compatTestCaseResultType // defaults to nonEmptyResult } @@ -88,17 +86,10 @@ func testCountCompat(t *testing.T, testCases map[string]countCompatTestCase) { if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altMessage != "" { - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr) - } else { - assert.Equal(t, compatErr, targetErr) - } + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } @@ -223,7 +214,6 @@ func TestCountCompat(t *testing.T) { filter: bson.D{}, optSkip: "foo", resultType: emptyResult, - altMessage: `BSON field 'count.skip' is the wrong type 'string', expected types '[long, int, decimal, double]'`, }, } diff --git a/integration/count_test.go b/integration/count_test.go deleted file mode 100644 index 594b11fc414b..000000000000 --- a/integration/count_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package integration - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - - "github.com/FerretDB/FerretDB/integration/setup" -) - -func TestQueryBadCountType(t *testing.T) { - t.Parallel() - s := setup.SetupWithOpts(t, nil) - - ctx, collection := s.Ctx, s.Collection - - for name, tc := range map[string]struct { - value any - err string - }{ - "Document": { - value: bson.D{}, - err: "object", - }, - "Array": { - value: primitive.A{}, - err: "array", - }, - "Double": { - value: 3.14, - err: "double", - }, - "Binary": { - value: primitive.Binary{}, - err: "binData", - }, - "ObjectID": { - value: primitive.ObjectID{}, - err: "objectId", - }, - "Bool": { - value: true, - err: "bool", - }, - "Date": { - value: time.Now(), - err: "date", - }, - "Null": { - value: nil, - err: "null", - }, - "Regex": { - value: primitive.Regex{Pattern: "/foo/"}, - err: "regex", - }, - "Int": { - value: int32(42), - err: "int", - }, - "Timestamp": { - value: primitive.Timestamp{}, - err: "timestamp", - }, - "Long": { - value: int64(42), - err: "long", - }, - } { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - var actual bson.D - cmd := bson.D{ - {"count", tc.value}, - {"query", bson.D{{"v", "some"}}}, - } - err := collection.Database().RunCommand(ctx, cmd).Decode(&actual) - require.Error(t, err) - - expected := mongo.CommandError{ - Code: 73, - Name: "InvalidNamespace", - Message: "collection name has invalid type " + tc.err, - } - AssertEqualError(t, expected, err) - }) - } -} diff --git a/integration/delete_test.go b/integration/delete_test.go index b6b4948202ac..bebcae304ab7 100644 --- a/integration/delete_test.go +++ b/integration/delete_test.go @@ -58,14 +58,23 @@ func TestDeleteSimple(t *testing.T) { } } -func TestDeleteLimitErrors(t *testing.T) { +func TestDeleteErrors(t *testing.T) { t.Parallel() for name, tc := range map[string]struct { deletes bson.A expectedErr *mongo.CommandError skip string + altMessage string }{ + "QueryNotSet": { + deletes: bson.A{bson.D{}}, + expectedErr: &mongo.CommandError{ + Code: int32(commonerrors.ErrMissingField), + Name: commonerrors.ErrMissingField.String(), + Message: "BSON field 'delete.deletes.q' is missing but a required field", + }, + }, "NotSet": { deletes: bson.A{bson.D{{"q", bson.D{{"v", "foo"}}}}}, expectedErr: &mongo.CommandError{ @@ -88,6 +97,7 @@ func TestDeleteLimitErrors(t *testing.T) { Name: commonerrors.ErrFailedToParse.String(), Message: "The limit field in delete objects must be 0 or 1. Got 42.13", }, + altMessage: "The 'delete.deletes.limit' field must be 0 or 1. Got 42.13", }, "InvalidInt": { deletes: bson.A{bson.D{{"q", bson.D{{"v", "foo"}}}, {"limit", 100}}}, @@ -96,6 +106,7 @@ func TestDeleteLimitErrors(t *testing.T) { Name: commonerrors.ErrFailedToParse.String(), Message: "The limit field in delete objects must be 0 or 1. Got 100", }, + altMessage: "The 'delete.deletes.limit' field must be 0 or 1. Got 100", }, } { name, tc := name, tc @@ -113,10 +124,10 @@ func TestDeleteLimitErrors(t *testing.T) { }) if tc.expectedErr != nil { - AssertEqualError(t, *tc.expectedErr, res.Err()) + AssertEqualAltCommandError(t, *tc.expectedErr, tc.altMessage, res.Err()) return } - assert.Equal(t, nil, res.Err()) + require.NoError(t, res.Err()) }) } } diff --git a/integration/explain_compat_test.go b/integration/explain_compat_test.go index 16754ce2c902..d6a203ef3774 100644 --- a/integration/explain_compat_test.go +++ b/integration/explain_compat_test.go @@ -93,6 +93,8 @@ func testExplainCompatError(t *testing.T, testCases map[string]explainCompatTest if targetErr != nil { t.Logf("Target error: %v", targetErr) t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return diff --git a/integration/findandmodify_compat_test.go b/integration/findandmodify_compat_test.go index 7977f9b61e8c..a10f806f643d 100644 --- a/integration/findandmodify_compat_test.go +++ b/integration/findandmodify_compat_test.go @@ -20,7 +20,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "github.com/FerretDB/FerretDB/integration/setup" @@ -214,7 +213,6 @@ func TestFindAndModifyCompatUpdate(t *testing.T) { {"$min", bson.D{{"v.foo", "val"}}}, }}, }, - altMessage: "Updating the path 'v' would create a conflict at 'v'", }, "ConflictKeyPrefix": { command: bson.D{ @@ -224,7 +222,6 @@ func TestFindAndModifyCompatUpdate(t *testing.T) { {"$min", bson.D{{"v", "val"}}}, }}, }, - altMessage: "Updating the path 'v.foo' would create a conflict at 'v.foo'", }, } @@ -263,7 +260,6 @@ func TestFindAndModifyCompatUpdateSet(t *testing.T) { command: bson.D{ {"update", bson.D{{"$set", bson.D{{"_id", "non-existent"}}}}}, }, - altMessage: "Performing an update on the path '_id' would modify the immutable field '_id'", }, } @@ -562,7 +558,6 @@ func TestFindAndModifyCompatUpsertSet(t *testing.T) { {"upsert", true}, {"update", bson.D{{"$set", bson.D{{"_id", "double"}}}}}, }, - altMessage: "Performing an update on the path '_id' would modify the immutable field '_id'", }, } @@ -645,8 +640,7 @@ func TestFindAndModifyCompatRemove(t *testing.T) { // findAndModifyCompatTestCase describes findAndModify compatibility test case. type findAndModifyCompatTestCase struct { - command bson.D - altMessage string + command bson.D skip string // skips test if non-empty skipForTigris string // skips test for Tigris if non-empty @@ -699,22 +693,15 @@ func testFindAndModifyCompat(t *testing.T, testCases map[string]findAndModifyCom if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) + t.Logf("Compat error: %v", compatErr) - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altMessage != "" { - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr) - } else { - assert.Equal(t, compatErr, targetErr) - } + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } + require.NoError(t, compatErr, "compat error; target returned no error") - require.Equal(t, compatErr, targetErr) AssertEqualDocuments(t, compatMod, targetMod) // To make sure that the results of modification are equal, @@ -739,9 +726,8 @@ func testFindAndModifyCompat(t *testing.T, testCases map[string]findAndModifyCom } require.NoError(t, compatErr, "compat error; target returned no error") - var targetRes, compatRes []bson.D - require.NoError(t, targetCursor.All(ctx, &targetRes)) - require.NoError(t, compatCursor.All(ctx, &compatRes)) + targetRes := FetchAll(t, ctx, targetCursor) + compatRes := FetchAll(t, ctx, compatCursor) t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) t.Logf("Target (actual) IDs: %v", CollectIDs(t, targetRes)) diff --git a/integration/findandmodify_test.go b/integration/findandmodify_test.go index 1b1f5c35a85f..5e586f6417be 100644 --- a/integration/findandmodify_test.go +++ b/integration/findandmodify_test.go @@ -15,6 +15,7 @@ package integration import ( + "math" "testing" "github.com/stretchr/testify/assert" @@ -57,8 +58,10 @@ func TestFindAndModifyEmptyCollectionName(t *testing.T) { func TestFindAndModifyErrors(t *testing.T) { t.Parallel() - for name, tc := range map[string]struct { - command bson.D + for name, tc := range map[string]struct { //nolint:vet // it is used for test only + command bson.D + provider shareddata.Provider // optional, default uses shareddata.ArrayDocuments + err *mongo.CommandError altMessage string }{ @@ -84,7 +87,7 @@ func TestFindAndModifyErrors(t *testing.T) { Name: "TypeMismatch", Message: "BSON field 'findAndModify.sort' is the wrong type 'string', expected type 'object'", }, - altMessage: "BSON field 'sort' is the wrong type 'string', expected type 'object'", + altMessage: "BSON field 'findAndModify.sort' is the wrong type 'string', expected type 'object'", }, "BadRemoveType": { command: bson.D{ @@ -96,7 +99,7 @@ func TestFindAndModifyErrors(t *testing.T) { Name: "TypeMismatch", Message: "BSON field 'findAndModify.remove' is the wrong type 'string', expected types '[bool, long, int, decimal, double']", }, - altMessage: "BSON field 'remove' is the wrong type 'string', expected types '[bool, long, int, decimal, double]'", + altMessage: "BSON field 'findAndModify.remove' is the wrong type 'string', expected type 'bool'", }, "BadNewType": { command: bson.D{ @@ -120,13 +123,280 @@ func TestFindAndModifyErrors(t *testing.T) { Name: "TypeMismatch", Message: "BSON field 'findAndModify.upsert' is the wrong type 'string', expected types '[bool, long, int, decimal, double']", }, - altMessage: "BSON field 'upsert' is the wrong type 'string', expected types '[bool, long, int, decimal, double]'", + altMessage: "BSON field 'findAndModify.upsert' is the wrong type 'string', expected type 'bool'", + }, + "SetUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$set", bson.D{{"v.foo", "foo"}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: Cannot create field 'foo' " + + "in element {v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + altMessage: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + "RenameEmptyFieldName": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$rename", bson.D{{"", "v"}}}}}, + }, + err: &mongo.CommandError{ + Code: 56, + Name: "EmptyFieldName", + Message: "An empty update path is not valid.", + }, + }, + "RenameEmptyPath": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$rename", bson.D{{"v.", "v"}}}}}, + }, + err: &mongo.CommandError{ + Code: 56, + Name: "EmptyFieldName", + Message: "The update path 'v.' contains an empty field name, which is not allowed.", + }, + }, + "RenameArrayInvalidIndex": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$rename", bson.D{{"v.-1", "f"}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "cannot use the part (v of v.-1) to traverse the element " + + "({v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]})", + }, + altMessage: "cannot use path 'v.-1' to traverse the document", + }, + "RenameUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$rename", bson.D{{"v.0.foo.0.bar.z", "f"}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "cannot use the part (bar of v.0.foo.0.bar.z) to traverse the element ({bar: \"hello\"})", + }, + altMessage: "types.getByPath: can't access string by path \"z\"", + }, + "IncTypeMismatch": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$inc", bson.D{{"v", "string"}}}}}, + }, + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "Cannot increment with non-numeric argument: {v: \"string\"}", + }, + }, + "IncUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$inc", bson.D{{"v.foo", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + altMessage: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + "IncNonNumeric": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$inc", bson.D{{"v.0.foo.0.bar", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot apply $inc to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + altMessage: "Cannot apply $inc to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + "IncInt64BadValue": { + command: bson.D{ + {"query", bson.D{{"_id", "int64-max"}}}, + {"update", bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}}}, + }, + err: &mongo.CommandError{ + Code: 2, + Name: "BadValue", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Failed to apply $inc operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + provider: shareddata.Int64s, + altMessage: "Failed to apply $inc operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + "IncInt32BadValue": { + command: bson.D{ + {"query", bson.D{{"_id", "int32"}}}, + {"update", bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}}}, + }, + err: &mongo.CommandError{ + Code: 2, + Name: "BadValue", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Failed to apply $inc operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + provider: shareddata.Int32s, + altMessage: "Failed to apply $inc operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + "MaxUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$max", bson.D{{"v.foo", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + altMessage: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + "MinUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$min", bson.D{{"v.foo", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + altMessage: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + "MulTypeMismatch": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$mul", bson.D{{"v", "string"}}}}}, + }, + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "Cannot multiply with non-numeric argument: {v: \"string\"}", + }, + }, + "MulTypeMismatchNonExistent": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$mul", bson.D{{"non-existent", "string"}}}}}, + }, + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "Cannot multiply with non-numeric argument: {non-existent: \"string\"}", + }, + }, + "MulUnsuitableValue": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$mul", bson.D{{"v.foo", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 28, + Name: "PathNotViable", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + altMessage: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + "MulNonNumeric": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$mul", bson.D{{"v.0.foo.0.bar", 1}}}}}, + }, + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Cannot apply $mul to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + altMessage: "Cannot apply $mul to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + "MulInt64BadValue": { + command: bson.D{ + {"query", bson.D{{"_id", "int64-max"}}}, + {"update", bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}}}, + }, + err: &mongo.CommandError{ + Code: 2, + Name: "BadValue", + Message: "Failed to apply $mul operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + provider: shareddata.Int64s, + altMessage: "Plan executor error during findAndModify :: caused by :: " + + "Failed to apply $mul operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + "MulInt32BadValue": { + command: bson.D{ + {"query", bson.D{{"_id", "int32"}}}, + {"update", bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}}}, + }, + err: &mongo.CommandError{ + Code: 2, + Name: "BadValue", + Message: "Plan executor error during findAndModify :: caused by :: " + + "Failed to apply $mul operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + provider: shareddata.Int32s, + altMessage: "Failed to apply $mul operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + "MulEmptyPath": { + command: bson.D{ + {"query", bson.D{{"_id", "array-documents-nested"}}}, + {"update", bson.D{{"$mul", bson.D{{"v.", "v"}}}}}, + }, + err: &mongo.CommandError{ + Code: 56, + Name: "EmptyFieldName", + Message: "The update path 'v.' contains an empty field name, which is not allowed.", + }, }, } { name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() - ctx, collection := setup.Setup(t, shareddata.DocumentsStrings) + + provider := tc.provider + if provider == nil { + provider = shareddata.ArrayDocuments + } + + ctx, collection := setup.Setup(t, provider) command := bson.D{{"findAndModify", collection.Name()}} command = append(command, tc.command...) diff --git a/integration/go.mod b/integration/go.mod index 10b5e781c475..b31c5c94f458 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -7,79 +7,86 @@ replace github.com/FerretDB/FerretDB => ../ require ( github.com/AlekSi/pointer v1.2.0 github.com/FerretDB/FerretDB v0.0.0-00010101000000-000000000000 - github.com/prometheus/client_golang v1.15.0 - github.com/stretchr/testify v1.8.2 - go.mongodb.org/mongo-driver v1.11.4 - go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.40.0 - go.opentelemetry.io/otel v1.14.0 - go.opentelemetry.io/otel/exporters/jaeger v1.14.0 - go.opentelemetry.io/otel/sdk v1.14.0 + github.com/prometheus/client_golang v1.15.1 + github.com/stretchr/testify v1.8.3 + go.mongodb.org/mongo-driver v1.11.6 + go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.41.1 + go.opentelemetry.io/otel v1.15.1 + go.opentelemetry.io/otel/exporters/jaeger v1.15.1 + go.opentelemetry.io/otel/sdk v1.15.1 go.uber.org/zap v1.24.0 - golang.org/x/exp v0.0.0-20230418202329-0354be287a23 + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc ) require ( - cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute v1.19.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/SAP/go-hdb v1.2.1 // indirect + github.com/SAP/go-hdb v1.3.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect - github.com/getkin/kin-openapi v0.114.0 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/golang/glog v1.0.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx-zap v0.0.0-20221202020421-94b1cb2f889f // indirect github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jackc/puddle/v2 v2.2.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.13.6 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect - github.com/perimeterx/marshmallow v1.1.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.43.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/tigrisdata/tigris-client-go v1.0.0-beta.29 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tigrisdata/tigris-client-go v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.8.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect - google.golang.org/grpc v1.53.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/sqlite v1.22.1 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) diff --git a/integration/go.sum b/integration/go.sum index d9ba95cb2ae6..b2fc984b478d 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= @@ -9,8 +9,8 @@ github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tS github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/SAP/go-hdb v1.2.1 h1:Bd9jS47v10rVSCFSeMLjXIJUKDbLzlfwZAKjsKb4Fg8= -github.com/SAP/go-hdb v1.2.1/go.mod h1:eWjj7L4p1HWEDbYjWasS0xp+jxSGaAwkZPK2KMyCfk0= +github.com/SAP/go-hdb v1.3.0 h1:ldNnAfI1dOHARYxagtvvJj5y6jK+joHUZ5DXRr2QgXM= +github.com/SAP/go-hdb v1.3.0/go.mod h1:Aqn8GpDM15AOEV83O9CA0Q28MOKszDCCo83FvJx5cl4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= @@ -19,6 +19,7 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -28,13 +29,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.12.4 h1:pPmn6qI9MuOtCz82WY2Xaw46EQjgvxednXXrP7g5Q2s= github.com/deepmap/oapi-codegen v1.12.4/go.mod h1:3lgHGMu6myQ2vqbbTXH2H1o4eXFTGnFiDaOaKKl5yas= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -43,30 +45,19 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas= -github.com/getkin/kin-openapi v0.114.0 h1:ar7QiJpDdlR+zSyPjrLf8mNnpoFP/lI90XcywMCFNe8= -github.com/getkin/kin-openapi v0.114.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -74,7 +65,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -100,18 +90,15 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1 h1:I6ITHEanAwjB0FvaxmGm8pKqmCLR7QIe05ZmO4QAXMw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.1/go.mod h1:gYC+WX4YJFarA2ie73G2epzt7TBWpo9pzcBnK1g0MSw= -github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -124,30 +111,26 @@ github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk= github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jhump/protoreflect v1.14.1 h1:N88q7JkxTHWFEqReuTsYH1dPIwXxA0ITNQp7avLY10s= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -155,27 +138,26 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= -github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= -github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= +github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -184,28 +166,19 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tigrisdata/tigris-client-go v1.0.0-beta.29 h1:Acv8E3y+Y8yg2gznUnn7Q6ykBm2jEwYUY3wOyThdqFM= -github.com/tigrisdata/tigris-client-go v1.0.0-beta.29/go.mod h1:+0L8NNdNpaw9vWwGKc+S3P75f+Sru3KktpW0oUtZSJw= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/tigrisdata/tigris-client-go v1.0.0 h1:07Qw8Tm0qL15WiadP0hp4iBiRzfNSJ+GH4/ozO0nNs0= +github.com/tigrisdata/tigris-client-go v1.0.0/go.mod h1:2n6TQUdoTbzuTtakHT/ZNuK5X+I/i57BqqCcYAzG7y4= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= @@ -220,45 +193,47 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas= -go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.40.0 h1:hATJDiGtTPWglqQRlWUiT5df32bOu9AJV41djhfF4Ig= -go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.40.0/go.mod h1:nkEFz9FW/KZC65rsd8yrHm4aBKa5STMpe4/Xb5+LG64= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/exporters/jaeger v1.14.0 h1:CjbUNd4iN2hHmWekmOqZ+zSCU+dzZppG8XsV+A3oc8Q= -go.opentelemetry.io/otel/exporters/jaeger v1.14.0/go.mod h1:4Ay9kk5vELRrbg5z4cpP9EtmQRFap2Wb0woPG4lujZA= -go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= -go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.mongodb.org/mongo-driver v1.11.6 h1:XM7G6PjiGAO5betLF13BIa5TlLUUE3uJ/2Ox3Lz1K+o= +go.mongodb.org/mongo-driver v1.11.6/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.41.1 h1:fq9wxmwH+DaSFIZib7/GpzJfxiq3Lsa5UAUMCp8rXbE= +go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.41.1/go.mod h1:INZZJ3JUpiVA8tINhOzuuekc56fI1mQbWRN0rd4b6Og= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/exporters/jaeger v1.15.1 h1:x3SLvwli0OyAJapNcOIzf1xXBRBA+HD3elrMQmFfmXo= +go.opentelemetry.io/otel/exporters/jaeger v1.15.1/go.mod h1:0Ck9b5oLL/bFZvfAEEqtrb1U0jZXjm5fWXMCOCG3vvM= +go.opentelemetry.io/otel/sdk v1.15.1 h1:5FKR+skgpzvhPQHIEfcwMYjCBr14LWzs3uSqKiQzETI= +go.opentelemetry.io/otel/sdk v1.15.1/go.mod h1:8rVtxQfrbmbHKfqzpQkT5EzZMcbMBwTzNAggbEAM0KA= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230418202329-0354be287a23 h1:4NKENAGIctmZYLK9W+X1kDK8ObBFqOSCJM6WE7CvkJY= -golang.org/x/exp v0.0.0-20230418202329-0354be287a23/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -273,12 +248,12 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -299,8 +274,10 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -315,10 +292,13 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -333,8 +313,8 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec h1:6rwgChOSUfpzJF2/KnLgo+gMaxGpujStSkPWrbhXArU= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -343,8 +323,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -363,14 +343,37 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= +modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= diff --git a/integration/helpers.go b/integration/helpers.go index f8b9fe5afba8..cdf17f9cc9a9 100644 --- a/integration/helpers.go +++ b/integration/helpers.go @@ -17,7 +17,6 @@ package integration import ( "context" - "errors" "testing" "time" @@ -222,26 +221,31 @@ func AssertMatchesCommandError(t testing.TB, expected, actual error) { } } -// AssertMatchesWriteError asserts error codes are the same. -// -// TODO check not only code; make is look similar to AssertMatchesCommandError above. -// https://github.com/FerretDB/FerretDB/issues/2545 +// AssertMatchesWriteError asserts that both errors are WriteExceptions containing exactly one WriteError, +// and those WriteErrors are equal, except messages (and ignoring the Raw part). func AssertMatchesWriteError(t testing.TB, expected, actual error) { t.Helper() - var aErr, eErr mongo.WriteException + a, ok := actual.(mongo.WriteException) //nolint:errorlint // do not inspect error chain + require.Truef(t, ok, "actual is %T, not mongo.WriteException", a) + require.Lenf(t, a.WriteErrors, 1, "actual is %v, expected one mongo.WriteError", a.WriteErrors) - if ok := errors.As(actual, &aErr); !ok || len(aErr.WriteErrors) != 1 { - assert.Equal(t, expected, actual) - return - } + e, ok := expected.(mongo.WriteException) //nolint:errorlint // do not inspect error chain + require.Truef(t, ok, "expected is %T, not mongo.WriteException", e) + require.Lenf(t, e.WriteErrors, 1, "expected is %v, expected one mongo.WriteError", e.WriteErrors) - if ok := errors.As(expected, &eErr); !ok || len(eErr.WriteErrors) != 1 { - assert.Equal(t, expected, actual) - return - } + aErr := a.WriteErrors[0] + eErr := e.WriteErrors[0] + + aErr.Raw = nil + eErr.Raw = nil - assert.Equal(t, eErr.WriteErrors[0].Code, aErr.WriteErrors[0].Code) + actualMessage := aErr.Message + aErr.Message = eErr.Message + + if !AssertEqualWriteError(t, eErr, aErr) { + t.Logf("actual message: %s", actualMessage) + } } // AssertEqualAltError is a deprecated alias for AssertEqualAltCommandError. diff --git a/integration/indexes_compat_test.go b/integration/indexes_compat_test.go index 4997cca14cf9..a02215c54ae1 100644 --- a/integration/indexes_compat_test.go +++ b/integration/indexes_compat_test.go @@ -64,10 +64,9 @@ func TestIndexesCreate(t *testing.T) { t.Parallel() for name, tc := range map[string]struct { //nolint:vet // for readability - models []mongo.IndexModel - altErrorMsg string // optional, alternative error message in case of error - resultType compatTestCaseResultType // defaults to nonEmptyResult - skip string // optional, skip test with a specified reason + models []mongo.IndexModel + resultType compatTestCaseResultType // defaults to nonEmptyResult + skip string // optional, skip test with a specified reason }{ "Empty": { models: []mongo.IndexModel{}, @@ -121,8 +120,7 @@ func TestIndexesCreate(t *testing.T) { models: []mongo.IndexModel{ {Keys: bson.D{{"v", -1}, {"v", 1}}}, }, - resultType: emptyResult, - altErrorMsg: `Error in specification { v: -1, v: 1 }, the field "v" appears multiple times`, + resultType: emptyResult, }, "CustomName": { models: []mongo.IndexModel{ @@ -170,8 +168,7 @@ func TestIndexesCreate(t *testing.T) { Keys: bson.D{{"v", -1}, {"v", 1}}, }, }, - resultType: emptyResult, - altErrorMsg: `Error in specification { v: -1, v: 1 }, the field "v" appears multiple times`, + resultType: emptyResult, }, "SameKeyDifferentNames": { models: []mongo.IndexModel{ @@ -184,8 +181,7 @@ func TestIndexesCreate(t *testing.T) { Options: new(options.IndexOptions).SetName("bar"), }, }, - resultType: emptyResult, - altErrorMsg: "One of the specified indexes already exists with a different name", + resultType: emptyResult, }, "SameNameDifferentKeys": { models: []mongo.IndexModel{ @@ -198,8 +194,7 @@ func TestIndexesCreate(t *testing.T) { Options: new(options.IndexOptions).SetName("index-name"), }, }, - resultType: emptyResult, - altErrorMsg: "One of the specified indexes already exists with a different key", + resultType: emptyResult, }, } { name, tc := name, tc @@ -230,17 +225,16 @@ func TestIndexesCreate(t *testing.T) { targetRes, targetErr := targetCollection.Indexes().CreateMany(ctx, tc.models) compatRes, compatErr := compatCollection.Indexes().CreateMany(ctx, tc.models) - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altErrorMsg != "" { + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - expectedErr.Raw = nil - AssertEqualAltError(t, expectedErr, tc.altErrorMsg, targetErr) - } else { - require.Equal(t, compatErr, targetErr) + return } + require.NoError(t, compatErr, "compat error; target returned no error") assert.Equal(t, compatRes, targetRes) @@ -380,12 +374,22 @@ func TestIndexesCreateRunCommand(t *testing.T) { }, ).Decode(&compatRes) + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) + + return + } + require.NoError(t, compatErr, "compat error; target returned no error") + if tc.resultType == emptyResult { require.Nil(t, targetRes) require.Nil(t, compatRes) } - AssertMatchesCommandError(t, compatErr, targetErr) assert.Equal(t, compatRes, targetRes) targetErr = targetCollection.Database().RunCommand( @@ -399,7 +403,16 @@ func TestIndexesCreateRunCommand(t *testing.T) { require.Nil(t, targetRes) require.Nil(t, compatRes) - AssertMatchesCommandError(t, compatErr, targetErr) + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) + + return + } + require.NoError(t, compatErr, "compat error; target returned no error") }) } } @@ -693,21 +706,20 @@ func TestIndexesDropRunCommand(t *testing.T) { var compatRes bson.D compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes) - if tc.resultType == emptyResult { - require.Nil(t, targetRes) - require.Nil(t, compatRes) - } + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altErrorMsg != "" { + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - expectedErr.Raw = nil - AssertEqualAltError(t, expectedErr, tc.altErrorMsg, targetErr) - } else { - require.Equal(t, compatErr, targetErr) + return + } + require.NoError(t, compatErr, "compat error; target returned no error") + + if tc.resultType == emptyResult { + require.Nil(t, targetRes) + require.Nil(t, compatRes) } require.Equal(t, compatRes, targetRes) diff --git a/integration/insert_run_command_compat_test.go b/integration/insert_run_command_compat_test.go index 1af4b7aa18ec..0a9ce4cbc05c 100644 --- a/integration/insert_run_command_compat_test.go +++ b/integration/insert_run_command_compat_test.go @@ -20,7 +20,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" "github.com/FerretDB/FerretDB/integration/setup" ) @@ -73,19 +72,10 @@ func testInsertRunCommandCompat(t *testing.T, testCases map[string]insertRunComm if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - - // TODO https://github.com/FerretDB/FerretDB/issues/2545 - if tc.altErrorMsg != "" { - AssertMatchesCommandError(t, compatErr, targetErr) - - var expectedErr mongo.CommandError - require.ErrorAs(t, targetErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altErrorMsg, targetErr) - } else { - require.Equal(t, compatErr, targetErr) - } + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } diff --git a/integration/query_compat_command_test.go b/integration/query_compat_command_test.go index bbdb4622f297..c57c30ebf6d9 100644 --- a/integration/query_compat_command_test.go +++ b/integration/query_compat_command_test.go @@ -135,6 +135,9 @@ func testQueryCompatCommand(t *testing.T, testCases map[string]queryCompatComman if targetErr != nil { t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return diff --git a/integration/query_compat_test.go b/integration/query_compat_test.go index 894f425de297..37230d84fb6d 100644 --- a/integration/query_compat_test.go +++ b/integration/query_compat_test.go @@ -147,15 +147,17 @@ func testQueryCompatWithProviders(t *testing.T, providers shareddata.Providers, if targetErr != nil { t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return } require.NoError(t, compatErr, "compat error; target returned no error") - var targetRes, compatRes []bson.D - require.NoError(t, targetCursor.All(ctx, &targetRes)) - require.NoError(t, compatCursor.All(ctx, &compatRes)) + targetRes := FetchAll(t, ctx, targetCursor) + compatRes := FetchAll(t, ctx, compatCursor) if !tc.skipIDCheck { t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) @@ -229,6 +231,22 @@ func TestQueryCompatSort(t *testing.T) { filter: bson.D{}, sort: bson.D{{"v", -1}, {"_id", 1}}, }, + "AscDesc": { + filter: bson.D{}, + sort: bson.D{{"v", 1}, {"_id", -1}}, + }, + "DescDesc": { + filter: bson.D{}, + sort: bson.D{{"v", -1}, {"_id", -1}}, + }, + "AscSingle": { + filter: bson.D{}, + sort: bson.D{{"_id", 1}}, + }, + "DescSingle": { + filter: bson.D{}, + sort: bson.D{{"_id", -1}}, + }, "Bad": { filter: bson.D{}, @@ -246,10 +264,6 @@ func TestQueryCompatSort(t *testing.T) { resultType: emptyResult, }, - "DotNotation": { - filter: bson.D{}, - sort: bson.D{{"v.foo", 1}, {"_id", 1}}, - }, "DotNotationIndex": { filter: bson.D{}, sort: bson.D{{"v.0", 1}, {"_id", 1}}, @@ -288,6 +302,22 @@ func TestQueryCompatSort(t *testing.T) { testQueryCompat(t, testCases) } +func TestQueryCompatSortDotNotation(t *testing.T) { + t.Parallel() + + providers := shareddata.AllProviders(). + // TODO: https://github.com/FerretDB/FerretDB/issues/2618 + Remove("ArrayDocuments") + + testCases := map[string]queryCompatTestCase{ + "DotNotation": { + filter: bson.D{}, + sort: bson.D{{"v.foo", 1}, {"_id", 1}}, + }, + } + testQueryCompatWithProviders(t, providers, testCases) +} + func TestQueryCompatSkip(t *testing.T) { t.Parallel() diff --git a/integration/query_projection_compat_test.go b/integration/query_projection_compat_test.go index 27de972fb66e..8f08275e6d5d 100644 --- a/integration/query_projection_compat_test.go +++ b/integration/query_projection_compat_test.go @@ -30,7 +30,7 @@ func TestQueryProjectionCompat(t *testing.T) { // topLevelFieldsIntegers contains documents with several top level fields with integer values. topLevelFieldsIntegers := shareddata.NewTopLevelFieldsProvider( "TopLevelFieldsIntegers", - []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + nil, map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -75,6 +75,10 @@ func TestQueryProjectionCompat(t *testing.T) { filter: bson.D{}, projection: bson.D{{"foo", 1.24}, {"bar", true}}, }, + "Include2FieldsReverse": { + filter: bson.D{}, + projection: bson.D{{"bar", true}, {"foo", 1.24}}, + }, "Exclude2Fields": { filter: bson.D{}, projection: bson.D{{"foo", int32(0)}, {"bar", false}}, @@ -141,33 +145,89 @@ func TestQueryProjectionCompat(t *testing.T) { "DotNotationInclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationIncludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", true}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", + }, + "DotNotationIncludeTwoReverse": { + filter: bson.D{}, + projection: bson.D{{"v.array", true}, {"v.foo", true}}, + }, + "DotNotationIncludeTwoArray": { + filter: bson.D{}, + projection: bson.D{{"v.foo", true}, {"v.bar", true}}, }, "DotNotationExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", false}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationExcludeTwo": { filter: bson.D{}, projection: bson.D{{"v.foo", false}, {"v.array", false}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", }, "DotNotationExcludeSecondLevel": { filter: bson.D{}, projection: bson.D{{"v.array.42", false}}, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", + }, + "DotNotationIncludeSecondLevel": { + filter: bson.D{}, + projection: bson.D{{"v.array.42", true}}, }, "DotNotationIncludeExclude": { filter: bson.D{}, projection: bson.D{{"v.foo", true}, {"v.array", false}}, resultType: emptyResult, - skip: "https://github.com/FerretDB/FerretDB/issues/2430", + }, + "DotNotation5LevelInclude": { + filter: bson.D{}, + projection: bson.D{{"v.a.b.c.d", true}}, + }, + "DotNotation5LevelExclude": { + filter: bson.D{}, + projection: bson.D{{"v.a.b.c.d", false}}, + }, + "DotNotation4LevelInclude": { + filter: bson.D{}, + projection: bson.D{{"v.a.b.c", true}}, + }, + "DotNotation4LevelExclude": { + filter: bson.D{}, + projection: bson.D{{"v.a.b.c", false}}, + }, + "DotNotationArrayInclude": { + filter: bson.D{}, + projection: bson.D{{"v.array.0", true}}, + }, + "DotNotationArrayExclude": { + filter: bson.D{}, + projection: bson.D{{"v.array.0", false}}, + }, + "DotNotationArrayPathInclude": { + filter: bson.D{}, + projection: bson.D{{"v.0.foo", true}}, + }, + "DotNotationArrayPathExclude": { + filter: bson.D{}, + projection: bson.D{{"v.0.foo", false}}, + }, + "DotNotationManyInclude": { + filter: bson.D{}, + projection: bson.D{ + {"v.42", true}, + {"v.non-existent", true}, + {"v.foo", true}, + {"v.array", true}, + }, + }, + "DotNotationManyExclude": { + filter: bson.D{}, + projection: bson.D{ + {"v.42", false}, + {"v.non-existent", false}, + {"v.foo", false}, + {"v.array", false}, + }, }, } diff --git a/integration/query_test.go b/integration/query_test.go index ade2be855880..9e1988d9fc5d 100644 --- a/integration/query_test.go +++ b/integration/query_test.go @@ -138,7 +138,7 @@ func TestQueryBadSortType(t *testing.T) { Name: "TypeMismatch", Message: "Expected field sortto be of type object", }, - altMessage: "Expected field sort to be of type object", + altMessage: "BSON field 'find.sort' is the wrong type 'double', expected type 'object'", }, "BadSortType": { command: bson.D{ @@ -151,7 +151,7 @@ func TestQueryBadSortType(t *testing.T) { Name: "TypeMismatch", Message: "Expected field sortto be of type object", }, - altMessage: "Expected field sort to be of type object", + altMessage: "BSON field 'find.sort' is the wrong type 'string', expected type 'object'", }, "BadSortTypeValue": { command: bson.D{ diff --git a/integration/renamecollection_compat_test.go b/integration/renamecollection_compat_test.go index d9c883860cf3..f9110f0a3f22 100644 --- a/integration/renamecollection_compat_test.go +++ b/integration/renamecollection_compat_test.go @@ -21,7 +21,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" "github.com/FerretDB/FerretDB/integration/setup" "github.com/FerretDB/FerretDB/integration/shareddata" @@ -52,7 +51,6 @@ func TestRenameCollectionCompat(t *testing.T) { compatNSFrom any targetNSTo any compatNSTo any - altMessage string resultType compatTestCaseResultType }{ "Valid": { @@ -172,7 +170,6 @@ func TestRenameCollectionCompat(t *testing.T) { targetNSTo: targetDB.Name() + "." + strings.Repeat("aB", 150), compatNSTo: targetDB.Name() + "." + strings.Repeat("aB", 150), resultType: emptyResult, - altMessage: "error with target namespace: Invalid collection name: " + strings.Repeat("aB", 150), }, } { name, tc := name, tc @@ -190,22 +187,16 @@ func TestRenameCollectionCompat(t *testing.T) { compatCommand := bson.D{{"renameCollection", tc.compatNSFrom}, {"to", tc.compatNSTo}} compatErr := compatDBConnect.RunCommand(ctx, compatCommand).Decode(&compatRes) - if tc.resultType == emptyResult { - require.Error(t, compatErr) + if targetErr != nil { + t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - - if tc.altMessage != "" { - var expectedErr mongo.CommandError - require.ErrorAs(t, compatErr, &expectedErr) - AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr) - } else { - assert.Equal(t, compatErr, targetErr) - } + // error messages are intentionally not compared + AssertMatchesCommandError(t, compatErr, targetErr) return } + require.NoError(t, compatErr, "compat error; target returned no error") // Collection lists after rename must be the same targetNames, err := targetDB.ListCollectionNames(ctx, bson.D{}) diff --git a/integration/setup/listener.go b/integration/setup/listener.go index 445b208a8a33..d79f84bafd17 100644 --- a/integration/setup/listener.go +++ b/integration/setup/listener.go @@ -82,6 +82,10 @@ func setupListener(tb testing.TB, ctx context.Context, logger *zap.Logger) (*mon require.NotEmpty(tb, *postgreSQLURLF, "-postgresql-url must be set for %q", *targetBackendF) require.Empty(tb, *tigrisURLSF, "-tigris-urls must be empty for %q", *targetBackendF) handler = "pg" + case "ferretdb-sqlite": + require.Empty(tb, *postgreSQLURLF, "-postgresql-url must be empty for %q", *targetBackendF) + require.Empty(tb, *tigrisURLSF, "-tigris-urls must be empty for %q", *targetBackendF) + handler = "sqlite" case "ferretdb-tigris": require.Empty(tb, *postgreSQLURLF, "-postgresql-url must be empty for %q", *targetBackendF) require.NotEmpty(tb, *tigrisURLSF, "-tigris-urls must be set for %q", *targetBackendF) @@ -105,10 +109,13 @@ func setupListener(tb testing.TB, ctx context.Context, logger *zap.Logger) (*mon PostgreSQLURL: *postgreSQLURLF, + SQLiteURI: filepath.Join("..", "tmp", "sqlite-tests"), + TigrisURL: nextTigrisUrl(), TestOpts: registry.TestOpts{ DisableFilterPushdown: *disableFilterPushdownF, + EnableSortPushdown: *enableSortPushdownF, EnableCursors: *enableCursorsF, }, } diff --git a/integration/setup/setup.go b/integration/setup/setup.go index 362b0da0e10f..59086b081037 100644 --- a/integration/setup/setup.go +++ b/integration/setup/setup.go @@ -56,12 +56,13 @@ var ( logLevelF = zap.LevelFlag("log-level", zap.DebugLevel, "log level for tests") disableFilterPushdownF = flag.Bool("disable-filter-pushdown", false, "disable filter pushdown") + enableSortPushdownF = flag.Bool("enable-sort-pushdown", false, "enable sort pushdown") enableCursorsF = flag.Bool("enable-cursors", false, "enable cursors") ) // Other globals. var ( - allBackends = []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"} + allBackends = []string{"ferretdb-pg", "ferretdb-sqlite", "ferretdb-tigris", "mongodb"} CertsRoot = filepath.Join("..", "build", "certs") // relative to `integration` directory ) diff --git a/integration/shareddata/composites.go b/integration/shareddata/composites.go index 5f71f46d1336..0c49791205f8 100644 --- a/integration/shareddata/composites.go +++ b/integration/shareddata/composites.go @@ -27,7 +27,7 @@ import ( // This shared data set is not frozen yet, but please add to it only if it is really shared. var Composites = &Values[string]{ name: "Composites", - backends: []string{"ferretdb-pg", "mongodb"}, + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, data: map[string]any{ "document": bson.D{{"foo", int32(42)}}, "document-composite": bson.D{ @@ -77,7 +77,7 @@ var Composites = &Values[string]{ // mixture of array and scalar documents. var Mixed = &Values[string]{ name: "Mixed", - backends: []string{"ferretdb-pg", "mongodb"}, + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, data: map[string]any{ "null": nil, "unset": unset, @@ -90,7 +90,7 @@ var Mixed = &Values[string]{ // dot notation to find values from both document and array. var ArrayAndDocuments = &Values[string]{ name: "ArrayAndDocuments", - backends: []string{"ferretdb-pg", "mongodb"}, + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, data: map[string]any{ "document": bson.D{{"foo", int32(42)}}, "array-documents": bson.A{ @@ -105,7 +105,7 @@ var ArrayAndDocuments = &Values[string]{ // on pg backend. var PostgresEdgeCases = &Values[string]{ name: "PostgresEdgeCases", - backends: []string{"ferretdb-pg", "mongodb"}, + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, data: map[string]any{ "document-notations": bson.D{ {"foo[0]", int32(42)}, @@ -119,8 +119,7 @@ var PostgresEdgeCases = &Values[string]{ // DocumentsDoubles contains documents with double values for tests. var DocumentsDoubles = &Values[string]{ - name: "DocumentsDoubles", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "DocumentsDoubles", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "object", "properties": {"v": {"type": "number"}}`), @@ -140,8 +139,7 @@ var DocumentsDoubles = &Values[string]{ // DocumentsStrings contains documents with string values for tests. var DocumentsStrings = &Values[string]{ - name: "DocumentsStrings", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "DocumentsStrings", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "object", "properties": {"v": {"type": "string"}}`), @@ -159,8 +157,7 @@ var DocumentsStrings = &Values[string]{ // DocumentsDocuments contains documents with documents for tests. var DocumentsDocuments = &Values[primitive.ObjectID]{ - name: "DocumentsDocuments", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "DocumentsDocuments", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -185,11 +182,42 @@ var DocumentsDocuments = &Values[primitive.ObjectID]{ }, } +// DocumentsDeeplyNested contains documents nested in multiple levels for tests. +var DocumentsDeeplyNested = &Values[string]{ + name: "DocumentsDeeplyNested", + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, + data: map[string]any{ + "two": bson.D{{"a", bson.D{{"b", 12}}}}, + "three": bson.D{{"a", bson.D{{"b", bson.D{{"c", 12}}}}}}, + "four": bson.D{ + {"a", bson.D{ + {"b", bson.D{ + {"c", bson.D{ + {"d", 123}, + }}, + {"e", 13}, + }}, + {"f", 14}, + }}, + {"g", 15}, + }, + "array": bson.D{ + {"a", bson.D{ + {"b", bson.D{ + {"c", bson.A{1, 2}}, + {"e", 13}, + }}, + {"f", 14}, + }}, + {"g", 15}, + }, + }, +} + // ArrayStrings contains an array with string values for tests. // Tigris JSON schema validator contains extra properties to make it suitable for more tests. var ArrayStrings = &Values[string]{ - name: "ArrayStrings", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayStrings", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -216,8 +244,7 @@ var ArrayStrings = &Values[string]{ // ArrayDoubles contains an array with float64 values for tests. var ArrayDoubles = &Values[string]{ - name: "ArrayDoubles", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayDoubles", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -247,8 +274,7 @@ var ArrayDoubles = &Values[string]{ // ArrayInt32s contains an array with int32 values for tests. var ArrayInt32s = &Values[string]{ - name: "ArrayInt32s", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayInt32s", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -275,10 +301,9 @@ var ArrayInt32s = &Values[string]{ // ArrayInt64s contains an array with int64 values for tests. var ArrayInt64s = &Values[string]{ - name: "ArrayInt64s", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayInt64s", validators: map[string]map[string]any{ - "tigris": { + "ferretdb-tigris": { "$tigrisSchemaString": `{ "title": "%%collection%%", "primary_key": ["_id"], @@ -300,8 +325,7 @@ var ArrayInt64s = &Values[string]{ // ArrayRegexes contains an array with regex values for tests. var ArrayRegexes = &Values[string]{ - name: "ArrayRegexes", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayRegexes", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -330,8 +354,7 @@ var ArrayRegexes = &Values[string]{ // ArrayDocuments contains array with documents with arrays: {"v": [{"foo": [{"bar": "hello"}]}, ...]}. // This data set is helpful for dot notation tests: v.0.foo.0.bar. var ArrayDocuments = &Values[string]{ - name: "ArrayDocuments", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ArrayDocuments", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -380,5 +403,19 @@ var ArrayDocuments = &Values[string]{ bson.A{bson.D{{"bar", "hello"}}}, }}, }, + "array-three-documents": bson.A{ + bson.D{{ + "bar", + bson.A{bson.D{{"a", "b"}}}, + }}, + bson.D{{ + "foo", + bson.A{bson.D{{"bar", "hello"}}}, + }}, + bson.D{{ + "foo", + bson.A{bson.D{{"bar", "hello"}}}, + }}, + }, }, } diff --git a/integration/shareddata/provider.go b/integration/shareddata/provider.go index 75cac35fe763..f05e9d0df9ec 100644 --- a/integration/shareddata/provider.go +++ b/integration/shareddata/provider.go @@ -43,9 +43,9 @@ type Provider interface { // Values stores shared data documents as {"_id": key, "v": value} documents. type Values[idType comparable] struct { name string + backends []string // empty values means all backends validators map[string]map[string]any // backend -> validator name -> validator data map[idType]any - backends []string } // Name implement Provider interface. @@ -89,6 +89,10 @@ func (values *Values[idType]) Docs() []bson.D { // IsCompatible returns true if the given backend is compatible with this provider. func (values *Values[idType]) IsCompatible(backend string) bool { + if len(values.backends) == 0 { + return true + } + return slices.Contains(values.backends, backend) } @@ -117,7 +121,7 @@ func NewTopLevelFieldsProvider[id comparable](name string, backends []string, va //nolint:vet // for readability type topLevelValues[id comparable] struct { name string - backends []string + backends []string // empty values means all backends validators map[string]map[string]any // backend -> validator name -> validator data map[id]Fields } @@ -166,6 +170,10 @@ func (t *topLevelValues[id]) Docs() []bson.D { // IsCompatible implements Provider interface. func (t *topLevelValues[id]) IsCompatible(backend string) bool { + if len(t.backends) == 0 { + return true + } + return slices.Contains(t.backends, backend) } diff --git a/integration/shareddata/scalars.go b/integration/shareddata/scalars.go index 8c639a383a38..5ea9f1a6b02b 100644 --- a/integration/shareddata/scalars.go +++ b/integration/shareddata/scalars.go @@ -37,7 +37,7 @@ const ( // This shared data set is frozen. If you need more values, add them in the test itself. var Scalars = &Values[string]{ name: "Scalars", - backends: []string{"ferretdb-pg", "mongodb"}, + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, data: map[string]any{ "double": 42.13, "double-whole": 42.0, @@ -120,8 +120,7 @@ var Scalars = &Values[string]{ // Doubles contains double values for tests. var Doubles = &Values[string]{ - name: "Doubles", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Doubles", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "number"`), @@ -175,8 +174,7 @@ var Doubles = &Values[string]{ // OverflowVergeDoubles may be excluded on such update tests and tested // in diff tests https://github.com/FerretDB/dance. var OverflowVergeDoubles = &Values[string]{ - name: "OverflowVergeDoubles", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "OverflowVergeDoubles", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "number"`), @@ -191,8 +189,7 @@ var OverflowVergeDoubles = &Values[string]{ // SmallDoubles contains double values that does not go close to // the maximum safe precision for tests. var SmallDoubles = &Values[string]{ - name: "SmallDoubles", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "SmallDoubles", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "number"`), @@ -210,8 +207,7 @@ var SmallDoubles = &Values[string]{ // Strings contains string values for tests. // Tigris JSON schema validator contains extra properties to make it suitable for more tests. var Strings = &Values[string]{ - name: "Strings", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Strings", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -238,8 +234,7 @@ var Strings = &Values[string]{ // Binaries contains binary values for tests. var Binaries = &Values[string]{ - name: "Binaries", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Binaries", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "object", "properties": {"$b": {"type": "string", "format": "byte"}, "s": {"type": "integer", "format": "int32"}}`), @@ -254,8 +249,7 @@ var Binaries = &Values[string]{ // ObjectIDs contains ObjectID values for tests. var ObjectIDs = &Values[string]{ - name: "ObjectIDs", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ObjectIDs", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "string", "format": "byte"`), @@ -270,8 +264,7 @@ var ObjectIDs = &Values[string]{ // Bools contains bool values for tests. var Bools = &Values[string]{ - name: "Bools", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Bools", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": `{ @@ -294,8 +287,7 @@ var Bools = &Values[string]{ // DateTimes contains datetime values for tests. var DateTimes = &Values[string]{ - name: "DateTimes", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "DateTimes", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "string", "format": "date-time"`), @@ -313,7 +305,7 @@ var DateTimes = &Values[string]{ // Nulls contains null value for tests. var Nulls = &Values[string]{ name: "Nulls", - backends: []string{"ferretdb-pg", "mongodb"}, // Not compatible with Tigris as it needs a data type to be set. + backends: []string{"ferretdb-pg", "ferretdb-sqlite", "mongodb"}, // Not compatible with Tigris as it needs a data type to be set. data: map[string]any{ "null": nil, }, @@ -321,8 +313,7 @@ var Nulls = &Values[string]{ // Regexes contains regex values for tests. var Regexes = &Values[string]{ - name: "Regexes", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Regexes", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "object", "properties": {"$r": {"type": "string"}, "o": {"type": "string"}}`), @@ -337,8 +328,7 @@ var Regexes = &Values[string]{ // Int32s contains int32 values for tests. var Int32s = &Values[string]{ - name: "Int32s", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Int32s", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "integer", "format": "int32"`), @@ -358,8 +348,7 @@ var Int32s = &Values[string]{ // Timestamps contains timestamp values for tests. var Timestamps = &Values[string]{ - name: "Timestamps", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Timestamps", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "object", "properties": {"$t": {"type": "string"}}`), @@ -374,8 +363,7 @@ var Timestamps = &Values[string]{ // Int64s contains int64 values for tests. var Int64s = &Values[string]{ - name: "Int64s", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Int64s", validators: map[string]map[string]any{ "ferretdb-tigris": { "$tigrisSchemaString": tigrisSchema(`"type": "integer", "format": "int64"`), @@ -417,8 +405,7 @@ var Int64s = &Values[string]{ // Unsets contains unset value for tests. var Unsets = &Values[string]{ - name: "Unsets", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "Unsets", data: map[string]any{ "unset": unset, }, @@ -426,8 +413,7 @@ var Unsets = &Values[string]{ // ObjectIDKeys contains documents with ObjectID keys for tests. var ObjectIDKeys = &Values[primitive.ObjectID]{ - name: "ObjectIDKeys", - backends: []string{"ferretdb-pg", "ferretdb-tigris", "mongodb"}, + name: "ObjectIDKeys", data: map[primitive.ObjectID]any{ {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11}: "objectid", primitive.NilObjectID: "objectid-empty", diff --git a/integration/shareddata/shareddata.go b/integration/shareddata/shareddata.go index 2f9b223f0997..54363b9c2570 100644 --- a/integration/shareddata/shareddata.go +++ b/integration/shareddata/shareddata.go @@ -51,6 +51,7 @@ func AllProviders() Providers { DocumentsDoubles, DocumentsStrings, DocumentsDocuments, + DocumentsDeeplyNested, ArrayStrings, ArrayDoubles, diff --git a/integration/update_compat_test.go b/integration/update_compat_test.go index 702e0ff0f17a..30ed40aa2d2f 100644 --- a/integration/update_compat_test.go +++ b/integration/update_compat_test.go @@ -35,11 +35,13 @@ import ( // updateCompatTestCase describes update compatibility test case. type updateCompatTestCase struct { - update bson.D // required if replace is nil - replace bson.D // required if update is nil - filter bson.D // defaults to bson.D{{"_id", id}} - resultType compatTestCaseResultType // defaults to nonEmptyResult - providers []shareddata.Provider // defaults to shareddata.AllProviders() + update bson.D // required if replace is nil + replace bson.D // required if update is nil + filter bson.D // defaults to bson.D{{"_id", id}} + updateOpts *options.UpdateOptions // defaults to nil + replaceOpts *options.ReplaceOptions // defaults to nil + resultType compatTestCaseResultType // defaults to nonEmptyResult + providers []shareddata.Provider // defaults to shareddata.AllProviders() skip string // skips test if non-empty skipForTigris string // skips test for Tigris if non-empty @@ -108,17 +110,22 @@ func testUpdateCompat(t *testing.T, testCases map[string]updateCompatTestCase) { // TODO replace with UpdateMany/ReplaceMany // https://github.com/FerretDB/FerretDB/issues/1507 if update != nil { - targetUpdateRes, targetErr = targetCollection.UpdateOne(ctx, filter, update) - compatUpdateRes, compatErr = compatCollection.UpdateOne(ctx, filter, update) + targetUpdateRes, targetErr = targetCollection.UpdateOne(ctx, filter, update, tc.updateOpts) + compatUpdateRes, compatErr = compatCollection.UpdateOne(ctx, filter, update, tc.updateOpts) } else { - targetUpdateRes, targetErr = targetCollection.ReplaceOne(ctx, filter, replace) - compatUpdateRes, compatErr = compatCollection.ReplaceOne(ctx, filter, replace) + targetUpdateRes, targetErr = targetCollection.ReplaceOne(ctx, filter, replace, tc.replaceOpts) + compatUpdateRes, compatErr = compatCollection.ReplaceOne(ctx, filter, replace, tc.replaceOpts) } if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) + + if targetErr.Error() == "update document must have at least one element" { + // mongo go driver sent error that the update document is empty. + require.Equal(t, compatErr, targetErr) + + return + } // Skip updates that could not be performed due to Tigris schema validation. var e mongo.CommandError @@ -126,6 +133,7 @@ func testUpdateCompat(t *testing.T, testCases map[string]updateCompatTestCase) { setup.SkipForTigrisWithReason(t, targetErr.Error()) } + // AssertMatchesWriteError compares error types and codes, it does not compare messages. AssertMatchesWriteError(t, compatErr, targetErr) } else { require.NoError(t, compatErr, "compat error; target returned no error") @@ -239,8 +247,7 @@ func testUpdateCommandCompat(t *testing.T, testCases map[string]updateCommandCom if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) + t.Logf("Compat error: %v", compatErr) // Skip updates that could not be performed due to Tigris schema validation. var e mongo.CommandError @@ -248,6 +255,7 @@ func testUpdateCommandCompat(t *testing.T, testCases map[string]updateCommandCom setup.SkipForTigrisWithReason(t, targetErr.Error()) } + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) } else { require.NoError(t, compatErr, "compat error; target returned no error") @@ -275,15 +283,17 @@ func testUpdateCommandCompat(t *testing.T, testCases map[string]updateCommandCom if targetErr != nil { t.Logf("Target error: %v", targetErr) + t.Logf("Compat error: %v", compatErr) + + // error messages are intentionally not compared AssertMatchesCommandError(t, compatErr, targetErr) return } require.NoError(t, compatErr, "compat error; target returned no error") - var targetRes, compatRes []bson.D - require.NoError(t, targetCursor.All(ctx, &targetRes)) - require.NoError(t, compatCursor.All(ctx, &compatRes)) + targetRes := FetchAll(t, ctx, targetCursor) + compatRes := FetchAll(t, ctx, compatCursor) t.Logf("Compat (expected) IDs: %v", CollectIDs(t, compatRes)) t.Logf("Target (actual) IDs: %v", CollectIDs(t, targetRes)) @@ -371,9 +381,7 @@ func testUpdateCurrentDateCompat(t *testing.T, testCases map[string]updateCurren if targetErr != nil { t.Logf("Target error: %v", targetErr) - targetErr = UnsetRaw(t, targetErr) - compatErr = UnsetRaw(t, compatErr) - + // AssertMatchesWriteError compares error types and codes, it does not compare messages. AssertMatchesWriteError(t, compatErr, targetErr) } else { require.NoError(t, compatErr, "compat error; target returned no error") @@ -435,6 +443,18 @@ func TestUpdateCompat(t *testing.T) { "ReplaceEmptyDocument": { replace: bson.D{}, }, + "ReplaceNonExistentUpsert": { + filter: bson.D{{"non-existent", "no-match"}}, + replace: bson.D{{"_id", "new"}}, + replaceOpts: &options.ReplaceOptions{Upsert: pointer.ToBool(true)}, + resultType: emptyResult, + }, + "UpdateNonExistentUpsert": { + filter: bson.D{{"_id", "non-existent"}}, + update: bson.D{{"$set", bson.D{{"v", int32(42)}}}}, + updateOpts: &options.UpdateOptions{Upsert: pointer.ToBool(true)}, + resultType: emptyResult, + }, } testUpdateCompat(t, testCases) @@ -484,6 +504,15 @@ func TestUpdateCompatMultiFlagCommand(t *testing.T) { multi: int32(0), resultType: emptyResult, }, + "TrueEmptyDocument": { + update: bson.D{}, + multi: true, + skip: "https://github.com/FerretDB/FerretDB/issues/2630", + }, + "FalseEmptyDocument": { + update: bson.D{}, + multi: false, + }, } testUpdateCommandCompat(t, testCases) diff --git a/integration/update_field_test.go b/integration/update_field_test.go index e37efa67fc91..4780f373403a 100644 --- a/integration/update_field_test.go +++ b/integration/update_field_test.go @@ -15,6 +15,7 @@ package integration import ( + "math" "testing" "github.com/stretchr/testify/require" @@ -39,7 +40,6 @@ func TestUpdateFieldSet(t *testing.T) { alt string }{ "ArrayNil": { - // TODO remove https://github.com/FerretDB/FerretDB/issues/1662 id: "string", update: bson.D{{"$set", bson.D{{"v", bson.A{nil}}}}}, expected: bson.D{{"_id", "string"}, {"v", bson.A{nil}}}, @@ -50,7 +50,6 @@ func TestUpdateFieldSet(t *testing.T) { }, }, "SetSameValueInt": { - // TODO remove https://github.com/FerretDB/FerretDB/issues/1662 id: "int32", update: bson.D{{"$set", bson.D{{"v", int32(42)}}}}, expected: bson.D{{"_id", "int32"}, {"v", int32(42)}}, @@ -83,3 +82,203 @@ func TestUpdateFieldSet(t *testing.T) { }) } } + +func TestUpdateFieldErrors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { //nolint:vet // it is used for test only + id string + update bson.D + provider shareddata.Provider // optional, default uses shareddata.ArrayDocuments + + err *mongo.WriteError + altMessage string + }{ + "SetUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$rename", bson.D{{"v.foo", "foo"}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "cannot use the part (v of v.foo) to traverse the element " + + "({v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]})", + }, + altMessage: "cannot use path 'v.foo' to traverse the document", + }, + "RenameEmptyFieldName": { + id: "array-documents-nested", + update: bson.D{{"$rename", bson.D{{"", "v"}}}}, + err: &mongo.WriteError{ + Code: 56, + Message: "An empty update path is not valid.", + }, + }, + "RenameEmptyPath": { + id: "array-documents-nested", + update: bson.D{{"$rename", bson.D{{"v.", "v"}}}}, + err: &mongo.WriteError{ + Code: 56, + Message: "The update path 'v.' contains an empty field name, which is not allowed.", + }, + }, + "RenameArrayInvalidIndex": { + id: "array-documents-nested", + update: bson.D{{"$rename", bson.D{{"v.-1", "f"}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "cannot use the part (v of v.-1) to traverse the element " + + "({v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]})", + }, + altMessage: "cannot use path 'v.-1' to traverse the document", + }, + "RenameUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$rename", bson.D{{"v.0.foo.0.bar.z", "f"}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "cannot use the part (bar of v.0.foo.0.bar.z) to traverse the element ({bar: \"hello\"})", + }, + altMessage: "types.getByPath: can't access string by path \"z\"", + }, + "IncTypeMismatch": { + id: "array-documents-nested", + update: bson.D{{"$inc", bson.D{{"v", "string"}}}}, + err: &mongo.WriteError{ + Code: 14, + Message: "Cannot increment with non-numeric argument: {v: \"string\"}", + }, + }, + "IncUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$inc", bson.D{{"v.foo", 1}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + }, + "IncNonNumeric": { + id: "array-documents-nested", + update: bson.D{{"$inc", bson.D{{"v.0.foo.0.bar", 1}}}}, + err: &mongo.WriteError{ + Code: 14, + Message: "Cannot apply $inc to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + }, + "IncInt64BadValue": { + id: "int64-max", + update: bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}}, + err: &mongo.WriteError{ + Code: 2, + Message: "Failed to apply $inc operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + provider: shareddata.Int64s, + }, + "IncInt32BadValue": { + id: "int32", + update: bson.D{{"$inc", bson.D{{"v", math.MaxInt64}}}}, + err: &mongo.WriteError{ + Code: 2, + Message: "Failed to apply $inc operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + provider: shareddata.Int32s, + }, + "MaxUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$max", bson.D{{"v.foo", 1}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + }, + "MinUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$min", bson.D{{"v.foo", 1}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + }, + "MulTypeMismatch": { + id: "array-documents-nested", + update: bson.D{{"$mul", bson.D{{"v", "string"}}}}, + err: &mongo.WriteError{ + Code: 14, + Message: "Cannot multiply with non-numeric argument: {v: \"string\"}", + }, + }, + "MulTypeMismatchNonExistent": { + id: "array-documents-nested", + update: bson.D{{"$mul", bson.D{{"non-existent", "string"}}}}, + err: &mongo.WriteError{ + Code: 14, + Message: "Cannot multiply with non-numeric argument: {non-existent: \"string\"}", + }, + }, + "MulUnsuitableValue": { + id: "array-documents-nested", + update: bson.D{{"$mul", bson.D{{"v.foo", 1}}}}, + err: &mongo.WriteError{ + Code: 28, + Message: "Cannot create field 'foo' in element " + + "{v: [ { foo: [ { bar: \"hello\" }, { bar: \"world\" } ] } ]}", + }, + }, + "MulNonNumeric": { + id: "array-documents-nested", + update: bson.D{{"$mul", bson.D{{"v.0.foo.0.bar", 1}}}}, + err: &mongo.WriteError{ + Code: 14, + Message: "Cannot apply $mul to a value of non-numeric type. " + + "{_id: \"array-documents-nested\"} has the field 'bar' of non-numeric type string", + }, + }, + "MulInt64BadValue": { + id: "int64-max", + update: bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}}, + err: &mongo.WriteError{ + Code: 2, + Message: "Failed to apply $mul operations to current value " + + "((NumberLong)9223372036854775807) for document {_id: \"int64-max\"}", + }, + provider: shareddata.Int64s, + }, + "MulInt32BadValue": { + id: "int32", + update: bson.D{{"$mul", bson.D{{"v", math.MaxInt64}}}}, + err: &mongo.WriteError{ + Code: 2, + Message: "Failed to apply $mul operations to current value " + + "((NumberInt)42) for document {_id: \"int32\"}", + }, + provider: shareddata.Int32s, + }, + "MulEmptyPath": { + id: "array-documents-nested", + update: bson.D{{"$mul", bson.D{{"v.", "v"}}}}, + err: &mongo.WriteError{ + Code: 56, + Message: "The update path 'v.' contains an empty field name, which is not allowed.", + }, + }, + } { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + provider := tc.provider + if provider == nil { + provider = shareddata.ArrayDocuments + } + + ctx, collection := setup.Setup(t, provider) + + _, err := collection.UpdateOne(ctx, bson.D{{"_id", tc.id}}, tc.update) + AssertEqualAltWriteError(t, *tc.err, tc.altMessage, err) + }) + } +} diff --git a/internal/backends/backend.go b/internal/backends/backend.go new file mode 100644 index 000000000000..f15ec0696c33 --- /dev/null +++ b/internal/backends/backend.go @@ -0,0 +1,117 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backends + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/util/observability" + "github.com/FerretDB/FerretDB/internal/util/resource" +) + +// Backend is a generic interface for all backends for accessing them. +// +// Backend object is expected to be stateful and wrap database connection(s). +// Handler can create one Backend or multiple Backends with different authentication credentials. +// +// Backend(s) methods can be called by multiple client connections / command handlers concurrently. +// They should be thread-safe. +// +// See backendContract and its methods for additional details. +type Backend interface { + Close() + Database(string) Database + ListDatabases(context.Context, *ListDatabasesParams) (*ListDatabasesResult, error) + DropDatabase(context.Context, *DropDatabaseParams) error + + // There is no interface method to create a database; see package documentation. +} + +// backendContract implements Backend interface. +type backendContract struct { + b Backend + token *resource.Token +} + +// BackendContract wraps Backend and enforces its contract. +// +// All backend implementations should use that function when they create new Backend instances. +// The handler should not use that function. +// +// See backendContract and its methods for additional details. +func BackendContract(b Backend) Backend { + bc := &backendContract{ + b: b, + token: resource.NewToken(), + } + resource.Track(bc, bc.token) + + return bc +} + +// Close closes all database connections and frees all resources associated with the backend. +func (bc *backendContract) Close() { + bc.b.Close() + + resource.Untrack(bc, bc.token) +} + +// Database returns a Database instance for the given name. +// +// The database does not need to exist; even parameters like name could be invalid. +func (bc *backendContract) Database(name string) Database { + return bc.b.Database(name) +} + +// ListDatabasesParams represents the parameters of Backend.ListDatabases method. +type ListDatabasesParams struct{} + +// ListDatabasesResult represents the results of Backend.ListDatabases method. +type ListDatabasesResult struct { + Databases []DatabaseInfo +} + +// DatabaseInfo represents information about a single database. +type DatabaseInfo struct { + Name string + Size int64 +} + +// ListDatabases returns a Database instance for given parameters. +func (bc *backendContract) ListDatabases(ctx context.Context, params *ListDatabasesParams) (res *ListDatabasesResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = bc.b.ListDatabases(ctx, params) + return +} + +// DropDatabaseParams represents the parameters of Backend.DropDatabase method. +type DropDatabaseParams struct { + Name string +} + +// DropDatabase drops existing database for given parameters. +func (bc *backendContract) DropDatabase(ctx context.Context, params *DropDatabaseParams) (err error) { + defer observability.FuncCall(ctx)() + defer checkError(err, ErrorCodeDatabaseDoesNotExist) + err = bc.b.DropDatabase(ctx, params) + + return +} + +// check interfaces +var ( + _ Backend = (*backendContract)(nil) +) diff --git a/internal/backends/collection.go b/internal/backends/collection.go new file mode 100644 index 000000000000..703d98673f60 --- /dev/null +++ b/internal/backends/collection.go @@ -0,0 +1,142 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backends + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/observability" +) + +// Collection is a generic interface for all backends for accessing collection. +// +// Collection object is expected to be stateless and temporary; +// all state should be in the Backend that created Database instance that created this Collection instance. +// Handler can create and destroy Collection objects on the fly. +// Creating a Collection object does not imply the creation of the database or collection. +// +// Collection methods should be thread-safe. +// +// See collectionContract and its methods for additional details. +type Collection interface { + Query(context.Context, *QueryParams) (*QueryResult, error) + Insert(context.Context, *InsertParams) (*InsertResult, error) + Update(context.Context, *UpdateParams) (*UpdateResult, error) + Delete(context.Context, *DeleteParams) (*DeleteResult, error) +} + +// collectionContract implements Collection interface. +type collectionContract struct { + c Collection +} + +// CollectionContract wraps Collection and enforces its contract. +// +// All backend implementations should use that function when they create new Collection instances. +// The handler should not use that function. +// +// See collectionContract and its methods for additional details. +func CollectionContract(c Collection) Collection { + return &collectionContract{ + c: c, + } +} + +// QueryParams represents the parameters of Collection.Query method. +type QueryParams struct { + // nothing for now - no pushdowns yet +} + +// QueryResult represents the results of Collection.Query method. +type QueryResult struct { + DocsIterator types.DocumentsIterator +} + +// Query executes a query against the collection. +func (cc *collectionContract) Query(ctx context.Context, params *QueryParams) (res *QueryResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = cc.c.Query(ctx, params) + + return +} + +// InsertParams represents the parameters of Collection.Insert method. +type InsertParams struct { + Docs *types.Array + Ordered bool +} + +// InsertResult represents the results of Collection.Insert method. +type InsertResult struct { + Errors []error + InsertedCount int64 +} + +// Insert inserts documents into the collection. +// +// Both database and collection may or may not exist; they should be created automatically if needed. +func (cc *collectionContract) Insert(ctx context.Context, params *InsertParams) (res *InsertResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = cc.c.Insert(ctx, params) + + return +} + +// UpdateParams represents the parameters of Collection.Update method. +type UpdateParams struct { + Docs *types.Array +} + +// UpdateResult represents the results of Collection.Update method. +type UpdateResult struct { + Updated int64 +} + +// Update updates documents in collection. +func (cc *collectionContract) Update(ctx context.Context, params *UpdateParams) (res *UpdateResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = cc.c.Update(ctx, params) + + return +} + +// DeleteParams represents the parameters of Collection.Delete method. +type DeleteParams struct { + Filter *types.Document + Limited bool +} + +// DeleteResult represents the results of Collection.Delete method. +type DeleteResult struct { + Deleted int64 +} + +// Delete deletes documents in collection. +func (cc *collectionContract) Delete(ctx context.Context, params *DeleteParams) (res *DeleteResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = cc.c.Delete(ctx, params) + + return +} + +// check interfaces +var ( + _ Collection = (*collectionContract)(nil) +) diff --git a/internal/backends/database.go b/internal/backends/database.go new file mode 100644 index 000000000000..5d561a1b3970 --- /dev/null +++ b/internal/backends/database.go @@ -0,0 +1,138 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backends + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/util/observability" + "github.com/FerretDB/FerretDB/internal/util/resource" +) + +// Database is a generic interface for all backends for accessing databases. +// +// Database object is expected to be mostly stateless and temporary; +// all state should be in the Backend that created this Database instance. +// Handler can create and destroy Database objects on the fly (but it should Close() them). +// Creating a Database object does not imply the creating of the database itself. +// +// Database methods should be thread-safe. +// +// See databaseContract and its methods for additional details. +type Database interface { + Close() + Collection(string) Collection + ListCollections(context.Context, *ListCollectionsParams) (*ListCollectionsResult, error) + CreateCollection(context.Context, *CreateCollectionParams) error + DropCollection(context.Context, *DropCollectionParams) error +} + +// databaseContract implements Database interface. +type databaseContract struct { + db Database + token *resource.Token +} + +// DatabaseContract wraps Database and enforces its contract. +// +// All backend implementations should use that function when they create new Database instances. +// The handler should not use that function. +// +// See databaseContract and its methods for additional details. +func DatabaseContract(db Database) Database { + dbc := &databaseContract{ + db: db, + token: resource.NewToken(), + } + resource.Track(dbc, dbc.token) + + return dbc +} + +// Close marks this Database instance as not being used anymore. +// The implementation may close an associated database connection, decrease a reference counter, etc. +func (dbc *databaseContract) Close() { + dbc.db.Close() + + resource.Untrack(dbc, dbc.token) +} + +// Collection returns a Collection instance for the given name. +// +// The collection (or database) does not need to exist; even parameters like name could be invalid. +func (dbc *databaseContract) Collection(name string) Collection { + return dbc.db.Collection(name) +} + +// ListCollectionsParams represents the parameters of Database.ListCollections method. +type ListCollectionsParams struct{} + +// ListCollectionsResult represents the results of Database.ListCollections method. +type ListCollectionsResult struct { + Collections []CollectionInfo +} + +// CollectionInfo represents information about a single collection. +type CollectionInfo struct { + Name string +} + +// ListCollections returns information about collections in the database. +// +// Database doesn't have to exist; that's not an error. +// +//nolint:lll // for readability +func (dbc *databaseContract) ListCollections(ctx context.Context, params *ListCollectionsParams) (res *ListCollectionsResult, err error) { + defer observability.FuncCall(ctx)() + defer checkError(err) + res, err = dbc.db.ListCollections(ctx, params) + + return +} + +// CreateCollectionParams represents the parameters of Database.CreateCollection method. +type CreateCollectionParams struct { + Name string +} + +// CreateCollection creates a new collection in the database; it should not already exist. +// +// Database may or may not exist; it should be created automatically if needed. +func (dbc *databaseContract) CreateCollection(ctx context.Context, params *CreateCollectionParams) (err error) { + defer observability.FuncCall(ctx)() + defer checkError(err, ErrorCodeCollectionAlreadyExists, ErrorCodeCollectionNameIsInvalid) + err = dbc.db.CreateCollection(ctx, params) + + return +} + +// DropCollectionParams represents the parameters of Database.DropCollection method. +type DropCollectionParams struct { + Name string +} + +// DropCollection drops existing collection in the database. +func (dbc *databaseContract) DropCollection(ctx context.Context, params *DropCollectionParams) (err error) { + defer observability.FuncCall(ctx)() + defer checkError(err, ErrorCodeCollectionDoesNotExist) + err = dbc.db.DropCollection(ctx, params) + + return +} + +// check interfaces +var ( + _ Database = (*databaseContract)(nil) +) diff --git a/internal/backends/doc.go b/internal/backends/doc.go new file mode 100644 index 000000000000..ded5361ef300 --- /dev/null +++ b/internal/backends/doc.go @@ -0,0 +1,35 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package backends provides common interfaces and code for all backend implementations. +// +// # Design principles. +// +// 1. Interfaces are relatively high-level and "fat". +// We are generally doing one backend interface call per handler call. +// For example, `insert` command handler calls only +// `db.Database("database").Collection("collection").Insert(ctx, params)` method that would +// create a database if needed, create a collection if needed, and insert all documents with correct parameters. +// There is no method to insert one document into an existing collection. +// That shifts some complexity from a single handler into multiple backend implementations; +// for example, support for `insert` with `ordered: true` and `ordered: false` should be implemented multiple times. +// But that allows those implementations to be much more effective. +// 2. Backend objects are stateful. +// Database objects are almost stateless but should be Close()'d to avoid connection leaks. +// Collection objects are fully stateless. +// 3. Contexts are per-operation and should not be stored. +// 4. Errors returned by methods could be nil, *Error, or some other opaque error type. +// Contracts enforce *Error codes; they are not documented in the code comments +// but are visible in the contract's code (to avoid duplication). +package backends diff --git a/internal/backends/error.go b/internal/backends/error.go new file mode 100644 index 000000000000..abe28168f1e5 --- /dev/null +++ b/internal/backends/error.go @@ -0,0 +1,129 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backends + +import ( + "fmt" + + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/util/debugbuild" +) + +//go:generate ../../bin/stringer -type ErrorCode + +// ErrorCode represent a backend error code. +type ErrorCode int + +// Error codes. +const ( + _ ErrorCode = iota + + ErrorCodeDatabaseDoesNotExist + + ErrorCodeCollectionDoesNotExist + ErrorCodeCollectionAlreadyExists + ErrorCodeCollectionNameIsInvalid +) + +// Error represents a backend error returned by all Backend, Database and Collection methods. +type Error struct { + // this internal error can't be accessed by the caller + err error + + code ErrorCode +} + +// NewError creates a new backend error. +func NewError(code ErrorCode, err error) *Error { + if code == 0 { + panic("backends.NewError: code must not be 0") + } + + // TODO we might allow nil error if needed + if err == nil { + panic("backends.NewError: err must not be nil") + } + + return &Error{ + code: code, + err: err, + } +} + +// Code returns the error code. +func (err *Error) Code() ErrorCode { + return err.code +} + +// There is intentionally no method to return the internal error. + +// Error implements error interface. +func (err *Error) Error() string { + return fmt.Sprintf("%s: %v", err.code, err.err) +} + +// ErrorCodeIs returns true if err is *Error with one of the given error codes. +func ErrorCodeIs(err error, codes ...ErrorCode) bool { + if len(codes) == 0 { + panic("zero error codes") + } + + e, ok := err.(*Error) //nolint:errorlint // do not inspect error chain + if !ok { + return false + } + + return slices.Contains(codes, e.code) +} + +// checkError enforces backend interfaces contracts. +// +// Err must be nil, *Error, or some other opaque error. +// If err is *Error, it must have one of the given error codes. +// If that's not the case, checkError panics in debug builds. +// +// It does nothing in non-debug builds. +func checkError(err error, codes ...ErrorCode) { + if !debugbuild.Enabled { + return + } + + if err == nil { + return + } + + e, ok := err.(*Error) //nolint:errorlint // do not inspect error chain + if !ok { + return + } + + if e.code == 0 { + panic(fmt.Sprintf("error code is 0: %v", err)) + } + + if len(codes) == 0 { + panic(fmt.Sprintf("no allowed error codes: %v", err)) + } + + if !slices.Contains(codes, e.code) { + panic(fmt.Sprintf("error code is not in %v: %v", codes, err)) + } +} + +// check interfaces +var ( + _ error = (*Error)(nil) +) diff --git a/internal/backends/error_test.go b/internal/backends/error_test.go new file mode 100644 index 000000000000..4c9175674e96 --- /dev/null +++ b/internal/backends/error_test.go @@ -0,0 +1,44 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package backends + +import ( + "io" + "io/fs" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + t.Parallel() + + pe := &fs.PathError{ + Op: "open", + Path: "database.db", + Err: io.EOF, + } + err := NewError(ErrorCodeCollectionDoesNotExist, pe) + + assert.NotErrorIs(t, err, pe, "internal error should be hidden") + assert.NotErrorIs(t, err, io.EOF, "internal error should be hidden") + + var e *Error + assert.ErrorAs(t, err, &e) + assert.Equal(t, ErrorCodeCollectionDoesNotExist, e.code) + assert.Equal(t, pe, e.err) + + assert.Equal(t, `ErrorCodeCollectionDoesNotExist: open database.db: EOF`, err.Error()) +} diff --git a/internal/backends/errorcode_string.go b/internal/backends/errorcode_string.go new file mode 100644 index 000000000000..e04ad00dd5a1 --- /dev/null +++ b/internal/backends/errorcode_string.go @@ -0,0 +1,27 @@ +// Code generated by "stringer -type ErrorCode"; DO NOT EDIT. + +package backends + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ErrorCodeDatabaseDoesNotExist-1] + _ = x[ErrorCodeCollectionDoesNotExist-2] + _ = x[ErrorCodeCollectionAlreadyExists-3] + _ = x[ErrorCodeCollectionNameIsInvalid-4] +} + +const _ErrorCode_name = "ErrorCodeDatabaseDoesNotExistErrorCodeCollectionDoesNotExistErrorCodeCollectionAlreadyExistsErrorCodeCollectionNameIsInvalid" + +var _ErrorCode_index = [...]uint8{0, 29, 60, 92, 124} + +func (i ErrorCode) String() string { + i -= 1 + if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) { + return "ErrorCode(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] +} diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go new file mode 100644 index 000000000000..edaaef8c287f --- /dev/null +++ b/internal/backends/sqlite/backend.go @@ -0,0 +1,95 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "os" + "path/filepath" + + _ "modernc.org/sqlite" + + "github.com/FerretDB/FerretDB/internal/backends" +) + +// backend implements backends.Backend interface. +type backend struct { + dir string + pool *connPool + metadataStorage *metadataStorage +} + +// NewBackendParams represents the parameters of NewBackend function. +type NewBackendParams struct { + Dir string +} + +// NewBackend creates a new SQLite backend. +func NewBackend(params *NewBackendParams) (backends.Backend, error) { + pool := newConnPool(params.Dir) + + storage, err := newMetadataStorage(params.Dir, pool) + if err != nil { + return nil, err + } + + return backends.BackendContract(&backend{ + dir: params.Dir, + pool: pool, + metadataStorage: storage, + }), nil +} + +// Close implements backends.Backend interface. +func (b *backend) Close() { + b.pool.Close() +} + +// Database implements backends.Backend interface. +func (b *backend) Database(name string) backends.Database { + return newDatabase(b, name) +} + +// ListDatabases implements backends.Backend interface. +// +//nolint:lll // for readability +func (b *backend) ListDatabases(ctx context.Context, params *backends.ListDatabasesParams) (*backends.ListDatabasesResult, error) { + list, err := b.metadataStorage.listDatabases() + if err != nil { + return nil, err + } + + var result backends.ListDatabasesResult + for _, db := range list { + result.Databases = append(result.Databases, backends.DatabaseInfo{Name: db}) + } + + return &result, nil +} + +// DropDatabase implements backends.Backend interface. +func (b *backend) DropDatabase(ctx context.Context, params *backends.DropDatabaseParams) error { + err := os.Remove(filepath.Join(b.dir, params.Name+dbExtension)) + if os.IsNotExist(err) { + return backends.NewError(backends.ErrorCodeDatabaseDoesNotExist, err) + } + + return err +} + +// check interfaces +var ( + _ backends.Backend = (*backend)(nil) +) diff --git a/internal/backends/sqlite/collection.go b/internal/backends/sqlite/collection.go new file mode 100644 index 000000000000..5697241447d2 --- /dev/null +++ b/internal/backends/sqlite/collection.go @@ -0,0 +1,336 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "database/sql" + "errors" + "fmt" + "strings" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/sjson" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +// collection implements backends.Collection interface. +type collection struct { + db *database + name string +} + +// newDatabase creates a new Collection. +func newCollection(db *database, name string) backends.Collection { + return backends.CollectionContract(&collection{ + db: db, + name: name, + }) +} + +// Query implements backends.Collection interface. +func (c *collection) Query(ctx context.Context, params *backends.QueryParams) (*backends.QueryResult, error) { + conn, err := c.db.b.pool.DB(c.db.name) + if err != nil { + return nil, err + } + + table, err := c.db.b.metadataStorage.tableName(ctx, c.db.name, c.name) + if err != nil { + return nil, err + } + + query := fmt.Sprintf(`SELECT sjson FROM "%s"`, table) + + rows, err := conn.QueryContext(ctx, query) + if err != nil { + return nil, err + } + + return &backends.QueryResult{ + DocsIterator: newQueryIterator(ctx, rows, &queryIteratorParams{unmarshal: sjson.Unmarshal}), + }, nil +} + +// Insert implements backends.Collection interface. +func (c *collection) Insert(ctx context.Context, params *backends.InsertParams) (*backends.InsertResult, error) { + err := c.db.CreateCollection(ctx, &backends.CreateCollectionParams{Name: c.name}) + if err != nil { + return nil, err + } + + conn, err := c.db.b.pool.DB(c.db.name) + if err != nil { + return nil, err + } + + table, err := c.db.b.metadataStorage.tableName(ctx, c.db.name, c.name) + if err != nil { + return nil, err + } + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + + defer func() { + if err != nil { + // TODO: check error + tx.Rollback() + } + }() + + var inserted int64 + + iter := params.Docs.Iterator() + defer iter.Close() + + for { + var val any + + _, val, err = iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return nil, err + } + + doc, ok := val.(*types.Document) + if !ok { + return nil, lazyerrors.Errorf("expected document, got %T", val) + } + + query := fmt.Sprintf(`INSERT INTO "%s" (sjson) VALUES (?)`, table) + + var bytes []byte + + bytes, err = sjson.Marshal(doc) + if err != nil { + return nil, err + } + + _, err = tx.ExecContext(ctx, query, bytes) + if err != nil { + return nil, err + } + + inserted++ + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + return &backends.InsertResult{ + InsertedCount: inserted, + Errors: []error{}, + }, nil +} + +// Update implements backends.Collection interface. +func (c *collection) Update(ctx context.Context, params *backends.UpdateParams) (*backends.UpdateResult, error) { + var err error + + conn, err := c.db.b.pool.DB(c.db.name) + if err != nil { + return nil, err + } + + table, err := c.db.b.metadataStorage.tableName(ctx, c.db.name, c.name) + if err != nil { + return nil, err + } + + query := `UPDATE "%s" SET sjson = '%s' WHERE json_extract(sjson, '$._id') = '%s'` + + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + + var committed bool + + defer func() { + if committed { + return + } + + if rerr := tx.Rollback(); rerr != nil { + if err == nil { + err = rerr + } + } + }() + + iter := params.Docs.Iterator() + defer iter.Close() + + var updated int64 + + for { + var val any + + _, val, err = iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return nil, err + } + + doc, ok := val.(*types.Document) + if !ok { + panic(fmt.Sprintf("expected document, got %T", val)) + } + + id := must.NotFail(doc.Get("_id")) + idBytes := strings.ReplaceAll(string(must.NotFail(sjson.MarshalSingleValue(id))), `"`, "") + docBytes := must.NotFail(sjson.Marshal(doc)) + + var res sql.Result + + res, err = tx.ExecContext(ctx, fmt.Sprintf(query, table, docBytes, idBytes)) + if err != nil { + return nil, err + } + + var rowsUpdated int64 + + rowsUpdated, err = res.RowsAffected() + if err != nil { + return nil, err + } + + updated += rowsUpdated + } + + err = tx.Commit() + if err != nil { + return nil, err + } + + committed = true + + return &backends.UpdateResult{ + Updated: updated, + }, nil +} + +// Delete implements backends.Collection interface. +func (c *collection) Delete(ctx context.Context, params *backends.DeleteParams) (*backends.DeleteResult, error) { + conn, err := c.db.b.pool.DB(c.db.name) + if err != nil { + return nil, err + } + + res, err := c.Query(ctx, nil) + if err != nil { + return nil, err + } + + iter := res.DocsIterator + + resDocs := make([]*types.Document, 0, 16) + + for { + var doc *types.Document + + if _, doc, err = iter.Next(); err != nil { + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + return nil, err + } + + var matches bool + + if matches, err = common.FilterDocument(doc, params.Filter); err != nil { + return nil, err + } + + if !matches { + continue + } + + resDocs = append(resDocs, doc) + + // if limit is set, no need to fetch all the documents + if params.Limited { + break + } + } + + iter.Close() + + // if no documents matched, there is nothing to delete + if len(resDocs) == 0 { + return new(backends.DeleteResult), nil + } + + rowsDeleted, err := c.deleteDocuments(ctx, conn, resDocs) + if err != nil { + return nil, err + } + + return &backends.DeleteResult{ + Deleted: rowsDeleted, + }, nil +} + +func (c *collection) deleteDocuments(ctx context.Context, db *sql.DB, docs []*types.Document) (int64, error) { + var deleted int64 + var ids []any + + for _, doc := range docs { + id := must.NotFail(doc.Get("_id")) + + ids = append(ids, strings.ReplaceAll(string(must.NotFail(sjson.MarshalSingleValue(id))), `"`, ``)) + } + + query := fmt.Sprintf( + "DELETE FROM %s WHERE json_extract(sjson, '$._id') IN (?%s)", + c.name, + strings.Repeat(", ?", len(ids)-1), + ) + + res, err := db.ExecContext(ctx, query, ids...) + if err != nil { + return 0, err + } + + d, err := res.RowsAffected() + if err != nil { + return 0, err + } + + deleted += d + + return deleted, nil +} + +// check interfaces +var ( + _ backends.Collection = (*collection)(nil) +) diff --git a/internal/backends/sqlite/database.go b/internal/backends/sqlite/database.go new file mode 100644 index 000000000000..58c3350a9f71 --- /dev/null +++ b/internal/backends/sqlite/database.go @@ -0,0 +1,161 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" +) + +// database implements backends.Database interface. +type database struct { + b *backend + name string +} + +// newDatabase creates a new Database. +func newDatabase(b *backend, name string) backends.Database { + return backends.DatabaseContract(&database{ + b: b, + name: name, + }) +} + +// Close implements backends.Database interface. +func (db *database) Close() { + db.b.pool.CloseDB(db.name) // TODO: Implement +} + +// Collection implements backends.Database interface. +func (db *database) Collection(name string) backends.Collection { + return newCollection(db, name) +} + +// ListCollections implements backends.Database interface. +// +//nolint:lll // for readability +func (db *database) ListCollections(ctx context.Context, params *backends.ListCollectionsParams) (*backends.ListCollectionsResult, error) { + var result backends.ListCollectionsResult + + exists, err := db.b.metadataStorage.dbExists(db.name) + if err != nil { + return nil, lazyerrors.Error(err) + } + + if !exists { + return &result, nil + } + + list, err := db.b.metadataStorage.listCollections(ctx, db.name) + if err != nil { + return nil, err + } + + for _, name := range list { + result.Collections = append(result.Collections, backends.CollectionInfo{ + Name: name, + }) + } + + return &result, nil +} + +// CreateCollection implements backends.Database interface. +func (db *database) CreateCollection(ctx context.Context, params *backends.CreateCollectionParams) error { + exists, err := db.b.metadataStorage.dbExists(db.name) + if err != nil { + return lazyerrors.Error(err) + } + + if !exists { + if err = db.create(ctx); err != nil { + return lazyerrors.Error(err) + } + } + + tableName, err := db.b.metadataStorage.createCollection(ctx, db.name, params.Name) + if err != nil { + return err + } + + conn, err := db.b.pool.DB(db.name) + if err != nil { + return err + } + + query := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS "%s" (sjson string)`, tableName) + + _, err = conn.ExecContext(ctx, query) + if err != nil { + return err + } + + return nil +} + +// DropCollection implements backends.Database interface. +func (db *database) DropCollection(ctx context.Context, params *backends.DropCollectionParams) error { + table, err := db.b.metadataStorage.tableName(ctx, db.name, params.Name) + if err != nil { + return err + } + + err = db.b.metadataStorage.removeCollection(ctx, db.name, params.Name) + if err != nil { + return err + } + + conn, err := db.b.pool.DB(db.name) + if err != nil { + return err + } + + query := fmt.Sprintf("DROP TABLE %s", table) + + _, err = conn.ExecContext(ctx, query) + if err != nil { + return err + } + + return nil +} + +func (db *database) create(ctx context.Context) error { + f, err := os.Create(filepath.Join(db.b.dir, db.name+dbExtension)) + if err != nil { + return err + } + + if err = f.Close(); err != nil { + return err + } + + err = db.b.metadataStorage.createDatabase(ctx, db.name) + if err != nil { + return err + } + + return nil +} + +// check interfaces +var ( + _ backends.Database = (*database)(nil) +) diff --git a/internal/backends/sqlite/metadata_storage.go b/internal/backends/sqlite/metadata_storage.go new file mode 100644 index 000000000000..937e294a0c32 --- /dev/null +++ b/internal/backends/sqlite/metadata_storage.go @@ -0,0 +1,294 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/handlers/sjson" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +const ( + // Reserved prefix for database and collection names. + reservedPrefix = "_ferretdb_" + + // Database metadata table name. + dbMetadataTableName = reservedPrefix + "database_metadata" + + dbExtension = ".sqlite" +) + +var ( + errDatabaseNotFound = errors.New("database not found") + errCollectionNotFound = errors.New("collection not found") +) + +// newMetadataStorage checks that dir is not empty, +// creates instance of metadataStorage and populates it with databases info. +func newMetadataStorage(dbPath string, pool *connPool) (*metadataStorage, error) { + if dbPath == "" { + return nil, errors.New("db path is empty") + } + + if err := os.MkdirAll(dbPath, 0o777); err != nil { + return nil, lazyerrors.Error(err) + } + + storage := metadataStorage{ + connPool: pool, + dir: dbPath, + } + + _, err := storage.listDatabases() + if err != nil { + return nil, err + } + + return &storage, nil +} + +// metadataStorage provide access to database metadata. +// It uses connection pool to load and store metadata. +type metadataStorage struct { //nolint:vet // for readability + dir string + connPool *connPool +} + +// listDatabases list database names. +func (m *metadataStorage) listDatabases() ([]string, error) { + var dbs []string + + err := filepath.WalkDir(m.dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if strings.Contains(d.Name(), dbExtension) { + dbName, _ := strings.CutSuffix(d.Name(), dbExtension) + + dbs = append(dbs, dbName) + } + + return nil + }) + if err != nil { + return nil, err + } + + return dbs, nil +} + +// listCollections list collection names for given database. +func (m *metadataStorage) listCollections(ctx context.Context, database string) ([]string, error) { + conn, err := m.connPool.DB(database) + if err != nil { + return nil, err + } + + query := fmt.Sprintf("SELECT sjson FROM %s", dbMetadataTableName) + + rows, err := conn.QueryContext(ctx, query) + if err != nil { + return nil, err + } + // TODO: check error + defer rows.Close() + + var collections []string + + for rows.Next() { + var rawBytes []byte + + err = rows.Scan(&rawBytes) + if err != nil { + return nil, err + } + + doc, err := sjson.Unmarshal(rawBytes) + if err != nil { + return nil, errors.New("failed to unmarshal collection metadata") + } + + // TODO: proper errors check + collName := must.NotFail(doc.Get("collection")).(string) + + collections = append(collections, collName) + } + + return collections, nil +} + +// createDatabase adds database to metadata storage. +// It doesn't create database file. +func (m *metadataStorage) createDatabase(ctx context.Context, database string) error { + exists, err := m.dbExists(database) + if err != nil { + return err + } + + if !exists { + return errDatabaseNotFound + } + + conn, err := m.connPool.DB(database) + if err != nil { + return err + } + + query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (sjson TEXT)", dbMetadataTableName) + + _, err = conn.ExecContext(ctx, query) + if err != nil { + return err + } + + return nil +} + +// createCollection saves collection metadata to database file. +func (m *metadataStorage) createCollection(ctx context.Context, database, collection string) (string, error) { + tableName := collection + + conn, err := m.connPool.DB(database) + if err != nil { + return "", err + } + + query := fmt.Sprintf("INSERT INTO %s (sjson) VALUES (?)", dbMetadataTableName) + + bytes, err := sjson.Marshal(must.NotFail(types.NewDocument("collection", collection, "table", tableName))) + if err != nil { + return "", err + } + + _, err = conn.ExecContext(ctx, query, bytes) + if err != nil { + return "", err + } + + return tableName, nil +} + +// tableName returns table name for given database name and collection name. +func (m *metadataStorage) tableName(ctx context.Context, dbName, collName string) (string, error) { + exists, err := m.dbExists(dbName) + if err != nil { + return "", err + } + + if !exists { + return "", errDatabaseNotFound + } + + conn, err := m.connPool.DB(dbName) + if err != nil { + return "", err + } + + query := fmt.Sprintf("SELECT sjson FROM %s WHERE json_extract(sjson, '$.collection') = ?", dbMetadataTableName) + + rows, err := conn.QueryContext(ctx, query, collName) + if err != nil { + return "", err + } + defer rows.Close() + + var bytes []byte + + if !rows.Next() { + return "", errCollectionNotFound + } + + err = rows.Scan(&bytes) + if err != nil { + return "", err + } + + doc, err := sjson.Unmarshal(bytes) + if err != nil { + return "", err + } + + // TODO: proper error handling + table := must.NotFail(doc.Get("table")).(string) + + return table, nil +} + +// removeCollection removes collection metadata. +// It does not remove collection from database. +func (m *metadataStorage) removeCollection(ctx context.Context, database, collection string) error { + exists, err := m.dbExists(database) + if err != nil { + return err + } + + if !exists { + return errDatabaseNotFound + } + + colls, err := m.listCollections(ctx, database) + if err != nil { + return err + } + + // collection not found, nothing to do + if !slices.Contains(colls, collection) { + return nil + } + + conn, err := m.connPool.DB(database) + if err != nil { + return err + } + + query := fmt.Sprintf("DELETE FROM %s WHERE json_extract(sjson, '$.collection') = ?", dbMetadataTableName) + + res, err := conn.ExecContext(ctx, query, collection) + if err != nil { + return err + } + + deleted, err := res.RowsAffected() + if err != nil { + return err + } + + if deleted != 1 { + return errors.New("failed to remove collection") + } + + return nil +} + +func (m *metadataStorage) dbExists(database string) (bool, error) { + dbs, err := m.listDatabases() + if err != nil { + return false, err + } + + return slices.Contains(dbs, database), nil +} diff --git a/internal/backends/sqlite/pool.go b/internal/backends/sqlite/pool.go new file mode 100644 index 000000000000..952b6cdf18b2 --- /dev/null +++ b/internal/backends/sqlite/pool.go @@ -0,0 +1,105 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "database/sql" + "errors" + "path/filepath" + "sync" + + "github.com/FerretDB/FerretDB/internal/util/resource" +) + +// newConnPool creates a new connection pool. +func newConnPool(dir string) *connPool { + pool := &connPool{ + mx: sync.Mutex{}, + dbs: map[string]*sql.DB{}, + token: resource.NewToken(), + dir: dir, + } + + resource.Track(pool, pool.token) + + return pool +} + +// connPool is a pool of database connections. +type connPool struct { //nolint:vet // for readability + mx sync.Mutex + dbs map[string]*sql.DB + + token *resource.Token + dir string +} + +// DB returns a database connection for the given name. +func (c *connPool) DB(name string) (*sql.DB, error) { + c.mx.Lock() + defer c.mx.Unlock() + + path := filepath.Join(c.dir, name+dbExtension) + + if db, ok := c.dbs[path]; ok { + return db, nil + } + + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, err + } + + c.dbs[path] = db + + return db, nil +} + +// Close closes all database connections. +func (c *connPool) Close() error { + var errs error + + c.mx.Lock() + defer c.mx.Unlock() + + for _, conn := range c.dbs { + if err := conn.Close(); err != nil { + errors.Join(err) + } + } + + resource.Untrack(c, c.token) + + return errs +} + +// CloseDB closes a database connection for the given name. +func (c *connPool) CloseDB(name string) { + c.mx.Lock() + defer c.mx.Unlock() + + path := filepath.Join(c.dir, name+dbExtension) + + db, ok := c.dbs[path] + if !ok { + return + } + + if err := db.Close(); err != nil { + errors.Join(err) + } + + delete(c.dbs, path) +} diff --git a/internal/backends/sqlite/query_iterator.go b/internal/backends/sqlite/query_iterator.go new file mode 100644 index 000000000000..f3af5bf632e6 --- /dev/null +++ b/internal/backends/sqlite/query_iterator.go @@ -0,0 +1,139 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "database/sql" + "sync" + + "github.com/FerretDB/FerretDB/internal/handlers/sjson" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/resource" +) + +// queryIterator implements iterator.Interface to fetch documents from the database. +type queryIterator struct { //nolint:vet // for readability + ctx context.Context + unmarshal func(b []byte) (*types.Document, error) // defaults to sjson.Unmarshal + + m sync.Mutex + rows *sql.Rows + + token *resource.Token +} + +type queryIteratorParams struct { + unmarshal func(b []byte) (*types.Document, error) // defaults to sjson.Unmarshal +} + +// newQueryIterator returns a new queryIterator for the given pgx.Rows. +// +// Iterator's Close method closes rows. +// +// Nil rows are possible and return already done iterator. +func newQueryIterator(ctx context.Context, rows *sql.Rows, params *queryIteratorParams) types.DocumentsIterator { + unmarshalFunc := params.unmarshal + if unmarshalFunc == nil { + unmarshalFunc = sjson.Unmarshal + } + + iter := &queryIterator{ + ctx: ctx, + unmarshal: unmarshalFunc, + rows: rows, + token: resource.NewToken(), + } + resource.Track(iter, iter.token) + + return iter +} + +// Next implements iterator.Interface. +// +// Errors (possibly wrapped) are: +// - iterator.ErrIteratorDone; +// - context.Canceled; +// - context.DeadlineExceeded; +// - something else. +// +// Otherwise, as the first value it returns the number of the current iteration (starting from 0), +// as the second value it returns the document. +func (iter *queryIterator) Next() (struct{}, *types.Document, error) { + iter.m.Lock() + defer iter.m.Unlock() + + var unused struct{} + + // ignore context error, if any, if iterator is already closed + if iter.rows == nil { + return unused, nil, iterator.ErrIteratorDone + } + + if err := context.Cause(iter.ctx); err != nil { + return unused, nil, lazyerrors.Error(err) + } + + if !iter.rows.Next() { + if err := iter.rows.Err(); err != nil { + return unused, nil, lazyerrors.Error(err) + } + + // to avoid context cancellation changing the next `Next()` error + // from `iterator.ErrIteratorDone` to `context.Canceled` + iter.close() + + return unused, nil, iterator.ErrIteratorDone + } + + var b []byte + if err := iter.rows.Scan(&b); err != nil { + return unused, nil, lazyerrors.Error(err) + } + + doc, err := iter.unmarshal(b) + if err != nil { + return unused, nil, lazyerrors.Error(err) + } + + return unused, doc, nil +} + +// Close implements iterator.Interface. +func (iter *queryIterator) Close() { + iter.m.Lock() + defer iter.m.Unlock() + + iter.close() +} + +// close closes iterator without holding mutex. +// +// This should be called only when the caller already holds the mutex. +func (iter *queryIterator) close() { + if iter.rows != nil { + iter.rows.Close() + iter.rows = nil + } + + resource.Untrack(iter, iter.token) +} + +// check interfaces +var ( + _ types.DocumentsIterator = (*queryIterator)(nil) +) diff --git a/internal/backends/sqlite/sqlite.go b/internal/backends/sqlite/sqlite.go new file mode 100644 index 000000000000..30a0e41707c5 --- /dev/null +++ b/internal/backends/sqlite/sqlite.go @@ -0,0 +1,16 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package sqlite provides SQLite backend. +package sqlite diff --git a/internal/handlers/dummy/dummy_test.go b/internal/backends/sqlite/sqlite_test.go similarity index 97% rename from internal/handlers/dummy/dummy_test.go rename to internal/backends/sqlite/sqlite_test.go index ffe0973c2a55..94c2c9d3af10 100644 --- a/internal/handlers/dummy/dummy_test.go +++ b/internal/backends/sqlite/sqlite_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import "testing" diff --git a/internal/clientconn/conn.go b/internal/clientconn/conn.go index d317fc293aa6..70e59a1b4294 100644 --- a/internal/clientconn/conn.go +++ b/internal/clientconn/conn.go @@ -38,7 +38,7 @@ import ( "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" "github.com/FerretDB/FerretDB/internal/clientconn/connmetrics" "github.com/FerretDB/FerretDB/internal/handlers" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/handlers/proxy" "github.com/FerretDB/FerretDB/internal/types" @@ -329,11 +329,22 @@ func (c *conn) run(ctx context.Context) (err error) { return } + // resBody can be nil if we got a message we could not handle at all, like unsupported OpQuery. + var resBodyString, proxyBodyString string + + if resBody != nil { + resBodyString = resBody.String() + } + + if proxyBody != nil { + proxyBodyString = proxyBody.String() + } + var diffBody string diffBody, err = difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(resBody.String()), + A: difflib.SplitLines(resBodyString), FromFile: "res body", - B: difflib.SplitLines(proxyBody.String()), + B: difflib.SplitLines(proxyBodyString), ToFile: "proxy body", Context: 1, }) @@ -373,6 +384,8 @@ func (c *conn) run(ctx context.Context) (err error) { // // Handlers to which it routes, should not panic on bad input, but may do so in "impossible" cases. // They also should not use recover(). That allows us to use fuzzing. +// +// Returned resBody can be nil. func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wire.MsgBody) (resHeader *wire.MsgHeader, resBody wire.MsgBody, closeConn bool) { //nolint:lll // argument list is too long var command, result, argument string defer func() { @@ -400,7 +413,7 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir resHeader.OpCode = wire.OpCodeMsg if err == nil { - // do not store typed nil in interface, it make it non-nil + // do not store typed nil in interface, it makes it non-nil var resMsg *wire.OpMsg resMsg, err = c.handleOpMsg(ctx, msg, command) @@ -414,7 +427,7 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir query := reqBody.(*wire.OpQuery) resHeader.OpCode = wire.OpCodeReply - // do not store typed nil in interface, it make it non-nil + // do not store typed nil in interface, it makes it non-nil var resReply *wire.OpReply resReply, err = c.h.CmdQuery(ctx, query) @@ -488,7 +501,8 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir // do not panic to make fuzzing easier closeConn = true result = "unhandled" - c.l.Error( + + c.l.Desugar().Error( "Handler error for unhandled response opcode", zap.Error(err), zap.Stringer("opcode", resHeader.OpCode), ) @@ -498,7 +512,8 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir // do not panic to make fuzzing easier closeConn = true result = "unexpected" - c.l.Error( + + c.l.Desugar().Error( "Handler error for unexpected response opcode", zap.Error(err), zap.Stringer("opcode", resHeader.OpCode), ) @@ -526,7 +541,7 @@ func (c *conn) route(ctx context.Context, reqHeader *wire.MsgHeader, reqBody wir } func (c *conn) handleOpMsg(ctx context.Context, msg *wire.OpMsg, command string) (*wire.OpMsg, error) { - if cmd, ok := common.Commands[command]; ok { + if cmd, ok := commoncommands.Commands[command]; ok { if cmd.Handler != nil { // TODO move it to route, closer to Prometheus metrics defer trace.StartRegion(ctx, command).End() diff --git a/internal/clientconn/conninfo/conn_info.go b/internal/clientconn/conninfo/conn_info.go index 0119ebb398c5..33ff235b5b16 100644 --- a/internal/clientconn/conninfo/conn_info.go +++ b/internal/clientconn/conninfo/conn_info.go @@ -44,7 +44,6 @@ func NewConnInfo() *ConnInfo { connInfo := &ConnInfo{ token: resource.NewToken(), } - resource.Track(connInfo, connInfo.token) return connInfo diff --git a/internal/clientconn/cursor/cursor.go b/internal/clientconn/cursor/cursor.go index 28eb0d847eea..b946c3b35b5f 100644 --- a/internal/clientconn/cursor/cursor.go +++ b/internal/clientconn/cursor/cursor.go @@ -41,7 +41,6 @@ func New(params *NewParams) *cursor { NewParams: params, token: resource.NewToken(), } - resource.Track(c, c.token) return c diff --git a/internal/types/expression.go b/internal/handlers/common/aggregations/expression.go similarity index 64% rename from internal/types/expression.go rename to internal/handlers/common/aggregations/expression.go index 4f45765a755f..eedad104b45e 100644 --- a/internal/types/expression.go +++ b/internal/handlers/common/aggregations/expression.go @@ -12,31 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -package types +package aggregations import ( "strings" + "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" ) -//go:generate ../../bin/stringer -linecomment -type ExpressionErrorCode +//go:generate ../../../../bin/stringer -linecomment -type ExpressionErrorCode -// ExpressionErrorCode represents FieldPath error code. +// ExpressionErrorCode represents Expression error code. type ExpressionErrorCode int const ( _ ExpressionErrorCode = iota - // ErrNotFieldPath indicates that field is not a path. - ErrNotFieldPath + // ErrNotExpression indicates that field is not an expression. + ErrNotExpression - // ErrEmptyFieldPath indicates that path is empty. - ErrEmptyFieldPath + // ErrInvalidExpression indicates that expression is invalid. + ErrInvalidExpression - // ErrInvalidFieldPath indicates that path is invalid. - ErrInvalidFieldPath + // ErrEmptyFieldPath indicates that field path expression is empty. + ErrEmptyFieldPath // ErrUndefinedVariable indicates that variable name is not defined. ErrUndefinedVariable @@ -45,36 +46,30 @@ const ( ErrEmptyVariable ) -// FieldPathError describes an error that occurs getting path from field. -type FieldPathError struct { +// ExpressionError describes an error that occurs while evaluating expression. +type ExpressionError struct { code ExpressionErrorCode } -// newFieldPathError creates a new FieldPathError. -func newFieldPathError(code ExpressionErrorCode) error { - return &FieldPathError{code: code} +// newExpressionError creates a new ExpressionError. +func newExpressionError(code ExpressionErrorCode) error { + return &ExpressionError{code: code} } // Error implements the error interface. -func (e *FieldPathError) Error() string { +func (e *ExpressionError) Error() string { return e.code.String() } -// Code returns the FieldPathError code. -func (e *FieldPathError) Code() ExpressionErrorCode { +// Code returns the ExpressionError code. +func (e *ExpressionError) Code() ExpressionErrorCode { return e.code } // Expression is an expression constructed from field value. -type Expression interface { - Evaluate(doc *Document) any - GetExpressionSuffix() string -} - -// pathExpression is field path constructed from expression. -type pathExpression struct { - path Path +type Expression struct { *ExpressionOpts + path types.Path } // ExpressionOpts represents options used to modify behavior of Expression functions. @@ -88,7 +83,7 @@ type ExpressionOpts struct { // NewExpressionWithOpts creates a new instance by checking expression string. // It can take additional opts that specify how expressions should be evaluated. -func NewExpressionWithOpts(expression string, opts *ExpressionOpts) (Expression, error) { +func NewExpressionWithOpts(expression string, opts *ExpressionOpts) (*Expression, error) { // TODO https://github.com/FerretDB/FerretDB/issues/2348 var val string @@ -97,54 +92,55 @@ func NewExpressionWithOpts(expression string, opts *ExpressionOpts) (Expression, // `$$` indicates field is a variable. v := strings.TrimPrefix(expression, "$$") if v == "" { - return nil, newFieldPathError(ErrEmptyVariable) + return nil, newExpressionError(ErrEmptyVariable) } if strings.HasPrefix(v, "$") { - return nil, newFieldPathError(ErrInvalidFieldPath) + return nil, newExpressionError(ErrInvalidExpression) } // TODO https://github.com/FerretDB/FerretDB/issues/2275 - return nil, newFieldPathError(ErrUndefinedVariable) + return nil, newExpressionError(ErrUndefinedVariable) case strings.HasPrefix(expression, "$"): // `$` indicates field is a path. val = strings.TrimPrefix(expression, "$") if val == "" { - return nil, newFieldPathError(ErrEmptyFieldPath) + return nil, newExpressionError(ErrEmptyFieldPath) } default: - return nil, newFieldPathError(ErrNotFieldPath) + return nil, newExpressionError(ErrNotExpression) } var err error - path, err := NewPathFromString(val) + path, err := types.NewPathFromString(val) if err != nil { return nil, lazyerrors.Error(err) } - return &pathExpression{ + return &Expression{ path: path, ExpressionOpts: opts, }, nil } // NewExpression creates a new instance by checking expression string. -func NewExpression(expression string) (Expression, error) { +func NewExpression(expression string) (*Expression, error) { // TODO https://github.com/FerretDB/FerretDB/issues/2348 return NewExpressionWithOpts(expression, new(ExpressionOpts)) } // Evaluate gets the value at the path. -func (p *pathExpression) Evaluate(doc *Document) any { - path := p.path +// It returns `types.Null` if the path does not exists. +func (e *Expression) Evaluate(doc *types.Document) any { + path := e.path if path.Len() == 1 { val, err := doc.Get(path.String()) if err != nil { - // if the path does not exist, return nil. - return Null + // $group stage groups non-existent paths with `Null` + return types.Null } return val @@ -154,20 +150,21 @@ func (p *pathExpression) Evaluate(doc *Document) any { prefix := path.Prefix() if v, err := doc.Get(prefix); err == nil { - if _, isArray := v.(*Array); isArray { + if _, isArray := v.(*types.Array); isArray { isPrefixArray = true } } - vals := p.getExpressionPathValue(doc, path) + vals := e.getPathValue(doc, path) if len(vals) == 0 { if isPrefixArray { // when the prefix is array, return empty array. - return must.NotFail(NewArray()) + return must.NotFail(types.NewArray()) } - return Null + // $group stage groups non-existent paths with `Null` + return types.Null } if len(vals) == 1 && !isPrefixArray { @@ -176,7 +173,7 @@ func (p *pathExpression) Evaluate(doc *Document) any { } // when the prefix is array, return an array of value. - arr := MakeArray(len(vals)) + arr := types.MakeArray(len(vals)) for _, v := range vals { arr.Append(v) } @@ -185,22 +182,22 @@ func (p *pathExpression) Evaluate(doc *Document) any { } // GetExpressionSuffix returns suffix of pathExpression. -func (p *pathExpression) GetExpressionSuffix() string { - return p.path.Suffix() +func (e *Expression) GetExpressionSuffix() string { + return e.path.Suffix() } -// getExpressionPathValue go through each key of the path iteratively to +// getPathValue go through each key of the path iteratively to // find values that exist at suffix. // An array may return multiple values. // At each key of the path, it checks: -// - if the document has the key. -// - if the array contains documents which have the key. (This check can -// be disabled by setting ExpressionOpts.IgnoreArrays field). +// - if the document has the key. +// - if the array contains documents which have the key. (This check can +// be disabled by setting ExpressionOpts.IgnoreArrays field). // -// It is different from `getDocumentsAtSuffix`, it does not find array item by +// It is different from `common.getDocumentsAtSuffix`, it does not find array item by // array dot notation `foo.0.bar`. It returns empty array [] because using index // such as `0` does not match using expression path. -func (p *pathExpression) getExpressionPathValue(doc *Document, path Path) []any { +func (e *Expression) getPathValue(doc *types.Document, path types.Path) []any { // TODO https://github.com/FerretDB/FerretDB/issues/2348 keys := path.Slice() vals := []any{doc} @@ -211,22 +208,22 @@ func (p *pathExpression) getExpressionPathValue(doc *Document, path Path) []any for _, valAtKey := range vals { switch val := valAtKey.(type) { - case *Document: + case *types.Document: embeddedVal, err := val.Get(key) if err != nil { continue } embeddedVals = append(embeddedVals, embeddedVal) - case *Array: - if p.IgnoreArrays { + case *types.Array: + if e.IgnoreArrays { continue } // iterate elements to get documents that contain the key. for j := 0; j < val.Len(); j++ { elem := must.NotFail(val.Get(j)) - docElem, isDoc := elem.(*Document) + docElem, isDoc := elem.(*types.Document) if !isDoc { continue } diff --git a/internal/types/expressionerrorcode_string.go b/internal/handlers/common/aggregations/expressionerrorcode_string.go similarity index 68% rename from internal/types/expressionerrorcode_string.go rename to internal/handlers/common/aggregations/expressionerrorcode_string.go index 19f6beb590f5..faa081aa1d33 100644 --- a/internal/types/expressionerrorcode_string.go +++ b/internal/handlers/common/aggregations/expressionerrorcode_string.go @@ -1,6 +1,6 @@ // Code generated by "stringer -linecomment -type ExpressionErrorCode"; DO NOT EDIT. -package types +package aggregations import "strconv" @@ -8,16 +8,16 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[ErrNotFieldPath-1] - _ = x[ErrEmptyFieldPath-2] - _ = x[ErrInvalidFieldPath-3] + _ = x[ErrNotExpression-1] + _ = x[ErrInvalidExpression-2] + _ = x[ErrEmptyFieldPath-3] _ = x[ErrUndefinedVariable-4] _ = x[ErrEmptyVariable-5] } -const _ExpressionErrorCode_name = "ErrNotFieldPathErrEmptyFieldPathErrInvalidFieldPathErrUndefinedVariableErrEmptyVariable" +const _ExpressionErrorCode_name = "ErrNotExpressionErrInvalidExpressionErrEmptyFieldPathErrUndefinedVariableErrEmptyVariable" -var _ExpressionErrorCode_index = [...]uint8{0, 15, 32, 51, 71, 87} +var _ExpressionErrorCode_index = [...]uint8{0, 16, 36, 53, 73, 89} func (i ExpressionErrorCode) String() string { i -= 1 diff --git a/internal/handlers/common/aggregations/operators/sum.go b/internal/handlers/common/aggregations/operators/sum.go index 9fda68dc8912..ebc696757306 100644 --- a/internal/handlers/common/aggregations/operators/sum.go +++ b/internal/handlers/common/aggregations/operators/sum.go @@ -19,6 +19,7 @@ import ( "math" "math/big" + "github.com/FerretDB/FerretDB/internal/handlers/common/aggregations" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/must" @@ -26,7 +27,7 @@ import ( // sum represents $sum aggregation operator. type sum struct { - expression types.Expression + expression *aggregations.Expression number any } @@ -46,7 +47,7 @@ func newSum(accumulation *types.Document) (Accumulator, error) { accumulator.number = expr case string: var err error - if accumulator.expression, err = types.NewExpression(expr); err != nil { + if accumulator.expression, err = aggregations.NewExpression(expr); err != nil { // $sum returns 0 on non-existent field. accumulator.number = int32(0) } diff --git a/internal/handlers/common/aggregations/projection.go b/internal/handlers/common/aggregations/projection.go new file mode 100644 index 000000000000..1faf613cdfed --- /dev/null +++ b/internal/handlers/common/aggregations/projection.go @@ -0,0 +1,493 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregations + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +// ValidateProjection check projection document. +// Document fields could be either included or excluded but not both. +// Exception is for the _id field that could be included or excluded. +// +// Errors: +// - `ErrEmptyProject` when projection document is empty; +// - `ErrProjectionExIn` when there is exclusion in inclusion projection; +// - `ErrProjectionInEx` when there is inclusion in exclusion projection; +// - `ErrNotImplemented` when there is unimplemented projection operators and expressions; +func ValidateProjection(projection *types.Document) (*types.Document, bool, error) { + validated := types.MakeDocument(0) + + if projection.Len() == 0 { + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrEmptyProject, + "Invalid $project :: caused by :: projection specification must have at least one field", + "$project (stage)", + ) + } + + var projectionVal *bool + + iter := projection.Iterator() + defer iter.Close() + + for { + key, value, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return nil, false, lazyerrors.Error(err) + } + + if strings.HasPrefix(key, "$") { + return nil, false, commonerrors.NewCommandErrorMsg( + commonerrors.ErrNotImplemented, + fmt.Sprintf("projection operator $ is not supported in %s", key), + ) + } + + var result bool + + switch value := value.(type) { + case *types.Document: + return nil, false, commonerrors.NewCommandErrorMsg( + commonerrors.ErrNotImplemented, + fmt.Sprintf("projection expression %s is not supported", types.FormatAnyValue(value)), + ) + case *types.Array, string, types.Binary, types.ObjectID, + time.Time, types.NullType, types.Regex, types.Timestamp: // all this types are treated as new fields value + result = true + + validated.Set(key, value) + case float64, int32, int64: + // projection treats 0 as false and any other value as true + comparison := types.Compare(value, int32(0)) + + if comparison != types.Equal { + result = true + } + + // set the value with boolean result to omit type assertion when we will apply projection + validated.Set(key, result) + case bool: + result = value + + // set the value with boolean result to omit type assertion when we will apply projection + validated.Set(key, result) + default: + return nil, false, lazyerrors.Errorf("unsupported operation %s %value (%T)", key, value, value) + } + + if projection.Len() == 1 && key == "_id" { + return validated, result, nil + } + + // if projectionVal is nil we are processing the first field + if projectionVal == nil { + if key == "_id" { + continue + } + + projectionVal = &result + + continue + } + + if *projectionVal != result { + if *projectionVal { + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionExIn, + fmt.Sprintf("Cannot do exclusion on field %s in inclusion projection", key), + "projection", + ) + } + + return nil, false, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrProjectionInEx, + fmt.Sprintf("Cannot do inclusion on field %s in exclusion projection", key), + "projection", + ) + } + } + + return validated, *projectionVal, nil +} + +// ProjectDocument applies projection to the copy of the document. +func ProjectDocument(doc, projection *types.Document, inclusion bool) (*types.Document, error) { + projected, err := types.NewDocument("_id", must.NotFail(doc.Get("_id"))) + if err != nil { + return nil, err + } + + if projection.Has("_id") { + idValue := must.NotFail(projection.Get("_id")) + + var set bool + + switch idValue := idValue.(type) { + case *types.Document: // field: { $elemMatch: { field2: value }} + // TODO: https://github.com/FerretDB/FerretDB/issues/2633 + return nil, commonerrors.NewCommandErrorMsg( + commonerrors.ErrCommandNotFound, + fmt.Sprintf("projection %s is not supported", + types.FormatAnyValue(idValue), + ), + ) + + case *types.Array, string, types.Binary, types.ObjectID, + time.Time, types.NullType, types.Regex, types.Timestamp: // all this types are treated as new fields value + projected.Set("_id", idValue) + + set = true + case bool: + set = idValue + + default: + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", "_id", idValue, idValue) + } + + if !set { + projected.Remove("_id") + } + } + + projectedWithoutID, err := projectDocumentWithoutID(doc, projection, inclusion) + if err != nil { + // TODO: https://github.com/FerretDB/FerretDB/issues/2633 + return nil, err + } + + for _, key := range projectedWithoutID.Keys() { + projected.Set(key, must.NotFail(projectedWithoutID.Get(key))) + } + + return projected, nil +} + +// projectDocumentWithoutID applies projection to the copy of the document and returns projected document. +// It ignores _id field in the projection. +func projectDocumentWithoutID(doc *types.Document, projection *types.Document, inclusion bool) (*types.Document, error) { + projectionWithoutID := projection.DeepCopy() + projectionWithoutID.Remove("_id") + + docWithoutID := doc.DeepCopy() + docWithoutID.Remove("_id") + + projected := types.MakeDocument(0) + + if !inclusion { + projected = docWithoutID.DeepCopy() + } + + iter := projectionWithoutID.Iterator() + defer iter.Close() + + for { + key, value, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return nil, lazyerrors.Error(err) + } + + path, err := types.NewPathFromString(key) + if err != nil { + return nil, lazyerrors.Error(err) + } + + switch value := value.(type) { // found in the projection + case *types.Document: // field: { $elemMatch: { field2: value }} + // TODO: https://github.com/FerretDB/FerretDB/issues/2633 + return nil, commonerrors.NewCommandErrorMsg( + commonerrors.ErrCommandNotFound, + fmt.Sprintf("projection %s is not supported", + types.FormatAnyValue(value), + ), + ) + + case *types.Array, string, types.Binary, types.ObjectID, + time.Time, types.NullType, types.Regex, types.Timestamp: // all these types are treated as new fields value + projected.Set(key, value) + + case bool: // field: bool + if inclusion { + // inclusion projection copies the field on the path from docWithoutID to projected. + if _, err = includeProjection(path, docWithoutID, projected); err != nil { + return nil, err + } + + continue + } + + // exclusion projection removes the field on the path in projected. + excludeProjection(path, projected) + default: + return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) + } + } + + return projected, nil +} + +// includeProjection copies the field on the path from source to projected. +// When an array is on the path, it returns the array containing any document +// with the same key. Dot notation with array index path does not include +// the field unlike document.SetByPath(path). +// Inclusion projection with non-existent path creates an empty document +// or an empty array based on what source has. +// It returns iterator errors other than ErrIteratorDone. +// If the projected contains field that is not expected in source, it panics. +// +// Example: "v.foo" path inclusion projection: +// {v: {foo: 1, bar: 1}} -> {v: {foo: 1}} +// {v: {bar: 1}} -> {v: {}} +// {v: [{bar: 1}]} -> {v: [{}]} +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{foo: 1}, {foo: 2}, {}]} +// +// Example: "v.0.foo" path inclusion projection: +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{}, {}, {}]} +func includeProjection(path types.Path, source any, projected *types.Document) (*types.Array, error) { + key := path.Prefix() + + switch source := source.(type) { + case *types.Document: + embeddedSource, err := source.Get(key) + if err != nil { + // key does not exist, nothing to set. + return nil, nil + } + + if path.Len() <= 1 { + // path reached suffix, set field in projected. + setBySourceOrder(key, embeddedSource, source, projected) + return nil, nil + } + + doc := new(types.Document) + + if projected.Has(key) { + // set doc if projected has field from other projection field. + v := must.NotFail(projected.Get(key)) + if d, ok := v.(*types.Document); ok { + doc = d + } + + if arr, ok := v.(*types.Array); ok { + // use next prefix key with arr value, allowing array to parse existing + // projection fields. + doc = must.NotFail(types.NewDocument(path.TrimPrefix().Prefix(), arr)) + } + } + + // when next prefix has an array use returned value arr, + // if it has a document, field in the doc is set by includeProjection. + arr, err := includeProjection(path.TrimPrefix(), embeddedSource, doc) + if err != nil { + return nil, err + } + + switch embeddedSource.(type) { + case *types.Document: + setBySourceOrder(key, doc, source, projected) + case *types.Array: + projected.Set(key, arr) + } + + return nil, nil + case *types.Array: + iter := source.Iterator() + defer iter.Close() + + arr := new(types.Array) + var inclusionExists bool + + if v, err := projected.Get(key); err == nil { + projectedArr, ok := v.(*types.Array) + if ok { + arr = projectedArr + inclusionExists = true + } + } + + i := 0 + + for { + _, arrElem, err := iter.Next() + if err != nil { + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + return nil, lazyerrors.Error(err) + } + + if _, ok := arrElem.(*types.Document); !ok { + continue + } + + doc := new(types.Document) + + if inclusionExists { + // when there are multiple inclusion fields, first inclusion + // inserts all documents from source to arr, they could be empty + // if it did not match previous inclusion fields. + // But number of documents in arr must be the same as number of documents + // in source. + var v any + + v, err = arr.Get(i) + if err != nil { + panic(err) + } + + docVal, ok := v.(*types.Document) + if !ok { + panic("projected field must be a document") + } + + doc = docVal + } else { + // first inclusion field, insert it to the doc. + arr.Append(doc) + } + + if _, err = includeProjection(path, arrElem, doc); err != nil { + return nil, err + } + + arr.Set(i, doc) + i++ + } + + return arr, nil + default: + // field is not a document or an array, nothing to set. + return nil, nil + } +} + +// excludeProjection removes the field on the path in projected. +// When an array is on the path, it checks if the array contains any document +// with the key to remove that document. This is not the case in document.Remove(key). +// Dot notation with array index path do not exclude unlike document.RemoveByPath(key). +// +// Examples: "v.foo" path exclusion projection: +// {v: {foo: 1}} -> {v: {}} +// {v: {foo: 1, bar: 1}} -> {v: {bar: 1}} +// {v: [{foo: 1}, {foo: 2}]} -> {v: [{}, {}]} +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{}, {}, {bar: 1}]} +// +// Example: "v.0.foo" path exclusion projection: +// {v: [{foo: 1}, {foo: 2}]} -> {v: [{foo: 1}, {foo: 2}]} +func excludeProjection(path types.Path, projected any) { + key := path.Prefix() + + switch projected := projected.(type) { + case *types.Document: + embeddedSource, err := projected.Get(key) + if err != nil { + // key does not exist, nothing to exclude. + return + } + + if path.Len() <= 1 { + // path reached suffix, remove the field from the document. + projected.Remove(key) + return + } + + // recursively remove field from the embeddedSource. + excludeProjection(path.TrimPrefix(), embeddedSource) + + return + case *types.Array: + // modifies the field of projected, hence not using iterator. + for i := 0; i < projected.Len(); i++ { + arrElem := must.NotFail(projected.Get(i)) + + if _, ok := arrElem.(*types.Document); !ok { + // not a document, cannot possibly be part of path, do nothing. + continue + } + + excludeProjection(path, arrElem) + } + + return + default: + // not a path, nothing to exclude. + return + } +} + +// setBySourceOrder sets the key value field to projected in same field order as the source. +// Example: +// +// key: foo +// val: 1 +// source: {foo: 1, bar: 2} +// projected: {bar: 2} +// +// setBySourceOrder sets projected to {foo: 1, bar: 2} rather than adding it to the last field. +func setBySourceOrder(key string, val any, source, projected *types.Document) { + projectedKeys := projected.Keys() + + // newFieldIndex is where new field is to be inserted in projected document. + newFieldIndex := 0 + + for _, sourceKey := range source.Keys() { + if sourceKey == key { + break + } + + if newFieldIndex >= len(projectedKeys) { + break + } + + if sourceKey == projectedKeys[newFieldIndex] { + newFieldIndex++ + } + } + + tmp := projected.DeepCopy() + + // remove fields of projected from newFieldIndex to the end + for i := newFieldIndex; i < len(projectedKeys); i++ { + projected.Remove(projectedKeys[i]) + } + + projected.Set(key, val) + + // copy newFieldIndex-th to the end from tmp to projected + i := newFieldIndex + for _, key := range tmp.Keys()[newFieldIndex:] { + projected.Set(key, must.NotFail(tmp.Get(tmp.Keys()[i]))) + i++ + } +} diff --git a/internal/handlers/common/aggregations/projection_iterator.go b/internal/handlers/common/aggregations/projection_iterator.go new file mode 100644 index 000000000000..05d886daf7b6 --- /dev/null +++ b/internal/handlers/common/aggregations/projection_iterator.go @@ -0,0 +1,77 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package aggregations + +import ( + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" +) + +// ProjectionIterator returns an iterator that projects documents returned by the underlying iterator. +// It will be added to the given closer. +// +// Next method returns the next projected document. +// +// Close method closes the underlying iterator. +func ProjectionIterator(iter types.DocumentsIterator, closer *iterator.MultiCloser, projection *types.Document) (types.DocumentsIterator, error) { //nolint:lll // for readability + projectionValidated, inclusion, err := ValidateProjection(projection) + if err != nil { + return nil, err + } + + res := &projectionIterator{ + iter: iter, + projection: projectionValidated, + inclusion: inclusion, + } + closer.Add(res) + + return res, nil +} + +// projectionIterator is returned by ProjectionIterator. +type projectionIterator struct { + iter types.DocumentsIterator + projection *types.Document + inclusion bool +} + +// Next implements iterator.Interface. See ProjectionIterator for details. +func (iter *projectionIterator) Next() (struct{}, *types.Document, error) { + var unused struct{} + + _, doc, err := iter.iter.Next() + if err != nil { + return unused, nil, lazyerrors.Error(err) + } + + projected, err := ProjectDocument(doc, iter.projection, iter.inclusion) + if err != nil { + return unused, nil, lazyerrors.Error(err) + } + + return unused, projected, nil +} + +// Close implements iterator.Interface. See ProjectionIterator for details. +func (iter *projectionIterator) Close() { + iter.iter.Close() +} + +// check interfaces +var ( + _ types.DocumentsIterator = (*projectionIterator)(nil) +) diff --git a/internal/handlers/common/aggregations/stages/collstats.go b/internal/handlers/common/aggregations/stages/collstats.go index 2a41602d67e2..47b8879cf879 100644 --- a/internal/handlers/common/aggregations/stages/collstats.go +++ b/internal/handlers/common/aggregations/stages/collstats.go @@ -102,7 +102,7 @@ func (c *collStats) Process(ctx context.Context, iter types.DocumentsIterator, c for _, key := range scalable { path := types.NewStaticPath("storageStats", key) val := must.NotFail(res.GetByPath(path)) - must.NoError(res.SetByPath(path, val.(int32)/scale)) + must.NoError(res.SetByPath(path, val.(int64)/int64(scale))) } } diff --git a/internal/handlers/common/aggregations/stages/group.go b/internal/handlers/common/aggregations/stages/group.go index 478144028c43..378cee41abb0 100644 --- a/internal/handlers/common/aggregations/stages/group.go +++ b/internal/handlers/common/aggregations/stages/group.go @@ -187,46 +187,48 @@ func (g *group) groupDocuments(ctx context.Context, in []*types.Document) ([]gro }}, nil } - expression, err := types.NewExpression(groupKey) + expression, err := aggregations.NewExpression(groupKey) if err != nil { - var fieldPathErr *types.FieldPathError - if !errors.As(err, &fieldPathErr) { + var exprErr *aggregations.ExpressionError + if !errors.As(err, &exprErr) { return nil, lazyerrors.Error(err) } - switch fieldPathErr.Code() { - case types.ErrNotFieldPath: + switch exprErr.Code() { + case aggregations.ErrNotExpression: // constant value aggregates values of all `in` documents into one aggregated document. return []groupedDocuments{{ groupID: groupKey, documents: in, }}, nil - case types.ErrEmptyFieldPath: + case aggregations.ErrEmptyFieldPath: return nil, commonerrors.NewCommandErrorMsgWithArgument( + // TODO commonerrors.ErrGroupInvalidFieldPath, - "'$' by itself is not a valid FieldPath", + "'$' by itself is not a valid Expression", "$group (stage)", ) - case types.ErrInvalidFieldPath: + case aggregations.ErrInvalidExpression: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrFailedToParse, fmt.Sprintf("'%s' starts with an invalid character for a user variable name", types.FormatAnyValue(groupKey)), "$group (stage)", ) - case types.ErrEmptyVariable: + case aggregations.ErrEmptyVariable: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrFailedToParse, "empty variable names are not allowed", "$group (stage)", ) - case types.ErrUndefinedVariable: + // TODO https://github.com/FerretDB/FerretDB/issues/2275 + case aggregations.ErrUndefinedVariable: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrGroupUndefinedVariable, fmt.Sprintf("Use of undefined variable: %s", types.FormatAnyValue(groupKey)), "$group (stage)", ) default: - panic(fmt.Sprintf("unhandled field path error %s", fieldPathErr.Error())) + panic(fmt.Sprintf("unhandled field path error %s", exprErr.Error())) } } diff --git a/internal/handlers/common/aggregations/stages/project.go b/internal/handlers/common/aggregations/stages/project.go index e9cae4183707..a525674085c7 100644 --- a/internal/handlers/common/aggregations/stages/project.go +++ b/internal/handlers/common/aggregations/stages/project.go @@ -16,8 +16,6 @@ package stages import ( "context" - "errors" - "fmt" "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/common/aggregations" @@ -50,17 +48,7 @@ func newProject(stage *types.Document) (aggregations.Stage, error) { ) } - var cmdErr *commonerrors.CommandError - - validated, inclusion, err := common.ValidateProjection(fields) - if errors.As(err, &cmdErr) { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - cmdErr.Code(), - fmt.Sprintf("Invalid $project :: caused by :: %s", cmdErr.Unwrap()), - "$project (stage)", - ) - } - + validated, inclusion, err := aggregations.ValidateProjection(fields) if err != nil { return nil, err } @@ -75,7 +63,7 @@ func newProject(stage *types.Document) (aggregations.Stage, error) { // //nolint:lll // for readability func (p *project) Process(_ context.Context, iter types.DocumentsIterator, closer *iterator.MultiCloser) (types.DocumentsIterator, error) { - return common.ProjectionIterator(iter, closer, p.projection) + return aggregations.ProjectionIterator(iter, closer, p.projection) } // Type implements Stage interface. diff --git a/internal/handlers/common/aggregations/stages/stages.go b/internal/handlers/common/aggregations/stages/stages.go index 81b8d4f531a8..ddd143b833cc 100644 --- a/internal/handlers/common/aggregations/stages/stages.go +++ b/internal/handlers/common/aggregations/stages/stages.go @@ -26,8 +26,8 @@ import ( // newStageFunc is a type for a function that creates a new aggregation stage. type newStageFunc func(stage *types.Document) (aggregations.Stage, error) -// stages maps all supported aggregation stages. -var stages = map[string]newStageFunc{ +// Stages maps all supported aggregation Stages. +var Stages = map[string]newStageFunc{ // sorted alphabetically "$collStats": newCollStats, "$count": newCount, @@ -56,7 +56,6 @@ var unsupportedStages = map[string]struct{}{ "$geoNear": {}, "$graphLookup": {}, "$indexStats": {}, - "$limit": {}, "$listLocalSessions": {}, "$listSessions": {}, "$lookup": {}, @@ -72,7 +71,6 @@ var unsupportedStages = map[string]struct{}{ "$set": {}, "$setWindowFields": {}, "$sharedDataDistribution": {}, - "$skip": {}, "$sortByCount": {}, "$unionWith": {}, "$unset": {}, @@ -91,16 +89,24 @@ func NewStage(stage *types.Document) (aggregations.Stage, error) { name := stage.Command() - f, ok := stages[name] - if !ok { - if _, ok := unsupportedStages[name]; ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrNotImplemented, - fmt.Sprintf("`aggregate` stage %q is not implemented yet", name), - name+" (stage)", // to differentiate update operator $set from aggregation stage $set, etc - ) - } + f, supported := Stages[name] + _, unsupported := unsupportedStages[name] + switch { + case supported && unsupported: + panic(fmt.Sprintf("stage %q is in both `stages` and `unsupportedStages`", name)) + + case supported && !unsupported: + return f(stage) + + case !supported && unsupported: + return nil, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrNotImplemented, + fmt.Sprintf("`aggregate` stage %q is not implemented yet", name), + name+" (stage)", // to differentiate update operator $set from aggregation stage $set, etc + ) + + case !supported && !unsupported: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageGroupInvalidAccumulator, fmt.Sprintf("Unrecognized pipeline stage name: %q", name), @@ -108,5 +114,5 @@ func NewStage(stage *types.Document) (aggregations.Stage, error) { ) } - return f(stage) + panic("not reached") } diff --git a/internal/handlers/common/aggregations/stages/unwind.go b/internal/handlers/common/aggregations/stages/unwind.go index cdfe76500138..b7c37ab6e7d5 100644 --- a/internal/handlers/common/aggregations/stages/unwind.go +++ b/internal/handlers/common/aggregations/stages/unwind.go @@ -30,7 +30,7 @@ import ( // unwind represents $unwind stage. type unwind struct { - field types.Expression + field *aggregations.Expression } // newUnwind creates a new $unwind stage. @@ -40,7 +40,7 @@ func newUnwind(stage *types.Document) (aggregations.Stage, error) { return nil, err } - var expr types.Expression + var expr *aggregations.Expression switch field := field.(type) { case *types.Document: @@ -54,34 +54,34 @@ func newUnwind(stage *types.Document) (aggregations.Stage, error) { ) } - opts := types.ExpressionOpts{ + opts := aggregations.ExpressionOpts{ IgnoreArrays: true, } - expr, err = types.NewExpressionWithOpts(field, &opts) + expr, err = aggregations.NewExpressionWithOpts(field, &opts) if err != nil { - var fieldPathErr *types.FieldPathError - if !errors.As(err, &fieldPathErr) { + var exprErr *aggregations.ExpressionError + if !errors.As(err, &exprErr) { return nil, lazyerrors.Error(err) } - switch fieldPathErr.Code() { - case types.ErrNotFieldPath: + switch exprErr.Code() { + case aggregations.ErrNotExpression: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageUnwindNoPrefix, fmt.Sprintf("path option to $unwind stage should be prefixed with a '$': %v", types.FormatAnyValue(field)), "$unwind (stage)", ) - case types.ErrEmptyFieldPath: + case aggregations.ErrEmptyFieldPath: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrEmptyFieldPath, - "FieldPath cannot be constructed with empty string", + "Expression cannot be constructed with empty string", "$unwind (stage)", ) - case types.ErrEmptyVariable, types.ErrInvalidFieldPath, types.ErrUndefinedVariable: + case aggregations.ErrEmptyVariable, aggregations.ErrInvalidExpression, aggregations.ErrUndefinedVariable: return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrFieldPathInvalidName, - "FieldPath field names may not start with '$'. Consider using $getField or $setField", + "Expression field names may not start with '$'. Consider using $getField or $setField", "$unwind (stage)", ) default: diff --git a/internal/handlers/common/count.go b/internal/handlers/common/count.go index 046d288df189..925ffa115677 100644 --- a/internal/handlers/common/count.go +++ b/internal/handlers/common/count.go @@ -15,67 +15,36 @@ package common import ( - "fmt" + "go.uber.org/zap" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" ) -// CountParams represents the parameters for the count command. +// CountParams represents parameters for the count command. type CountParams struct { - Filter *types.Document - DB, Collection string - Skip, Limit int64 -} - -// GetCountParams returns the parameters for the count command. -func GetCountParams(document *types.Document) (*CountParams, error) { - var err error - var filter *types.Document - - if filter, err = GetOptionalParam(document, "query", filter); err != nil { - return nil, err - } + Filter *types.Document `ferretdb:"query,opt"` + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` - var skip, limit int64 + Skip int64 `ferretdb:"skip,opt,positiveNumber"` + Limit int64 `ferretdb:"limit,opt,positiveNumber"` - if s, _ := document.Get("skip"); s != nil { - if skip, err = GetSkipParam("count", s); err != nil { - return nil, err - } - } + Collation *types.Document `ferretdb:"collation,unimplemented"` - if l, _ := document.Get("limit"); l != nil { - if limit, err = GetLimitParam("count", l); err != nil { - return nil, err - } - } - - var db, collection string + Hint any `ferretdb:"hint,ignored"` + ReadConcern *types.Document `ferretdb:"readConcern,ignored"` + Comment string `ferretdb:"comment,ignored"` +} - if db, err = GetRequiredParam[string](document, "$db"); err != nil { - return nil, err - } +// GetCountParams returns the parameters for the count command. +func GetCountParams(document *types.Document, l *zap.Logger) (*CountParams, error) { + var count CountParams - collectionParam, err := document.Get(document.Command()) + err := commonparams.ExtractParams(document, "count", &count, l) if err != nil { return nil, err } - var ok bool - if collection, ok = collectionParam.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrInvalidNamespace, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collectionParam)), - document.Command(), - ) - } - - return &CountParams{ - DB: db, - Collection: collection, - Filter: filter, - Skip: skip, - Limit: limit, - }, nil + return &count, nil } diff --git a/internal/handlers/common/delete.go b/internal/handlers/common/delete.go index ff72a98b24d8..ba8f1faf5370 100644 --- a/internal/handlers/common/delete.go +++ b/internal/handlers/common/delete.go @@ -15,146 +15,48 @@ package common import ( - "fmt" - "go.uber.org/zap" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" - "github.com/FerretDB/FerretDB/internal/util/must" ) -// DeleteParams represents parameters for delete operation. +// DeleteParams represents parameters for the delete command. type DeleteParams struct { - DB, Collection string - Comment string - Deletes []Delete - Ordered bool + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + + Comment string `ferretdb:"comment,opt"` + Deletes []Delete `ferretdb:"deletes,opt"` + Ordered bool `ferretdb:"ordered,opt"` + + Let *types.Document `ferretdb:"let,unimplemented"` + + WriteConcern *types.Document `ferretdb:"writeConcern,ignored"` } // Delete represents single delete operation parameters. type Delete struct { - Filter *types.Document - Comment string - Limited bool + Filter *types.Document `ferretdb:"q"` + Limited bool `ferretdb:"limit,zeroOrOneAsBool"` + // TODO: https://github.com/FerretDB/FerretDB/issues/2627 + Comment string `ferretdb:"comment,opt"` + + Collation *types.Document `ferretdb:"collation,unimplemented"` + + Hint string `ferretdb:"hint,ignored"` } // GetDeleteParams returns parameters for delete operation. func GetDeleteParams(document *types.Document, l *zap.Logger) (*DeleteParams, error) { - var err error - - if err = Unimplemented(document, "let"); err != nil { - return nil, err - } - - Ignored(document, l, "writeConcern") - - var deletesArray *types.Array - if deletesArray, err = GetOptionalParam(document, "deletes", deletesArray); err != nil { - return nil, err - } - - ordered := true - if ordered, err = GetOptionalParam(document, "ordered", ordered); err != nil { - return nil, err + params := DeleteParams{ + Ordered: true, } - var db, collection string - - if db, err = GetRequiredParam[string](document, "$db"); err != nil { - return nil, err - } - - collectionParam, err := document.Get(document.Command()) + err := commonparams.ExtractParams(document, "delete", ¶ms, l) if err != nil { return nil, err } - var ok bool - if collection, ok = collectionParam.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collectionParam)), - document.Command(), - ) - } - - // get comment from options.Delete().SetComment() method - var comment string - if comment, err = GetOptionalParam(document, "comment", comment); err != nil { - return nil, err - } - - var deletes []Delete - - for i := 0; i < deletesArray.Len(); i++ { - // get document with filter - deleteDoc, err := AssertType[*types.Document](must.NotFail(deletesArray.Get(i))) - if err != nil { - return nil, err - } - - deleteFilter, limited, err := prepareDeleteParams(deleteDoc, l) - if err != nil { - return nil, err - } - - // get comment from query, e.g. db.collection.DeleteOne({"_id":"string", "$comment: "test"}) - if comment, err = GetOptionalParam(deleteFilter, "$comment", comment); err != nil { - return nil, err - } - - deletes = append(deletes, Delete{ - Filter: deleteFilter, - Limited: limited, - Comment: comment, - }) - } - - return &DeleteParams{ - DB: db, - Collection: collection, - Ordered: ordered, - Comment: comment, - Deletes: deletes, - }, nil -} - -// prepareDeleteParams returns filter and limit parameters for delete operation. -func prepareDeleteParams(deleteDoc *types.Document, l *zap.Logger) (*types.Document, bool, error) { - var err error - - if err = Unimplemented(deleteDoc, "collation"); err != nil { - return nil, false, err - } - - Ignored(deleteDoc, l, "hint") - - // get filter from document - var filter *types.Document - if filter, err = GetOptionalParam(deleteDoc, "q", filter); err != nil { - return nil, false, err - } - - // TODO use `GetLimitParam` - // https://github.com/FerretDB/FerretDB/issues/2255 - limitValue, err := deleteDoc.Get("limit") - if err != nil { - return nil, false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrMissingField, - "BSON field 'delete.deletes.limit' is missing but a required field", - "limit", - ) - } - - var limit int64 - if limit, err = GetWholeNumberParam(limitValue); err != nil || limit < 0 || limit > 1 { - return nil, false, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrFailedToParse, - fmt.Sprintf("The limit field in delete objects must be 0 or 1. Got %v", limitValue), - "limit", - ) - } - - return filter, limit == 1, nil + return ¶ms, nil } diff --git a/internal/handlers/common/distinct.go b/internal/handlers/common/distinct.go index a25d381037a3..167f4b2e3b42 100644 --- a/internal/handlers/common/distinct.go +++ b/internal/handlers/common/distinct.go @@ -15,12 +15,12 @@ package common import ( - "fmt" "strings" "go.uber.org/zap" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) @@ -29,53 +29,26 @@ import ( // //nolint:vet // for readability type DistinctParams struct { - DB string - Collection string - Key string - Filter *types.Document - Comment string + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Key string `ferretdb:"key"` + Filter *types.Document `ferretdb:"query,opt"` + Comment string `ferretdb:"comment,opt"` + + Collation *types.Document `ferretdb:"collation,unimplemented"` + + ReadConcern *types.Document `ferretdb:"readConcern,ignored"` } // GetDistinctParams returns `distinct` command parameters. func GetDistinctParams(document *types.Document, l *zap.Logger) (*DistinctParams, error) { - var err error - - unimplementedFields := []string{ - "collation", - } - if err = Unimplemented(document, unimplementedFields...); err != nil { - return nil, err - } - - ignoredFields := []string{ - "readConcern", - } - Ignored(document, l, ignoredFields...) - var dp DistinctParams - if dp.DB, err = GetRequiredParam[string](document, "$db"); err != nil { - return nil, err - } - - collectionParam, err := document.Get(document.Command()) + err := commonparams.ExtractParams(document, "distinct", &dp, l) if err != nil { return nil, err } - var ok bool - if dp.Collection, ok = collectionParam.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrInvalidNamespace, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collectionParam)), - document.Command(), - ) - } - - if dp.Key, err = GetRequiredParam[string](document, "key"); err != nil { - return nil, err - } - if dp.Key == "" { return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrEmptyFieldPath, @@ -83,14 +56,6 @@ func GetDistinctParams(document *types.Document, l *zap.Logger) (*DistinctParams ) } - if dp.Filter, err = GetOptionalParam(document, "query", dp.Filter); err != nil { - return nil, err - } - - if dp.Comment, err = GetOptionalParam(document, "comment", dp.Comment); err != nil { - return nil, err - } - return &dp, nil } diff --git a/internal/handlers/common/explain.go b/internal/handlers/common/explain.go index f903f1e10061..17778694b438 100644 --- a/internal/handlers/common/explain.go +++ b/internal/handlers/common/explain.go @@ -26,13 +26,19 @@ import ( // ExplainParams represents the parameters for the explain command. type ExplainParams struct { - DB string - Collection string - Command *types.Document - Filter *types.Document - Sort *types.Document - StagesDocs []any - Aggregate bool + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + + Explain *types.Document `ferretdb:"explain"` + + Filter *types.Document `ferretdb:"filter,opt"` + Sort *types.Document `ferretdb:"sort,opt"` + + StagesDocs []any `ferretdb:"-"` + Aggregate bool `ferretdb:"-"` + Command *types.Document `ferretdb:"-"` + + Verbosity string `ferretdb:"verbosity,ignored"` } // GetExplainParams returns the parameters for the explain command. diff --git a/internal/handlers/common/filter.go b/internal/handlers/common/filter.go index 94590407c39d..f74515f5d832 100644 --- a/internal/handlers/common/filter.go +++ b/internal/handlers/common/filter.go @@ -25,6 +25,7 @@ import ( "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -932,22 +933,22 @@ func filterFieldExprRegex(fieldValue any, regexValue, optionsValue any) (bool, e // filterFieldExprSize handles {field: {$size: sizeValue}} filter. func filterFieldExprSize(fieldValue any, sizeValue any) (bool, error) { - size, err := GetWholeNumberParam(sizeValue) + size, err := commonparams.GetWholeNumberParam(sizeValue) if err != nil { switch err { - case errUnexpectedType: + case commonparams.ErrUnexpectedType: return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, fmt.Sprintf(`Failed to parse $size. Expected a number in: $size: %s`, types.FormatAnyValue(sizeValue)), "$size", ) - case errNotWholeNumber: + case commonparams.ErrNotWholeNumber: return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, fmt.Sprintf(`Failed to parse $size. Expected an integer: $size: %s`, types.FormatAnyValue(sizeValue)), "$size", ) - case errInfinity: + case commonparams.ErrInfinity: return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, fmt.Sprintf( @@ -1356,7 +1357,7 @@ func filterFieldExprExists(fieldExist bool, exprValue any) (bool, error) { func filterFieldExprType(fieldValue, exprValue any) (bool, error) { switch exprValue := exprValue.(type) { case *types.Array: - hasSameType := hasSameTypeElements(exprValue) + hasSameType := commonparams.HasSameTypeElements(exprValue) for i := 0; i < exprValue.Len(); i++ { exprValue := must.NotFail(exprValue.Get(i)) @@ -1378,7 +1379,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { ) } - code, err := newTypeCode(int32(exprValue)) + code, err := commonparams.NewTypeCode(int32(exprValue)) if err != nil { return false, err } @@ -1396,7 +1397,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { } case string: - code, err := parseTypeCode(exprValue) + code, err := commonparams.ParseTypeCode(exprValue) if err != nil { return false, err } @@ -1408,7 +1409,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { return true, nil } case int32: - code, err := newTypeCode(exprValue) + code, err := commonparams.NewTypeCode(exprValue) if err != nil { return false, err } @@ -1450,7 +1451,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { ) } - code, err := newTypeCode(int32(exprValue)) + code, err := commonparams.NewTypeCode(int32(exprValue)) if err != nil { return false, err } @@ -1458,7 +1459,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { return filterFieldValueByTypeCode(fieldValue, code) case string: - code, err := parseTypeCode(exprValue) + code, err := commonparams.ParseTypeCode(exprValue) if err != nil { return false, err } @@ -1466,7 +1467,7 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { return filterFieldValueByTypeCode(fieldValue, code) case int32: - code, err := newTypeCode(exprValue) + code, err := commonparams.NewTypeCode(exprValue) if err != nil { return false, err } @@ -1483,9 +1484,9 @@ func filterFieldExprType(fieldValue, exprValue any) (bool, error) { } // filterFieldValueByTypeCode filters fieldValue by given type code. -func filterFieldValueByTypeCode(fieldValue any, code typeCode) (bool, error) { +func filterFieldValueByTypeCode(fieldValue any, code commonparams.TypeCode) (bool, error) { // check types.Array elements for match to given code. - if array, ok := fieldValue.(*types.Array); ok && code != typeCodeArray { + if array, ok := fieldValue.(*types.Array); ok && code != commonparams.TypeCodeArray { for i := 0; i < array.Len(); i++ { value, err := array.Get(i) if err != nil { @@ -1509,67 +1510,67 @@ func filterFieldValueByTypeCode(fieldValue any, code typeCode) (bool, error) { } switch code { - case typeCodeArray: + case commonparams.TypeCodeArray: if _, ok := fieldValue.(*types.Array); !ok { return false, nil } - case typeCodeObject: + case commonparams.TypeCodeObject: if _, ok := fieldValue.(*types.Document); !ok { return false, nil } - case typeCodeDouble: + case commonparams.TypeCodeDouble: if _, ok := fieldValue.(float64); !ok { return false, nil } - case typeCodeString: + case commonparams.TypeCodeString: if _, ok := fieldValue.(string); !ok { return false, nil } - case typeCodeBinData: + case commonparams.TypeCodeBinData: if _, ok := fieldValue.(types.Binary); !ok { return false, nil } - case typeCodeObjectID: + case commonparams.TypeCodeObjectID: if _, ok := fieldValue.(types.ObjectID); !ok { return false, nil } - case typeCodeBool: + case commonparams.TypeCodeBool: if _, ok := fieldValue.(bool); !ok { return false, nil } - case typeCodeDate: + case commonparams.TypeCodeDate: if _, ok := fieldValue.(time.Time); !ok { return false, nil } - case typeCodeNull: + case commonparams.TypeCodeNull: if _, ok := fieldValue.(types.NullType); !ok { return false, nil } - case typeCodeRegex: + case commonparams.TypeCodeRegex: if _, ok := fieldValue.(types.Regex); !ok { return false, nil } - case typeCodeInt: + case commonparams.TypeCodeInt: if _, ok := fieldValue.(int32); !ok { return false, nil } - case typeCodeTimestamp: + case commonparams.TypeCodeTimestamp: if _, ok := fieldValue.(types.Timestamp); !ok { return false, nil } - case typeCodeLong: + case commonparams.TypeCodeLong: if _, ok := fieldValue.(int64); !ok { return false, nil } - case typeCodeNumber: - // typeCodeNumber should match int32, int64 and float64 types + case commonparams.TypeCodeNumber: + // TypeCodeNumber should match int32, int64 and float64 types switch fieldValue.(type) { case float64, int32, int64: return true, nil default: return false, nil } - case typeCodeDecimal, typeCodeMinKey, typeCodeMaxKey: + case commonparams.TypeCodeDecimal, commonparams.TypeCodeMinKey, commonparams.TypeCodeMaxKey: return false, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrNotImplemented, fmt.Sprintf(`Type code %v not implemented`, code), @@ -1651,14 +1652,14 @@ func filterFieldExprElemMatch(doc *types.Document, filterKey, filterSuffix strin // Mask value used in error message. func formatBitwiseOperatorErr(err error, operator string, maskValue any) error { switch { - case errors.Is(err, errNotWholeNumber): + case errors.Is(err, commonparams.ErrNotWholeNumber): return commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrFailedToParse, fmt.Sprintf("Expected an integer: %s: %#v", operator, maskValue), operator, ) - case errors.Is(err, errNegativeNumber): + case errors.Is(err, commonparams.ErrNegativeNumber): if _, ok := maskValue.(float64); ok { return commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrFailedToParse, @@ -1673,7 +1674,7 @@ func formatBitwiseOperatorErr(err error, operator string, maskValue any) error { operator, ) - case errors.Is(err, errNotBinaryMask): + case errors.Is(err, commonparams.ErrNotBinaryMask): return commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, fmt.Sprintf(`value takes an Array, a number, or a BinData but received: %s: %#v`, operator, maskValue), diff --git a/internal/handlers/common/find.go b/internal/handlers/common/find.go index 9c820c68ca7d..f52a86f1de53 100644 --- a/internal/handlers/common/find.go +++ b/internal/handlers/common/find.go @@ -15,136 +15,67 @@ package common import ( - "fmt" + "errors" "go.uber.org/zap" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" ) -// FindParams contains `find` command parameters supported by at least one handler. +// FindParams represents parameters for the find command. // //nolint:vet // for readability type FindParams struct { - DB string - Collection string - Filter *types.Document - Sort *types.Document - Projection *types.Document - Skip int64 - Limit int64 - BatchSize int32 - SingleBatch bool - Comment string - MaxTimeMS int32 + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Filter *types.Document `ferretdb:"filter,opt"` + Sort *types.Document `ferretdb:"sort,opt"` + Projection *types.Document `ferretdb:"projection,opt"` + Skip int64 `ferretdb:"skip,opt,positiveNumber"` + Limit int64 `ferretdb:"limit,opt,positiveNumber"` + BatchSize int64 `ferretdb:"batchSize,opt,positiveNumber"` + SingleBatch bool `ferretdb:"singleBatch,opt"` + Comment string `ferretdb:"comment,opt"` + MaxTimeMS int64 `ferretdb:"maxTimeMS,opt,wholePositiveNumber"` + + Collation *types.Document `ferretdb:"collation,unimplemented"` + Let *types.Document `ferretdb:"let,unimplemented"` + + AllowDiskUse bool `ferretdb:"allowDiskUse,ignored"` + ReadConcern *types.Document `ferretdb:"readConcern,ignored"` + Max *types.Document `ferretdb:"max,ignored"` + Min *types.Document `ferretdb:"min,ignored"` + Hint any `ferretdb:"hint,ignored"` + + ReturnKey bool `ferretdb:"returnKey,non-default"` + ShowRecordId bool `ferretdb:"showRecordId,non-default"` + Tailable bool `ferretdb:"tailable,non-default"` + OplogReplay bool `ferretdb:"oplogReplay,non-default"` + NoCursorTimeout bool `ferretdb:"noCursorTimeout,non-default"` + AwaitData bool `ferretdb:"awaitData,non-default"` + AllowPartialResults bool `ferretdb:"allowPartialResults,non-default"` } // GetFindParams returns `find` command parameters. func GetFindParams(doc *types.Document, l *zap.Logger) (*FindParams, error) { - var res FindParams - var err error - - if res.DB, err = GetRequiredParam[string](doc, "$db"); err != nil { - return nil, err - } - - collection, err := doc.Get(doc.Command()) - if err != nil { - return nil, err - } - - var ok bool - if res.Collection, ok = collection.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collection)), - doc.Command(), - ) - } - - if res.Filter, err = GetOptionalParam(doc, "filter", res.Filter); err != nil { - return nil, err - } - - if res.Sort, err = GetOptionalParam(doc, "sort", res.Sort); err != nil { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrTypeMismatch, - "Expected field sort to be of type object", - "sort", - ) - } - - if res.Projection, err = GetOptionalParam(doc, "projection", res.Projection); err != nil { - return nil, err + params := FindParams{ + BatchSize: 101, } - Ignored(doc, l, "hint") + err := commonparams.ExtractParams(doc, "find", ¶ms, l) - if s, _ := doc.Get("skip"); s != nil { - if res.Skip, err = GetSkipParam("find", s); err != nil { - return nil, err + var ce *commonerrors.CommandError + if errors.As(err, &ce) { + if ce.Code() == commonerrors.ErrInvalidNamespace { + return nil, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrBadValue, ce.Unwrap().Error(), "find") } } - if l, _ := doc.Get("limit"); l != nil { - if res.Limit, err = GetLimitParam("find", l); err != nil { - return nil, err - } - } - - // TODO https://github.com/FerretDB/FerretDB/issues/2005 - - if res.BatchSize, err = GetOptionalParam(doc, "batchSize", int32(101)); err != nil { - return nil, err - } - - if res.BatchSize < 0 { - return nil, commonerrors.NewCommandError( - commonerrors.ErrValueNegative, - fmt.Errorf("BSON field 'batchSize' value must be >= 0, actual value '%d'", res.BatchSize), - ) - } - - if res.SingleBatch, err = GetOptionalParam(doc, "singleBatch", false); err != nil { - return nil, err - } - - if res.Comment, err = GetOptionalParam(doc, "comment", ""); err != nil { - return nil, err - } - - if res.MaxTimeMS, err = GetOptionalPositiveNumber(doc, "maxTimeMS"); err != nil { + if err != nil { return nil, err } - Ignored(doc, l, "readConcern") - - Ignored(doc, l, "max", "min") - - // boolean flags that default to false - for _, k := range []string{ - "returnKey", - "showRecordId", - "tailable", - "oplogReplay", - "noCursorTimeout", - "awaitData", - "allowPartialResults", - } { - if err = UnimplementedNonDefault(doc, k, func(v any) bool { - b, ok := v.(bool) - return ok && !b - }); err != nil { - return nil, err - } - } - - Unimplemented(doc, "collation") - - Ignored(doc, l, "allowDiskUse") - - Unimplemented(doc, "let") - - return &res, nil + return ¶ms, nil } diff --git a/internal/handlers/common/findandmodify.go b/internal/handlers/common/findandmodify.go index 43542bbd4468..8ed88cbed6bd 100644 --- a/internal/handlers/common/findandmodify.go +++ b/internal/handlers/common/findandmodify.go @@ -21,6 +21,7 @@ import ( "go.uber.org/zap" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/must" ) @@ -40,14 +41,32 @@ const ( UpsertOperationUpdate ) -// FindAndModifyParams represent all findAndModify requests' fields. -// It's filled by calling prepareFindAndModifyParams. +// FindAndModifyParams represent parameters for the findAndModify command. type FindAndModifyParams struct { - DB, Collection, Comment string - Query, Sort, Update *types.Document - Remove, Upsert bool - ReturnNewDocument, HasUpdateOperators bool - MaxTimeMS int32 + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Comment string `ferretdb:"comment,opt"` + Query *types.Document `ferretdb:"query,opt"` + Sort *types.Document `ferretdb:"sort,opt"` + UpdateValue any `ferretdb:"update,opt"` + Remove bool `ferretdb:"remove,opt"` + Upsert bool `ferretdb:"upsert,opt"` + ReturnNewDocument bool `ferretdb:"new,opt,numericBool"` + MaxTimeMS int64 `ferretdb:"maxTimeMS,opt,wholePositiveNumber"` + + Update *types.Document `ferretdb:"-"` + Aggregation *types.Array `ferretdb:"-"` + + HasUpdateOperators bool `ferretdb:"-"` + + Let *types.Document `ferretdb:"let,unimplemented"` + Collation *types.Document `ferretdb:"collation,unimplemented"` + Fields *types.Document `ferretdb:"fields,unimplemented"` + ArrayFilters *types.Array `ferretdb:"arrayFilters,unimplemented"` + + Hint string `ferretdb:"hint,ignored"` + WriteConcern *types.Document `ferretdb:"writeConcern,ignored"` + BypassDocumentValidation bool `ferretdb:"bypassDocumentValidation,ignored"` } // UpsertParams represents parameters for upsert, if the document exists UpdateParams is set. @@ -67,86 +86,38 @@ type UpsertParams struct { // GetFindAndModifyParams returns `findAndModifyParams` command parameters. func GetFindAndModifyParams(doc *types.Document, l *zap.Logger) (*FindAndModifyParams, error) { - command := doc.Command() + var params FindAndModifyParams - db, err := GetRequiredParam[string](doc, "$db") + err := commonparams.ExtractParams(doc, "findAndModify", ¶ms, l) if err != nil { return nil, err } - collection, err := GetRequiredParam[string](doc, command) - if err != nil { - return nil, err - } - - if collection == "" { + if params.Collection == "" { return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrInvalidNamespace, - fmt.Sprintf("Invalid namespace specified '%s.'", db), + fmt.Sprintf("Invalid namespace specified '%s.'", params.DB), ) } - remove, err := GetBoolOptionalParam(doc, "remove") - if err != nil { - return nil, err - } - - returnNewDocument, err := GetBoolOptionalParam(doc, "new") - if err != nil { - return nil, err - } - - upsert, err := GetBoolOptionalParam(doc, "upsert") - if err != nil { - return nil, err - } - - query, err := GetOptionalParam(doc, "query", new(types.Document)) - if err != nil { - return nil, err - } - - sort, err := GetOptionalParam(doc, "sort", new(types.Document)) - if err != nil { - return nil, err - } - - maxTimeMS, err := GetOptionalPositiveNumber(doc, "maxTimeMS") - if err != nil { - return nil, err - } - - unimplementedFields := []string{ - "fields", - "collation", - "arrayFilters", - "let", - } - if err = Unimplemented(doc, unimplementedFields...); err != nil { - return nil, err - } - - ignoredFields := []string{ - "bypassDocumentValidation", - "writeConcern", - "hint", + if params.UpdateValue == nil && !params.Remove { + return nil, commonerrors.NewCommandErrorMsg( + commonerrors.ErrFailedToParse, + "Either an update or remove=true must be specified", + ) } - Ignored(doc, l, ignoredFields...) - - var update *types.Document - updateParam, err := doc.Get("update") - if err != nil && !remove { + if params.ReturnNewDocument && params.Remove { return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrFailedToParse, - "Either an update or remove=true must be specified", + "Cannot specify both new=true and remove=true; 'remove' always returns the deleted document", ) } - if err == nil { - switch updateParam := updateParam.(type) { + if params.UpdateValue != nil { + switch updateParam := params.UpdateValue.(type) { case *types.Document: - update = updateParam + params.Update = updateParam case *types.Array: // TODO aggregation pipeline stages metrics return nil, commonerrors.NewCommandErrorMsgWithArgument( @@ -163,56 +134,28 @@ func GetFindAndModifyParams(doc *types.Document, l *zap.Logger) (*FindAndModifyP } } - if update != nil && remove { + if params.Update != nil && params.Remove { return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrFailedToParse, "Cannot specify both an update and remove=true", ) } - if upsert && remove { + if params.Upsert && params.Remove { return nil, commonerrors.NewCommandErrorMsg( commonerrors.ErrFailedToParse, "Cannot specify both upsert=true and remove=true", ) } - if returnNewDocument && remove { - return nil, commonerrors.NewCommandErrorMsg( - commonerrors.ErrFailedToParse, - "Cannot specify both new=true and remove=true; 'remove' always returns the deleted document", - ) - } - - hasUpdateOperators, err := HasSupportedUpdateModifiers(command, update) + hasUpdateOperators, err := HasSupportedUpdateModifiers("findAndModify", params.Update) if err != nil { return nil, err } - var comment string - // get comment from a "comment" field - if comment, err = GetOptionalParam(doc, "comment", comment); err != nil { - return nil, err - } - - // get comment from query, e.g. db.collection.FindAndModify({"_id":"string", "$comment: "test"},{$set:{"v":"foo""}}) - if comment, err = GetOptionalParam(query, "$comment", comment); err != nil { - return nil, err - } + params.HasUpdateOperators = hasUpdateOperators - return &FindAndModifyParams{ - DB: db, - Collection: collection, - Comment: comment, - Query: query, - Update: update, - Sort: sort, - Remove: remove, - Upsert: upsert, - ReturnNewDocument: returnNewDocument, - HasUpdateOperators: hasUpdateOperators, - MaxTimeMS: maxTimeMS, - }, nil + return ¶ms, nil } // PrepareDocumentForUpsert prepares the document used for upsert operation. @@ -257,7 +200,7 @@ func prepareDocumentForInsert(params *FindAndModifyParams) (*types.Document, err insert := must.NotFail(types.NewDocument()) if params.HasUpdateOperators { - if _, err := UpdateDocument(insert, params.Update); err != nil { + if _, err := UpdateDocument("findAndModify", insert, params.Update); err != nil { return nil, err } } else { @@ -281,7 +224,7 @@ func prepareDocumentForUpdate(docs []*types.Document, params *FindAndModifyParam update := docs[0].DeepCopy() if params.HasUpdateOperators { - if _, err := UpdateDocument(update, params.Update); err != nil { + if _, err := UpdateDocument("findAndModify", update, params.Update); err != nil { return nil, err } diff --git a/internal/handlers/common/insert.go b/internal/handlers/common/insert.go index 43c9234e5284..9f17b1c3bb7b 100644 --- a/internal/handlers/common/insert.go +++ b/internal/handlers/common/insert.go @@ -15,61 +15,34 @@ package common import ( - "fmt" - "go.uber.org/zap" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" ) // InsertParams represents the parameters for an insert command. type InsertParams struct { - Docs *types.Array - DB, Collection string - Ordered bool + Docs *types.Array `ferretdb:"documents,opt"` + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Ordered bool `ferretdb:"ordered,opt"` + + WriteConcern any `ferretdb:"writeConcern,ignored"` + BypassDocumentValidation bool `ferretdb:"bypassDocumentValidation,ignored"` + Comment string `ferretdb:"comment,ignored"` } // GetInsertParams returns the parameters for an insert command. func GetInsertParams(document *types.Document, l *zap.Logger) (*InsertParams, error) { - var err error - - Ignored(document, l, "writeConcern", "bypassDocumentValidation", "comment") - - var db, collection string - - if db, err = GetRequiredParam[string](document, "$db"); err != nil { - return nil, err + params := InsertParams{ + Ordered: true, } - collectionParam, err := document.Get(document.Command()) + err := commonparams.ExtractParams(document, "insert", ¶ms, l) if err != nil { return nil, err } - var ok bool - if collection, ok = collectionParam.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collectionParam)), - document.Command(), - ) - } - - var docs *types.Array - if docs, err = GetOptionalParam(document, "documents", docs); err != nil { - return nil, err - } - - ordered := true - if ordered, err = GetOptionalParam(document, "ordered", ordered); err != nil { - return nil, err - } - - return &InsertParams{ - DB: db, - Collection: collection, - Ordered: ordered, - Docs: docs, - }, nil + return ¶ms, nil } diff --git a/internal/handlers/common/limit.go b/internal/handlers/common/limit.go index fd255429e26e..02945c6621f7 100644 --- a/internal/handlers/common/limit.go +++ b/internal/handlers/common/limit.go @@ -15,31 +15,10 @@ package common import ( - "math" - "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) -// GetLimitParam validates the given limit value for find, count, and delete commands. -func GetLimitParam(command string, value any) (int64, error) { - l, err := GetWholeNumberParam(value) - if err != nil { - return 0, lazyerrors.Error(err) - } - - // TODO return proper errors - // https://github.com/FerretDB/FerretDB/issues/2255 - switch { - case l < 0: - return 0, lazyerrors.Errorf("invalid %s limit value: %d", command, l) - case l > math.MaxUint32: - return 0, lazyerrors.Errorf("invalid %s limit value: %d", command, l) - default: - return l, nil - } -} - // LimitDocuments returns a subslice of given documents according to the given limit value. func LimitDocuments(docs []*types.Document, limit int64) ([]*types.Document, error) { switch { diff --git a/internal/handlers/common/params.go b/internal/handlers/common/params.go index 1d95ecb7c829..fa95164e5c4e 100644 --- a/internal/handlers/common/params.go +++ b/internal/handlers/common/params.go @@ -20,6 +20,7 @@ import ( "math" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" @@ -58,7 +59,7 @@ func GetOptionalParam[T types.Type](doc *types.Document, key string, defaultValu if !ok { msg := fmt.Sprintf( `BSON field '%s' is the wrong type '%s', expected type '%s'`, - key, AliasFromType(v), AliasFromType(defaultValue), + key, commonparams.AliasFromType(v), commonparams.AliasFromType(defaultValue), ) return defaultValue, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrTypeMismatch, msg, key) @@ -81,38 +82,6 @@ func GetOptionalNullParam[T types.Type](doc *types.Document, key string, default return v, err } -// GetBoolOptionalParam returns doc's bool value for key. -// Non-zero double, long, and int values return true. -// Zero values for those types, as well as nulls and missing fields, return false. -// Other types return a protocol error. -func GetBoolOptionalParam(doc *types.Document, key string) (bool, error) { - v, _ := doc.Get(key) - if v == nil { - return false, nil - } - - switch v := v.(type) { - case float64: - return v != 0, nil - case bool: - return v, nil - case types.NullType: - return false, nil - case int32: - return v != 0, nil - case int64: - return v != 0, nil - default: - msg := fmt.Sprintf( - `BSON field '%s' is the wrong type '%s', expected types '[bool, long, int, decimal, double]'`, - key, - AliasFromType(v), - ) - - return false, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrTypeMismatch, msg, key) - } -} - // AssertType asserts value's type, returning protocol error for unexpected types. // // If a custom error is needed, use a normal Go type assertion instead: @@ -131,66 +100,26 @@ func AssertType[T types.Type](value any) (T, error) { return res, nil } -var ( - errUnexpectedType = fmt.Errorf("unexpected type") - errNotWholeNumber = fmt.Errorf("not a whole number") - errNegativeNumber = fmt.Errorf("negative number") - errNotBinaryMask = fmt.Errorf("not a binary mask") - errUnexpectedLeftOpType = fmt.Errorf("unexpected left operand type") - errUnexpectedRightOpType = fmt.Errorf("unexpected right operand type") - errLongExceededPositive = fmt.Errorf("long exceeded - positive value") - errLongExceededNegative = fmt.Errorf("long exceeded - negative value") - errIntExceeded = fmt.Errorf("int exceeded") - errInfinity = fmt.Errorf("infinity") -) - -// GetWholeNumberParam checks if the given value is int32, int64, or float64 containing a whole number, -// such as used in the limit, $size, etc. -func GetWholeNumberParam(value any) (int64, error) { - switch value := value.(type) { - // TODO: add string support https://github.com/FerretDB/FerretDB/issues/1089 - case float64: - switch { - case math.IsInf(value, 1): - return 0, errInfinity - case value > float64(math.MaxInt64): - return 0, errLongExceededPositive - case value < float64(math.MinInt64): - return 0, errLongExceededNegative - case value != math.Trunc(value): - return 0, errNotWholeNumber - } - - return int64(value), nil - case int32: - return int64(value), nil - case int64: - return value, nil - default: - return 0, errUnexpectedType - } -} - // GetLimitStageParam returns $limit stage argument from the provided value. // It returns the proper error if value doesn't meet requirements. func GetLimitStageParam(value any) (int64, error) { - limit, err := GetWholeNumberParam(value) + limit, err := commonparams.GetWholeNumberParam(value) switch { case err == nil: - case errors.Is(err, errUnexpectedType): + case errors.Is(err, commonparams.ErrUnexpectedType): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageLimitInvalidArg, fmt.Sprintf("invalid argument to $limit stage: Expected a number in: $limit: %#v", value), "$limit (stage)", ) - case errors.Is(err, errNotWholeNumber), errors.Is(err, errInfinity): + case errors.Is(err, commonparams.ErrNotWholeNumber), errors.Is(err, commonparams.ErrInfinity): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageLimitInvalidArg, fmt.Sprintf("invalid argument to $limit stage: Expected an integer: $limit: %#v", value), "$limit (stage)", ) - case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative): + case errors.Is(err, commonparams.ErrLongExceededPositive), errors.Is(err, commonparams.ErrLongExceededNegative): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageLimitInvalidArg, fmt.Sprintf("invalid argument to $limit stage: Cannot represent as a 64-bit integer: $limit: %#v", value), @@ -221,17 +150,19 @@ func GetLimitStageParam(value any) (int64, error) { // GetSkipStageParam returns $skip stage argument from the provided value. // It returns the proper error if value doesn't meet requirements. func GetSkipStageParam(value any) (int64, error) { - limit, err := GetWholeNumberParam(value) + limit, err := commonparams.GetWholeNumberParam(value) switch { case err == nil: - case errors.Is(err, errNotWholeNumber), errors.Is(err, errInfinity), errors.Is(err, errUnexpectedType): + case errors.Is(err, commonparams.ErrNotWholeNumber), + errors.Is(err, commonparams.ErrInfinity), + errors.Is(err, commonparams.ErrUnexpectedType): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageSkipBadValue, fmt.Sprintf("invalid argument to $skip stage: Expected an integer: $skip: %#v", value), "$skip (stage)", ) - case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative): + case errors.Is(err, commonparams.ErrLongExceededPositive), errors.Is(err, commonparams.ErrLongExceededNegative): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrStageSkipBadValue, fmt.Sprintf("invalid argument to $skip stage: Cannot represent as a 64-bit integer: $skip: %#v", value), @@ -284,11 +215,11 @@ func getBinaryMaskParam(mask any) (uint64, error) { case float64: // {field: {$bitsAllClear: bitmask}} if mask != math.Trunc(mask) || math.IsInf(mask, 0) { - return 0, errNotWholeNumber + return 0, commonparams.ErrNotWholeNumber } if mask < 0 { - return 0, errNegativeNumber + return 0, commonparams.ErrNegativeNumber } bitmask = uint64(mask) @@ -312,7 +243,7 @@ func getBinaryMaskParam(mask any) (uint64, error) { case int32: // {field: {$bitsAllClear: bitmask}} if mask < 0 { - return 0, errNegativeNumber + return 0, commonparams.ErrNegativeNumber } bitmask = uint64(mask) @@ -320,32 +251,18 @@ func getBinaryMaskParam(mask any) (uint64, error) { case int64: // {field: {$bitsAllClear: bitmask}} if mask < 0 { - return 0, errNegativeNumber + return 0, commonparams.ErrNegativeNumber } bitmask = uint64(mask) default: - return 0, errNotBinaryMask + return 0, commonparams.ErrNotBinaryMask } return bitmask, nil } -// parseTypeCode returns typeCode and error by given type code alias. -func parseTypeCode(alias string) (typeCode, error) { - code, ok := aliasToTypeCode[alias] - if !ok { - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf(`Unknown type name alias: %s`, alias), - "$type", - ) - } - - return code, nil -} - // addNumbers returns the result of v1 and v2 addition and error if addition failed. // The v1 and v2 parameters could be float64, int32, int64. // The result would be the broader type possible, i.e. int32 + int64 produces int64. @@ -360,7 +277,7 @@ func addNumbers(v1, v2 any) (any, error) { case int64: return v1 + float64(v2), nil default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } case int32: switch v2 := v2.(type) { @@ -379,17 +296,17 @@ func addNumbers(v1, v2 any) (any, error) { case int64: if v2 > 0 { if int64(v1) > math.MaxInt64-v2 { - return nil, errLongExceededPositive + return nil, commonparams.ErrLongExceededPositive } } else { if int64(v1) < math.MinInt64-v2 { - return nil, errLongExceededNegative + return nil, commonparams.ErrLongExceededNegative } } return v2 + int64(v1), nil default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } case int64: switch v2 := v2.(type) { @@ -398,11 +315,11 @@ func addNumbers(v1, v2 any) (any, error) { case int32: if v2 > 0 { if v1 > math.MaxInt64-int64(v2) { - return nil, errIntExceeded + return nil, commonparams.ErrIntExceeded } } else { if v1 < math.MinInt64-int64(v2) { - return nil, errIntExceeded + return nil, commonparams.ErrIntExceeded } } @@ -410,20 +327,20 @@ func addNumbers(v1, v2 any) (any, error) { case int64: if v2 > 0 { if v1 > math.MaxInt64-v2 { - return nil, errLongExceededPositive + return nil, commonparams.ErrLongExceededPositive } } else { if v1 < math.MinInt64-v2 { - return nil, errLongExceededNegative + return nil, commonparams.ErrLongExceededNegative } } return v1 + v2, nil default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } default: - return nil, errUnexpectedLeftOpType + return nil, commonparams.ErrUnexpectedLeftOpType } } @@ -445,7 +362,7 @@ func multiplyNumbers(v1, v2 any) (any, error) { case int64: res = v1 * float64(v2) default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } return res, nil @@ -464,23 +381,27 @@ func multiplyNumbers(v1, v2 any) (any, error) { return multiplyLongSafely(int64(v1), v2) default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } case int64: switch v2 := v2.(type) { case float64: return float64(v1) * v2, nil case int32: - return multiplyLongSafely(v1, int64(v2)) + v, err := multiplyLongSafely(v1, int64(v2)) + if err != nil { + return 0, commonparams.ErrIntExceeded + } + return v, nil case int64: return multiplyLongSafely(v1, v2) default: - return nil, errUnexpectedRightOpType + return nil, commonparams.ErrUnexpectedRightOpType } default: - return nil, errUnexpectedLeftOpType + return nil, commonparams.ErrUnexpectedLeftOpType } } @@ -499,68 +420,13 @@ func multiplyLongSafely(v1, v2 int64) (int64, error) { // This check is necessary only for MinInt64, as multiplying MinInt64 by -1 // results in overflow with the MinInt64 as result. case v1 == math.MinInt64 || v2 == math.MinInt64: - return 0, errLongExceededNegative + return 0, commonparams.ErrLongExceededNegative } res := v1 * v2 if res/v2 != v1 { - return 0, errLongExceededPositive + return 0, commonparams.ErrLongExceededPositive } return res, nil } - -// GetOptionalPositiveNumber returns doc's value for key or protocol error for invalid parameter. -func GetOptionalPositiveNumber(document *types.Document, key string) (int32, error) { - v, _ := document.Get(key) - if v == nil { - return 0, nil - } - - wholeNumberParam, err := GetWholeNumberParam(v) - if err != nil { - switch err { - case errUnexpectedType: - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("%s must be a number", key), - key, - ) - case errNotWholeNumber: - if _, ok := v.(float64); ok { - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("%v has non-integral value", key), - key, - ) - } - - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("%s must be a whole number", key), - key, - ) - default: - return 0, err - } - } - - if wholeNumberParam > math.MaxInt32 || wholeNumberParam < math.MinInt32 { - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("%v value for %s is out of range", int64(wholeNumberParam), key), - key, - ) - } - - value := int32(wholeNumberParam) - if value < 0 { - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("%v value for %s is out of range", value, key), - key, - ) - } - - return value, nil -} diff --git a/internal/handlers/common/params_test.go b/internal/handlers/common/params_test.go index 464784dc1606..43457791305a 100644 --- a/internal/handlers/common/params_test.go +++ b/internal/handlers/common/params_test.go @@ -19,6 +19,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" ) func TestMultiplyLongSafely(t *testing.T) { @@ -51,12 +53,12 @@ func TestMultiplyLongSafely(t *testing.T) { "OverflowLarge": { v1: 1 << 60, v2: 42, - err: errLongExceededPositive, + err: commonparams.ErrLongExceededPositive, }, "OverflowMax": { v1: math.MaxInt64, v2: 2, - err: errLongExceededPositive, + err: commonparams.ErrLongExceededPositive, }, "MaxMinusOne": { v1: math.MaxInt64, @@ -66,17 +68,17 @@ func TestMultiplyLongSafely(t *testing.T) { "OverflowMaxMinusTwo": { v1: math.MaxInt64, v2: -2, - err: errLongExceededPositive, + err: commonparams.ErrLongExceededPositive, }, "OverflowMin": { v1: math.MinInt64, v2: 2, - err: errLongExceededNegative, + err: commonparams.ErrLongExceededNegative, }, "OverflowMinMinusOne": { v1: math.MinInt64, v2: -1, - err: errLongExceededNegative, + err: commonparams.ErrLongExceededNegative, }, } { name, tc := name, tc diff --git a/internal/handlers/common/parse_osrelease.go b/internal/handlers/common/parse_osrelease.go deleted file mode 100644 index 941c259afebb..000000000000 --- a/internal/handlers/common/parse_osrelease.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package common - -import ( - "bufio" - "io" - "strconv" - "strings" - - "github.com/FerretDB/FerretDB/internal/util/lazyerrors" -) - -// parseOSRelease parses the /etc/os-release or /usr/lib/os-release file content, -// returning the OS name and version. -func parseOSRelease(r io.Reader) (string, string, error) { - scanner := bufio.NewScanner(r) - - configParams := map[string]string{} - for scanner.Scan() { - key, value, ok := strings.Cut(scanner.Text(), "=") - if !ok { - continue - } - - if v, err := strconv.Unquote(value); err == nil { - value = v - } - - configParams[key] = value - } - - if err := scanner.Err(); err != nil { - return "", "", lazyerrors.Error(err) - } - - return configParams["NAME"], configParams["VERSION"], nil -} diff --git a/internal/handlers/common/projection.go b/internal/handlers/common/projection.go index b955ada3be3e..fe6853bee2dc 100644 --- a/internal/handlers/common/projection.go +++ b/internal/handlers/common/projection.go @@ -27,16 +27,22 @@ import ( "github.com/FerretDB/FerretDB/internal/util/must" ) -var errProjectionEmpty = errors.New("projection is empty") - // ValidateProjection check projection document. // Document fields could be either included or excluded but not both. // Exception is for the _id field that could be included or excluded. +// ValidateProjection returns errProjectionEmpty for empty projection and +// CommandError for invalid projection fields. +// +// Errors: +// - `ErrProjectionExIn` when there is exclusion in inclusion projection; +// - `ErrProjectionInEx` when there is inclusion in exclusion projection; +// - `ErrNotImplemented` when there is unimplemented projection operators and expressions; func ValidateProjection(projection *types.Document) (*types.Document, bool, error) { validated := types.MakeDocument(0) if projection.Len() == 0 { - return nil, false, errProjectionEmpty + // empty projection is exclusion project. + return types.MakeDocument(0), false, nil } var projectionVal *bool @@ -54,7 +60,7 @@ func ValidateProjection(projection *types.Document) (*types.Document, bool, erro return nil, false, lazyerrors.Error(err) } - if strings.Contains(key, "$") { + if strings.HasPrefix(key, "$") { return nil, false, commonerrors.NewCommandErrorMsg( commonerrors.ErrNotImplemented, fmt.Sprintf("projection operator $ is not supported in %s", key), @@ -221,24 +227,21 @@ func projectDocumentWithoutID(doc *types.Document, projection *types.Document, i ) case *types.Array, string, types.Binary, types.ObjectID, - time.Time, types.NullType, types.Regex, types.Timestamp: // all this types are treated as new fields value + time.Time, types.NullType, types.Regex, types.Timestamp: // all these types are treated as new fields value projected.Set(key, value) case bool: // field: bool - // process top level fields - if path.Len() == 1 { - if inclusion { - if docWithoutID.Has(key) { - projected.Set(key, must.NotFail(docWithoutID.Get(key))) - } - - continue + if inclusion { + // inclusion projection copies the field on the path from docWithoutID to projected. + if _, err = includeProjection(path, docWithoutID, projected); err != nil { + return nil, err } - projected.Remove(key) + continue } - // TODO: process dot notation here https://github.com/FerretDB/FerretDB/issues/2430 + // exclusion projection removes the field on the path in projected. + excludeProjection(path, projected) default: return nil, lazyerrors.Errorf("unsupported operation %s %v (%T)", key, value, value) } @@ -246,3 +249,240 @@ func projectDocumentWithoutID(doc *types.Document, projection *types.Document, i return projected, nil } + +// includeProjection copies the field on the path from source to projected. +// When an array is on the path, it returns the array containing any document +// with the same key. Dot notation with array index path does not include +// the field unlike document.SetByPath(path). +// Inclusion projection with non-existent path creates an empty document +// or an empty array based on what source has. +// It returns iterator errors other than ErrIteratorDone. +// If the projected contains field that is not expected in source, it panics. +// +// Example: "v.foo" path inclusion projection: +// {v: {foo: 1, bar: 1}} -> {v: {foo: 1}} +// {v: {bar: 1}} -> {v: {}} +// {v: [{bar: 1}]} -> {v: [{}]} +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{foo: 1}, {foo: 2}, {}]} +// +// Example: "v.0.foo" path inclusion projection: +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{}, {}, {}]} +func includeProjection(path types.Path, source any, projected *types.Document) (*types.Array, error) { + key := path.Prefix() + + switch source := source.(type) { + case *types.Document: + embeddedSource, err := source.Get(key) + if err != nil { + // key does not exist, nothing to set. + return nil, nil + } + + if path.Len() <= 1 { + // path reached suffix, set field in projected. + setBySourceOrder(key, embeddedSource, source, projected) + return nil, nil + } + + doc := new(types.Document) + + if projected.Has(key) { + // set doc if projected has field from other projection field. + v := must.NotFail(projected.Get(key)) + if d, ok := v.(*types.Document); ok { + doc = d + } + + if arr, ok := v.(*types.Array); ok { + // use next prefix key with arr value, allowing array to parse existing + // projection fields. + doc = must.NotFail(types.NewDocument(path.TrimPrefix().Prefix(), arr)) + } + } + + // when next prefix has an array use returned value arr, + // if it has a document, field in the doc is set by includeProjection. + arr, err := includeProjection(path.TrimPrefix(), embeddedSource, doc) + if err != nil { + return nil, err + } + + switch embeddedSource.(type) { + case *types.Document: + setBySourceOrder(key, doc, source, projected) + case *types.Array: + projected.Set(key, arr) + } + + return nil, nil + case *types.Array: + iter := source.Iterator() + defer iter.Close() + + arr := new(types.Array) + var inclusionExists bool + + if v, err := projected.Get(key); err == nil { + projectedArr, ok := v.(*types.Array) + if ok { + arr = projectedArr + inclusionExists = true + } + } + + i := 0 + + for { + _, arrElem, err := iter.Next() + if err != nil { + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + return nil, lazyerrors.Error(err) + } + + if _, ok := arrElem.(*types.Document); !ok { + continue + } + + doc := new(types.Document) + + if inclusionExists { + // when there are multiple inclusion fields, first inclusion + // inserts all documents from source to arr, they could be empty + // if it did not match previous inclusion fields. + // But number of documents in arr must be the same as number of documents + // in source. + var v any + + v, err = arr.Get(i) + if err != nil { + panic(err) + } + + docVal, ok := v.(*types.Document) + if !ok { + panic("projected field must be a document") + } + + doc = docVal + } else { + // first inclusion field, insert it to the doc. + arr.Append(doc) + } + + if _, err = includeProjection(path, arrElem, doc); err != nil { + return nil, err + } + + arr.Set(i, doc) + i++ + } + + return arr, nil + default: + // field is not a document or an array, nothing to set. + return nil, nil + } +} + +// excludeProjection removes the field on the path in projected. +// When an array is on the path, it checks if the array contains any document +// with the key to remove that document. This is not the case in document.Remove(key). +// Dot notation with array index path do not exclude unlike document.RemoveByPath(key). +// +// Examples: "v.foo" path exclusion projection: +// {v: {foo: 1}} -> {v: {}} +// {v: {foo: 1, bar: 1}} -> {v: {bar: 1}} +// {v: [{foo: 1}, {foo: 2}]} -> {v: [{}, {}]} +// {v: [{foo: 1}, {foo: 2}, {bar: 1}]} -> {v: [{}, {}, {bar: 1}]} +// +// Example: "v.0.foo" path exclusion projection: +// {v: [{foo: 1}, {foo: 2}]} -> {v: [{foo: 1}, {foo: 2}]} +func excludeProjection(path types.Path, projected any) { + key := path.Prefix() + + switch projected := projected.(type) { + case *types.Document: + embeddedSource, err := projected.Get(key) + if err != nil { + // key does not exist, nothing to exclude. + return + } + + if path.Len() <= 1 { + // path reached suffix, remove the field from the document. + projected.Remove(key) + return + } + + // recursively remove field from the embeddedSource. + excludeProjection(path.TrimPrefix(), embeddedSource) + + return + case *types.Array: + // modifies the field of projected, hence not using iterator. + for i := 0; i < projected.Len(); i++ { + arrElem := must.NotFail(projected.Get(i)) + + if _, ok := arrElem.(*types.Document); !ok { + // not a document, cannot possibly be part of path, do nothing. + continue + } + + excludeProjection(path, arrElem) + } + + return + default: + // not a path, nothing to exclude. + return + } +} + +// setBySourceOrder sets the key value field to projected in same field order as the source. +// Example: +// +// key: foo +// val: 1 +// source: {foo: 1, bar: 2} +// projected: {bar: 2} +// +// setBySourceOrder sets projected to {foo: 1, bar: 2} rather than adding it to the last field. +func setBySourceOrder(key string, val any, source, projected *types.Document) { + projectedKeys := projected.Keys() + + // newFieldIndex is where new field is to be inserted in projected document. + newFieldIndex := 0 + + for _, sourceKey := range source.Keys() { + if sourceKey == key { + break + } + + if newFieldIndex >= len(projectedKeys) { + break + } + + if sourceKey == projectedKeys[newFieldIndex] { + newFieldIndex++ + } + } + + tmp := projected.DeepCopy() + + // remove fields of projected from newFieldIndex to the end + for i := newFieldIndex; i < len(projectedKeys); i++ { + projected.Remove(projectedKeys[i]) + } + + projected.Set(key, val) + + // copy newFieldIndex-th to the end from tmp to projected + i := newFieldIndex + for _, key := range tmp.Keys()[newFieldIndex:] { + projected.Set(key, must.NotFail(tmp.Get(tmp.Keys()[i]))) + i++ + } +} diff --git a/internal/handlers/common/projection_iterator.go b/internal/handlers/common/projection_iterator.go index 933669b39d14..8008651b3b1f 100644 --- a/internal/handlers/common/projection_iterator.go +++ b/internal/handlers/common/projection_iterator.go @@ -15,8 +15,6 @@ package common import ( - "errors" - "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -30,10 +28,6 @@ import ( // Close method closes the underlying iterator. func ProjectionIterator(iter types.DocumentsIterator, closer *iterator.MultiCloser, projection *types.Document) (types.DocumentsIterator, error) { //nolint:lll // for readability projectionValidated, inclusion, err := ValidateProjection(projection) - if errors.Is(err, errProjectionEmpty) { - return iter, nil - } - if err != nil { return nil, lazyerrors.Error(err) } diff --git a/internal/handlers/common/scale.go b/internal/handlers/common/scale.go index 94de0ac25ee9..4f522ae8190d 100644 --- a/internal/handlers/common/scale.go +++ b/internal/handlers/common/scale.go @@ -20,6 +20,7 @@ import ( "math" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) @@ -29,7 +30,7 @@ import ( // If the value is valid, it returns its int32 representation, // otherwise it returns a command error with the given command being mentioned. func GetScaleParam(command string, value any) (int32, error) { - scaleValue, err := GetWholeNumberParam(value) + scaleValue, err := commonparams.GetWholeNumberParam(value) if err == nil { if scaleValue <= 0 { @@ -48,7 +49,7 @@ func GetScaleParam(command string, value any) (int32, error) { } switch { - case errors.Is(err, errUnexpectedType): + case errors.Is(err, commonparams.ErrUnexpectedType): if _, ok := value.(types.NullType); ok { return 1, nil } @@ -57,11 +58,11 @@ func GetScaleParam(command string, value any) (int32, error) { commonerrors.ErrTypeMismatch, fmt.Sprintf( `BSON field '%s.scale' is the wrong type '%s', expected types '[long, int, decimal, double]'`, - command, AliasFromType(value), + command, commonparams.AliasFromType(value), ), "scale", ) - case errors.Is(err, errNotWholeNumber): + case errors.Is(err, commonparams.ErrNotWholeNumber): if math.Signbit(value.(float64)) { return 0, commonerrors.NewCommandError( commonerrors.ErrValueNegative, @@ -72,10 +73,10 @@ func GetScaleParam(command string, value any) (int32, error) { // for non-integer numbers, scale value is rounded to the greatest integer value less than the given value. return int32(math.Floor(value.(float64))), nil - case errors.Is(err, errLongExceededPositive): + case errors.Is(err, commonparams.ErrLongExceededPositive): return math.MaxInt32, nil - case errors.Is(err, errLongExceededNegative): + case errors.Is(err, commonparams.ErrLongExceededNegative): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrValueNegative, fmt.Sprintf("BSON field 'scale' value must be >= 1, actual value '%d'", math.MinInt32), diff --git a/internal/handlers/common/serverstatus.go b/internal/handlers/common/serverstatus.go index a69562d911c9..030bf414a5da 100644 --- a/internal/handlers/common/serverstatus.go +++ b/internal/handlers/common/serverstatus.go @@ -75,6 +75,10 @@ func ServerStatus(state *state.State, cm *connmetrics.ConnMetrics) (*types.Docum "metrics", must.NotFail(types.NewDocument( "commands", metricsDoc, )), + + // our extensions + "ferretdbVersion", version.Get().Version, + "ok", float64(1), )) diff --git a/internal/handlers/common/skip.go b/internal/handlers/common/skip.go index 006aa4b6c3ad..be7dbefe06e7 100644 --- a/internal/handlers/common/skip.go +++ b/internal/handlers/common/skip.go @@ -15,72 +15,10 @@ package common import ( - "errors" - "fmt" - "math" - - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) -// GetSkipParam validates the given skip value for find and count commands. -// -// If the value is valid, it returns its int64 representation, -// otherwise it returns a command error with the given command being mentioned. -func GetSkipParam(command string, value any) (int64, error) { - skipValue, err := GetWholeNumberParam(value) - - if err == nil { - if skipValue < 0 { - return 0, commonerrors.NewCommandError( - commonerrors.ErrValueNegative, - fmt.Errorf("BSON field 'skip' value must be >= 0, actual value '%d'", skipValue), - ) - } - - return skipValue, nil - } - - switch { - case errors.Is(err, errUnexpectedType): - if _, ok := value.(types.NullType); ok { - return 0, nil - } - - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrTypeMismatch, - fmt.Sprintf( - `BSON field '%s.skip' is the wrong type '%s', expected types '[long, int, decimal, double]'`, - command, AliasFromType(value), - ), - "skip", - ) - case errors.Is(err, errNotWholeNumber): - if math.Signbit(value.(float64)) { - return 0, commonerrors.NewCommandError( - commonerrors.ErrValueNegative, - fmt.Errorf("BSON field 'skip' value must be >= 0, actual value '%d'", int(math.Ceil(value.(float64)))), - ) - } - - // for non-integer numbers, skip value is rounded to the greatest integer value less than the given value. - return int64(math.Floor(value.(float64))), nil - - case errors.Is(err, errLongExceededPositive): - return math.MaxInt64, nil - - case errors.Is(err, errLongExceededNegative): - return 0, commonerrors.NewCommandError( - commonerrors.ErrValueNegative, - fmt.Errorf("BSON field 'skip' value must be >= 0, actual value '%d'", int(math.Ceil(value.(float64)))), - ) - - default: - return 0, lazyerrors.Error(err) - } -} - // SkipDocuments returns a subslice of given documents according to the given skip value. func SkipDocuments(docs []*types.Document, skip int64) ([]*types.Document, error) { switch { diff --git a/internal/handlers/common/sort.go b/internal/handlers/common/sort.go index bf44a87e0e26..f68839919ebf 100644 --- a/internal/handlers/common/sort.go +++ b/internal/handlers/common/sort.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" @@ -91,7 +92,8 @@ func lessFunc(sortPath types.Path, sortType types.SortType) func(a, b *types.Doc bField, err := b.GetByPath(sortPath) if err != nil { - return false + // same logic as above + bField = types.Null } result := types.CompareOrderForSort(aField, bField, sortType) @@ -139,10 +141,10 @@ func (ds *docsSorter) Less(i, j int) bool { // GetSortType determines SortType from input sort value. func GetSortType(key string, value any) (types.SortType, error) { - sortValue, err := GetWholeNumberParam(value) + sortValue, err := commonparams.GetWholeNumberParam(value) if err != nil { switch { - case errors.Is(err, errUnexpectedType): + case errors.Is(err, commonparams.ErrUnexpectedType): if _, ok := value.(types.NullType); ok { value = "null" } @@ -152,7 +154,7 @@ func GetSortType(key string, value any) (types.SortType, error) { fmt.Sprintf(`Illegal key in $sort specification: %v: %v`, key, value), "$sort", ) - case errors.Is(err, errNotWholeNumber): + case errors.Is(err, commonparams.ErrNotWholeNumber): return 0, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, "$sort must be a whole number", diff --git a/internal/handlers/common/typecode.go b/internal/handlers/common/typecode.go deleted file mode 100644 index bdf5df458c5d..000000000000 --- a/internal/handlers/common/typecode.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package common - -import ( - "fmt" - "math" - "time" - - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" - "github.com/FerretDB/FerretDB/internal/types" - "github.com/FerretDB/FerretDB/internal/util/must" -) - -//go:generate ../../../bin/stringer -linecomment -type typeCode - -// typeCode represents BSON type codes. -// BSON type codes represent corresponding codes in BSON specification. -// They could be used to query fields with particular type values using $type operator. -// Type code `number` is added to support MongoDB surrogate alias `number` which matches double, int and long type values. -type typeCode int32 - -const ( - typeCodeDouble = typeCode(1) // double - typeCodeString = typeCode(2) // string - typeCodeObject = typeCode(3) // object - typeCodeArray = typeCode(4) // array - typeCodeBinData = typeCode(5) // binData - typeCodeObjectID = typeCode(7) // objectId - typeCodeBool = typeCode(8) // bool - typeCodeDate = typeCode(9) // date - typeCodeNull = typeCode(10) // null - typeCodeRegex = typeCode(11) // regex - typeCodeInt = typeCode(16) // int - typeCodeTimestamp = typeCode(17) // timestamp - typeCodeLong = typeCode(18) // long - // Not implemented. - typeCodeDecimal = typeCode(19) // decimal - typeCodeMinKey = typeCode(-1) // minKey - typeCodeMaxKey = typeCode(127) // maxKey - // Not actual type code. `number` matches double, int and long. - typeCodeNumber = typeCode(-128) // number -) - -// newTypeCode returns typeCode and error by given code. -func newTypeCode(code int32) (typeCode, error) { - c := typeCode(code) - switch c { - case typeCodeDouble, typeCodeString, typeCodeObject, typeCodeArray, - typeCodeBinData, typeCodeObjectID, typeCodeBool, typeCodeDate, - typeCodeNull, typeCodeRegex, typeCodeInt, typeCodeTimestamp, typeCodeLong, typeCodeNumber: - return c, nil - case typeCodeDecimal, typeCodeMinKey, typeCodeMaxKey: - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrNotImplemented, - fmt.Sprintf(`Type code %v not implemented`, code), - "$type", - ) - default: - return 0, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf(`Invalid numerical type code: %d`, code), - "$type", - ) - } -} - -// hasSameTypeElements returns true if types.Array elements has the same type. -// MongoDB consider int32, int64 and float64 that could be converted to int as the same type. -func hasSameTypeElements(array *types.Array) bool { - var prev string - for i := 0; i < array.Len(); i++ { - var cur string - - element := must.NotFail(array.Get(i)) - - if isWholeNumber(element) { - cur = typeCodeNumber.String() - } else { - cur = AliasFromType(element) - } - - if prev == "" { - prev = cur - continue - } - - if prev != cur { - return false - } - } - - return true -} - -// aliasToTypeCode matches string type aliases to the corresponding typeCode value. -var aliasToTypeCode = map[string]typeCode{} - -func init() { - for _, i := range []typeCode{ - typeCodeDouble, typeCodeString, typeCodeObject, typeCodeArray, - typeCodeBinData, typeCodeObjectID, typeCodeBool, typeCodeDate, typeCodeNull, - typeCodeRegex, typeCodeInt, typeCodeTimestamp, typeCodeLong, typeCodeNumber, - } { - aliasToTypeCode[i.String()] = i - } -} - -// AliasFromType returns type alias name for given value. -func AliasFromType(v any) string { - switch v := v.(type) { - case *types.Document: - return typeCodeObject.String() - case *types.Array: - return typeCodeArray.String() - case float64: - return typeCodeDouble.String() - case string: - return typeCodeString.String() - case types.Binary: - return typeCodeBinData.String() - case types.ObjectID: - return typeCodeObjectID.String() - case bool: - return typeCodeBool.String() - case time.Time: - return typeCodeDate.String() - case types.NullType: - return typeCodeNull.String() - case types.Regex: - return typeCodeRegex.String() - case int32: - return typeCodeInt.String() - case types.Timestamp: - return typeCodeTimestamp.String() - case int64: - return typeCodeLong.String() - default: - panic(fmt.Sprintf("not supported type %T", v)) - } -} - -// isWholeNumber checks is the given value represents a whole number type. -func isWholeNumber(v any) bool { - switch v := v.(type) { - case float64: - return !(v != math.Trunc(v) || math.IsNaN(v) || math.IsInf(v, 0)) - case int32: - return true - case int64: - return true - default: - return false - } -} diff --git a/internal/handlers/common/typecode_string.go b/internal/handlers/common/typecode_string.go deleted file mode 100644 index de4bc02ed3f6..000000000000 --- a/internal/handlers/common/typecode_string.go +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated by "stringer -linecomment -type typeCode"; DO NOT EDIT. - -package common - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[typeCodeDouble-1] - _ = x[typeCodeString-2] - _ = x[typeCodeObject-3] - _ = x[typeCodeArray-4] - _ = x[typeCodeBinData-5] - _ = x[typeCodeObjectID-7] - _ = x[typeCodeBool-8] - _ = x[typeCodeDate-9] - _ = x[typeCodeNull-10] - _ = x[typeCodeRegex-11] - _ = x[typeCodeInt-16] - _ = x[typeCodeTimestamp-17] - _ = x[typeCodeLong-18] - _ = x[typeCodeDecimal-19] - _ = x[typeCodeMinKey - -1] - _ = x[typeCodeMaxKey-127] - _ = x[typeCodeNumber - -128] -} - -const ( - _typeCode_name_0 = "number" - _typeCode_name_1 = "minKey" - _typeCode_name_2 = "doublestringobjectarraybinData" - _typeCode_name_3 = "objectIdbooldatenullregex" - _typeCode_name_4 = "inttimestamplongdecimal" - _typeCode_name_5 = "maxKey" -) - -var ( - _typeCode_index_2 = [...]uint8{0, 6, 12, 18, 23, 30} - _typeCode_index_3 = [...]uint8{0, 8, 12, 16, 20, 25} - _typeCode_index_4 = [...]uint8{0, 3, 12, 16, 23} -) - -func (i typeCode) String() string { - switch { - case i == -128: - return _typeCode_name_0 - case i == -1: - return _typeCode_name_1 - case 1 <= i && i <= 5: - i -= 1 - return _typeCode_name_2[_typeCode_index_2[i]:_typeCode_index_2[i+1]] - case 7 <= i && i <= 11: - i -= 7 - return _typeCode_name_3[_typeCode_index_3[i]:_typeCode_index_3[i+1]] - case 16 <= i && i <= 19: - i -= 16 - return _typeCode_name_4[_typeCode_index_4[i]:_typeCode_index_4[i+1]] - case i == 127: - return _typeCode_name_5 - default: - return "typeCode(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/internal/handlers/common/update.go b/internal/handlers/common/update.go index ec1a94ef3509..db4563426625 100644 --- a/internal/handlers/common/update.go +++ b/internal/handlers/common/update.go @@ -25,6 +25,7 @@ import ( "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -34,8 +35,9 @@ import ( // UpdateDocument updates the given document with a series of update operators. // Returns true if document was changed. // To validate update document, must call ValidateUpdateOperators before calling UpdateDocument. -// TODO findAndModify returns CommandError https://github.com/FerretDB/FerretDB/issues/2440 -func UpdateDocument(doc, update *types.Document) (bool, error) { +// UpdateDocument returns CommandError for findAndModify case-insensitive command name, +// WriteError for other commands. +func UpdateDocument(command string, doc, update *types.Document) (bool, error) { var changed bool var err error @@ -63,13 +65,13 @@ func UpdateDocument(doc, update *types.Document) (bool, error) { } case "$set": - changed, err = processSetFieldExpression(doc, updateV.(*types.Document), false) + changed, err = processSetFieldExpression(command, doc, updateV.(*types.Document), false) if err != nil { return false, err } case "$setOnInsert": - changed, err = processSetFieldExpression(doc, updateV.(*types.Document), true) + changed, err = processSetFieldExpression(command, doc, updateV.(*types.Document), true) if err != nil { return false, err } @@ -83,14 +85,8 @@ func UpdateDocument(doc, update *types.Document) (bool, error) { path, err = types.NewPathFromString(key) if err != nil { - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrEmptyName, - fmt.Sprintf("Cannot apply $unset to a value of non-numeric type. "+ - "{_id: %s} has the field '%s' of non-numeric type object", - must.NotFail(doc.Get("_id")), - key, - ), - ) + // ValidateUpdateOperators checked already $unset contains valid path. + panic(err) } if doc.HasByPath(path) { @@ -100,19 +96,19 @@ func UpdateDocument(doc, update *types.Document) (bool, error) { } case "$inc": - changed, err = processIncFieldExpression(doc, updateV) + changed, err = processIncFieldExpression(command, doc, updateV) if err != nil { return false, err } case "$max": - changed, err = processMaxFieldExpression(doc, updateV) + changed, err = processMaxFieldExpression(command, doc, updateV) if err != nil { return false, err } case "$min": - changed, err = processMinFieldExpression(doc, updateV) + changed, err = processMinFieldExpression(command, doc, updateV) if err != nil { return false, err } @@ -120,14 +116,14 @@ func UpdateDocument(doc, update *types.Document) (bool, error) { case "$mul": var mulChanged bool - if mulChanged, err = processMulFieldExpression(doc, updateV); err != nil { + if mulChanged, err = processMulFieldExpression(command, doc, updateV); err != nil { return false, err } changed = changed || mulChanged case "$rename": - changed, err = processRenameFieldExpression(doc, updateV.(*types.Document)) + changed, err = processRenameFieldExpression(command, doc, updateV.(*types.Document)) if err != nil { return false, err } @@ -193,7 +189,7 @@ func UpdateDocument(doc, update *types.Document) (bool, error) { // processSetFieldExpression changes document according to $set and $setOnInsert operators. // If the document was changed it returns true. -func processSetFieldExpression(doc, setDoc *types.Document, setOnInsert bool) (bool, error) { +func processSetFieldExpression(command string, doc, setDoc *types.Document, setOnInsert bool) (bool, error) { var changed bool setDocKeys := setDoc.Keys() @@ -229,10 +225,7 @@ func processSetFieldExpression(doc, setDoc *types.Document, setOnInsert bool) (b } if err := doc.SetByPath(path, setValue); err != nil { - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrUnsuitableValueType, - err.Error(), - ) + return false, newUpdateError(commonerrors.ErrUnsuitableValueType, err.Error(), command) } changed = true @@ -243,7 +236,7 @@ func processSetFieldExpression(doc, setDoc *types.Document, setOnInsert bool) (b // processRenameFieldExpression changes document according to $rename operator. // If the document was changed it returns true. -func processRenameFieldExpression(doc *types.Document, update *types.Document) (bool, error) { +func processRenameFieldExpression(command string, doc *types.Document, update *types.Document) (bool, error) { update.SortFieldsByKey() var changed bool @@ -252,7 +245,11 @@ func processRenameFieldExpression(doc *types.Document, update *types.Document) ( renameRawValue := must.NotFail(update.Get(key)) if key == "" || renameRawValue == "" { - return changed, commonerrors.NewWriteErrorMsg(commonerrors.ErrEmptyName, "An empty update path is not valid.") + return changed, newUpdateError( + commonerrors.ErrEmptyName, + "An empty update path is not valid.", + command, + ) } // this is covered in validateRenameExpression @@ -262,13 +259,13 @@ func processRenameFieldExpression(doc *types.Document, update *types.Document) ( if err != nil { var pathErr *types.DocumentPathError if errors.As(err, &pathErr) && pathErr.Code() == types.ErrDocumentPathEmptyKey { - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrEmptyName, - fmt.Sprintf("Cannot apply $rename to a value of non-numeric type. "+ - "{_id: %s} has the field '%s' of non-numeric type object", - must.NotFail(doc.Get("_id")), + fmt.Sprintf( + "The update path '%s' contains an empty field name, which is not allowed.", key, ), + command, ) } } @@ -291,13 +288,14 @@ func processRenameFieldExpression(doc *types.Document, update *types.Document) ( } if dpe.Code() == types.ErrDocumentPathArrayInvalidIndex { - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrUnsuitableValueType, - fmt.Sprintf("cannot use path %s to traverse the document", sourcePath), + fmt.Sprintf("cannot use path '%s' to traverse the document", sourcePath), + command, ) } - return changed, commonerrors.NewWriteErrorMsg(commonerrors.ErrUnsuitableValueType, dpe.Error()) + return changed, newUpdateError(commonerrors.ErrUnsuitableValueType, dpe.Error(), command) } // Remove old document @@ -316,7 +314,7 @@ func processRenameFieldExpression(doc *types.Document, update *types.Document) ( // processIncFieldExpression changes document according to $inc operator. // If the document was changed it returns true. -func processIncFieldExpression(doc *types.Document, updateV any) (bool, error) { +func processIncFieldExpression(command string, doc *types.Document, updateV any) (bool, error) { // updateV is document, checked in ValidateUpdateOperators. incDoc := updateV.(*types.Document) @@ -325,27 +323,29 @@ func processIncFieldExpression(doc *types.Document, updateV any) (bool, error) { for _, incKey := range incDoc.Keys() { incValue := must.NotFail(incDoc.Get(incKey)) + // ensure incValue is a valid number type. + switch incValue.(type) { + case float64, int32, int64: + default: + return false, newUpdateError( + commonerrors.ErrTypeMismatch, + fmt.Sprintf(`Cannot increment with non-numeric argument: {%s: %#v}`, incKey, incValue), + command, + ) + } + var err error // incKey has valid path, checked in ValidateUpdateOperators. path := must.NotFail(types.NewPathFromString(incKey)) if !doc.HasByPath(path) { - // ensure incValue is a valid number type. - switch incValue.(type) { - case float64, int32, int64: - default: - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrTypeMismatch, - fmt.Sprintf(`Cannot increment with non-numeric argument: {%s: %#v}`, incKey, incValue), - ) - } - // $inc sets the field if it does not exist. if err := doc.SetByPath(path, incValue); err != nil { - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrUnsuitableValueType, err.Error(), + command, ) } @@ -385,48 +385,42 @@ func processIncFieldExpression(doc *types.Document, updateV any) (bool, error) { } switch { - case errors.Is(err, errUnexpectedLeftOpType): - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrTypeMismatch, - fmt.Sprintf( - `Cannot increment with non-numeric argument: {%s: %#v}`, - incKey, - incValue, - ), - ) - case errors.Is(err, errUnexpectedRightOpType): + case errors.Is(err, commonparams.ErrUnexpectedRightOpType): k := incKey if path.Len() > 1 { k = path.Suffix() } - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrTypeMismatch, fmt.Sprintf( `Cannot apply $inc to a value of non-numeric type. `+ `{_id: %s} has the field '%s' of non-numeric type %s`, types.FormatAnyValue(must.NotFail(doc.Get("_id"))), k, - AliasFromType(docValue), + commonparams.AliasFromType(docValue), ), + command, ) - case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative): - return false, commonerrors.NewWriteErrorMsg( + case errors.Is(err, commonparams.ErrLongExceededPositive), errors.Is(err, commonparams.ErrLongExceededNegative): + return false, newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf( `Failed to apply $inc operations to current value ((NumberLong)%d) for document {_id: "%s"}`, docValue, must.NotFail(doc.Get("_id")), ), + command, ) - case errors.Is(err, errIntExceeded): - return false, commonerrors.NewWriteErrorMsg( + case errors.Is(err, commonparams.ErrIntExceeded): + return false, newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf( `Failed to apply $inc operations to current value ((NumberInt)%d) for document {_id: "%s"}`, docValue, must.NotFail(doc.Get("_id")), ), + command, ) default: return false, lazyerrors.Error(err) @@ -438,7 +432,7 @@ func processIncFieldExpression(doc *types.Document, updateV any) (bool, error) { // processMaxFieldExpression changes document according to $max operator. // If the document was changed it returns true. -func processMaxFieldExpression(doc *types.Document, updateV any) (bool, error) { +func processMaxFieldExpression(command string, doc *types.Document, updateV any) (bool, error) { maxExpression := updateV.(*types.Document) maxExpression.SortFieldsByKey() @@ -463,7 +457,7 @@ func processMaxFieldExpression(doc *types.Document, updateV any) (bool, error) { if !doc.HasByPath(path) { err = doc.SetByPath(path, maxVal) if err != nil { - return false, commonerrors.NewWriteErrorMsg(commonerrors.ErrUnsuitableValueType, err.Error()) + return false, newUpdateError(commonerrors.ErrUnsuitableValueType, err.Error(), command) } changed = true @@ -506,7 +500,7 @@ func processMaxFieldExpression(doc *types.Document, updateV any) (bool, error) { // processMinFieldExpression changes document according to $min operator. // If the document was changed it returns true. -func processMinFieldExpression(doc *types.Document, updateV any) (bool, error) { +func processMinFieldExpression(command string, doc *types.Document, updateV any) (bool, error) { minExpression := updateV.(*types.Document) minExpression.SortFieldsByKey() @@ -531,7 +525,7 @@ func processMinFieldExpression(doc *types.Document, updateV any) (bool, error) { if !doc.HasByPath(path) { err = doc.SetByPath(path, minVal) if err != nil { - return false, commonerrors.NewWriteErrorMsg(commonerrors.ErrUnsuitableValueType, err.Error()) + return false, newUpdateError(commonerrors.ErrUnsuitableValueType, err.Error(), command) } changed = true @@ -567,17 +561,9 @@ func processMinFieldExpression(doc *types.Document, updateV any) (bool, error) { // processMulFieldExpression updates document according to $mul operator. // If the document was changed it returns true. -func processMulFieldExpression(doc *types.Document, updateV any) (bool, error) { - mulDoc, ok := updateV.(*types.Document) - if !ok { - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrFailedToParse, - fmt.Sprintf(`Modifiers operate on fields but we found type %[1]s instead. `+ - `For example: {$mod: {: ...}} not {$rename: %[1]s}`, - AliasFromType(updateV), - ), - ) - } +func processMulFieldExpression(command string, doc *types.Document, updateV any) (bool, error) { + // updateV is document, checked in ValidateUpdateOperators. + mulDoc := updateV.(*types.Document) var changed bool @@ -589,14 +575,8 @@ func processMulFieldExpression(doc *types.Document, updateV any) (bool, error) { path, err = types.NewPathFromString(mulKey) if err != nil { - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrEmptyName, - fmt.Sprintf("Cannot apply $mul to a value of non-numeric type. "+ - "{_id: %s} has the field '%s' of non-numeric type object", - must.NotFail(doc.Get("_id")), - mulKey, - ), - ) + // ValidateUpdateOperators checked already $mul contains valid path. + panic(err) } if !doc.HasByPath(path) { @@ -609,17 +589,19 @@ func processMulFieldExpression(doc *types.Document, updateV any) (bool, error) { case int64: mulValue = int64(0) default: - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrTypeMismatch, fmt.Sprintf(`Cannot multiply with non-numeric argument: {%s: %#v}`, mulKey, mulValue), + command, ) } err := doc.SetByPath(path, mulValue) if err != nil { - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrUnsuitableValueType, err.Error(), + command, ) } @@ -649,10 +631,8 @@ func processMulFieldExpression(doc *types.Document, updateV any) (bool, error) { err = doc.SetByPath(path, multiplied) if err != nil { - return false, commonerrors.NewWriteErrorMsg( - commonerrors.ErrUnsuitableValueType, - fmt.Sprintf(`Cannot create field in element {%s: %v}`, path.Prefix(), docValue), - ) + // after successfully getting value from path, setting it back cannot fail. + panic(err) } // A change from int32(0) to int64(0) is considered changed. @@ -666,48 +646,52 @@ func processMulFieldExpression(doc *types.Document, updateV any) (bool, error) { continue - case errors.Is(err, errUnexpectedLeftOpType): - return false, commonerrors.NewWriteErrorMsg( + case errors.Is(err, commonparams.ErrUnexpectedLeftOpType): + return false, newUpdateError( commonerrors.ErrTypeMismatch, fmt.Sprintf( `Cannot multiply with non-numeric argument: {%s: %#v}`, mulKey, mulValue, ), + command, ) - case errors.Is(err, errUnexpectedRightOpType): + case errors.Is(err, commonparams.ErrUnexpectedRightOpType): k := mulKey if path.Len() > 1 { k = path.Suffix() } - return false, commonerrors.NewWriteErrorMsg( + return false, newUpdateError( commonerrors.ErrTypeMismatch, fmt.Sprintf( `Cannot apply $mul to a value of non-numeric type. `+ `{_id: %s} has the field '%s' of non-numeric type %s`, types.FormatAnyValue(must.NotFail(doc.Get("_id"))), k, - AliasFromType(docValue), + commonparams.AliasFromType(docValue), ), + command, ) - case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative): - return false, commonerrors.NewWriteErrorMsg( + case errors.Is(err, commonparams.ErrLongExceededPositive), errors.Is(err, commonparams.ErrLongExceededNegative): + return false, newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf( `Failed to apply $mul operations to current value ((NumberLong)%d) for document {_id: "%s"}`, docValue, must.NotFail(doc.Get("_id")), ), + command, ) - case errors.Is(err, errIntExceeded): - return false, commonerrors.NewWriteErrorMsg( + case errors.Is(err, commonparams.ErrIntExceeded): + return false, newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf( `Failed to apply $mul operations to current value ((NumberInt)%d) for document {_id: "%s"}`, docValue, must.NotFail(doc.Get("_id")), ), + command, ) default: return false, err @@ -759,6 +743,8 @@ func processCurrentDateFieldExpression(doc *types.Document, currentDateVal any) } // ValidateUpdateOperators validates update statement. +// ValidateUpdateOperators returns CommandError for findAndModify case-insensitive command name, +// WriteError for other commands. func ValidateUpdateOperators(command string, update *types.Document) error { var err error if _, err = HasSupportedUpdateModifiers(command, update); err != nil { @@ -905,7 +891,8 @@ func HasSupportedUpdateModifiers(command string, update *types.Document) (bool, // newUpdateError returns CommandError for findAndModify command, WriteError for other commands. func newUpdateError(code commonerrors.ErrorCode, msg, command string) error { - if command == "findAndModify" { + // Depending on the driver, the command may be camel case or lower case. + if strings.ToLower(command) == "findandmodify" { return commonerrors.NewCommandErrorMsgWithArgument(code, msg, command) } @@ -948,7 +935,8 @@ func validateOperatorKeys(command string, docs ...*types.Document) error { return nil } -// extractValueFromUpdateOperator gets operator "op" value and returns WriteError error if it is not a document. +// extractValueFromUpdateOperator gets operator "op" value and returns CommandError for `findAndModify` +// WriteError error other commands if it is not a document. // For example, for update document // // bson.D{ @@ -974,7 +962,7 @@ func extractValueFromUpdateOperator(command, op string, update *types.Document) commonerrors.ErrFailedToParse, fmt.Sprintf(`Modifiers operate on fields but we found type %[1]s instead. `+ `For example: {$mod: {: ...}} not {%s: %s}`, - AliasFromType(updateExpression), + commonparams.AliasFromType(updateExpression), op, types.FormatAnyValue(updateExpression), ), @@ -1134,7 +1122,7 @@ func validateCurrentDateExpression(command string, update *types.Document) error return newUpdateError( commonerrors.ErrBadValue, fmt.Sprintf("%s is not valid type for $currentDate. Please use a boolean ('true') "+ - "or a $type expression ({$type: 'timestamp/date'}).", AliasFromType(setValue), + "or a $type expression ({$type: 'timestamp/date'}).", commonparams.AliasFromType(setValue), ), command, ) diff --git a/internal/handlers/common/update_array_operators.go b/internal/handlers/common/update_array_operators.go index c6a9086f3007..6c0ab419ab4b 100644 --- a/internal/handlers/common/update_array_operators.go +++ b/internal/handlers/common/update_array_operators.go @@ -19,6 +19,7 @@ import ( "fmt" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -43,7 +44,7 @@ func processPopArrayUpdateExpression(doc *types.Document, update *types.Document return false, lazyerrors.Error(err) } - popValue, err := GetWholeNumberParam(popValueRaw) + popValue, err := commonparams.GetWholeNumberParam(popValueRaw) if err != nil { return false, commonerrors.NewWriteErrorMsg( commonerrors.ErrFailedToParse, @@ -78,7 +79,7 @@ func processPopArrayUpdateExpression(doc *types.Document, update *types.Document if !ok { return false, commonerrors.NewWriteErrorMsg( commonerrors.ErrTypeMismatch, - fmt.Sprintf("Path '%s' contains an element of non-array type '%s'", key, AliasFromType(val)), + fmt.Sprintf("Path '%s' contains an element of non-array type '%s'", key, commonparams.AliasFromType(val)), ) } @@ -105,9 +106,9 @@ func processPopArrayUpdateExpression(doc *types.Document, update *types.Document // checkUnsuitableValueError returns ErrUnsuitableValueType if path contains // a non-document value. If no element exists on path, it returns nil. -// For example, if the path is "v.foo" and -// - doc is {v: 42}, it returns ErrUnsuitableValueType, v is used by unsuitable value type; -// - doc is {c: 10}, it returns no error since the path does not exist. +// For example, if the path is "v.foo" and: +// - doc is {v: 42}, it returns ErrUnsuitableValueType, v is used by unsuitable value type; +// - doc is {c: 10}, it returns no error since the path does not exist. func checkUnsuitableValueError(doc *types.Document, key string, path types.Path) error { // return no error if path is suffix or key. if path.Len() == 1 { @@ -171,7 +172,7 @@ func processPushArrayUpdateExpression(doc *types.Document, update *types.Documen commonerrors.ErrBadValue, fmt.Sprintf( "The argument to $each in $push must be an array but it was of type: %s", - AliasFromType(eachRaw), + commonparams.AliasFromType(eachRaw), ), ) } @@ -206,7 +207,7 @@ func processPushArrayUpdateExpression(doc *types.Document, update *types.Documen commonerrors.ErrBadValue, fmt.Sprintf( "The field '%s' must be an array but is of type '%s' in document {_id: %s}", - key, AliasFromType(val), must.NotFail(doc.Get("_id")), + key, commonparams.AliasFromType(val), must.NotFail(doc.Get("_id")), ), ) } @@ -260,7 +261,7 @@ func processAddToSetArrayUpdateExpression(doc, update *types.Document) (bool, er commonerrors.ErrTypeMismatch, fmt.Sprintf( "The argument to $each in $addToSet must be an array but it was of type %s", - AliasFromType(eachRaw), + commonparams.AliasFromType(eachRaw), ), ) } @@ -295,7 +296,7 @@ func processAddToSetArrayUpdateExpression(doc, update *types.Document) (bool, er commonerrors.ErrBadValue, fmt.Sprintf( "The field '%s' must be an array but is of type '%s' in document {_id: %s}", - key, AliasFromType(val), must.NotFail(doc.Get("_id")), + key, commonparams.AliasFromType(val), must.NotFail(doc.Get("_id")), ), ) } @@ -369,7 +370,7 @@ func processPullAllArrayUpdateExpression(doc, update *types.Document) (bool, err commonerrors.ErrBadValue, fmt.Sprintf( "The field '%s' must be an array but is of type '%s' in document {_id: %s}", - key, AliasFromType(val), must.NotFail(doc.Get("_id")), + key, commonparams.AliasFromType(val), must.NotFail(doc.Get("_id")), ), ) } @@ -380,7 +381,7 @@ func processPullAllArrayUpdateExpression(doc, update *types.Document) (bool, err commonerrors.ErrBadValue, fmt.Sprintf( "The field '%s' must be an array but is of type '%s'", - key, AliasFromType(pullAllValueRaw), + key, commonparams.AliasFromType(pullAllValueRaw), ), ) } diff --git a/internal/handlers/common/update_params.go b/internal/handlers/common/update_params.go index dc4babc71320..d3d083118664 100644 --- a/internal/handlers/common/update_params.go +++ b/internal/handlers/common/update_params.go @@ -15,131 +15,63 @@ package common import ( - "fmt" - "go.uber.org/zap" - "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" - "github.com/FerretDB/FerretDB/internal/util/must" ) -// UpdatesParams represents parameters for update command. +// UpdatesParams represents parameters for the update command. type UpdatesParams struct { - DB string - Collection string - Updates []UpdateParams + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Updates []UpdateParams `ferretdb:"updates"` + + Comment string `ferretdb:"comment,opt"` + + Let *types.Document `ferretdb:"let,unimplemented"` + + Ordered bool `ferretdb:"ordered,ignored"` + BypassDocumentValidation bool `ferretdb:"bypassDocumentValidation,ignored"` + WriteConcern *types.Document `ferretdb:"writeConcern,ignored"` } // UpdateParams represents a single update operation parameters. type UpdateParams struct { - Filter *types.Document - Update *types.Document - Comment string - Multi bool - Upsert bool + // TODO: https://github.com/FerretDB/FerretDB/issues/2627 + // get comment from query, e.g. db.collection.UpdateOne({"_id":"string", "$comment: "test"},{$set:{"v":"foo""}}) + Filter *types.Document `ferretdb:"q,opt"` + Update *types.Document `ferretdb:"u,opt"` + Multi bool `ferretdb:"multi,opt"` + Upsert bool `ferretdb:"upsert,opt,numericBool"` + + C *types.Document `ferretdb:"c,unimplemented"` + Collation *types.Document `ferretdb:"collation,unimplemented"` + ArrayFilters *types.Array `ferretdb:"arrayFilters,unimplemented"` + + Hint string `ferretdb:"hint,ignored"` } // GetUpdateParams returns parameters for update command. func GetUpdateParams(document *types.Document, l *zap.Logger) (*UpdatesParams, error) { - var err error - - if err = Unimplemented(document, "let"); err != nil { - return nil, err - } - - Ignored(document, l, "ordered", "writeConcern", "bypassDocumentValidation") - - var db, collection string - - if db, err = GetRequiredParam[string](document, "$db"); err != nil { - return nil, err - } + var params UpdatesParams - collectionParam, err := document.Get(document.Command()) + err := commonparams.ExtractParams(document, "update", ¶ms, l) if err != nil { return nil, err } - var ok bool - if collection, ok = collectionParam.(string); !ok { - return nil, commonerrors.NewCommandErrorMsgWithArgument( - commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", AliasFromType(collectionParam)), - document.Command(), - ) - } - - var updatesArray *types.Array - if updatesArray, err = GetOptionalParam(document, "updates", updatesArray); err != nil { - return nil, err - } - - var updates []UpdateParams - - for i := 0; i < updatesArray.Len(); i++ { - update, err := AssertType[*types.Document](must.NotFail(updatesArray.Get(i))) - if err != nil { - return nil, err - } - - if err = Unimplemented(update, "c", "collation", "arrayFilters"); err != nil { - return nil, err - } - - Ignored(update, l, "hint") - - var q, u *types.Document - - if q, err = GetOptionalParam(update, "q", q); err != nil { - return nil, err - } - - if u, err = GetOptionalParam(update, "u", u); err != nil { - // TODO check if u is an array of aggregation pipeline stages - return nil, err - } - - var comment string - - // get comment from options.UpdateParams().SetComment() method - if comment, err = GetOptionalParam(document, "comment", comment); err != nil { - return nil, err - } - - // get comment from query, e.g. db.collection.UpdateOne({"_id":"string", "$comment: "test"},{$set:{"v":"foo""}}) - if comment, err = GetOptionalParam(q, "$comment", comment); err != nil { - return nil, err - } + if len(params.Updates) > 0 { + for _, update := range params.Updates { + if update.Update == nil { + continue + } - if u != nil { - if err = ValidateUpdateOperators(document.Command(), u); err != nil { + if err := ValidateUpdateOperators(document.Command(), update.Update); err != nil { return nil, err } } - - var upsert, multi bool - - if upsert, err = GetOptionalParam(update, "upsert", upsert); err != nil { - return nil, err - } - - if multi, err = GetOptionalParam(update, "multi", multi); err != nil { - return nil, err - } - - updates = append(updates, UpdateParams{ - Filter: q, - Update: u, - Upsert: upsert, - Multi: multi, - Comment: comment, - }) } - return &UpdatesParams{ - DB: db, - Collection: collection, - Updates: updates, - }, nil + return ¶ms, nil } diff --git a/internal/handlers/commoncommands/commoncommands.go b/internal/handlers/commoncommands/commoncommands.go new file mode 100644 index 000000000000..14aa9dcff787 --- /dev/null +++ b/internal/handlers/commoncommands/commoncommands.go @@ -0,0 +1,16 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commoncommands provides command handlers shared by all handlers. +package commoncommands diff --git a/internal/handlers/common/msg_buildinfo.go b/internal/handlers/commoncommands/msg_buildinfo.go similarity index 90% rename from internal/handlers/common/msg_buildinfo.go rename to internal/handlers/commoncommands/msg_buildinfo.go index a75d2b135bcf..0e0ea9ed3be8 100644 --- a/internal/handlers/common/msg_buildinfo.go +++ b/internal/handlers/commoncommands/msg_buildinfo.go @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" "strconv" "github.com/FerretDB/FerretDB/build/version" + "github.com/FerretDB/FerretDB/internal/handlers/common/aggregations/stages" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/wire" @@ -26,6 +27,9 @@ import ( // MsgBuildInfo is a common implementation of the buildInfo command. func MsgBuildInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + // TODO https://github.com/FerretDB/FerretDB/issues/2650 + _ = stages.Stages + var reply wire.OpMsg must.NoError(reply.SetSections(wire.OpMsgSection{ Documents: []*types.Document{must.NotFail(types.NewDocument( diff --git a/internal/handlers/common/msg_connectionstatus.go b/internal/handlers/commoncommands/msg_connectionstatus.go similarity index 98% rename from internal/handlers/common/msg_connectionstatus.go rename to internal/handlers/commoncommands/msg_connectionstatus.go index 3f4b2fdea0b8..6f6cb587dc13 100644 --- a/internal/handlers/common/msg_connectionstatus.go +++ b/internal/handlers/commoncommands/msg_connectionstatus.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/common/msg_currentop.go b/internal/handlers/commoncommands/msg_currentop.go similarity index 98% rename from internal/handlers/common/msg_currentop.go rename to internal/handlers/commoncommands/msg_currentop.go index bf0b462d0981..9b26beebf751 100644 --- a/internal/handlers/common/msg_currentop.go +++ b/internal/handlers/commoncommands/msg_currentop.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/common/msg_debugerror.go b/internal/handlers/commoncommands/msg_debugerror.go similarity index 92% rename from internal/handlers/common/msg_debugerror.go rename to internal/handlers/commoncommands/msg_debugerror.go index aaf96ff488cb..289b2884333c 100644 --- a/internal/handlers/common/msg_debugerror.go +++ b/internal/handlers/commoncommands/msg_debugerror.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" @@ -20,6 +20,7 @@ import ( "strconv" "strings" + "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -34,7 +35,7 @@ func MsgDebugError(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { return nil, lazyerrors.Error(err) } - expected, err := GetRequiredParam[string](document, document.Command()) + expected, err := common.GetRequiredParam[string](document, document.Command()) if err != nil { return nil, err } diff --git a/internal/handlers/common/msg_getcmdlineopts.go b/internal/handlers/commoncommands/msg_getcmdlineopts.go similarity index 98% rename from internal/handlers/common/msg_getcmdlineopts.go rename to internal/handlers/commoncommands/msg_getcmdlineopts.go index a99541e8e57a..1e5a9b04f5ec 100644 --- a/internal/handlers/common/msg_getcmdlineopts.go +++ b/internal/handlers/commoncommands/msg_getcmdlineopts.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/common/msg_hostinfo.go b/internal/handlers/commoncommands/msg_hostinfo.go similarity index 77% rename from internal/handlers/common/msg_hostinfo.go rename to internal/handlers/commoncommands/msg_hostinfo.go index e9c531fe0b42..6b7fc20221a8 100644 --- a/internal/handlers/common/msg_hostinfo.go +++ b/internal/handlers/commoncommands/msg_hostinfo.go @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( + "bufio" "context" + "io" "os" "runtime" "strconv" + "strings" "time" "github.com/FerretDB/FerretDB/internal/types" @@ -82,3 +85,30 @@ func MsgHostInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { return &reply, nil } + +// parseOSRelease parses the /etc/os-release or /usr/lib/os-release file content, +// returning the OS name and version. +func parseOSRelease(r io.Reader) (string, string, error) { + scanner := bufio.NewScanner(r) + + configParams := map[string]string{} + + for scanner.Scan() { + key, value, ok := strings.Cut(scanner.Text(), "=") + if !ok { + continue + } + + if v, err := strconv.Unquote(value); err == nil { + value = v + } + + configParams[key] = value + } + + if err := scanner.Err(); err != nil { + return "", "", lazyerrors.Error(err) + } + + return configParams["NAME"], configParams["VERSION"], nil +} diff --git a/internal/handlers/common/parse_osrelease_test.go b/internal/handlers/commoncommands/msg_hostinfo_test.go similarity index 99% rename from internal/handlers/common/parse_osrelease_test.go rename to internal/handlers/commoncommands/msg_hostinfo_test.go index 9b8c8df20b8b..0d887991066f 100644 --- a/internal/handlers/common/parse_osrelease_test.go +++ b/internal/handlers/commoncommands/msg_hostinfo_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "bytes" diff --git a/internal/handlers/common/msg_listcommands.go b/internal/handlers/commoncommands/msg_listcommands.go similarity index 99% rename from internal/handlers/common/msg_listcommands.go rename to internal/handlers/commoncommands/msg_listcommands.go index 8e4122701279..2c08197e8861 100644 --- a/internal/handlers/common/msg_listcommands.go +++ b/internal/handlers/commoncommands/msg_listcommands.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/common/msg_whatsmyuri.go b/internal/handlers/commoncommands/msg_whatsmyuri.go similarity index 98% rename from internal/handlers/common/msg_whatsmyuri.go rename to internal/handlers/commoncommands/msg_whatsmyuri.go index 64fd321b2754..efd53a4ca788 100644 --- a/internal/handlers/common/msg_whatsmyuri.go +++ b/internal/handlers/commoncommands/msg_whatsmyuri.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/common/msg_whatsmyuri_test.go b/internal/handlers/commoncommands/msg_whatsmyuri_test.go similarity index 97% rename from internal/handlers/common/msg_whatsmyuri_test.go rename to internal/handlers/commoncommands/msg_whatsmyuri_test.go index 3e31f0cf532e..31e915e60f7b 100644 --- a/internal/handlers/common/msg_whatsmyuri_test.go +++ b/internal/handlers/commoncommands/msg_whatsmyuri_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commoncommands import ( "context" diff --git a/internal/handlers/commonerrors/error.go b/internal/handlers/commonerrors/error.go index 2c7656b2b6ca..8f41bb4083e5 100644 --- a/internal/handlers/commonerrors/error.go +++ b/internal/handlers/commonerrors/error.go @@ -54,7 +54,7 @@ const ( ErrIndexNotFound = ErrorCode(27) // IndexNotFound // ErrUnsuitableValueType indicates that field could not be created for given value. - ErrUnsuitableValueType = ErrorCode(28) // UnsuitableValueType + ErrUnsuitableValueType = ErrorCode(28) // PathNotViable // ErrConflictingUpdateOperators indicates that $set, $inc or $setOnInsert were used together. ErrConflictingUpdateOperators = ErrorCode(40) // ConflictingUpdateOperators @@ -72,7 +72,7 @@ const ( ErrInvalidID = ErrorCode(53) // InvalidID // ErrEmptyName indicates that the field name is empty. - ErrEmptyName = ErrorCode(56) // EmptyName + ErrEmptyName = ErrorCode(56) // EmptyFieldName // ErrCommandNotFound indicates unknown command input. ErrCommandNotFound = ErrorCode(59) // CommandNotFound @@ -227,6 +227,9 @@ const ( // ErrBadRegexOption indicates bad regex option value passed. ErrBadRegexOption = ErrorCode(51108) // Location51108 + // ErrEmptyProject indicates that projection specification must have at least one field. + ErrEmptyProject = ErrorCode(51272) // Location51272 + // ErrDuplicateField indicates duplicate field is specified. ErrDuplicateField = ErrorCode(4822819) // Location4822819 diff --git a/internal/handlers/commonerrors/errorcode_string.go b/internal/handlers/commonerrors/errorcode_string.go index c7d3b4d8b485..bf8ff97d97b6 100644 --- a/internal/handlers/commonerrors/errorcode_string.go +++ b/internal/handlers/commonerrors/errorcode_string.go @@ -73,13 +73,14 @@ func _() { _ = x[ErrRegexOptions-51075] _ = x[ErrRegexMissingParen-51091] _ = x[ErrBadRegexOption-51108] + _ = x[ErrEmptyProject-51272] _ = x[ErrDuplicateField-4822819] _ = x[ErrStageSkipBadValue-5107200] _ = x[ErrStageLimitInvalidArg-5107201] _ = x[ErrStageCollStatsInvalidArg-5447000] } -const _ErrorCode_name = "UnsetInternalErrorBadValueFailedToParseTypeMismatchIllegalOperationNamespaceNotFoundIndexNotFoundUnsuitableValueTypeConflictingUpdateOperatorsCursorNotFoundNamespaceExistsDollarPrefixedFieldNameInvalidIDEmptyNameCommandNotFoundImmutableFieldCannotCreateIndexInvalidOptionsInvalidNamespaceIndexOptionsConflictIndexKeySpecsConflictOperationFailedDocumentValidationFailureNotImplementedMechanismUnavailableLocation11000Location15947Location15948Location15955Location15958Location15959Location15969Location15973Location15974Location15975Location15976Location15981Location15998Location16410Location16872Location17276Location28667Location28724Location28812Location28818Location31253Location31254Location40156Location40157Location40158Location40160Location40234Location40237Location40238Location40323Location40352Location40414Location40415Location50840Location51024Location51075Location51091Location51108Location4822819Location5107200Location5107201Location5447000" +const _ErrorCode_name = "UnsetInternalErrorBadValueFailedToParseTypeMismatchIllegalOperationNamespaceNotFoundIndexNotFoundPathNotViableConflictingUpdateOperatorsCursorNotFoundNamespaceExistsDollarPrefixedFieldNameInvalidIDEmptyFieldNameCommandNotFoundImmutableFieldCannotCreateIndexInvalidOptionsInvalidNamespaceIndexOptionsConflictIndexKeySpecsConflictOperationFailedDocumentValidationFailureNotImplementedMechanismUnavailableLocation11000Location15947Location15948Location15955Location15958Location15959Location15969Location15973Location15974Location15975Location15976Location15981Location15998Location16410Location16872Location17276Location28667Location28724Location28812Location28818Location31253Location31254Location40156Location40157Location40158Location40160Location40234Location40237Location40238Location40323Location40352Location40414Location40415Location50840Location51024Location51075Location51091Location51108Location51272Location4822819Location5107200Location5107201Location5447000" var _ErrorCode_map = map[ErrorCode]string{ 0: _ErrorCode_name[0:5], @@ -90,66 +91,67 @@ var _ErrorCode_map = map[ErrorCode]string{ 20: _ErrorCode_name[51:67], 26: _ErrorCode_name[67:84], 27: _ErrorCode_name[84:97], - 28: _ErrorCode_name[97:116], - 40: _ErrorCode_name[116:142], - 43: _ErrorCode_name[142:156], - 48: _ErrorCode_name[156:171], - 52: _ErrorCode_name[171:194], - 53: _ErrorCode_name[194:203], - 56: _ErrorCode_name[203:212], - 59: _ErrorCode_name[212:227], - 66: _ErrorCode_name[227:241], - 67: _ErrorCode_name[241:258], - 72: _ErrorCode_name[258:272], - 73: _ErrorCode_name[272:288], - 85: _ErrorCode_name[288:308], - 86: _ErrorCode_name[308:329], - 96: _ErrorCode_name[329:344], - 121: _ErrorCode_name[344:369], - 238: _ErrorCode_name[369:383], - 334: _ErrorCode_name[383:403], - 11000: _ErrorCode_name[403:416], - 15947: _ErrorCode_name[416:429], - 15948: _ErrorCode_name[429:442], - 15955: _ErrorCode_name[442:455], - 15958: _ErrorCode_name[455:468], - 15959: _ErrorCode_name[468:481], - 15969: _ErrorCode_name[481:494], - 15973: _ErrorCode_name[494:507], - 15974: _ErrorCode_name[507:520], - 15975: _ErrorCode_name[520:533], - 15976: _ErrorCode_name[533:546], - 15981: _ErrorCode_name[546:559], - 15998: _ErrorCode_name[559:572], - 16410: _ErrorCode_name[572:585], - 16872: _ErrorCode_name[585:598], - 17276: _ErrorCode_name[598:611], - 28667: _ErrorCode_name[611:624], - 28724: _ErrorCode_name[624:637], - 28812: _ErrorCode_name[637:650], - 28818: _ErrorCode_name[650:663], - 31253: _ErrorCode_name[663:676], - 31254: _ErrorCode_name[676:689], - 40156: _ErrorCode_name[689:702], - 40157: _ErrorCode_name[702:715], - 40158: _ErrorCode_name[715:728], - 40160: _ErrorCode_name[728:741], - 40234: _ErrorCode_name[741:754], - 40237: _ErrorCode_name[754:767], - 40238: _ErrorCode_name[767:780], - 40323: _ErrorCode_name[780:793], - 40352: _ErrorCode_name[793:806], - 40414: _ErrorCode_name[806:819], - 40415: _ErrorCode_name[819:832], - 50840: _ErrorCode_name[832:845], - 51024: _ErrorCode_name[845:858], - 51075: _ErrorCode_name[858:871], - 51091: _ErrorCode_name[871:884], - 51108: _ErrorCode_name[884:897], - 4822819: _ErrorCode_name[897:912], - 5107200: _ErrorCode_name[912:927], - 5107201: _ErrorCode_name[927:942], - 5447000: _ErrorCode_name[942:957], + 28: _ErrorCode_name[97:110], + 40: _ErrorCode_name[110:136], + 43: _ErrorCode_name[136:150], + 48: _ErrorCode_name[150:165], + 52: _ErrorCode_name[165:188], + 53: _ErrorCode_name[188:197], + 56: _ErrorCode_name[197:211], + 59: _ErrorCode_name[211:226], + 66: _ErrorCode_name[226:240], + 67: _ErrorCode_name[240:257], + 72: _ErrorCode_name[257:271], + 73: _ErrorCode_name[271:287], + 85: _ErrorCode_name[287:307], + 86: _ErrorCode_name[307:328], + 96: _ErrorCode_name[328:343], + 121: _ErrorCode_name[343:368], + 238: _ErrorCode_name[368:382], + 334: _ErrorCode_name[382:402], + 11000: _ErrorCode_name[402:415], + 15947: _ErrorCode_name[415:428], + 15948: _ErrorCode_name[428:441], + 15955: _ErrorCode_name[441:454], + 15958: _ErrorCode_name[454:467], + 15959: _ErrorCode_name[467:480], + 15969: _ErrorCode_name[480:493], + 15973: _ErrorCode_name[493:506], + 15974: _ErrorCode_name[506:519], + 15975: _ErrorCode_name[519:532], + 15976: _ErrorCode_name[532:545], + 15981: _ErrorCode_name[545:558], + 15998: _ErrorCode_name[558:571], + 16410: _ErrorCode_name[571:584], + 16872: _ErrorCode_name[584:597], + 17276: _ErrorCode_name[597:610], + 28667: _ErrorCode_name[610:623], + 28724: _ErrorCode_name[623:636], + 28812: _ErrorCode_name[636:649], + 28818: _ErrorCode_name[649:662], + 31253: _ErrorCode_name[662:675], + 31254: _ErrorCode_name[675:688], + 40156: _ErrorCode_name[688:701], + 40157: _ErrorCode_name[701:714], + 40158: _ErrorCode_name[714:727], + 40160: _ErrorCode_name[727:740], + 40234: _ErrorCode_name[740:753], + 40237: _ErrorCode_name[753:766], + 40238: _ErrorCode_name[766:779], + 40323: _ErrorCode_name[779:792], + 40352: _ErrorCode_name[792:805], + 40414: _ErrorCode_name[805:818], + 40415: _ErrorCode_name[818:831], + 50840: _ErrorCode_name[831:844], + 51024: _ErrorCode_name[844:857], + 51075: _ErrorCode_name[857:870], + 51091: _ErrorCode_name[870:883], + 51108: _ErrorCode_name[883:896], + 51272: _ErrorCode_name[896:909], + 4822819: _ErrorCode_name[909:924], + 5107200: _ErrorCode_name[924:939], + 5107201: _ErrorCode_name[939:954], + 5447000: _ErrorCode_name[954:969], } func (i ErrorCode) String() string { diff --git a/internal/handlers/commonparams/commonparams.go b/internal/handlers/commonparams/commonparams.go new file mode 100644 index 000000000000..9ce5fe3f19fd --- /dev/null +++ b/internal/handlers/commonparams/commonparams.go @@ -0,0 +1,16 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package commonparams contains functions for parsing handlers parameters. +package commonparams diff --git a/internal/handlers/commonparams/extract_params.go b/internal/handlers/commonparams/extract_params.go new file mode 100644 index 000000000000..ee8cbfef1c3a --- /dev/null +++ b/internal/handlers/commonparams/extract_params.go @@ -0,0 +1,425 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commonparams + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "go.uber.org/zap" + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" +) + +// ExtractParams fill passed value structure with parameters from the document. +// If the passed value is not a pointer to the structure it panics. +// Parameters are extracted by the field name or by the `ferretdb` tag. +// +// Possible tags: +// - `opt` - field is optional, the field value would not be set if it's not present in the document; +// - `unimplemented-non-default` - error would be returned if non-default value for the field is provided; +// - `unimplemented` - error would be returned if the value is present in the document; +// - `ignored` - field is ignored and would not be set, but it would be logged; +// - `positiveNumber` - provided value must be of types [int, long, double] and greater than 0, +// double values would be rounded to long; +// - `wholePositiveNumber` - provided value must be of types [int, long] and greater than 0; +// - `numericBool` - provided value must be of types [bool, int, long, double] and would be converted to bool; +// - `zeroOrOneAsBool` - provided value must be of types [int, long, double] with possible values `0` or `1`. +// +// Collection field processed in a special way. For the commands that require collection name +// it is extracted from the command name. +// If the field could have different types (e.g. `*types.Document` and `*types.Array`) then +// the field must be of type `any`. +// +// Errors list: +// - `ErrFailedToParse` when provided field is not present in passed structure; +// - `ErrFailedToParse` when provided field must be 0 or 1, but it is not; +// - `ErrNotImplemented` when support for provided field is not implemented yet; +// - `ErrNotImplemented`when support for non-default field value is not implemented yet; +// - `ErrValueNegative` - field of numeric type is negative; +// - `ErrTypeMismatch` - field is type is not matched with the type of the value; +// - `ErrBadValue` when field is not a number; +// - `ErrBadValue` when field is not of integer type; +// - `ErrBadValue` when field is out of integer range; +// - `ErrBadValue` when field has negative value; +// - `ErrInvalidNamespace` - collection name has invalid type. +func ExtractParams(doc *types.Document, command string, value any, l *zap.Logger) error { + rv := reflect.ValueOf(value) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + panic("value must be a non-nil pointer") + } + + elem := rv.Elem() + if elem.Kind() != reflect.Struct { + panic("value must be a struct pointer") + } + + iter := doc.Iterator() + defer iter.Close() + + for { + key, val, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return lazyerrors.Error(err) + } + + lookup := key + + // If the key is the same as the command name, then it is a collection name. + // Depending on the driver, the key may be camel case or lower case for a collection name. + if strings.ToLower(key) == strings.ToLower(command) { //nolint:staticcheck // for clarity + lookup = "collection" + } + + fieldIndex, options, err := lookupFieldTag(lookup, &elem) + if err != nil { + panic(err) + } + + if fieldIndex == nil { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrFailedToParse, + fmt.Sprintf("%s: unknown field %q", command, key), + command, + ) + } + + if options.ignored { + l.Debug( + "ignoring field", + zap.String("command", doc.Command()), zap.String("field", key), zap.Any("value", val), + ) + + continue + } + + if options.unimplemented { + msg := fmt.Sprintf( + "%s: support for field %q with value %v is not implemented yet", + doc.Command(), key, val, + ) + + return commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrNotImplemented, msg, key) + } + + if options.nonDefault { + v, ok := val.(bool) + + if ok && v { + msg := fmt.Sprintf( + "%s: support for field %q with non-default value %v is not implemented yet", + doc.Command(), key, val, + ) + + return commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrNotImplemented, msg, key) + } + } + + err = setStructField(&elem, options, *fieldIndex, command, key, val, l) + if err != nil { + return err + } + } + + err := checkAllRequiredFieldsPopulated(&elem, command, doc.Keys()) + if err != nil { + return err + } + + return nil +} + +// tagOptions contains options for the structure field tag. +type tagOptions struct { + optional bool + nonDefault bool + unimplemented bool + ignored bool + positiveNumber bool + wholePositiveNumber bool + numericBool bool + zeroOrOneAsBool bool +} + +// lookupFieldTag looks for the tag and returns its options. +func lookupFieldTag(key string, value *reflect.Value) (*int, *tagOptions, error) { + var to *tagOptions + var i int + var found bool + + for ; i < value.NumField(); i++ { + field := value.Type().Field(i) + + tag := field.Tag.Get("ferretdb") + + if tag == "" { + return nil, nil, lazyerrors.Errorf("no tag provided for %s", field.Name) + } + + optionsList := strings.Split(tag, ",") + + if optionsList[0] != key { + continue + } + + to = tagOptionsFromList(optionsList[1:]) + + found = true + + break + } + + if !found { + return nil, nil, nil + } + + return &i, to, nil +} + +func tagOptionsFromList(optionsList []string) *tagOptions { + var to tagOptions + + for _, tt := range optionsList { + switch tt { + case "opt": + to.optional = true + case "non-default": + to.nonDefault = true + case "unimplemented": + to.unimplemented = true + case "ignored": + to.ignored = true + case "numericBool": + to.numericBool = true + case "positiveNumber": + to.positiveNumber = true + case "wholePositiveNumber": + to.wholePositiveNumber = true + case "zeroOrOneAsBool": + to.zeroOrOneAsBool = true + default: + panic(fmt.Sprintf("unknown tag option %s", tt)) + } + } + + return &to +} + +// setStructField sets the value of the document field to the structure field. +func setStructField(elem *reflect.Value, o *tagOptions, i int, command, key string, val any, l *zap.Logger) error { + var err error + + field := elem.Type().Field(i) + + // Set the value of the field from the document. + fv := elem.Field(i) + if !fv.CanSet() { + panic(fmt.Sprintf("field %s is not settable", field.Name)) + } + + var settable any + + switch fv.Kind() { //nolint: exhaustive // all other types are not supported + case reflect.Int32, reflect.Int64, reflect.Float64: + if o.positiveNumber { + settable, err = getWholeParamStrict(command, key, val) + if err != nil { + return err + } + + break + } + + if o.wholePositiveNumber { + settable, err = getOptionalPositiveNumber(key, val) + if err != nil { + return err + } + + break + } + + settable, err = GetWholeNumberParam(val) + if err != nil { + return err + } + case reflect.String, reflect.Struct, reflect.Pointer, reflect.Interface: + settable = val + case reflect.Bool: + if o.numericBool { + settable, err = GetBoolOptionalParam(key, val) + if err != nil { + return err + } + + break + } + + if o.zeroOrOneAsBool { + var numeric int64 + + numeric, err = GetWholeNumberParam(val) + if err != nil || numeric < 0 || numeric > 1 { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrFailedToParse, + fmt.Sprintf("The '%s.%s' field must be 0 or 1. Got %v", command, key, types.FormatAnyValue(val)), + command, + ) + } + + settable = numeric == 1 + + break + } + + settable = val + case reflect.Slice: + array, ok := val.(*types.Array) + if !ok { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrTypeMismatch, + fmt.Sprintf( + `BSON field '%s.%s' is the wrong type '%s', expected type '%s'`, + command, key, AliasFromType(val), AliasFromType(fv.Interface()), + ), + command, + ) + } + + iter := array.Iterator() + defer iter.Close() + + arrayToSet := reflect.MakeSlice(fv.Type(), array.Len(), array.Len()) + + for { + i, arrayDoc, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return lazyerrors.Error(err) + } + + doc, ok := arrayDoc.(*types.Document) + if !ok { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrTypeMismatch, + fmt.Sprintf( + `BSON field '%s.%s' is the wrong type '%s', expected type '%s'`, + command, key, AliasFromType(val), AliasFromType(fv.Interface()), + ), + command, + ) + } + + params := reflect.New(fv.Type().Elem()) + + err = ExtractParams(doc, command+"."+key, params.Interface(), l) + if err != nil { + return err + } + + arrayToSet.Index(i).Set(params.Elem()) + } + + settable = arrayToSet.Interface() + default: + panic(fmt.Sprintf("field %s type %s is not supported", field.Name, fv.Type())) + } + + if settable != nil { + v := reflect.ValueOf(settable) + + if v.Type() != fv.Type() { + if key == command { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrInvalidNamespace, + fmt.Sprintf("collection name has invalid type %s", AliasFromType(settable)), + command, + ) + } + + if fv.Kind() == reflect.Interface { + fv.Set(reflect.ValueOf(v.Interface())) + return nil + } + + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrTypeMismatch, + fmt.Sprintf( + `BSON field '%s.%s' is the wrong type '%s', expected type '%s'`, + command, key, AliasFromType(val), AliasFromType(fv.Interface()), + ), + command, + ) + } + + fv.Set(v) + } + + return nil +} + +// checkAllRequiredFieldsPopulated checks that all required fields are populated. +func checkAllRequiredFieldsPopulated(v *reflect.Value, command string, keys []string) error { + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + + tag := field.Tag.Get("ferretdb") + + optionsList := strings.Split(tag, ",") + + if len(optionsList) == 0 { + return lazyerrors.Errorf("no tag provided for %s", field.Name) + } + + to := tagOptionsFromList(optionsList[1:]) + if to.ignored || to.optional || to.unimplemented || to.nonDefault { + continue + } + + key := optionsList[0] + if key == "collection" { + key = command + } + + // Fields with "-" are ignored when parsing parameters. + if key == "-" { + continue + } + + // Depending on the driver, the key may be camel case or lower case for a collection name. + if !slices.Contains(keys, key) && !slices.Contains(keys, strings.ToLower(key)) { + return commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrMissingField, + fmt.Sprintf("BSON field '%s.%s' is missing but a required field", command, key), + command, + ) + } + } + + return nil +} diff --git a/internal/handlers/commonparams/extract_params_test.go b/internal/handlers/commonparams/extract_params_test.go new file mode 100644 index 000000000000..ac557316d192 --- /dev/null +++ b/internal/handlers/commonparams/extract_params_test.go @@ -0,0 +1,336 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commonparams + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +func TestParse(t *testing.T) { + type allTagsThatPass struct { //nolint:vet // it's a test struct + DB string `ferretdb:"$db"` + Collection string `ferretdb:"collection"` + Filter *types.Document `ferretdb:"filter,opt"` + AllowDiskUse any `ferretdb:"allowDiskUse,ignored"` + } + + type unimplementedTag struct { + Find string `ferretdb:"find,unimplemented"` + } + + type nonDefaultTag struct { + Find bool `ferretdb:"find,non-default"` + } + + type update struct { + Filter *types.Document `ferretdb:"q,opt"` + } + + type updates struct { + Update []update `ferretdb:"updates"` + } + + type updateAny struct { + Update any `ferretdb:"u"` + } + + type numericBool struct { + Find bool `ferretdb:"f,numericBool"` + } + + type strict struct { + Find int64 `ferretdb:"f,positiveNumber"` + } + + type positive struct { + Find int64 `ferretdb:"f,wholePositiveNumber"` + } + + type zeroOrOneAsBool struct { + Find bool `ferretdb:"f,zeroOrOneAsBool"` + } + + tests := map[string]struct { //nolint:vet // it's a test table + doc *types.Document + command string + params any + wantParams any + wantErr string + }{ + "AllTagTypesThatPass": { + command: "find", + doc: must.NotFail(types.NewDocument( + "$db", "test", + "find", "test", + "filter", must.NotFail(types.NewDocument("a", "b")), + "allowDiskUse", "123", + )), + params: new(allTagsThatPass), + wantParams: &allTagsThatPass{ + DB: "test", + Collection: "test", + Filter: must.NotFail(types.NewDocument("a", "b")), + }, + }, + "UnimplementedTag": { + command: "command", + doc: must.NotFail(types.NewDocument( + "find", "test", + )), + params: new(unimplementedTag), + wantErr: "support for field \"find\" with value test is not implemented yet", + }, + "NonDefaultTag": { + command: "command", + doc: must.NotFail(types.NewDocument( + "find", true, + )), + params: new(nonDefaultTag), + wantErr: "support for field \"find\"" + + " with non-default value true is not implemented yet", + }, + "ExtraFieldPassed": { + command: "find", + doc: must.NotFail(types.NewDocument( + "$db", "test", + "find", "test", + "extra", "field", + )), + params: new(allTagsThatPass), + wantErr: `find: unknown field "extra"`, + }, + "MissingRequiredField": { + command: "find", + doc: must.NotFail(types.NewDocument( + "$db", "test", + )), + params: new(allTagsThatPass), + wantErr: "BSON field 'find.find' is missing but a required field", + }, + "ArrayTag": { + command: "update", + doc: must.NotFail(types.NewDocument( + "updates", must.NotFail(types.NewArray( + must.NotFail(types.NewDocument( + "q", must.NotFail(types.NewDocument("a", "b")), + )), + )), + )), + params: new(updates), + wantParams: &updates{ + Update: []update{ + { + Filter: must.NotFail(types.NewDocument("a", "b")), + }, + }, + }, + }, + "AnyTagWithDocumentValue": { + command: "update", + doc: must.NotFail(types.NewDocument( + "u", must.NotFail(types.NewDocument("a", "b")), + )), + params: new(updateAny), + wantParams: &updateAny{ + Update: must.NotFail(types.NewDocument("a", "b")), + }, + }, + "AnyTagWithArrayValue": { + command: "update", + doc: must.NotFail(types.NewDocument( + "u", must.NotFail(types.NewArray("a", "b")), + )), + params: new(updateAny), + wantParams: &updateAny{ + Update: must.NotFail(types.NewArray("a", "b")), + }, + }, + "AnyTagWithStringValue": { + command: "update", + doc: must.NotFail(types.NewDocument( + "u", "a", + )), + params: new(updateAny), + wantParams: &updateAny{ + Update: "a", + }, + }, + "BoolTagWithInt32Value": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(1), + )), + params: new(numericBool), + wantParams: &numericBool{ + Find: true, + }, + }, + "BoolTagWithInt64Value": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int64(1), + )), + params: new(numericBool), + wantParams: &numericBool{ + Find: true, + }, + }, + "BoolTagWithFloatValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", 3.14, + )), + params: new(numericBool), + wantParams: &numericBool{ + Find: true, + }, + }, + "BoolTagWithStringValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", "true", + )), + params: new(numericBool), + wantErr: "field 'f' is the wrong type 'string', expected types '\\[bool, long, int, decimal, double\\]'", + }, + "StrictTag": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", 12.23, + )), + params: new(strict), + wantParams: &strict{ + Find: 12, + }, + }, + "StrictTagWithWrongType": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", "12.23", + )), + params: new(strict), + wantErr: "field 'find.f' is the wrong type 'string', expected types '\\[long, int, decimal, double\\]'", + }, + "PositiveTag": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(12), + )), + params: new(positive), + wantParams: &positive{ + Find: 12, + }, + }, + "PositiveTagWithNegativeFloatValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", -12.23, + )), + params: new(positive), + wantErr: "f has non-integral value", + }, + "PositiveTagWithNegativeIntValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(-1), + )), + params: new(positive), + wantErr: "-1 value for f is out of range", + }, + "ZeroOrOneAsBoolTagWithInt32Value1": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(1), + )), + params: new(zeroOrOneAsBool), + wantParams: &zeroOrOneAsBool{ + Find: true, + }, + }, + "ZeroOrOneAsBoolTagWithInt32Value0": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(0), + )), + params: new(zeroOrOneAsBool), + wantParams: &zeroOrOneAsBool{ + Find: false, + }, + }, + "ZeroOrOneAsBoolTagWithInt32ValueNegative": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int32(-1), + )), + params: new(zeroOrOneAsBool), + wantErr: "The 'find.f' field must be 0 or 1. Got -1", + }, + "ZeroOrOneAsBoolTagWithInt64Value": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", int64(1), + )), + params: new(zeroOrOneAsBool), + wantParams: &zeroOrOneAsBool{ + Find: true, + }, + }, + "ZeroOrOneAsBoolTagWithFloatValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", 1.0, + )), + params: new(zeroOrOneAsBool), + wantParams: &zeroOrOneAsBool{ + Find: true, + }, + }, + "ZeroOrOneAsBoolTagWithIncorrectFloatValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", 3.14, + )), + params: new(zeroOrOneAsBool), + wantErr: "The 'find.f' field must be 0 or 1. Got 3.14", + }, + "ZeroOrOneAsBoolTagWithStringValue": { + command: "find", + doc: must.NotFail(types.NewDocument( + "f", "true", + )), + params: new(zeroOrOneAsBool), + wantErr: `The 'find.f' field must be 0 or 1. Got "true"`, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + err := ExtractParams(tt.doc, tt.command, tt.params, zap.NewNop()) + if tt.wantErr != "" { + require.Regexp(t, regexp.MustCompile(".*"+tt.wantErr), err.Error()) + return + } + require.NoError(t, err) + + require.Equal(t, tt.wantParams, tt.params) + }) + } +} diff --git a/internal/handlers/commonparams/params.go b/internal/handlers/commonparams/params.go new file mode 100644 index 000000000000..1af86ccff71a --- /dev/null +++ b/internal/handlers/commonparams/params.go @@ -0,0 +1,207 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commonparams + +import ( + "errors" + "fmt" + "math" + + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" +) + +var ( + // ErrNegativeNumber is returned when a negative number is given. + ErrNegativeNumber = fmt.Errorf("negative number") + // ErrNotWholeNumber is returned when a non-whole number is given. + ErrNotWholeNumber = fmt.Errorf("not a whole number") + // ErrNotBinaryMask is returned when a non-binary mask is given. + ErrNotBinaryMask = fmt.Errorf("not a binary mask") + // ErrUnexpectedLeftOpType is returned when an unexpected left operand type is given. + ErrUnexpectedLeftOpType = fmt.Errorf("unexpected left operand type") + // ErrUnexpectedRightOpType is returned when an unexpected right operand type is given. + ErrUnexpectedRightOpType = fmt.Errorf("unexpected right operand type") + // ErrLongExceededPositive is returned when a positive long value is given that exceeds the maximum value. + ErrLongExceededPositive = fmt.Errorf("long exceeded - positive value") + // ErrLongExceededNegative is returned when a negative long value is given that exceeds the minimum value. + ErrLongExceededNegative = fmt.Errorf("long exceeded - negative value") + // ErrIntExceeded is returned when an int value is given that exceeds the maximum value. + ErrIntExceeded = fmt.Errorf("int exceeded") + // ErrInfinity is returned when an infinity value is given. + ErrInfinity = fmt.Errorf("infinity") + // ErrUnexpectedType is returned when an unexpected type is given. + ErrUnexpectedType = fmt.Errorf("unexpected type") +) + +// GetWholeNumberParam checks if the given value is int32, int64, or float64 containing a whole number, +// such as used in the limit, $size, etc. +func GetWholeNumberParam(value any) (int64, error) { + switch value := value.(type) { + // TODO: add string support https://github.com/FerretDB/FerretDB/issues/1089 + case float64: + switch { + case math.IsInf(value, 1): + return 0, ErrInfinity + case value > float64(math.MaxInt64): + return 0, ErrLongExceededPositive + case value < float64(math.MinInt64): + return 0, ErrLongExceededNegative + case value != math.Trunc(value): + return 0, ErrNotWholeNumber + } + + return int64(value), nil + case int32: + return int64(value), nil + case int64: + return value, nil + default: + return 0, ErrUnexpectedType + } +} + +// getWholeParamStrict validates the given value for find and count commands. +// +// If the value is valid, it returns its int64 representation, +// otherwise it returns a command error with the given command being mentioned. +func getWholeParamStrict(command string, param string, value any) (int64, error) { + whole, err := GetWholeNumberParam(value) + if err != nil { + switch { + case errors.Is(err, ErrUnexpectedType): + if _, ok := value.(types.NullType); ok { + return 0, nil + } + + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrTypeMismatch, + fmt.Sprintf( + `BSON field '%s.%s' is the wrong type '%s', expected types '[long, int, decimal, double]'`, + command, param, AliasFromType(value), + ), + param, + ) + case errors.Is(err, ErrNotWholeNumber): + if math.Signbit(value.(float64)) { + return 0, commonerrors.NewCommandError( + commonerrors.ErrValueNegative, + fmt.Errorf("BSON field '%s' value must be >= 0, actual value '%d'", param, int(math.Ceil(value.(float64)))), + ) + } + + // for non-integer numbers, value is rounded to the greatest integer value less than the given value. + return int64(math.Floor(value.(float64))), nil + + case errors.Is(err, ErrLongExceededPositive): + return math.MaxInt64, nil + + case errors.Is(err, ErrLongExceededNegative): + return 0, commonerrors.NewCommandError( + commonerrors.ErrValueNegative, + fmt.Errorf("BSON field '%s' value must be >= 0, actual value '%d'", param, int(math.Ceil(value.(float64)))), + ) + + default: + return 0, lazyerrors.Error(err) + } + } + + if whole < 0 { + return 0, commonerrors.NewCommandError( + commonerrors.ErrValueNegative, + fmt.Errorf("BSON field '%s' value must be >= 0, actual value '%d'", param, whole), + ) + } + + return whole, nil +} + +// getOptionalPositiveNumber returns doc's value for key or protocol error for invalid parameter. +func getOptionalPositiveNumber(key string, value any) (int64, error) { + whole, err := GetWholeNumberParam(value) + if err != nil { + switch { + case errors.Is(err, ErrUnexpectedType): + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf("%s must be a number", key), + key, + ) + case errors.Is(err, ErrNotWholeNumber): + if _, ok := value.(float64); ok { + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf("%v has non-integral value", key), + key, + ) + } + + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf("%s must be a whole number", key), + key, + ) + default: + return 0, lazyerrors.Error(err) + } + } + + if whole > math.MaxInt32 || whole < math.MinInt32 { + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf("%v value for %s is out of range", whole, key), + key, + ) + } + + if whole < 0 { + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf("%v value for %s is out of range", value, key), + key, + ) + } + + return whole, nil +} + +// GetBoolOptionalParam returns bool value of v. +// Non-zero double, long, and int values return true. +// Zero values for those types, as well as nulls and missing fields, return false. +// Other types return a protocol error. +func GetBoolOptionalParam(key string, v any) (bool, error) { + switch v := v.(type) { + case float64: + return v != 0, nil + case bool: + return v, nil + case types.NullType: + return false, nil + case int32: + return v != 0, nil + case int64: + return v != 0, nil + default: + msg := fmt.Sprintf( + `BSON field '%s' is the wrong type '%s', expected types '[bool, long, int, decimal, double]'`, + key, + AliasFromType(v), + ) + + return false, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrTypeMismatch, msg, key) + } +} diff --git a/internal/handlers/commonparams/typecode.go b/internal/handlers/commonparams/typecode.go new file mode 100644 index 000000000000..701bd4602c3a --- /dev/null +++ b/internal/handlers/commonparams/typecode.go @@ -0,0 +1,203 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commonparams + +import ( + "fmt" + "math" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +//go:generate ../../../bin/stringer -linecomment -type TypeCode + +// TypeCode represents BSON type codes. +// BSON type codes represent corresponding codes in BSON specification. +// They could be used to query fields with particular type values using $type operator. +// Type code `number` is added to support MongoDB surrogate alias `number` which matches double, int and long type values. +type TypeCode int32 + +const ( + // TypeCodeDouble is a double type code. + TypeCodeDouble = TypeCode(1) // double + // TypeCodeString is a string type code. + TypeCodeString = TypeCode(2) // string + // TypeCodeObject is an object type code. + TypeCodeObject = TypeCode(3) // object + // TypeCodeArray is an array type code. + TypeCodeArray = TypeCode(4) // array + // TypeCodeBinData is a binary data type code. + TypeCodeBinData = TypeCode(5) // binData + // TypeCodeObjectID is an object id type code. + TypeCodeObjectID = TypeCode(7) // objectId + // TypeCodeBool is a boolean type code. + TypeCodeBool = TypeCode(8) // bool + // TypeCodeDate is a date type code. + TypeCodeDate = TypeCode(9) // date + // TypeCodeNull is a null type code. + TypeCodeNull = TypeCode(10) // null + // TypeCodeRegex is a regex type code. + TypeCodeRegex = TypeCode(11) // regex + // TypeCodeInt is an int type code. + TypeCodeInt = TypeCode(16) // int + // TypeCodeTimestamp is a timestamp type code. + TypeCodeTimestamp = TypeCode(17) // timestamp + // TypeCodeLong is a long type code. + TypeCodeLong = TypeCode(18) // long + + // Not implemented. + + // TypeCodeDecimal is a decimal type code. + TypeCodeDecimal = TypeCode(19) // decimal + // TypeCodeMinKey is a minKey type code. + TypeCodeMinKey = TypeCode(-1) // minKey + // TypeCodeMaxKey is a maxKey type code. + TypeCodeMaxKey = TypeCode(127) // maxKey + + // Not actual type code. `number` matches double, int and long. + + // TypeCodeNumber is a number type code. + TypeCodeNumber = TypeCode(-128) // number +) + +// NewTypeCode returns TypeCode and error by given code. +func NewTypeCode(code int32) (TypeCode, error) { + c := TypeCode(code) + switch c { + case TypeCodeDouble, TypeCodeString, TypeCodeObject, TypeCodeArray, + TypeCodeBinData, TypeCodeObjectID, TypeCodeBool, TypeCodeDate, + TypeCodeNull, TypeCodeRegex, TypeCodeInt, TypeCodeTimestamp, TypeCodeLong, TypeCodeNumber: + return c, nil + case TypeCodeDecimal, TypeCodeMinKey, TypeCodeMaxKey: + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrNotImplemented, + fmt.Sprintf(`Type code %v not implemented`, code), + "$type", + ) + default: + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf(`Invalid numerical type code: %d`, code), + "$type", + ) + } +} + +// HasSameTypeElements returns true if types.Array elements has the same type. +// MongoDB consider int32, int64 and float64 that could be converted to int as the same type. +func HasSameTypeElements(array *types.Array) bool { + var prev string + + for i := 0; i < array.Len(); i++ { + var cur string + + element := must.NotFail(array.Get(i)) + + if isWholeNumber(element) { + cur = TypeCodeNumber.String() + } else { + cur = AliasFromType(element) + } + + if prev == "" { + prev = cur + continue + } + + if prev != cur { + return false + } + } + + return true +} + +// aliasToTypeCode matches string type aliases to the corresponding TypeCode value. +var aliasToTypeCode = map[string]TypeCode{} + +func init() { + for _, i := range []TypeCode{ + TypeCodeDouble, TypeCodeString, TypeCodeObject, TypeCodeArray, + TypeCodeBinData, TypeCodeObjectID, TypeCodeBool, TypeCodeDate, TypeCodeNull, + TypeCodeRegex, TypeCodeInt, TypeCodeTimestamp, TypeCodeLong, TypeCodeNumber, + } { + aliasToTypeCode[i.String()] = i + } +} + +// AliasFromType returns type alias name for given value. +func AliasFromType(v any) string { + switch v := v.(type) { + case *types.Document: + return TypeCodeObject.String() + case *types.Array: + return TypeCodeArray.String() + case float64: + return TypeCodeDouble.String() + case string: + return TypeCodeString.String() + case types.Binary: + return TypeCodeBinData.String() + case types.ObjectID: + return TypeCodeObjectID.String() + case bool: + return TypeCodeBool.String() + case time.Time: + return TypeCodeDate.String() + case types.NullType: + return TypeCodeNull.String() + case types.Regex: + return TypeCodeRegex.String() + case int32: + return TypeCodeInt.String() + case types.Timestamp: + return TypeCodeTimestamp.String() + case int64: + return TypeCodeLong.String() + default: + panic(fmt.Sprintf("not supported type %T", v)) + } +} + +// isWholeNumber checks is the given value represents a whole number type. +func isWholeNumber(v any) bool { + switch v := v.(type) { + case float64: + return !(v != math.Trunc(v) || math.IsNaN(v) || math.IsInf(v, 0)) + case int32: + return true + case int64: + return true + default: + return false + } +} + +// ParseTypeCode returns TypeCode and error by given type code alias. +func ParseTypeCode(alias string) (TypeCode, error) { + code, ok := aliasToTypeCode[alias] + if !ok { + return 0, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrBadValue, + fmt.Sprintf(`Unknown type name alias: %s`, alias), + "$type", + ) + } + + return code, nil +} diff --git a/internal/handlers/commonparams/typecode_string.go b/internal/handlers/commonparams/typecode_string.go new file mode 100644 index 000000000000..456a8eec008f --- /dev/null +++ b/internal/handlers/commonparams/typecode_string.go @@ -0,0 +1,65 @@ +// Code generated by "stringer -linecomment -type TypeCode"; DO NOT EDIT. + +package commonparams + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TypeCodeDouble-1] + _ = x[TypeCodeString-2] + _ = x[TypeCodeObject-3] + _ = x[TypeCodeArray-4] + _ = x[TypeCodeBinData-5] + _ = x[TypeCodeObjectID-7] + _ = x[TypeCodeBool-8] + _ = x[TypeCodeDate-9] + _ = x[TypeCodeNull-10] + _ = x[TypeCodeRegex-11] + _ = x[TypeCodeInt-16] + _ = x[TypeCodeTimestamp-17] + _ = x[TypeCodeLong-18] + _ = x[TypeCodeDecimal-19] + _ = x[TypeCodeMinKey - -1] + _ = x[TypeCodeMaxKey-127] + _ = x[TypeCodeNumber - -128] +} + +const ( + _TypeCode_name_0 = "number" + _TypeCode_name_1 = "minKey" + _TypeCode_name_2 = "doublestringobjectarraybinData" + _TypeCode_name_3 = "objectIdbooldatenullregex" + _TypeCode_name_4 = "inttimestamplongdecimal" + _TypeCode_name_5 = "maxKey" +) + +var ( + _TypeCode_index_2 = [...]uint8{0, 6, 12, 18, 23, 30} + _TypeCode_index_3 = [...]uint8{0, 8, 12, 16, 20, 25} + _TypeCode_index_4 = [...]uint8{0, 3, 12, 16, 23} +) + +func (i TypeCode) String() string { + switch { + case i == -128: + return _TypeCode_name_0 + case i == -1: + return _TypeCode_name_1 + case 1 <= i && i <= 5: + i -= 1 + return _TypeCode_name_2[_TypeCode_index_2[i]:_TypeCode_index_2[i+1]] + case 7 <= i && i <= 11: + i -= 7 + return _TypeCode_name_3[_TypeCode_index_3[i]:_TypeCode_index_3[i+1]] + case 16 <= i && i <= 19: + i -= 16 + return _TypeCode_name_4[_TypeCode_index_4[i]:_TypeCode_index_4[i+1]] + case i == 127: + return _TypeCode_name_5 + default: + return "TypeCode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/handlers/common/typecode_test.go b/internal/handlers/commonparams/typecode_test.go similarity index 96% rename from internal/handlers/common/typecode_test.go rename to internal/handlers/commonparams/typecode_test.go index c85df2c88dbf..e5d6119bf51a 100644 --- a/internal/handlers/common/typecode_test.go +++ b/internal/handlers/commonparams/typecode_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common +package commonparams import ( "testing" @@ -55,7 +55,7 @@ func TestHasSameTypeElements(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - result := hasSameTypeElements(tc.array) + result := HasSameTypeElements(tc.array) assert.Equal(t, tc.same, result) }) } diff --git a/internal/handlers/dummy/msg_delete.go b/internal/handlers/dummy/msg_delete.go deleted file mode 100644 index b1442e9d4c85..000000000000 --- a/internal/handlers/dummy/msg_delete.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgDelete implements HandlerInterface. -func (h *Handler) MsgDelete(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_drop.go b/internal/handlers/dummy/msg_drop.go deleted file mode 100644 index 6e2ea2589cb2..000000000000 --- a/internal/handlers/dummy/msg_drop.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgDrop implements HandlerInterface. -func (h *Handler) MsgDrop(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_dropdatabase.go b/internal/handlers/dummy/msg_dropdatabase.go deleted file mode 100644 index faa9df8f7c3c..000000000000 --- a/internal/handlers/dummy/msg_dropdatabase.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgDropDatabase implements HandlerInterface. -func (h *Handler) MsgDropDatabase(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_find.go b/internal/handlers/dummy/msg_find.go deleted file mode 100644 index 2810d6dd13ca..000000000000 --- a/internal/handlers/dummy/msg_find.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgFind implements HandlerInterface. -func (h *Handler) MsgFind(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_hello.go b/internal/handlers/dummy/msg_hello.go deleted file mode 100644 index 56e5007f3597..000000000000 --- a/internal/handlers/dummy/msg_hello.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgHello implements HandlerInterface. -func (h *Handler) MsgHello(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_insert.go b/internal/handlers/dummy/msg_insert.go deleted file mode 100644 index fd77e174dd53..000000000000 --- a/internal/handlers/dummy/msg_insert.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgInsert implements HandlerInterface. -func (h *Handler) MsgInsert(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_ismaster.go b/internal/handlers/dummy/msg_ismaster.go deleted file mode 100644 index 8a5cb8d60d8e..000000000000 --- a/internal/handlers/dummy/msg_ismaster.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgIsMaster implements HandlerInterface. -func (h *Handler) MsgIsMaster(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_listcollections.go b/internal/handlers/dummy/msg_listcollections.go deleted file mode 100644 index 33433488fedf..000000000000 --- a/internal/handlers/dummy/msg_listcollections.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgListCollections implements HandlerInterface. -func (h *Handler) MsgListCollections(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_listdatabases.go b/internal/handlers/dummy/msg_listdatabases.go deleted file mode 100644 index 4b83cee88ada..000000000000 --- a/internal/handlers/dummy/msg_listdatabases.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgListDatabases implements HandlerInterface. -func (h *Handler) MsgListDatabases(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/dummy/msg_update.go b/internal/handlers/dummy/msg_update.go deleted file mode 100644 index b56907cee834..000000000000 --- a/internal/handlers/dummy/msg_update.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 FerretDB Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dummy - -import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// MsgUpdate implements HandlerInterface. -func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) -} diff --git a/internal/handlers/hana/cmd_query.go b/internal/handlers/hana/cmd_query.go index 84716454b5bd..24c7ec534db6 100644 --- a/internal/handlers/hana/cmd_query.go +++ b/internal/handlers/hana/cmd_query.go @@ -41,7 +41,7 @@ func (h *Handler) CmdQuery(ctx context.Context, query *wire.OpQuery) (*wire.OpRe "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/hana/msg_buildinfo.go b/internal/handlers/hana/msg_buildinfo.go index ef0c89501c42..8538ef55fe50 100644 --- a/internal/handlers/hana/msg_buildinfo.go +++ b/internal/handlers/hana/msg_buildinfo.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgBuildInfo implements HandlerInterface. func (h *Handler) MsgBuildInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgBuildInfo(ctx, msg) + return commoncommands.MsgBuildInfo(ctx, msg) } diff --git a/internal/handlers/hana/msg_connectionstatus.go b/internal/handlers/hana/msg_connectionstatus.go index dba6861c82bc..4da70c1b364f 100644 --- a/internal/handlers/hana/msg_connectionstatus.go +++ b/internal/handlers/hana/msg_connectionstatus.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgConnectionStatus implements HandlerInterface. func (h *Handler) MsgConnectionStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgConnectionStatus(ctx, msg) + return commoncommands.MsgConnectionStatus(ctx, msg) } diff --git a/internal/handlers/hana/msg_currentop.go b/internal/handlers/hana/msg_currentop.go index dc6766c71a71..ba10821a7282 100644 --- a/internal/handlers/hana/msg_currentop.go +++ b/internal/handlers/hana/msg_currentop.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgCurrentOp implements HandlerInterface. func (h *Handler) MsgCurrentOp(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgCurrentOp(ctx, msg) + return commoncommands.MsgCurrentOp(ctx, msg) } diff --git a/internal/handlers/hana/msg_debugerror.go b/internal/handlers/hana/msg_debugerror.go index a5defdfee40e..420ea26d01cb 100644 --- a/internal/handlers/hana/msg_debugerror.go +++ b/internal/handlers/hana/msg_debugerror.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgDebugError implements HandlerInterface. func (h *Handler) MsgDebugError(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgDebugError(ctx, msg) + return commoncommands.MsgDebugError(ctx, msg) } diff --git a/internal/handlers/hana/msg_getcmdlineopts.go b/internal/handlers/hana/msg_getcmdlineopts.go index abe00b90636b..5e17d4f15743 100644 --- a/internal/handlers/hana/msg_getcmdlineopts.go +++ b/internal/handlers/hana/msg_getcmdlineopts.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgGetCmdLineOpts implements HandlerInterface. func (h *Handler) MsgGetCmdLineOpts(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgGetCmdLineOpts(ctx, msg) + return commoncommands.MsgGetCmdLineOpts(ctx, msg) } diff --git a/internal/handlers/hana/msg_hostinfo.go b/internal/handlers/hana/msg_hostinfo.go index 66b6255e77e0..6280b9778422 100644 --- a/internal/handlers/hana/msg_hostinfo.go +++ b/internal/handlers/hana/msg_hostinfo.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgHostInfo implements HandlerInterface. func (h *Handler) MsgHostInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgHostInfo(ctx, msg) + return commoncommands.MsgHostInfo(ctx, msg) } diff --git a/internal/handlers/hana/msg_listcommands.go b/internal/handlers/hana/msg_listcommands.go index e35e5892f2d1..f062e103a73d 100644 --- a/internal/handlers/hana/msg_listcommands.go +++ b/internal/handlers/hana/msg_listcommands.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgListCommands implements handlers.Interface. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgListCommands(ctx, msg) + return commoncommands.MsgListCommands(ctx, msg) } diff --git a/internal/handlers/hana/msg_setfreemonitoring.go b/internal/handlers/hana/msg_setfreemonitoring.go index b6483ce7d0ba..2d3df82c8a1c 100644 --- a/internal/handlers/hana/msg_setfreemonitoring.go +++ b/internal/handlers/hana/msg_setfreemonitoring.go @@ -23,7 +23,5 @@ import ( // MsgSetFreeMonitoring implements HandlerInterface. func (h *Handler) MsgSetFreeMonitoring(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - // The state field in this function results in panic, though here it's used inside the stub, - // which is not executed on runtime. - return common.SetFreeMonitoring(ctx, msg, nil) + return common.SetFreeMonitoring(ctx, msg, h.StateProvider) } diff --git a/internal/handlers/hana/msg_whatsmyuri.go b/internal/handlers/hana/msg_whatsmyuri.go index 69637ffb4e9f..7028fcddd63c 100644 --- a/internal/handlers/hana/msg_whatsmyuri.go +++ b/internal/handlers/hana/msg_whatsmyuri.go @@ -17,11 +17,11 @@ package hana import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgWhatsMyURI implements HandlerInterface. func (h *Handler) MsgWhatsMyURI(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgWhatsMyURI(ctx, msg) + return commoncommands.MsgWhatsMyURI(ctx, msg) } diff --git a/internal/handlers/pg/cmd_query.go b/internal/handlers/pg/cmd_query.go index 18172656c3f0..69b497287338 100644 --- a/internal/handlers/pg/cmd_query.go +++ b/internal/handlers/pg/cmd_query.go @@ -41,7 +41,7 @@ func (h *Handler) CmdQuery(ctx context.Context, query *wire.OpQuery) (*wire.OpRe "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/pg/msg_aggregate.go b/internal/handlers/pg/msg_aggregate.go index 0cda21652914..90ad62bf6b9c 100644 --- a/internal/handlers/pg/msg_aggregate.go +++ b/internal/handlers/pg/msg_aggregate.go @@ -278,25 +278,25 @@ func processStagesStats(ctx context.Context, p *stagesStatsParams) ([]*types.Doc } if hasStorage { - var avgObjSize int32 + var avgObjSize int64 if collStats.CountObjects > 0 { - avgObjSize = int32(collStats.SizeCollection) / collStats.CountObjects + avgObjSize = collStats.SizeCollection / collStats.CountObjects } doc.Set( "storageStats", must.NotFail(types.NewDocument( - "size", int32(collStats.SizeTotal), + "size", collStats.SizeTotal, "count", collStats.CountObjects, "avgObjSize", avgObjSize, - "storageSize", int32(collStats.SizeCollection), - "freeStorageSize", int32(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "storageSize", collStats.SizeCollection, + "freeStorageSize", int64(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 "capped", false, // TODO https://github.com/FerretDB/FerretDB/issues/2342 "wiredTiger", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 "nindexes", collStats.CountIndexes, "indexDetails", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 "indexBuilds", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 - "totalIndexSize", int32(collStats.SizeIndexes), - "totalSize", int32(collStats.SizeTotal), + "totalIndexSize", collStats.SizeIndexes, + "totalSize", collStats.SizeTotal, "indexSizes", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 )), ) diff --git a/internal/handlers/pg/msg_buildinfo.go b/internal/handlers/pg/msg_buildinfo.go index d2012ba996d1..ac4c4997f05e 100644 --- a/internal/handlers/pg/msg_buildinfo.go +++ b/internal/handlers/pg/msg_buildinfo.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgBuildInfo implements HandlerInterface. func (h *Handler) MsgBuildInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgBuildInfo(ctx, msg) + return commoncommands.MsgBuildInfo(ctx, msg) } diff --git a/internal/handlers/pg/msg_collstats.go b/internal/handlers/pg/msg_collstats.go index b1fba2591a56..5c6c9893a524 100644 --- a/internal/handlers/pg/msg_collstats.go +++ b/internal/handlers/pg/msg_collstats.go @@ -82,7 +82,7 @@ func (h *Handler) MsgCollStats(ctx context.Context, msg *wire.OpMsg) (*wire.OpMs pairs := []any{ "ns", db + "." + collection, - "size", stats.SizeCollection / scale, + "size", stats.SizeCollection / int64(scale), "count", stats.CountObjects, } @@ -92,10 +92,10 @@ func (h *Handler) MsgCollStats(ctx context.Context, msg *wire.OpMsg) (*wire.OpMs } pairs = append(pairs, - "storageSize", stats.SizeTotal/scale, + "storageSize", stats.SizeTotal/int64(scale), "nindexes", stats.CountIndexes, - "totalIndexSize", stats.SizeIndexes/scale, - "totalSize", stats.SizeTotal/scale, + "totalIndexSize", stats.SizeIndexes/int64(scale), + "totalSize", stats.SizeTotal/int64(scale), "scaleFactor", scale, "ok", float64(1), ) diff --git a/internal/handlers/pg/msg_connectionstatus.go b/internal/handlers/pg/msg_connectionstatus.go index 2aa8eefa8069..fbd3cb72335a 100644 --- a/internal/handlers/pg/msg_connectionstatus.go +++ b/internal/handlers/pg/msg_connectionstatus.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgConnectionStatus implements HandlerInterface. func (h *Handler) MsgConnectionStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgConnectionStatus(ctx, msg) + return commoncommands.MsgConnectionStatus(ctx, msg) } diff --git a/internal/handlers/pg/msg_count.go b/internal/handlers/pg/msg_count.go index 611159ebceb5..cf837bd6fbd5 100644 --- a/internal/handlers/pg/msg_count.go +++ b/internal/handlers/pg/msg_count.go @@ -39,22 +39,18 @@ func (h *Handler) MsgCount(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, e return nil, lazyerrors.Error(err) } - if err = common.Unimplemented(document, "collation"); err != nil { - return nil, err - } - - common.Ignored(document, h.L, "hint", "readConcern", "comment") - - params, err := common.GetCountParams(document) + params, err := common.GetCountParams(document, h.L) if err != nil { return nil, err } - var qp pgdb.QueryParams + qp := pgdb.QueryParams{ + Filter: params.Filter, + DB: params.DB, + Collection: params.Collection, + } qp.Filter = params.Filter - qp.DB = params.DB - qp.Collection = params.Collection var resDocs []*types.Document err = dbPool.InTransaction(ctx, func(tx pgx.Tx) error { diff --git a/internal/handlers/pg/msg_createindexes.go b/internal/handlers/pg/msg_createindexes.go index 1321eebf66c3..f4194af59298 100644 --- a/internal/handlers/pg/msg_createindexes.go +++ b/internal/handlers/pg/msg_createindexes.go @@ -23,6 +23,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" @@ -180,7 +181,7 @@ func processIndexOptions(indexDoc *types.Document) (*pgdb.Index, error) { var order int64 if val, err = keyDoc.Get("_id"); err == nil { - if order, err = common.GetWholeNumberParam(val); err == nil && order == -1 { + if order, err = commonparams.GetWholeNumberParam(val); err == nil && order == -1 { return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, "The field 'key' for an _id index must be {_id: 1}, but got { _id: -1 }", @@ -271,7 +272,7 @@ func processIndexKey(keyDoc *types.Document) (pgdb.IndexKey, error) { var orderParam int64 - if orderParam, err = common.GetWholeNumberParam(order); err != nil { + if orderParam, err = commonparams.GetWholeNumberParam(order); err != nil { // TODO Add better validation and return proper error: https://github.com/FerretDB/FerretDB/issues/2311 return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrNotImplemented, diff --git a/internal/handlers/pg/msg_currentop.go b/internal/handlers/pg/msg_currentop.go index 884f73039be5..da3dfb29d801 100644 --- a/internal/handlers/pg/msg_currentop.go +++ b/internal/handlers/pg/msg_currentop.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgCurrentOp implements HandlerInterface. func (h *Handler) MsgCurrentOp(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgCurrentOp(ctx, msg) + return commoncommands.MsgCurrentOp(ctx, msg) } diff --git a/internal/handlers/pg/msg_dbstats.go b/internal/handlers/pg/msg_dbstats.go index 98d5723e51e3..6552e0e5e02d 100644 --- a/internal/handlers/pg/msg_dbstats.go +++ b/internal/handlers/pg/msg_dbstats.go @@ -77,11 +77,11 @@ func (h *Handler) MsgDBStats(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, } pairs = append(pairs, - "dataSize", stats.SizeCollections/scale, - "storageSize", stats.SizeCollections/scale, + "dataSize", stats.SizeCollections/int64(scale), + "storageSize", stats.SizeCollections/int64(scale), "indexes", stats.CountIndexes, - "indexSize", stats.SizeIndexes/scale, - "totalSize", stats.SizeTotal/scale, + "indexSize", stats.SizeIndexes/int64(scale), + "totalSize", stats.SizeTotal/int64(scale), "scaleFactor", float64(scale), "ok", float64(1), ) diff --git a/internal/handlers/pg/msg_debugerror.go b/internal/handlers/pg/msg_debugerror.go index 64947ef771da..52af45f1e206 100644 --- a/internal/handlers/pg/msg_debugerror.go +++ b/internal/handlers/pg/msg_debugerror.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgDebugError implements HandlerInterface. func (h *Handler) MsgDebugError(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgDebugError(ctx, msg) + return commoncommands.MsgDebugError(ctx, msg) } diff --git a/internal/handlers/pg/msg_explain.go b/internal/handlers/pg/msg_explain.go index 03e37cd6959e..18df5c172ffa 100644 --- a/internal/handlers/pg/msg_explain.go +++ b/internal/handlers/pg/msg_explain.go @@ -88,6 +88,8 @@ func (h *Handler) MsgExplain(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, "host", hostname, "version", version.Get().MongoDBVersion, "gitVersion", version.Get().Commit, + + // our extensions "ferretdbVersion", version.Get().Version, )) diff --git a/internal/handlers/pg/msg_find.go b/internal/handlers/pg/msg_find.go index 77aeb8a0b4a1..2c177920105c 100644 --- a/internal/handlers/pg/msg_find.go +++ b/internal/handlers/pg/msg_find.go @@ -139,7 +139,7 @@ func (h *Handler) MsgFind(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, er Iter: iter, DB: params.DB, Collection: params.Collection, - BatchSize: params.BatchSize, + BatchSize: int32(params.BatchSize), }) username, _ := conninfo.Get(ctx).Auth() diff --git a/internal/handlers/pg/msg_findandmodify.go b/internal/handlers/pg/msg_findandmodify.go index b7d899d128e4..372e89736d9d 100644 --- a/internal/handlers/pg/msg_findandmodify.go +++ b/internal/handlers/pg/msg_findandmodify.go @@ -129,7 +129,7 @@ func (h *Handler) MsgFindAndModify(ctx context.Context, msg *wire.OpMsg) (*wire. if params.HasUpdateOperators { upsert = resDocs[0].DeepCopy() - _, err = common.UpdateDocument(upsert, params.Update) + _, err = common.UpdateDocument(document.Command(), upsert, params.Update) if err != nil { return err } @@ -141,6 +141,7 @@ func (h *Handler) MsgFindAndModify(ctx context.Context, msg *wire.OpMsg) (*wire. } } + // TODO https://github.com/FerretDB/FerretDB/issues/2612 if _, err = updateDocument(ctx, tx, &qp, upsert); err != nil { return lazyerrors.Error(err) } @@ -184,6 +185,7 @@ func (h *Handler) MsgFindAndModify(ctx context.Context, msg *wire.OpMsg) (*wire. return nil } + // TODO https://github.com/FerretDB/FerretDB/issues/2612 if _, err = deleteDocuments(ctx, dbPool, &qp, resDocs); err != nil { return err } @@ -218,13 +220,14 @@ func upsertDocuments(ctx context.Context, dbPool *pgdb.Pool, tx pgx.Tx, docs []* switch res.Operation { case common.UpsertOperationInsert: - if err = insertDocument(ctx, dbPool, query, res.Upsert); err != nil { + // TODO https://github.com/FerretDB/FerretDB/issues/2612 + if err = insertDocument(ctx, tx, query, res.Upsert); err != nil { return nil, lazyerrors.Error(err) } return res, nil case common.UpsertOperationUpdate: - + // TODO https://github.com/FerretDB/FerretDB/issues/2612 _, err = updateDocument(ctx, tx, query, res.Upsert) if err != nil { return nil, lazyerrors.Error(err) diff --git a/internal/handlers/pg/msg_getcmdlineopts.go b/internal/handlers/pg/msg_getcmdlineopts.go index 868a36a65ac0..d95383a821ae 100644 --- a/internal/handlers/pg/msg_getcmdlineopts.go +++ b/internal/handlers/pg/msg_getcmdlineopts.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgGetCmdLineOpts implements HandlerInterface. func (h *Handler) MsgGetCmdLineOpts(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgGetCmdLineOpts(ctx, msg) + return commoncommands.MsgGetCmdLineOpts(ctx, msg) } diff --git a/internal/handlers/pg/msg_getlog.go b/internal/handlers/pg/msg_getlog.go index 516daab6837d..df3044e04ed4 100644 --- a/internal/handlers/pg/msg_getlog.go +++ b/internal/handlers/pg/msg_getlog.go @@ -24,8 +24,8 @@ import ( "go.uber.org/zap" "github.com/FerretDB/FerretDB/build/version" - "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/logging" @@ -59,7 +59,7 @@ func (h *Handler) MsgGetLog(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, commonerrors.ErrTypeMismatch, fmt.Errorf( "BSON field 'getLog.getLog' is the wrong type '%s', expected type 'string'", - common.AliasFromType(getLog), + commonparams.AliasFromType(getLog), ), ) } diff --git a/internal/handlers/pg/msg_getparameter.go b/internal/handlers/pg/msg_getparameter.go index 0611ad03e5c8..5ebb7ed8272c 100644 --- a/internal/handlers/pg/msg_getparameter.go +++ b/internal/handlers/pg/msg_getparameter.go @@ -20,6 +20,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/iterator" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -130,13 +131,18 @@ func extractParam(document *types.Document) (showDetails, allParameters bool, er } if param, ok := getPrm.(*types.Document); ok { - showDetails, err = common.GetBoolOptionalParam(param, "showDetails") - if err != nil { - return false, false, lazyerrors.Error(err) + if v, _ := param.Get("showDetails"); v != nil { + showDetails, err = commonparams.GetBoolOptionalParam("showDetails", v) + if err != nil { + return false, false, lazyerrors.Error(err) + } } - allParameters, err = common.GetBoolOptionalParam(param, "allParameters") - if err != nil { - return false, false, lazyerrors.Error(err) + + if v, _ := param.Get("allParameters"); v != nil { + allParameters, err = commonparams.GetBoolOptionalParam("allParameters", v) + if err != nil { + return false, false, lazyerrors.Error(err) + } } } if getPrm == "*" { diff --git a/internal/handlers/pg/msg_hello.go b/internal/handlers/pg/msg_hello.go index ee22466f31b1..afc8f81d5f58 100644 --- a/internal/handlers/pg/msg_hello.go +++ b/internal/handlers/pg/msg_hello.go @@ -36,7 +36,7 @@ func (h *Handler) MsgHello(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, e "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/pg/msg_hostinfo.go b/internal/handlers/pg/msg_hostinfo.go index 1a6ca77d81d5..afa606493061 100644 --- a/internal/handlers/pg/msg_hostinfo.go +++ b/internal/handlers/pg/msg_hostinfo.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgHostInfo implements HandlerInterface. func (h *Handler) MsgHostInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgHostInfo(ctx, msg) + return commoncommands.MsgHostInfo(ctx, msg) } diff --git a/internal/handlers/pg/msg_insert.go b/internal/handlers/pg/msg_insert.go index 8fdf3a75656c..a26f4c4999b1 100644 --- a/internal/handlers/pg/msg_insert.go +++ b/internal/handlers/pg/msg_insert.go @@ -24,6 +24,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -82,38 +83,56 @@ func insertMany(ctx context.Context, dbPool *pgdb.Pool, qp *pgdb.QueryParams, do var inserted int32 var insErrors commonerrors.WriteErrors - for i := 0; i < docs.Len(); i++ { - doc := must.NotFail(docs.Get(i)) + // attempt to insert all documents in a single transaction + err := dbPool.InTransaction(ctx, func(tx pgx.Tx) error { + for i := 0; i < docs.Len(); i++ { + doc := must.NotFail(docs.Get(i)) - err := insertDocument(ctx, dbPool, qp, doc) - - var we *commonerrors.WriteErrors - - switch { - case err == nil: - inserted++ - continue - case errors.As(err, &we): - insErrors.Merge(we, int32(i)) - default: - insErrors.Append(err, int32(i)) + err := insertDocument(ctx, tx, qp, doc) + if err != nil { + return err + } } - - if ordered { - return inserted, &insErrors + return nil + }) + // if transaction fails with err + // try inserting one document at a time + if err != nil { + for i := 0; i < docs.Len(); i++ { + doc := must.NotFail(docs.Get(i)) + + err := insertDocumentSeparately(ctx, dbPool, qp, doc) + + var we *commonerrors.WriteErrors + + switch { + case err == nil: + inserted++ + continue + case errors.As(err, &we): + insErrors.Merge(we, int32(i)) + default: + insErrors.Append(err, int32(i)) + } + + if ordered { + return inserted, &insErrors + } } + + return inserted, &insErrors } - return inserted, &insErrors + return int32(docs.Len()), &insErrors } -// insertDocument prepares and executes actual INSERT request to Postgres. -func insertDocument(ctx context.Context, dbPool *pgdb.Pool, qp *pgdb.QueryParams, doc any) error { +// insertDocument prepares and executes actual INSERT request to Postgres in provided transaction. +func insertDocument(ctx context.Context, tx pgx.Tx, qp *pgdb.QueryParams, doc any) error { d, ok := doc.(*types.Document) if !ok { return commonerrors.NewCommandErrorMsg( commonerrors.ErrBadValue, - fmt.Sprintf("document has invalid type %s", common.AliasFromType(doc)), + fmt.Sprintf("document has invalid type %s", commonparams.AliasFromType(doc)), ) } @@ -126,9 +145,7 @@ func insertDocument(ctx context.Context, dbPool *pgdb.Pool, qp *pgdb.QueryParams toInsert.Set("_id", types.NewObjectID()) } - err := dbPool.InTransactionRetry(ctx, func(tx pgx.Tx) error { - return pgdb.InsertDocument(ctx, tx, qp.DB, qp.Collection, toInsert) - }) + err := pgdb.InsertDocument(ctx, tx, qp.DB, qp.Collection, toInsert) switch { case err == nil: @@ -154,3 +171,12 @@ func insertDocument(ctx context.Context, dbPool *pgdb.Pool, qp *pgdb.QueryParams return commonerrors.CheckError(err) } } + +// insertDocumentSeparately prepares and executes actual INSERT request to Postgres in separate transaction. +// +// It should be used in places where we don't want to rollback previous inserted documents on error. +func insertDocumentSeparately(ctx context.Context, dbPool *pgdb.Pool, qp *pgdb.QueryParams, doc any) error { + return dbPool.InTransactionRetry(ctx, func(tx pgx.Tx) error { + return insertDocument(ctx, tx, qp, doc) + }) +} diff --git a/internal/handlers/pg/msg_ismaster.go b/internal/handlers/pg/msg_ismaster.go index 46992ced9663..b4d22581760a 100644 --- a/internal/handlers/pg/msg_ismaster.go +++ b/internal/handlers/pg/msg_ismaster.go @@ -36,7 +36,7 @@ func (h *Handler) MsgIsMaster(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/pg/msg_listcollections.go b/internal/handlers/pg/msg_listcollections.go index 748863b49f31..32b9ce50fd00 100644 --- a/internal/handlers/pg/msg_listcollections.go +++ b/internal/handlers/pg/msg_listcollections.go @@ -21,6 +21,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -52,9 +53,12 @@ func (h *Handler) MsgListCollections(ctx context.Context, msg *wire.OpMsg) (*wir return nil, err } - nameOnly, err := common.GetBoolOptionalParam(document, "nameOnly") - if err != nil { - return nil, err + var nameOnly bool + if v, _ := document.Get("nameOnly"); v != nil { + nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v) + if err != nil { + return nil, err + } } var names []string diff --git a/internal/handlers/pg/msg_listcommands.go b/internal/handlers/pg/msg_listcommands.go index 77b884e26e92..b21dbf4fa517 100644 --- a/internal/handlers/pg/msg_listcommands.go +++ b/internal/handlers/pg/msg_listcommands.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgListCommands implements handlers.Interface. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgListCommands(ctx, msg) + return commoncommands.MsgListCommands(ctx, msg) } diff --git a/internal/handlers/pg/msg_listdatabases.go b/internal/handlers/pg/msg_listdatabases.go index ed67f4c6f274..8c3cbe81de07 100644 --- a/internal/handlers/pg/msg_listdatabases.go +++ b/internal/handlers/pg/msg_listdatabases.go @@ -20,6 +20,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -46,9 +47,12 @@ func (h *Handler) MsgListDatabases(ctx context.Context, msg *wire.OpMsg) (*wire. common.Ignored(document, h.L, "comment", "authorizedDatabases") - nameOnly, err := common.GetBoolOptionalParam(document, "nameOnly") - if err != nil { - return nil, err + var nameOnly bool + if v, _ := document.Get("nameOnly"); v != nil { + nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v) + if err != nil { + return nil, err + } } var totalSize int64 diff --git a/internal/handlers/pg/msg_listindexes.go b/internal/handlers/pg/msg_listindexes.go index 9d1ed5cebb87..5cd8d2858e18 100644 --- a/internal/handlers/pg/msg_listindexes.go +++ b/internal/handlers/pg/msg_listindexes.go @@ -23,6 +23,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -60,7 +61,7 @@ func (h *Handler) MsgListIndexes(ctx context.Context, msg *wire.OpMsg) (*wire.Op if !ok { return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", common.AliasFromType(collectionParam)), + fmt.Sprintf("collection name has invalid type %s", commonparams.AliasFromType(collectionParam)), document.Command(), ) } diff --git a/internal/handlers/pg/msg_renamecollection.go b/internal/handlers/pg/msg_renamecollection.go index 9d2c4493aff5..9ec19161e5b4 100644 --- a/internal/handlers/pg/msg_renamecollection.go +++ b/internal/handlers/pg/msg_renamecollection.go @@ -25,6 +25,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/pg/pgdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -68,7 +69,7 @@ func (h *Handler) MsgRenameCollection(ctx context.Context, msg *wire.OpMsg) (*wi return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", common.AliasFromType(from)), + fmt.Sprintf("collection name has invalid type %s", commonparams.AliasFromType(from)), command, ) } diff --git a/internal/handlers/pg/msg_update.go b/internal/handlers/pg/msg_update.go index da9f82585f49..553e638535fa 100644 --- a/internal/handlers/pg/msg_update.go +++ b/internal/handlers/pg/msg_update.go @@ -70,7 +70,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, DB: params.DB, Collection: params.Collection, Filter: u.Filter, - Comment: u.Comment, + Comment: params.Comment, } resDocs, err := fetchAndFilterDocs(ctx, &fetchParams{tx, &qp, h.DisableFilterPushdown}) @@ -85,7 +85,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, } doc := u.Filter.DeepCopy() - if _, err = common.UpdateDocument(doc, u.Update); err != nil { + if _, err = common.UpdateDocument(document.Command(), doc, u.Update); err != nil { return err } if !doc.Has("_id") { @@ -97,7 +97,8 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, "_id", must.NotFail(doc.Get("_id")), ))) - if err = insertDocument(ctx, dbPool, &qp, doc); err != nil { + // TODO https://github.com/FerretDB/FerretDB/issues/2612 + if err = insertDocument(ctx, tx, &qp, doc); err != nil { return err } @@ -112,7 +113,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, matched += int32(len(resDocs)) for _, doc := range resDocs { - changed, err := common.UpdateDocument(doc, u.Update) + changed, err := common.UpdateDocument(document.Command(), doc, u.Update) if err != nil { return err } @@ -121,6 +122,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, continue } + // TODO https://github.com/FerretDB/FerretDB/issues/2612 rowsChanged, err := updateDocument(ctx, tx, &qp, doc) if err != nil { return err diff --git a/internal/handlers/pg/msg_whatsmyuri.go b/internal/handlers/pg/msg_whatsmyuri.go index bd5964961d25..1a0c852528b1 100644 --- a/internal/handlers/pg/msg_whatsmyuri.go +++ b/internal/handlers/pg/msg_whatsmyuri.go @@ -17,11 +17,11 @@ package pg import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgWhatsMyURI implements HandlerInterface. func (h *Handler) MsgWhatsMyURI(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgWhatsMyURI(ctx, msg) + return commoncommands.MsgWhatsMyURI(ctx, msg) } diff --git a/internal/handlers/pg/pg.go b/internal/handlers/pg/pg.go index 647f2382afcb..494decd9d46a 100644 --- a/internal/handlers/pg/pg.go +++ b/internal/handlers/pg/pg.go @@ -134,11 +134,11 @@ func (h *Handler) DBPool(ctx context.Context) (*pgdb.Pool, error) { p, err := pgdb.NewPool(ctx, url, h.L, h.StateProvider) if err != nil { - h.L.Warn("DBPool: authentication failed", zap.String("username", username), zap.Error(err)) + h.L.Warn("DBPool: connection failed", zap.String("username", username), zap.Error(err)) return nil, lazyerrors.Error(err) } - h.L.Info("DBPool: authentication succeed", zap.String("username", username)) + h.L.Info("DBPool: connection succeed", zap.String("username", username)) h.pools[url] = p return p, nil diff --git a/internal/handlers/pg/pgdb/databases.go b/internal/handlers/pg/pgdb/databases.go index 0cc07b861e54..aabcf59692d2 100644 --- a/internal/handlers/pg/pgdb/databases.go +++ b/internal/handlers/pg/pgdb/databases.go @@ -24,6 +24,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" + "github.com/FerretDB/FerretDB/internal/util/contract" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" ) @@ -89,25 +90,27 @@ func CreateDatabaseIfNotExists(ctx context.Context, tx pgx.Tx, db string) error } // DropDatabase drops FerretDB database. -// -// It returns ErrSchemaNotExist if schema does not exist. -func DropDatabase(ctx context.Context, tx pgx.Tx, db string) error { - _, err := tx.Exec(ctx, `DROP SCHEMA `+pgx.Identifier{db}.Sanitize()+` CASCADE`) - if err == nil { - return nil +func DropDatabase(ctx context.Context, tx pgx.Tx, db string) (err error) { + defer contract.EnsureError(err, ErrSchemaNotExist) // if schema does not exist + + if _, err = tx.Exec(ctx, `DROP SCHEMA `+pgx.Identifier{db}.Sanitize()+` CASCADE`); err == nil { + return } pgErr, ok := err.(*pgconn.PgError) if !ok { - return lazyerrors.Error(err) + err = lazyerrors.Error(err) + return } switch pgErr.Code { case pgerrcode.InvalidSchemaName: - return ErrSchemaNotExist + err = ErrSchemaNotExist default: - return lazyerrors.Error(err) + err = lazyerrors.Error(err) } + + return } // DatabaseSize returns the size of the current database in bytes. diff --git a/internal/handlers/pg/pgdb/indexes.go b/internal/handlers/pg/pgdb/indexes.go index 0ca74fb8887b..6d83f51b48c3 100644 --- a/internal/handlers/pg/pgdb/indexes.go +++ b/internal/handlers/pg/pgdb/indexes.go @@ -204,8 +204,15 @@ func createPgIndexIfNotExists(ctx context.Context, tx pgx.Tx, schema, table, ind return lazyerrors.Errorf("unknown sort order: %d", field.Order) } - // It's important to sanitize field.Field data here, as it's a user-provided value. - fieldsDef[i] = fmt.Sprintf(`((_jsonb->%s)) %s`, quoteString(field.Field), order) + // if the key is foo.bar, then need to modify it to foo -> bar + fs := strings.Split(field.Field, ".") + transformedParts := make([]string, len(fs)) + + for j, f := range fs { + // It's important to sanitize field.Field data here, as it's a user-provided value. + transformedParts[j] = quoteString(f) + } + fieldsDef[i] = fmt.Sprintf(`((_jsonb->%s)) %s`, strings.Join(transformedParts, " -> "), order) } sql := `CREATE` + unique + ` INDEX IF NOT EXISTS ` + pgx.Identifier{index}.Sanitize() + diff --git a/internal/handlers/pg/pgdb/indexes_test.go b/internal/handlers/pg/pgdb/indexes_test.go index a4431bfa0d65..f16da198a5b1 100644 --- a/internal/handlers/pg/pgdb/indexes_test.go +++ b/internal/handlers/pg/pgdb/indexes_test.go @@ -39,32 +39,68 @@ func TestCreateIndexIfNotExists(t *testing.T) { collectionName := testutil.CollectionName(t) setupDatabase(ctx, t, pool, databaseName) - indexName := "test" - err := pool.InTransaction(ctx, func(tx pgx.Tx) error { - idx := Index{ - Name: indexName, - Key: []IndexKeyPair{{Field: "foo", Order: types.Ascending}, {Field: "bar", Order: types.Descending}}, - } - return CreateIndexIfNotExists(ctx, tx, databaseName, collectionName, &idx) - }) - require.NoError(t, err) - - tableName := collectionNameToTableName(collectionName) - pgIndexName := indexNameToPgIndexName(collectionName, indexName) + for name, tc := range map[string]struct { + expectedDefinition string // the expected index definition in postgresql + index Index // the index to create + }{ + "keyWithoutNestedField": { + index: Index{ + Name: "foo_and_bar", + Key: []IndexKeyPair{ + {Field: "foo", Order: types.Ascending}, + {Field: "bar", Order: types.Descending}, + }, + }, + expectedDefinition: "((_jsonb -> 'foo'::text)), ((_jsonb -> 'bar'::text)) DESC", + }, + "keyWithNestedField_level1": { + index: Index{ + Name: "foo_dot_bar", + Key: []IndexKeyPair{ + {Field: "foo.bar", Order: types.Ascending}, + }, + }, + expectedDefinition: "(((_jsonb -> 'foo'::text) -> 'bar'::text))", + }, + "keyWithNestedField_level2": { + index: Index{ + Name: "foo_dot_bar_dot_c", + Key: []IndexKeyPair{ + {Field: "foo.bar.c", Order: types.Ascending}, + }, + }, + expectedDefinition: "((((_jsonb -> 'foo'::text) -> 'bar'::text) -> 'c'::text))", + }, + } { + tc := tc - var indexdef string - err = pool.p.QueryRow( - ctx, - "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", - databaseName, tableName, pgIndexName, - ).Scan(&indexdef) - require.NoError(t, err) + t.Run(name, func(t *testing.T) { + t.Helper() + err := pool.InTransaction(ctx, func(tx pgx.Tx) error { + if err := CreateIndexIfNotExists(ctx, tx, databaseName, collectionName, &tc.index); err != nil { + return err + } + return nil + }) + require.NoError(t, err) + tableName := collectionNameToTableName(collectionName) + pgIndexName := indexNameToPgIndexName(collectionName, tc.index.Name) + + var indexdef string + err = pool.p.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + databaseName, tableName, pgIndexName, + ).Scan(&indexdef) + require.NoError(t, err) - expectedIndexdef := fmt.Sprintf( - "CREATE INDEX %s ON \"%s\".%s USING btree (((_jsonb -> 'foo'::text)), ((_jsonb -> 'bar'::text)) DESC)", - pgIndexName, databaseName, tableName, - ) - assert.Equal(t, expectedIndexdef, indexdef) + expectedIndexdef := fmt.Sprintf( + "CREATE INDEX %s ON \"%s\".%s USING btree (%s)", + pgIndexName, databaseName, tableName, tc.expectedDefinition, + ) + assert.Equal(t, expectedIndexdef, indexdef) + }) + } } // TestDropIndexes checks that we correctly drop indexes for various combination of existing indexes. @@ -113,6 +149,15 @@ func TestDropIndexes(t *testing.T) { {Name: "_id_", Key: []IndexKeyPair{{Field: "_id", Order: types.Ascending}}, Unique: true}, }, }, + "DropNestedField": { + toCreate: []Index{ + {Name: "foo_1", Key: []IndexKeyPair{{Field: "foo.bar", Order: types.Ascending}}}, + }, + toDrop: []Index{{Key: []IndexKeyPair{{Field: "foo.bar", Order: types.Ascending}}}}, + expected: []Index{ + {Name: "_id_", Key: []IndexKeyPair{{Field: "_id", Order: types.Ascending}}, Unique: true}, + }, + }, "DropOneFromTheBeginning": { toCreate: []Index{ {Name: "foo_1", Key: []IndexKeyPair{{Field: "foo", Order: types.Ascending}}}, diff --git a/internal/handlers/pg/pgdb/pgdb_test.go b/internal/handlers/pg/pgdb/pgdb_test.go index 44762d9c3173..71c96796618e 100644 --- a/internal/handlers/pg/pgdb/pgdb_test.go +++ b/internal/handlers/pg/pgdb/pgdb_test.go @@ -57,21 +57,22 @@ func setupDatabase(ctx context.Context, tb testing.TB, pool *Pool, db string) { tb.Cleanup(dropDatabase) } -func TestValidUTF8Locale(t *testing.T) { +func TestIsSupportedLocale(t *testing.T) { t.Parallel() cases := []struct { locale string expected bool }{ + {"c", true}, + {"POSIX", true}, + {"C.UTF8", true}, {"en_US.utf8", true}, {"en_US.utf-8", true}, {"en_US.UTF8", true}, {"en_US.UTF-8", true}, {"en_UK.UTF-8", false}, - {"en_UK.utf--8", false}, {"en_US", false}, - {"utf8", false}, } for _, tc := range cases { @@ -79,7 +80,7 @@ func TestValidUTF8Locale(t *testing.T) { t.Run(tc.locale, func(t *testing.T) { t.Parallel() - actual := isValidUTF8Locale(tc.locale) + actual := isSupportedLocale(tc.locale) assert.Equal(t, tc.expected, actual) }) } diff --git a/internal/handlers/pg/pgdb/pool.go b/internal/handlers/pg/pgdb/pool.go index b0d9275cd151..73a48f41c021 100644 --- a/internal/handlers/pg/pgdb/pool.go +++ b/internal/handlers/pg/pgdb/pool.go @@ -30,13 +30,12 @@ import ( "github.com/FerretDB/FerretDB/internal/util/state" ) -const ( - // Supported encoding. - encUTF8 = "UTF8" +var ( + // The only supported encoding in canonical form. + supportedEncoding = "UTF8" - // Supported locales: (For more info see: https://www.gnu.org/software/libc/manual/html_node/Standard-Locales.html) - localeC = "C" - localePOSIX = "POSIX" + // Supported locales in canonical forms. + supportedLocales = []string{"POSIX", "C", "C.UTF8", "en_US.UTF8"} ) // Pool represents PostgreSQL concurrency-safe connection pool. @@ -55,14 +54,30 @@ func NewPool(ctx context.Context, uri string, logger *zap.Logger, p *state.Provi return nil, fmt.Errorf("pgdb.NewPool: %w", err) } - // pgx 'defaultMaxConns' is 4, which is not enough for us. - // Set it to 20 by default if no query parameter is defined. - // See: https://github.com/FerretDB/FerretDB/issues/1844 values := u.Query() + if !values.Has("pool_max_conns") { + // it default to 4 which is too low for us values.Set("pool_max_conns", "20") } + values.Set("application_name", "FerretDB") + + // That only affects text protocol; pgx mostly uses a binary one. + // See: + // - https://github.com/jackc/pgx/issues/520 + // - https://github.com/jackc/pgx/issues/789 + // - https://github.com/jackc/pgx/issues/863 + // + // TODO https://github.com/FerretDB/FerretDB/issues/43 + values.Set("timezone", "UTC") + + // Set (and overwrite) it in debug builds to ensure that all identifiers in code are fully-qualified. + // Don't do it in non-debug builds because it makes using tools like PgBouncer harder. + if debugbuild.Enabled { + values.Set("search_path", "") + } + u.RawQuery = values.Encode() config, err := pgxpool.ParseConfig(u.String()) @@ -83,17 +98,6 @@ func NewPool(ctx context.Context, uri string, logger *zap.Logger, p *state.Provi return nil } - // That only affects text protocol; pgx mostly uses a binary one. - // See: - // * https://github.com/jackc/pgx/issues/520 - // * https://github.com/jackc/pgx/issues/789 - // * https://github.com/jackc/pgx/issues/863 - // * https://github.com/FerretDB/FerretDB/issues/43 - config.ConnConfig.RuntimeParams["timezone"] = "UTC" - - config.ConnConfig.RuntimeParams["application_name"] = "FerretDB" - config.ConnConfig.RuntimeParams["search_path"] = "" - pgdbLogger := zapadapter.NewLogger(logger.Named("pgdb")) tracers := []pgx.QueryTracer{ @@ -137,18 +141,27 @@ func (pgPool *Pool) Close() { pgPool.p.Close() } -// isValidUTF8Locale Currently supported locale variants, compromised between https://www.postgresql.org/docs/9.3/multibyte.html -// and https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html. -// -// Valid examples: -// * en_US.utf8, -// * en_US.utf-8 -// * en_US.UTF8, -// * en_US.UTF-8. -func isValidUTF8Locale(setting string) bool { - lowered := strings.ToLower(setting) - - return lowered == "en_us.utf8" || lowered == "en_us.utf-8" +// simplifySetting simplifies PostgreSQL setting value for comparison. +func simplifySetting(v string) string { + return strings.ToLower(strings.ReplaceAll(v, "-", "")) +} + +// isSupportedEncoding checks `server_encoding` and `client_encoding` values. +func isSupportedEncoding(v string) bool { + return simplifySetting(v) == simplifySetting(supportedEncoding) +} + +// isSupportedLocale checks `lc_collate` and `lc_ctype` values. +func isSupportedLocale(v string) bool { + v = simplifySetting(v) + + for _, s := range supportedLocales { + if v == simplifySetting(s) { + return true + } + } + + return false } // checkConnection checks PostgreSQL settings. @@ -173,21 +186,13 @@ func (pgPool *Pool) checkConnection(ctx context.Context) error { setting := values[1].(string) switch name { - case "server_encoding": - if setting != encUTF8 { - return fmt.Errorf("pgdb.checkConnection: %q is %q, want %q", name, setting, encUTF8) - } - case "client_encoding": - if setting != encUTF8 { - return fmt.Errorf("pgdb.checkConnection: %q is %q, want %q", name, setting, encUTF8) - } - case "lc_collate": - if setting != localeC && setting != localePOSIX && !isValidUTF8Locale(setting) { - return fmt.Errorf("pgdb.checkConnection: %q is %q", name, setting) + case "server_encoding", "client_encoding": + if !isSupportedEncoding(setting) { + return fmt.Errorf("pgdb.checkConnection: %q is %q; supported value is %q", name, setting, supportedEncoding) } - case "lc_ctype": - if setting != localeC && setting != localePOSIX && !isValidUTF8Locale(setting) { - return fmt.Errorf("pgdb.checkConnection: %q is %q", name, setting) + case "lc_collate", "lc_ctype": + if !isSupportedLocale(setting) { + return fmt.Errorf("pgdb.checkConnection: %q is %q; supported values are %v", name, setting, supportedLocales) } case "standard_conforming_strings": // To sanitize safely: https://github.com/jackc/pgx/issues/868#issuecomment-725544647 if setting != "on" { diff --git a/internal/handlers/pg/pgdb/query_iterator.go b/internal/handlers/pg/pgdb/query_iterator.go index 8f28bd52fb18..200a3ed5c91e 100644 --- a/internal/handlers/pg/pgdb/query_iterator.go +++ b/internal/handlers/pg/pgdb/query_iterator.go @@ -55,7 +55,6 @@ func newIterator(ctx context.Context, rows pgx.Rows, p *iteratorParams) types.Do rows: rows, token: resource.NewToken(), } - resource.Track(iter, iter.token) return iter diff --git a/internal/handlers/pg/pgdb/stats.go b/internal/handlers/pg/pgdb/stats.go index 1cd16853a2fa..074be3733bd2 100644 --- a/internal/handlers/pg/pgdb/stats.go +++ b/internal/handlers/pg/pgdb/stats.go @@ -31,25 +31,31 @@ type ServerStats struct { // DBStats describes statistics for a FerretDB database (PostgreSQL schema). // +// MongoDB uses "numbers" for those values that could be int32 or int64. +// FerretDB always returns int64 for simplicity. +// // TODO Include more data https://github.com/FerretDB/FerretDB/issues/2447. type DBStats struct { - CountCollections int32 - CountObjects int32 - CountIndexes int32 - SizeTotal int32 - SizeIndexes int32 - SizeCollections int32 + CountCollections int64 + CountObjects int64 + CountIndexes int64 + SizeTotal int64 + SizeIndexes int64 + SizeCollections int64 } // CollStats describes statistics for a FerretDB collection (PostgreSQL table). // +// MongoDB uses "numbers" for those values that could be int32 or int64. +// FerretDB always returns int64 for simplicity. +// // TODO Include more data https://github.com/FerretDB/FerretDB/issues/2447. type CollStats struct { - CountObjects int32 - CountIndexes int32 - SizeTotal int32 - SizeIndexes int32 - SizeCollection int32 + CountObjects int64 + CountIndexes int64 + SizeTotal int64 + SizeIndexes int64 + SizeCollection int64 } // CalculateServerStats returns statistics for all the FerretDB databases on the server. @@ -88,14 +94,14 @@ func CalculateDBStats(ctx context.Context, tx pgx.Tx, db string) (*DBStats, erro // It also includes the size of FerretDB metadata relations. // See also https://www.postgresql.org/docs/15/functions-admin.html#FUNCTIONS-ADMIN-DBOBJECT sql = ` - SELECT - SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))) - FROM pg_tables + SELECT + SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename))) + FROM pg_tables WHERE schemaname = $1` args := []any{db} row := tx.QueryRow(ctx, sql, args...) - var schemaSize *int32 + var schemaSize *int64 if err := row.Scan(&schemaSize); err != nil { return nil, lazyerrors.Error(err) } @@ -109,7 +115,7 @@ func CalculateDBStats(ctx context.Context, tx pgx.Tx, db string) (*DBStats, erro // In this query we select all the tables in the given schema, but we exclude FerretDB metadata table (by reserved prefix). sql = ` - SELECT + SELECT COUNT(t.tablename) AS CountTables, COUNT(i.indexname) AS CountIndexes, COALESCE(SUM(c.reltuples), 0) AS CountRows, @@ -161,7 +167,7 @@ func CalculateCollStats(ctx context.Context, tx pgx.Tx, db, collection string) ( COALESCE(pg_total_relation_size(oid), 0) AS SizeTotal, COALESCE(pg_table_size(oid), 0) AS SizeTable, COALESCE(pg_indexes_size(oid), 0) AS SizeIndexes - FROM pg_class + FROM pg_class WHERE oid = '%s'::regclass`, pgx.Identifier{db, metadata.table}.Sanitize(), ) diff --git a/internal/handlers/registry/hana.go b/internal/handlers/registry/hana.go index 7de2ea6b8d45..302ca9aba1fc 100644 --- a/internal/handlers/registry/hana.go +++ b/internal/handlers/registry/hana.go @@ -24,12 +24,15 @@ import ( // init registers "hana" handler for Hana when "ferretdb_hana" build tag is provided. func init() { registry["hana"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { + opts.Logger.Warn("HANA handler is in alpha. It is not supported yet.") + handlerOpts := &hana.NewOpts{ HANAURL: opts.HANAURL, L: opts.Logger, Metrics: opts.Metrics, StateProvider: opts.StateProvider, } + return hana.New(handlerOpts) } } diff --git a/internal/handlers/registry/pg.go b/internal/handlers/registry/pg.go new file mode 100644 index 000000000000..6d8d6d0811fc --- /dev/null +++ b/internal/handlers/registry/pg.go @@ -0,0 +1,39 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "github.com/FerretDB/FerretDB/internal/handlers" + "github.com/FerretDB/FerretDB/internal/handlers/pg" +) + +// init registers "pg" handler. +func init() { + registry["pg"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { + handlerOpts := &pg.NewOpts{ + PostgreSQLURL: opts.PostgreSQLURL, + + L: opts.Logger, + Metrics: opts.Metrics, + StateProvider: opts.StateProvider, + + DisableFilterPushdown: opts.DisableFilterPushdown, + EnableSortPushdown: opts.EnableSortPushdown, + EnableCursors: opts.EnableCursors, + } + + return pg.New(handlerOpts) + } +} diff --git a/internal/handlers/registry/registry.go b/internal/handlers/registry/registry.go index 7893cdd38a30..9382d4497ef4 100644 --- a/internal/handlers/registry/registry.go +++ b/internal/handlers/registry/registry.go @@ -19,13 +19,9 @@ import ( "fmt" "go.uber.org/zap" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/internal/clientconn/connmetrics" "github.com/FerretDB/FerretDB/internal/handlers" - "github.com/FerretDB/FerretDB/internal/handlers/dummy" - "github.com/FerretDB/FerretDB/internal/handlers/pg" "github.com/FerretDB/FerretDB/internal/util/state" ) @@ -48,6 +44,9 @@ type NewHandlerOpts struct { // for `pg` handler PostgreSQLURL string + // for `sqlite` handler + SQLiteURI string + // for `tigris` handler TigrisURL string TigrisClientID string @@ -82,29 +81,16 @@ func NewHandler(name string, opts *NewHandlerOpts) (handlers.Interface, error) { // Handlers returns a list of all handlers registered at compile-time. func Handlers() []string { - handlers := maps.Keys(registry) - slices.Sort(handlers) - return handlers -} - -// init registers handlers that are always enabled. -func init() { - registry["dummy"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { - return dummy.New(opts.Logger) - } + res := make([]string, 0, len(registry)) - registry["pg"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { - handlerOpts := &pg.NewOpts{ - PostgreSQLURL: opts.PostgreSQLURL, - - L: opts.Logger, - Metrics: opts.Metrics, - StateProvider: opts.StateProvider, - - DisableFilterPushdown: opts.DisableFilterPushdown, - EnableSortPushdown: opts.EnableSortPushdown, - EnableCursors: opts.EnableCursors, + // double check registered names and return them in the right order + for _, h := range []string{"pg", "sqlite", "tigris", "hana"} { + if _, ok := registry[h]; !ok { + continue } - return pg.New(handlerOpts) + + res = append(res, h) } + + return res } diff --git a/internal/handlers/dummy/msg_create.go b/internal/handlers/registry/sqlite.go similarity index 53% rename from internal/handlers/dummy/msg_create.go rename to internal/handlers/registry/sqlite.go index eaf74e97e228..0cd4c4ed0893 100644 --- a/internal/handlers/dummy/msg_create.go +++ b/internal/handlers/registry/sqlite.go @@ -12,16 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package registry import ( - "context" - - "github.com/FerretDB/FerretDB/internal/util/must" - "github.com/FerretDB/FerretDB/internal/wire" + "github.com/FerretDB/FerretDB/internal/handlers" + "github.com/FerretDB/FerretDB/internal/handlers/sqlite" ) -// MsgCreate implements HandlerInterface. -func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) +// init registers "sqlite" handler. +func init() { + registry["sqlite"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { + opts.Logger.Warn("SQLite handler is in alpha. It is not supported yet.") + + handlerOpts := &sqlite.NewOpts{ + Dir: opts.SQLiteURI, + + L: opts.Logger, + Metrics: opts.Metrics, + StateProvider: opts.StateProvider, + } + + return sqlite.New(handlerOpts) + } } diff --git a/internal/handlers/registry/tigris.go b/internal/handlers/registry/tigris.go index 9903403d4d03..0e93a6394f26 100644 --- a/internal/handlers/registry/tigris.go +++ b/internal/handlers/registry/tigris.go @@ -24,6 +24,8 @@ import ( // init registers "tigris" handler for Tigris when "ferretdb_tigris" build tag is provided. func init() { registry["tigris"] = func(opts *NewHandlerOpts) (handlers.Interface, error) { + opts.Logger.Warn("Tigris handler is in beta.") + handlerOpts := &tigris.NewOpts{ AuthParams: tigris.AuthParams{ URL: opts.TigrisURL, @@ -38,6 +40,7 @@ func init() { DisableFilterPushdown: opts.DisableFilterPushdown, EnableCursors: opts.EnableCursors, } + return tigris.New(handlerOpts) } } diff --git a/internal/handlers/sjson/sjson.go b/internal/handlers/sjson/sjson.go index 67fbabd76b4f..36b793822d67 100644 --- a/internal/handlers/sjson/sjson.go +++ b/internal/handlers/sjson/sjson.go @@ -37,28 +37,28 @@ // // Composite types // -// Alias types package sjson package sjson schema JSON representation +// Alias types package sjson package sjson schema JSON representation // // object *types.Document *sjson.documentType {"t":"object", "$s": {"$k":[], "p":{}} JSON object -// array *types.Array *sjson.arrayType {"t":"array", "i": [, ]} JSON array +// array *types.Array *sjson.arrayType {"t":"array", "i": [, ]} JSON array // // Scalar types // -// Alias types package sjson package sjson schema JSON representation +// Alias types package sjson package sjson schema JSON representation // -// double float64 *sjson.doubleType {"t":"double"} JSON number -// string string *sjson.stringType {"t":"string"} JSON string -// binData types.Binary *sjson.binaryType {"t":"binData", -// "s":} "" -// objectId types.ObjectID *sjson.objectIDType {"t":"objectId"} "" -// bool bool *sjson.boolType {"t":"bool"} JSON true / false values -// date time.Time *sjson.dateTimeType {"t":"date"} milliseconds since epoch as JSON number -// null types.NullType *sjson.nullType {"t":"null"} JSON null -// regex types.Regex *sjson.regexType {"t":"regex", -// "o": ""} "" -// int int32 *sjson.int32Type {"t":"int"} JSON number -// timestamp types.Timestamp *sjson.timestampType {"t":"timestamp"} JSON number -// long int64 *sjson.int64Type {"t":"long"} JSON number +// double float64 *sjson.doubleType {"t":"double"} JSON number +// string string *sjson.stringType {"t":"string"} JSON string +// binData types.Binary *sjson.binaryType {"t":"binData", +// "s":} "" +// objectId types.ObjectID *sjson.objectIDType {"t":"objectId"} "" +// bool bool *sjson.boolType {"t":"bool"} JSON true / false values +// date time.Time *sjson.dateTimeType {"t":"date"} milliseconds since epoch as JSON number +// null types.NullType *sjson.nullType {"t":"null"} JSON null +// regex types.Regex *sjson.regexType {"t":"regex", +// "o": ""} "" +// int int32 *sjson.int32Type {"t":"int"} JSON number +// timestamp types.Timestamp *sjson.timestampType {"t":"timestamp"} JSON number +// long int64 *sjson.int64Type {"t":"long"} JSON number // //nolint:lll // for readability //nolint:dupword // false positive diff --git a/internal/handlers/sqlite/cmd_query.go b/internal/handlers/sqlite/cmd_query.go new file mode 100644 index 000000000000..f64c04b615b5 --- /dev/null +++ b/internal/handlers/sqlite/cmd_query.go @@ -0,0 +1,63 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "fmt" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// CmdQuery implements HandlerInterface. +func (h *Handler) CmdQuery(ctx context.Context, query *wire.OpQuery) (*wire.OpReply, error) { + if query.FullCollectionName == "admin.$cmd" { + switch cmd := query.Query.Command(); cmd { + case "ismaster", "isMaster": // both are valid + reply := &wire.OpReply{ + NumberReturned: 1, + Documents: []*types.Document{must.NotFail(types.NewDocument( + "ismaster", true, // only lowercase + // topologyVersion + "maxBsonObjectSize", int32(types.MaxDocumentLen), + "maxMessageSizeBytes", int32(wire.MaxMsgLen), + "maxWriteBatchSize", int32(100000), + "localTime", time.Now(), + // logicalSessionTimeoutMinutes + "connectionId", int32(42), + "minWireVersion", common.MinWireVersion, + "maxWireVersion", common.MaxWireVersion, + "readOnly", false, + "ok", float64(1), + ))}, + } + + return reply, nil + + default: + msg := fmt.Sprintf("CmdQuery: unhandled command %q", cmd) + return nil, commonerrors.NewCommandErrorMsg(commonerrors.ErrNotImplemented, msg) + } + } + + msg := fmt.Sprintf("CmdQuery: unhandled collection %q", query.FullCollectionName) + + return nil, commonerrors.NewCommandErrorMsg(commonerrors.ErrNotImplemented, msg) +} diff --git a/internal/handlers/dummy/msg_aggregate.go b/internal/handlers/sqlite/msg_aggregate.go similarity index 98% rename from internal/handlers/dummy/msg_aggregate.go rename to internal/handlers/sqlite/msg_aggregate.go index 914c4e61eb5d..3b88b7665a1e 100644 --- a/internal/handlers/dummy/msg_aggregate.go +++ b/internal/handlers/sqlite/msg_aggregate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_buildinfo.go b/internal/handlers/sqlite/msg_buildinfo.go similarity index 86% rename from internal/handlers/dummy/msg_buildinfo.go rename to internal/handlers/sqlite/msg_buildinfo.go index 4222588e6af0..ce6ecb751a6c 100644 --- a/internal/handlers/dummy/msg_buildinfo.go +++ b/internal/handlers/sqlite/msg_buildinfo.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgBuildInfo implements HandlerInterface. func (h *Handler) MsgBuildInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgBuildInfo(ctx, msg) + return commoncommands.MsgBuildInfo(ctx, msg) } diff --git a/internal/handlers/dummy/msg_collmod.go b/internal/handlers/sqlite/msg_collmod.go similarity index 98% rename from internal/handlers/dummy/msg_collmod.go rename to internal/handlers/sqlite/msg_collmod.go index 6918e9053b71..d41eb56e4fc0 100644 --- a/internal/handlers/dummy/msg_collmod.go +++ b/internal/handlers/sqlite/msg_collmod.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_collstats.go b/internal/handlers/sqlite/msg_collstats.go similarity index 98% rename from internal/handlers/dummy/msg_collstats.go rename to internal/handlers/sqlite/msg_collstats.go index ded3e831bdf7..33739d0bb074 100644 --- a/internal/handlers/dummy/msg_collstats.go +++ b/internal/handlers/sqlite/msg_collstats.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_connectionstatus.go b/internal/handlers/sqlite/msg_connectionstatus.go similarity index 85% rename from internal/handlers/dummy/msg_connectionstatus.go rename to internal/handlers/sqlite/msg_connectionstatus.go index 3d5f3b802458..af0342157f8d 100644 --- a/internal/handlers/dummy/msg_connectionstatus.go +++ b/internal/handlers/sqlite/msg_connectionstatus.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgConnectionStatus implements HandlerInterface. func (h *Handler) MsgConnectionStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgConnectionStatus(ctx, msg) + return commoncommands.MsgConnectionStatus(ctx, msg) } diff --git a/internal/handlers/dummy/msg_count.go b/internal/handlers/sqlite/msg_count.go similarity index 98% rename from internal/handlers/dummy/msg_count.go rename to internal/handlers/sqlite/msg_count.go index 7f76a3858493..505eb939e132 100644 --- a/internal/handlers/dummy/msg_count.go +++ b/internal/handlers/sqlite/msg_count.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/sqlite/msg_create.go b/internal/handlers/sqlite/msg_create.go new file mode 100644 index 000000000000..a7c509842d78 --- /dev/null +++ b/internal/handlers/sqlite/msg_create.go @@ -0,0 +1,110 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "fmt" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgCreate implements HandlerInterface. +func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + unimplementedFields := []string{ + "timeseries", + "expireAfterSeconds", + "size", + "max", + "validator", + "validationLevel", + "validationAction", + "viewOn", + "pipeline", + "collation", + } + if err = common.Unimplemented(document, unimplementedFields...); err != nil { + return nil, err + } + + if err = common.UnimplementedNonDefault(document, "capped", func(v any) bool { + b, ok := v.(bool) + return ok && !b + }); err != nil { + return nil, err + } + + ignoredFields := []string{ + "autoIndexId", + "storageEngine", + "indexOptionDefaults", + "writeConcern", + "comment", + } + common.Ignored(document, h.L, ignoredFields...) + + command := document.Command() + + dbName, err := common.GetRequiredParam[string](document, "$db") + if err != nil { + return nil, err + } + + collectionName, err := common.GetRequiredParam[string](document, command) + if err != nil { + return nil, err + } + + db := h.b.Database(dbName) + defer db.Close() + + err = db.CreateCollection(ctx, &backends.CreateCollectionParams{ + Name: collectionName, + }) + + switch { + case err == nil: + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "ok", float64(1), + ))}, + })) + + return &reply, nil + + case backends.ErrorCodeIs(err, backends.ErrorCodeCollectionAlreadyExists): + msg := fmt.Sprintf("Collection %s.%s already exists.", dbName, collectionName) + return nil, commonerrors.NewCommandErrorMsg(commonerrors.ErrNamespaceExists, msg) + + case backends.ErrorCodeIs(err, backends.ErrorCodeCollectionNameIsInvalid): + msg := fmt.Sprintf("Invalid namespace: %s.%s", dbName, collectionName) + return nil, commonerrors.NewCommandErrorMsg(commonerrors.ErrInvalidNamespace, msg) + + default: + return nil, lazyerrors.Error(err) + } +} diff --git a/internal/handlers/dummy/msg_createindexes.go b/internal/handlers/sqlite/msg_createindexes.go similarity index 98% rename from internal/handlers/dummy/msg_createindexes.go rename to internal/handlers/sqlite/msg_createindexes.go index c8c3f9e90695..6568182c9736 100644 --- a/internal/handlers/dummy/msg_createindexes.go +++ b/internal/handlers/sqlite/msg_createindexes.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_currentop.go b/internal/handlers/sqlite/msg_currentop.go similarity index 86% rename from internal/handlers/dummy/msg_currentop.go rename to internal/handlers/sqlite/msg_currentop.go index 99f45e7ce797..84fa65a1ca50 100644 --- a/internal/handlers/dummy/msg_currentop.go +++ b/internal/handlers/sqlite/msg_currentop.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgCurrentOp implements HandlerInterface. func (h *Handler) MsgCurrentOp(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgCurrentOp(ctx, msg) + return commoncommands.MsgCurrentOp(ctx, msg) } diff --git a/internal/handlers/dummy/msg_datasize.go b/internal/handlers/sqlite/msg_datasize.go similarity index 98% rename from internal/handlers/dummy/msg_datasize.go rename to internal/handlers/sqlite/msg_datasize.go index 55cbfb0beca6..6719ad090311 100644 --- a/internal/handlers/dummy/msg_datasize.go +++ b/internal/handlers/sqlite/msg_datasize.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_dbstats.go b/internal/handlers/sqlite/msg_dbstats.go similarity index 98% rename from internal/handlers/dummy/msg_dbstats.go rename to internal/handlers/sqlite/msg_dbstats.go index 913751e5c1da..e262026b2a5a 100644 --- a/internal/handlers/dummy/msg_dbstats.go +++ b/internal/handlers/sqlite/msg_dbstats.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_debugerror.go b/internal/handlers/sqlite/msg_debugerror.go similarity index 86% rename from internal/handlers/dummy/msg_debugerror.go rename to internal/handlers/sqlite/msg_debugerror.go index 6bd59937ae95..35aa0938eb25 100644 --- a/internal/handlers/dummy/msg_debugerror.go +++ b/internal/handlers/sqlite/msg_debugerror.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgDebugError implements HandlerInterface. func (h *Handler) MsgDebugError(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgDebugError(ctx, msg) + return commoncommands.MsgDebugError(ctx, msg) } diff --git a/internal/handlers/sqlite/msg_delete.go b/internal/handlers/sqlite/msg_delete.go new file mode 100644 index 000000000000..2662740a4417 --- /dev/null +++ b/internal/handlers/sqlite/msg_delete.go @@ -0,0 +1,84 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgDelete implements HandlerInterface. +func (h *Handler) MsgDelete(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + params, err := common.GetDeleteParams(document, h.L) + if err != nil { + return nil, lazyerrors.Error(err) + } + + var deleted int64 + var delErrors commonerrors.WriteErrors + + db := h.b.Database(params.DB) + defer db.Close() + + coll := db.Collection(params.Collection) + + // process every delete filter + for i, deleteParams := range params.Deletes { + res, err := coll.Delete(ctx, &backends.DeleteParams{ + Filter: deleteParams.Filter, + Limited: deleteParams.Limited, + }) + + if err == nil { + deleted += res.Deleted + continue + } + + delErrors.Append(err, int32(i)) + + if params.Ordered { + break + } + } + + replyDoc := must.NotFail(types.NewDocument( + "ok", float64(1), + )) + + if delErrors.Len() > 0 { + replyDoc = delErrors.Document() + } + + replyDoc.Set("n", deleted) + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{replyDoc}, + })) + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_distinct.go b/internal/handlers/sqlite/msg_distinct.go similarity index 98% rename from internal/handlers/dummy/msg_distinct.go rename to internal/handlers/sqlite/msg_distinct.go index 3228aedf557f..903ca0e2ef2c 100644 --- a/internal/handlers/dummy/msg_distinct.go +++ b/internal/handlers/sqlite/msg_distinct.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/sqlite/msg_drop.go b/internal/handlers/sqlite/msg_drop.go new file mode 100644 index 000000000000..a6e68e8340dd --- /dev/null +++ b/internal/handlers/sqlite/msg_drop.go @@ -0,0 +1,76 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgDrop implements HandlerInterface. +func (h *Handler) MsgDrop(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + common.Ignored(document, h.L, "writeConcern", "comment") + + command := document.Command() + + dbName, err := common.GetRequiredParam[string](document, "$db") + if err != nil { + return nil, err + } + + collectionName, err := common.GetRequiredParam[string](document, command) + if err != nil { + return nil, err + } + + db := h.b.Database(dbName) + defer db.Close() + + err = db.DropCollection(ctx, &backends.DropCollectionParams{ + Name: collectionName, + }) + + switch { + case err == nil: + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "nIndexesWas", int32(1), // TODO + "ns", dbName+"."+collectionName, + "ok", float64(1), + ))}, + })) + + return &reply, nil + + case backends.ErrorCodeIs(err, backends.ErrorCodeCollectionDoesNotExist): + return nil, commonerrors.NewCommandErrorMsg(commonerrors.ErrNamespaceNotFound, "ns not found") + + default: + return nil, lazyerrors.Error(err) + } +} diff --git a/internal/handlers/sqlite/msg_dropdatabase.go b/internal/handlers/sqlite/msg_dropdatabase.go new file mode 100644 index 000000000000..ae3cc22cc67e --- /dev/null +++ b/internal/handlers/sqlite/msg_dropdatabase.go @@ -0,0 +1,65 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgDropDatabase implements HandlerInterface. +func (h *Handler) MsgDropDatabase(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + common.Ignored(document, h.L, "writeConcern", "comment") + + dbName, err := common.GetRequiredParam[string](document, "$db") + if err != nil { + return nil, err + } + + err = h.b.DropDatabase(ctx, &backends.DropDatabaseParams{ + Name: dbName, + }) + + res := must.NotFail(types.NewDocument()) + + switch { + case err == nil: + res.Set("dropped", dbName) + case backends.ErrorCodeIs(err, backends.ErrorCodeDatabaseDoesNotExist): + // nothing + default: + return nil, lazyerrors.Error(err) + } + + res.Set("ok", float64(1)) + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{res}, + })) + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_dropindexes.go b/internal/handlers/sqlite/msg_dropindexes.go similarity index 98% rename from internal/handlers/dummy/msg_dropindexes.go rename to internal/handlers/sqlite/msg_dropindexes.go index 469c25e202b4..bd0ad90defaa 100644 --- a/internal/handlers/dummy/msg_dropindexes.go +++ b/internal/handlers/sqlite/msg_dropindexes.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_explain.go b/internal/handlers/sqlite/msg_explain.go similarity index 98% rename from internal/handlers/dummy/msg_explain.go rename to internal/handlers/sqlite/msg_explain.go index da91a7982969..44def4c6f160 100644 --- a/internal/handlers/dummy/msg_explain.go +++ b/internal/handlers/sqlite/msg_explain.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/sqlite/msg_find.go b/internal/handlers/sqlite/msg_find.go new file mode 100644 index 000000000000..45c512281488 --- /dev/null +++ b/internal/handlers/sqlite/msg_find.go @@ -0,0 +1,131 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "errors" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgFind implements HandlerInterface. +func (h *Handler) MsgFind(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + params, err := common.GetFindParams(document, h.L) + if err != nil { + return nil, err + } + + if params.MaxTimeMS != 0 { + ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Duration(params.MaxTimeMS)*time.Millisecond) + defer cancel() + + ctx = ctxWithTimeout + } + + if params.BatchSize == 0 { + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "cursor", must.NotFail(types.NewDocument( + "firstBatch", types.MakeArray(0), + "id", int64(0), + "ns", params.DB+"."+params.Collection, + )), + "ok", float64(1), + ))}, + })) + + return &reply, nil + } + + db := h.b.Database(params.DB) + defer db.Close() + + res, err := db.Collection(params.Collection).Query(ctx, nil) + if err != nil { + return nil, lazyerrors.Error(err) + } + + iter := res.DocsIterator + + closer := iterator.NewMultiCloser(iter) + defer closer.Close() + + iter = common.FilterIterator(iter, closer, params.Filter) + + iter, err = common.SortIterator(iter, closer, params.Sort) + if err != nil { + var pathErr *types.DocumentPathError + if errors.As(err, &pathErr) && pathErr.Code() == types.ErrDocumentPathEmptyKey { + return nil, commonerrors.NewCommandErrorMsgWithArgument( + commonerrors.ErrPathContainsEmptyElement, + "Empty field names in path are not allowed", + document.Command(), + ) + } + + return nil, lazyerrors.Error(err) + } + + iter = common.SkipIterator(iter, closer, params.Skip) + + iter = common.LimitIterator(iter, closer, params.Limit) + + iter, err = common.ProjectionIterator(iter, closer, params.Projection) + if err != nil { + return nil, lazyerrors.Error(err) + } + + var resDocs []*types.Document + + resDocs, err = iterator.ConsumeValues(iterator.Interface[struct{}, *types.Document](iter)) + if err != nil { + return nil, lazyerrors.Error(err) + } + + var cursorID int64 + + firstBatch := types.MakeArray(len(resDocs)) + for _, doc := range resDocs { + firstBatch.Append(doc) + } + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "cursor", must.NotFail(types.NewDocument( + "firstBatch", firstBatch, + "id", cursorID, + "ns", params.DB+"."+params.Collection, + )), + "ok", float64(1), + ))}, + })) + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_findandmodify.go b/internal/handlers/sqlite/msg_findandmodify.go similarity index 98% rename from internal/handlers/dummy/msg_findandmodify.go rename to internal/handlers/sqlite/msg_findandmodify.go index d1fb9fccb8d5..41a640435de0 100644 --- a/internal/handlers/dummy/msg_findandmodify.go +++ b/internal/handlers/sqlite/msg_findandmodify.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_getcmdlineopts.go b/internal/handlers/sqlite/msg_getcmdlineopts.go similarity index 86% rename from internal/handlers/dummy/msg_getcmdlineopts.go rename to internal/handlers/sqlite/msg_getcmdlineopts.go index 09fe7e558275..4f16531c6280 100644 --- a/internal/handlers/dummy/msg_getcmdlineopts.go +++ b/internal/handlers/sqlite/msg_getcmdlineopts.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgGetCmdLineOpts implements HandlerInterface. func (h *Handler) MsgGetCmdLineOpts(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgGetCmdLineOpts(ctx, msg) + return commoncommands.MsgGetCmdLineOpts(ctx, msg) } diff --git a/internal/handlers/dummy/msg_getfreemonitoringstatus.go b/internal/handlers/sqlite/msg_getfreemonitoringstatus.go similarity index 81% rename from internal/handlers/dummy/msg_getfreemonitoringstatus.go rename to internal/handlers/sqlite/msg_getfreemonitoringstatus.go index 2607e5c6fc7f..d53d3c2fd1b4 100644 --- a/internal/handlers/dummy/msg_getfreemonitoringstatus.go +++ b/internal/handlers/sqlite/msg_getfreemonitoringstatus.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" @@ -23,7 +23,5 @@ import ( // MsgGetFreeMonitoringStatus implements HandlerInterface. func (h *Handler) MsgGetFreeMonitoringStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - // The state field in this function results in panic, though here it's used inside the stub, - // which is not executed on runtime. - return common.GetFreeMonitoringStatus(ctx, msg, nil) + return common.GetFreeMonitoringStatus(ctx, msg, h.StateProvider.Get()) } diff --git a/internal/handlers/dummy/msg_getlog.go b/internal/handlers/sqlite/msg_getlog.go similarity index 98% rename from internal/handlers/dummy/msg_getlog.go rename to internal/handlers/sqlite/msg_getlog.go index fce188dca95e..b5854cca1533 100644 --- a/internal/handlers/dummy/msg_getlog.go +++ b/internal/handlers/sqlite/msg_getlog.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_getmore.go b/internal/handlers/sqlite/msg_getmore.go similarity index 98% rename from internal/handlers/dummy/msg_getmore.go rename to internal/handlers/sqlite/msg_getmore.go index 61be0cd38ea4..10507019c446 100644 --- a/internal/handlers/dummy/msg_getmore.go +++ b/internal/handlers/sqlite/msg_getmore.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_getparameter.go b/internal/handlers/sqlite/msg_getparameter.go similarity index 98% rename from internal/handlers/dummy/msg_getparameter.go rename to internal/handlers/sqlite/msg_getparameter.go index 67c38243b3ea..5ee07c131792 100644 --- a/internal/handlers/dummy/msg_getparameter.go +++ b/internal/handlers/sqlite/msg_getparameter.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/sqlite/msg_hello.go b/internal/handlers/sqlite/msg_hello.go new file mode 100644 index 000000000000..cacd99836d72 --- /dev/null +++ b/internal/handlers/sqlite/msg_hello.go @@ -0,0 +1,48 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgHello implements HandlerInterface. +func (h *Handler) MsgHello(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "isWritablePrimary", true, + // topologyVersion + "maxBsonObjectSize", int32(types.MaxDocumentLen), + "maxMessageSizeBytes", int32(wire.MaxMsgLen), + "maxWriteBatchSize", int32(100000), + "localTime", time.Now(), + // logicalSessionTimeoutMinutes + "connectionId", int32(42), + "minWireVersion", common.MinWireVersion, + "maxWireVersion", common.MaxWireVersion, + "readOnly", false, + "ok", float64(1), + ))}, + })) + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_hostinfo.go b/internal/handlers/sqlite/msg_hostinfo.go similarity index 86% rename from internal/handlers/dummy/msg_hostinfo.go rename to internal/handlers/sqlite/msg_hostinfo.go index 8448f04cf1e2..4988c3d65e56 100644 --- a/internal/handlers/dummy/msg_hostinfo.go +++ b/internal/handlers/sqlite/msg_hostinfo.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgHostInfo implements HandlerInterface. func (h *Handler) MsgHostInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgHostInfo(ctx, msg) + return commoncommands.MsgHostInfo(ctx, msg) } diff --git a/internal/handlers/sqlite/msg_insert.go b/internal/handlers/sqlite/msg_insert.go new file mode 100644 index 000000000000..932d0b5f3dbe --- /dev/null +++ b/internal/handlers/sqlite/msg_insert.go @@ -0,0 +1,73 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgInsert implements HandlerInterface. +func (h *Handler) MsgInsert(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + params, err := common.GetInsertParams(document, h.L) + if err != nil { + return nil, err + } + + db := h.b.Database(params.DB) + defer db.Close() + + res, err := db.Collection(params.Collection).Insert(ctx, &backends.InsertParams{ + Ordered: params.Ordered, + Docs: params.Docs, + }) + if err != nil { + return nil, lazyerrors.Error(err) + } + + replyDoc := must.NotFail(types.NewDocument( + "n", int32(res.InsertedCount), + "ok", float64(1), + )) + + if len(res.Errors) > 0 { + var errs *commonerrors.WriteErrors + + for i := 0; i < len(res.Errors); i++ { + errs.Append(err, int32(i)) + } + + replyDoc = errs.Document() + } + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{replyDoc}, + })) + + return &reply, nil +} diff --git a/internal/handlers/sqlite/msg_ismaster.go b/internal/handlers/sqlite/msg_ismaster.go new file mode 100644 index 000000000000..9994553c983e --- /dev/null +++ b/internal/handlers/sqlite/msg_ismaster.go @@ -0,0 +1,48 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "time" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgIsMaster implements HandlerInterface. +func (h *Handler) MsgIsMaster(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "ismaster", true, // only lowercase + // topologyVersion + "maxBsonObjectSize", int32(types.MaxDocumentLen), + "maxMessageSizeBytes", int32(wire.MaxMsgLen), + "maxWriteBatchSize", int32(100000), + "localTime", time.Now(), + // logicalSessionTimeoutMinutes + "connectionId", int32(42), + "minWireVersion", common.MinWireVersion, + "maxWireVersion", common.MaxWireVersion, + "readOnly", false, + "ok", float64(1), + ))}, + })) + + return &reply, nil +} diff --git a/internal/handlers/sqlite/msg_listcollections.go b/internal/handlers/sqlite/msg_listcollections.go new file mode 100644 index 000000000000..f82f9e9dc8a9 --- /dev/null +++ b/internal/handlers/sqlite/msg_listcollections.go @@ -0,0 +1,103 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgListCollections implements HandlerInterface. +func (h *Handler) MsgListCollections(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + var filter *types.Document + if filter, err = common.GetOptionalParam(document, "filter", filter); err != nil { + return nil, err + } + + common.Ignored(document, h.L, "comment", "authorizedCollections") + + dbName, err := common.GetRequiredParam[string](document, "$db") + if err != nil { + return nil, err + } + + var nameOnly bool + + if v, _ := document.Get("nameOnly"); v != nil { + if nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v); err != nil { + return nil, err + } + } + + db := h.b.Database(dbName) + defer db.Close() + + res, err := db.ListCollections(ctx, nil) + if err != nil { + return nil, lazyerrors.Error(err) + } + + collections := types.MakeArray(len(res.Collections)) + + for _, collection := range res.Collections { + d := must.NotFail(types.NewDocument( + "name", collection.Name, + "type", "collection", + )) + + matches, err := common.FilterDocument(d, filter) + if err != nil { + return nil, lazyerrors.Error(err) + } + + if !matches { + continue + } + + if nameOnly { + d = must.NotFail(types.NewDocument( + "name", collection.Name, + )) + } + + collections.Append(d) + } + + var reply wire.OpMsg + + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "cursor", must.NotFail(types.NewDocument( + "id", int64(0), + "ns", dbName+".$cmd.listCollections", + "firstBatch", collections, + )), + "ok", float64(1), + ))}, + })) + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_listcommands.go b/internal/handlers/sqlite/msg_listcommands.go similarity index 86% rename from internal/handlers/dummy/msg_listcommands.go rename to internal/handlers/sqlite/msg_listcommands.go index 0d6ea7fce610..d099907d607f 100644 --- a/internal/handlers/dummy/msg_listcommands.go +++ b/internal/handlers/sqlite/msg_listcommands.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgListCommands implements handlers.Interface. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgListCommands(ctx, msg) + return commoncommands.MsgListCommands(ctx, msg) } diff --git a/internal/handlers/sqlite/msg_listdatabases.go b/internal/handlers/sqlite/msg_listdatabases.go new file mode 100644 index 000000000000..a0cb14baba88 --- /dev/null +++ b/internal/handlers/sqlite/msg_listdatabases.go @@ -0,0 +1,108 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgListDatabases implements HandlerInterface. +func (h *Handler) MsgListDatabases(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + var filter *types.Document + if filter, err = common.GetOptionalParam(document, "filter", filter); err != nil { + return nil, err + } + + common.Ignored(document, h.L, "comment", "authorizedDatabases") + + var nameOnly bool + + if v, _ := document.Get("nameOnly"); v != nil { + if nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v); err != nil { + return nil, err + } + } + + res, err := h.b.ListDatabases(ctx, nil) + if err != nil { + return nil, lazyerrors.Error(err) + } + + var totalSize int64 + + databases := types.MakeArray(len(res.Databases)) + + for _, db := range res.Databases { + d := must.NotFail(types.NewDocument( + "name", db.Name, + "sizeOnDisk", db.Size, + "empty", db.Size == 0, + )) + + totalSize += db.Size + + matches, err := common.FilterDocument(d, filter) + if err != nil { + return nil, lazyerrors.Error(err) + } + + if !matches { + continue + } + + if nameOnly { + d = must.NotFail(types.NewDocument( + "name", db.Name, + )) + } + + databases.Append(d) + } + + var reply wire.OpMsg + + switch { + case nameOnly: + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "databases", databases, + "ok", float64(1), + ))}, + })) + default: + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "databases", databases, + "totalSize", totalSize, + "totalSizeMb", totalSize/1024/1024, + "ok", float64(1), + ))}, + })) + } + + return &reply, nil +} diff --git a/internal/handlers/dummy/msg_listindexes.go b/internal/handlers/sqlite/msg_listindexes.go similarity index 98% rename from internal/handlers/dummy/msg_listindexes.go rename to internal/handlers/sqlite/msg_listindexes.go index 02d51cdf440d..c69650721645 100644 --- a/internal/handlers/dummy/msg_listindexes.go +++ b/internal/handlers/sqlite/msg_listindexes.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_ping.go b/internal/handlers/sqlite/msg_ping.go similarity index 76% rename from internal/handlers/dummy/msg_ping.go rename to internal/handlers/sqlite/msg_ping.go index 0148646288a7..8a79f5975a91 100644 --- a/internal/handlers/dummy/msg_ping.go +++ b/internal/handlers/sqlite/msg_ping.go @@ -12,16 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" + "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgPing implements HandlerInterface. func (h *Handler) MsgPing(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{must.NotFail(types.NewDocument( + "ok", float64(1), + ))}, + })) + + return &reply, nil } diff --git a/internal/handlers/dummy/msg_renamecollection.go b/internal/handlers/sqlite/msg_renamecollection.go similarity index 98% rename from internal/handlers/dummy/msg_renamecollection.go rename to internal/handlers/sqlite/msg_renamecollection.go index e161e3912d31..3a4fba8506b1 100644 --- a/internal/handlers/dummy/msg_renamecollection.go +++ b/internal/handlers/sqlite/msg_renamecollection.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_saslstart.go b/internal/handlers/sqlite/msg_saslstart.go similarity index 98% rename from internal/handlers/dummy/msg_saslstart.go rename to internal/handlers/sqlite/msg_saslstart.go index ed36ae065888..c8eb164c1c00 100644 --- a/internal/handlers/dummy/msg_saslstart.go +++ b/internal/handlers/sqlite/msg_saslstart.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_serverstatus.go b/internal/handlers/sqlite/msg_serverstatus.go similarity index 55% rename from internal/handlers/dummy/msg_serverstatus.go rename to internal/handlers/sqlite/msg_serverstatus.go index 7da7c1d2c5cb..b5f2ebdfde3a 100644 --- a/internal/handlers/dummy/msg_serverstatus.go +++ b/internal/handlers/sqlite/msg_serverstatus.go @@ -12,16 +12,39 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgServerStatus implements HandlerInterface. func (h *Handler) MsgServerStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return nil, notImplemented(must.NotFail(msg.Document()).Command()) + res, err := common.ServerStatus(h.StateProvider.Get(), h.Metrics) + if err != nil { + return nil, lazyerrors.Error(err) + } + + res.Set("catalogStats", must.NotFail(types.NewDocument( + "collections", int32(0), // TODO + "capped", int32(0), + "clustered", int32(0), + "timeseries", int32(0), + "views", int32(0), + "internalCollections", int32(0), + "internalViews", int32(0), + ))) + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{res}, + })) + + return &reply, nil } diff --git a/internal/handlers/dummy/msg_setfreemonitoring.go b/internal/handlers/sqlite/msg_setfreemonitoring.go similarity index 81% rename from internal/handlers/dummy/msg_setfreemonitoring.go rename to internal/handlers/sqlite/msg_setfreemonitoring.go index 1ceb21f6b098..6e9e97c95996 100644 --- a/internal/handlers/dummy/msg_setfreemonitoring.go +++ b/internal/handlers/sqlite/msg_setfreemonitoring.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" @@ -23,7 +23,5 @@ import ( // MsgSetFreeMonitoring implements HandlerInterface. func (h *Handler) MsgSetFreeMonitoring(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - // The state field in this function results in panic, though here it's used inside the stub, - // which is not executed on runtime. - return common.SetFreeMonitoring(ctx, msg, nil) + return common.SetFreeMonitoring(ctx, msg, h.StateProvider) } diff --git a/internal/handlers/sqlite/msg_update.go b/internal/handlers/sqlite/msg_update.go new file mode 100644 index 000000000000..78198fc367f5 --- /dev/null +++ b/internal/handlers/sqlite/msg_update.go @@ -0,0 +1,179 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite + +import ( + "context" + "errors" + + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" + "github.com/FerretDB/FerretDB/internal/wire" +) + +// MsgUpdate implements HandlerInterface. +func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { + document, err := msg.Document() + if err != nil { + return nil, lazyerrors.Error(err) + } + + params, err := common.GetUpdateParams(document, h.L) + if err != nil { + return nil, lazyerrors.Error(err) + } + + matched, modified, upserted, err := h.updateDocument(ctx, params) + if err != nil { + return nil, lazyerrors.Error(err) + } + + res := must.NotFail(types.NewDocument( + "n", matched, + )) + + if upserted.Len() != 0 { + res.Set("upserted", &upserted) + } + + res.Set("nModified", modified) + res.Set("ok", float64(1)) + + var reply wire.OpMsg + must.NoError(reply.SetSections(wire.OpMsgSection{ + Documents: []*types.Document{res}, + })) + + return &reply, nil +} + +// updateDocument iterate through all documents in collection and update them. +func (h *Handler) updateDocument(ctx context.Context, params *common.UpdatesParams) (int32, int32, *types.Array, error) { + var matched, modified int32 + var upserted *types.Array + + db := h.b.Database(params.DB) + defer db.Close() + + err := db.CreateCollection(ctx, &backends.CreateCollectionParams{Name: params.Collection}) + if err != nil { + return 0, 0, nil, err + } + + for _, u := range params.Updates { + res, err := db.Collection(params.Collection).Query(ctx, nil) + if err != nil { + return 0, 0, nil, err + } + + var resDocs []*types.Document + + defer res.DocsIterator.Close() + + for { + var doc *types.Document + + _, doc, err = res.DocsIterator.Next() + if err != nil { + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + return 0, 0, nil, err + } + + var matches bool + + matches, err = common.FilterDocument(doc, u.Filter) + if err != nil { + return 0, 0, nil, err + } + + if !matches { + continue + } + + resDocs = append(resDocs, doc) + } + + res.DocsIterator.Close() + + if len(resDocs) == 0 { + if !u.Upsert { + // nothing to do, continue to the next update operation + continue + } + + doc := u.Filter.DeepCopy() + if _, err = common.UpdateDocument("update", doc, u.Update); err != nil { + return 0, 0, nil, err + } + + if !doc.Has("_id") { + doc.Set("_id", types.NewObjectID()) + } + + upserted.Append(must.NotFail(types.NewDocument( + "index", int32(0), // TODO + "_id", must.NotFail(doc.Get("_id")), + ))) + + // TODO https://github.com/FerretDB/FerretDB/issues/2612 + + _, err := db.Collection(params.Collection).Insert(ctx, &backends.InsertParams{ + Docs: must.NotFail(types.NewArray(doc)), + }) + if err != nil { + return 0, 0, nil, err + } + + matched++ + + continue + } + + if len(resDocs) > 1 && !u.Multi { + resDocs = resDocs[:1] + } + + matched += int32(len(resDocs)) + + for _, doc := range resDocs { + changed, err := common.UpdateDocument("update", doc, u.Update) + if err != nil { + return 0, 0, nil, err + } + + if !changed { + continue + } + + updateRes, err := db. + Collection(params.Collection). + Update(ctx, &backends.UpdateParams{Docs: must.NotFail(types.NewArray(doc))}) + if err != nil { + return 0, 0, nil, err + } + + modified += int32(updateRes.Updated) + } + } + + return matched, modified, upserted, nil +} diff --git a/internal/handlers/dummy/msg_validate.go b/internal/handlers/sqlite/msg_validate.go similarity index 98% rename from internal/handlers/dummy/msg_validate.go rename to internal/handlers/sqlite/msg_validate.go index 77bdf109bf44..7a5e07f5aad0 100644 --- a/internal/handlers/dummy/msg_validate.go +++ b/internal/handlers/sqlite/msg_validate.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" diff --git a/internal/handlers/dummy/msg_whatsmyuri.go b/internal/handlers/sqlite/msg_whatsmyuri.go similarity index 86% rename from internal/handlers/dummy/msg_whatsmyuri.go rename to internal/handlers/sqlite/msg_whatsmyuri.go index 0ed585e25353..d53cb8b5047f 100644 --- a/internal/handlers/dummy/msg_whatsmyuri.go +++ b/internal/handlers/sqlite/msg_whatsmyuri.go @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgWhatsMyURI implements HandlerInterface. func (h *Handler) MsgWhatsMyURI(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgWhatsMyURI(ctx, msg) + return commoncommands.MsgWhatsMyURI(ctx, msg) } diff --git a/internal/handlers/dummy/dummy.go b/internal/handlers/sqlite/sqlite.go similarity index 59% rename from internal/handlers/dummy/dummy.go rename to internal/handlers/sqlite/sqlite.go index 288503139eb7..a0c6b9c86a7e 100644 --- a/internal/handlers/dummy/dummy.go +++ b/internal/handlers/sqlite/sqlite.go @@ -12,16 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package dummy provides a basic handler implementation. +// Package sqlite provides SQLite handler. // -// The whole package can be copied to start a new handler implementation. -package dummy +// It is being converted into universal handler for all backends. +package sqlite import ( "go.uber.org/zap" + "github.com/FerretDB/FerretDB/internal/backends" + "github.com/FerretDB/FerretDB/internal/backends/sqlite" + "github.com/FerretDB/FerretDB/internal/clientconn/connmetrics" "github.com/FerretDB/FerretDB/internal/handlers" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/util/state" ) // notImplemented returns error for stub command handlers. @@ -32,25 +36,35 @@ func notImplemented(command string) error { ) } -// Handler implements handlers.Interface by stubbing all methods except the following handler-independent commands: -// -// - buildInfo; -// - connectionStatus; -// - debugError; -// - getCmdLineOpts; -// - getFreeMonitoringStatus; -// - hostInfo; -// - listCommands; -// - setFreeMonitoringStatus; -// - whatsmyuri. +// Handler implements handlers.Interface. type Handler struct { - L *zap.Logger + *NewOpts + b backends.Backend +} + +// NewOpts represents handler configuration. +// +//nolint:vet // for readability +type NewOpts struct { + Dir string + + L *zap.Logger + Metrics *connmetrics.ConnMetrics + StateProvider *state.Provider } // New returns a new handler. -func New(l *zap.Logger) (handlers.Interface, error) { +func New(opts *NewOpts) (handlers.Interface, error) { + b, err := sqlite.NewBackend(&sqlite.NewBackendParams{ + Dir: opts.Dir, + }) + if err != nil { + return nil, err + } + return &Handler{ - L: l, + NewOpts: opts, + b: b, }, nil } diff --git a/internal/handlers/dummy/cmd_query.go b/internal/handlers/sqlite/sqlite_test.go similarity index 68% rename from internal/handlers/dummy/cmd_query.go rename to internal/handlers/sqlite/sqlite_test.go index ce71fb1a72a8..94c2c9d3af10 100644 --- a/internal/handlers/dummy/cmd_query.go +++ b/internal/handlers/sqlite/sqlite_test.go @@ -12,15 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dummy +package sqlite -import ( - "context" +import "testing" - "github.com/FerretDB/FerretDB/internal/wire" -) - -// CmdQuery implements HandlerInterface. -func (h *Handler) CmdQuery(ctx context.Context, query *wire.OpQuery) (*wire.OpReply, error) { - return nil, notImplemented(query.Query.Command()) +func TestDummy(t *testing.T) { + // we need at least one test per package to correctly calculate coverage } diff --git a/internal/handlers/tigris/cmd_query.go b/internal/handlers/tigris/cmd_query.go index af91a442c9d2..b7ee320c1043 100644 --- a/internal/handlers/tigris/cmd_query.go +++ b/internal/handlers/tigris/cmd_query.go @@ -41,7 +41,7 @@ func (h *Handler) CmdQuery(ctx context.Context, query *wire.OpQuery) (*wire.OpRe "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/tigris/msg_aggregate.go b/internal/handlers/tigris/msg_aggregate.go index f87870854ffe..69a5fd812d5a 100644 --- a/internal/handlers/tigris/msg_aggregate.go +++ b/internal/handlers/tigris/msg_aggregate.go @@ -269,18 +269,18 @@ func processStagesStats(ctx context.Context, p *stagesStatsParams) ([]*types.Doc doc.Set( "storageStats", must.NotFail(types.NewDocument( - "size", int32(dbStats.Size), + "size", dbStats.Size, "count", dbStats.NumObjects, "avgObjSize", avgObjSize, - "storageSize", int32(dbStats.Size), - "freeStorageSize", int32(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "storageSize", dbStats.Size, + "freeStorageSize", int64(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 "capped", false, // TODO https://github.com/FerretDB/FerretDB/issues/2342 "wiredTiger", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 - "nindexes", int32(0), // Not supported for Tigris + "nindexes", int64(0), // Not supported for Tigris "indexDetails", must.NotFail(types.NewDocument()), // Not supported for Tigris "indexBuilds", must.NotFail(types.NewDocument()), // Not supported for Tigris - "totalIndexSize", int32(0), // Not supported for Tigris - "totalSize", int32(dbStats.Size), + "totalIndexSize", int64(0), // Not supported for Tigris + "totalSize", dbStats.Size, "indexSizes", must.NotFail(types.NewDocument()), // Not supported for Tigris )), ) diff --git a/internal/handlers/tigris/msg_buildinfo.go b/internal/handlers/tigris/msg_buildinfo.go index fc5ac4c3fe28..b59fd1f1922c 100644 --- a/internal/handlers/tigris/msg_buildinfo.go +++ b/internal/handlers/tigris/msg_buildinfo.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgBuildInfo implements HandlerInterface. func (h *Handler) MsgBuildInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgBuildInfo(ctx, msg) + return commoncommands.MsgBuildInfo(ctx, msg) } diff --git a/internal/handlers/tigris/msg_collstats.go b/internal/handlers/tigris/msg_collstats.go index 1fba5acb566e..8d4acac3d598 100644 --- a/internal/handlers/tigris/msg_collstats.go +++ b/internal/handlers/tigris/msg_collstats.go @@ -67,7 +67,7 @@ func (h *Handler) MsgCollStats(ctx context.Context, msg *wire.OpMsg) (*wire.OpMs pairs := []any{ "ns", db + "." + collection, - "size", int32(stats.Size) / scale, + "size", stats.Size / int64(scale), "count", stats.NumObjects, } diff --git a/internal/handlers/tigris/msg_connectionstatus.go b/internal/handlers/tigris/msg_connectionstatus.go index cb2452d2895b..20faa8db4c58 100644 --- a/internal/handlers/tigris/msg_connectionstatus.go +++ b/internal/handlers/tigris/msg_connectionstatus.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgConnectionStatus implements HandlerInterface. func (h *Handler) MsgConnectionStatus(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgConnectionStatus(ctx, msg) + return commoncommands.MsgConnectionStatus(ctx, msg) } diff --git a/internal/handlers/tigris/msg_count.go b/internal/handlers/tigris/msg_count.go index 9a6ec14879e9..77efaf466f9b 100644 --- a/internal/handlers/tigris/msg_count.go +++ b/internal/handlers/tigris/msg_count.go @@ -37,13 +37,7 @@ func (h *Handler) MsgCount(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, e return nil, lazyerrors.Error(err) } - if err = common.Unimplemented(document, "collation"); err != nil { - return nil, err - } - - common.Ignored(document, h.L, "hint", "readConcern", "comment") - - params, err := common.GetCountParams(document) + params, err := common.GetCountParams(document, h.L) if err != nil { return nil, err } diff --git a/internal/handlers/tigris/msg_currentop.go b/internal/handlers/tigris/msg_currentop.go index e65584c63898..997b84a6ee91 100644 --- a/internal/handlers/tigris/msg_currentop.go +++ b/internal/handlers/tigris/msg_currentop.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgCurrentOp implements HandlerInterface. func (h *Handler) MsgCurrentOp(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgCurrentOp(ctx, msg) + return commoncommands.MsgCurrentOp(ctx, msg) } diff --git a/internal/handlers/tigris/msg_datasize.go b/internal/handlers/tigris/msg_datasize.go index f3bf20e70feb..b131861f6b4d 100644 --- a/internal/handlers/tigris/msg_datasize.go +++ b/internal/handlers/tigris/msg_datasize.go @@ -75,7 +75,7 @@ func (h *Handler) MsgDataSize(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg pairs = append(pairs, "estimate", false) } pairs = append(pairs, - "size", int32(stats.Size), + "size", stats.Size, "numObjects", stats.NumObjects, "millis", int32(elapses.Milliseconds()), "ok", float64(1), diff --git a/internal/handlers/tigris/msg_debugerror.go b/internal/handlers/tigris/msg_debugerror.go index 7b495789d14e..620d7eba4fcc 100644 --- a/internal/handlers/tigris/msg_debugerror.go +++ b/internal/handlers/tigris/msg_debugerror.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgDebugError implements HandlerInterface. func (h *Handler) MsgDebugError(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgDebugError(ctx, msg) + return commoncommands.MsgDebugError(ctx, msg) } diff --git a/internal/handlers/tigris/msg_explain.go b/internal/handlers/tigris/msg_explain.go index 5232e03300ee..d49f5268414f 100644 --- a/internal/handlers/tigris/msg_explain.go +++ b/internal/handlers/tigris/msg_explain.go @@ -77,6 +77,8 @@ func (h *Handler) MsgExplain(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, "host", hostname, "version", version.Get().MongoDBVersion, "gitVersion", version.Get().Commit, + + // our extensions "ferretdbVersion", version.Get().Version, )) diff --git a/internal/handlers/tigris/msg_findandmodify.go b/internal/handlers/tigris/msg_findandmodify.go index 914cf4722cb8..99bbfc285e45 100644 --- a/internal/handlers/tigris/msg_findandmodify.go +++ b/internal/handlers/tigris/msg_findandmodify.go @@ -122,7 +122,7 @@ func (h *Handler) MsgFindAndModify(ctx context.Context, msg *wire.OpMsg) (*wire. if params.HasUpdateOperators { upsert = resDocs[0].DeepCopy() - if _, err = common.UpdateDocument(upsert, params.Update); err != nil { + if _, err = common.UpdateDocument(document.Command(), upsert, params.Update); err != nil { return nil, err } diff --git a/internal/handlers/tigris/msg_getcmdlineopts.go b/internal/handlers/tigris/msg_getcmdlineopts.go index cef79bb29c5f..1451c8879278 100644 --- a/internal/handlers/tigris/msg_getcmdlineopts.go +++ b/internal/handlers/tigris/msg_getcmdlineopts.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgGetCmdLineOpts implements HandlerInterface. func (h *Handler) MsgGetCmdLineOpts(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgGetCmdLineOpts(ctx, msg) + return commoncommands.MsgGetCmdLineOpts(ctx, msg) } diff --git a/internal/handlers/tigris/msg_getlog.go b/internal/handlers/tigris/msg_getlog.go index 625a42f9c219..f1623b52d39b 100644 --- a/internal/handlers/tigris/msg_getlog.go +++ b/internal/handlers/tigris/msg_getlog.go @@ -24,8 +24,8 @@ import ( "go.uber.org/zap" "github.com/FerretDB/FerretDB/build/version" - "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/logging" @@ -64,7 +64,7 @@ func (h *Handler) MsgGetLog(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, commonerrors.ErrTypeMismatch, fmt.Sprintf( "BSON field 'getLog.getLog' is the wrong type '%s', expected type 'string'", - common.AliasFromType(getLog), + commonparams.AliasFromType(getLog), ), "getLog", ) diff --git a/internal/handlers/tigris/msg_hello.go b/internal/handlers/tigris/msg_hello.go index 5bde8060f8f8..283fe7327105 100644 --- a/internal/handlers/tigris/msg_hello.go +++ b/internal/handlers/tigris/msg_hello.go @@ -36,7 +36,7 @@ func (h *Handler) MsgHello(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, e "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/tigris/msg_hostinfo.go b/internal/handlers/tigris/msg_hostinfo.go index 31b50a7a311b..3e5a3b41c4d7 100644 --- a/internal/handlers/tigris/msg_hostinfo.go +++ b/internal/handlers/tigris/msg_hostinfo.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgHostInfo implements HandlerInterface. func (h *Handler) MsgHostInfo(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgHostInfo(ctx, msg) + return commoncommands.MsgHostInfo(ctx, msg) } diff --git a/internal/handlers/tigris/msg_ismaster.go b/internal/handlers/tigris/msg_ismaster.go index a8ffb489609c..62dfd87d36eb 100644 --- a/internal/handlers/tigris/msg_ismaster.go +++ b/internal/handlers/tigris/msg_ismaster.go @@ -36,7 +36,7 @@ func (h *Handler) MsgIsMaster(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg "maxWriteBatchSize", int32(100000), "localTime", time.Now(), // logicalSessionTimeoutMinutes - // connectionId + "connectionId", int32(42), "minWireVersion", common.MinWireVersion, "maxWireVersion", common.MaxWireVersion, "readOnly", false, diff --git a/internal/handlers/tigris/msg_listcollections.go b/internal/handlers/tigris/msg_listcollections.go index 30ba7fcdd8b4..790ce8d59e27 100644 --- a/internal/handlers/tigris/msg_listcollections.go +++ b/internal/handlers/tigris/msg_listcollections.go @@ -19,6 +19,7 @@ import ( "sort" "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" @@ -49,9 +50,12 @@ func (h *Handler) MsgListCollections(ctx context.Context, msg *wire.OpMsg) (*wir return nil, err } - nameOnly, err := common.GetBoolOptionalParam(document, "nameOnly") - if err != nil { - return nil, err + var nameOnly bool + if v, _ := document.Get("nameOnly"); v != nil { + nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v) + if err != nil { + return nil, err + } } names, err := dbPool.Driver.UseDatabase(db).ListCollections(ctx) diff --git a/internal/handlers/tigris/msg_listcommands.go b/internal/handlers/tigris/msg_listcommands.go index 6f8c6f5523a2..ab5ead4ab4f4 100644 --- a/internal/handlers/tigris/msg_listcommands.go +++ b/internal/handlers/tigris/msg_listcommands.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgListCommands implements handlers.Interface. func (h *Handler) MsgListCommands(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgListCommands(ctx, msg) + return commoncommands.MsgListCommands(ctx, msg) } diff --git a/internal/handlers/tigris/msg_listdatabases.go b/internal/handlers/tigris/msg_listdatabases.go index 6988785f5031..000e046adbad 100644 --- a/internal/handlers/tigris/msg_listdatabases.go +++ b/internal/handlers/tigris/msg_listdatabases.go @@ -18,6 +18,7 @@ import ( "context" "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/handlers/tigris/tigrisdb" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -49,9 +50,12 @@ func (h *Handler) MsgListDatabases(ctx context.Context, msg *wire.OpMsg) (*wire. return nil, err } - nameOnly, err := common.GetBoolOptionalParam(document, "nameOnly") - if err != nil { - return nil, err + var nameOnly bool + if v, _ := document.Get("nameOnly"); v != nil { + nameOnly, err = commonparams.GetBoolOptionalParam("nameOnly", v) + if err != nil { + return nil, err + } } var totalSize int64 diff --git a/internal/handlers/tigris/msg_listindexes.go b/internal/handlers/tigris/msg_listindexes.go index c37ab3c0755a..716ad034cdc7 100644 --- a/internal/handlers/tigris/msg_listindexes.go +++ b/internal/handlers/tigris/msg_listindexes.go @@ -20,6 +20,7 @@ import ( "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" @@ -56,7 +57,7 @@ func (h *Handler) MsgListIndexes(ctx context.Context, msg *wire.OpMsg) (*wire.Op if !ok { return nil, commonerrors.NewCommandErrorMsgWithArgument( commonerrors.ErrBadValue, - fmt.Sprintf("collection name has invalid type %s", common.AliasFromType(collectionParam)), + fmt.Sprintf("collection name has invalid type %s", commonparams.AliasFromType(collectionParam)), document.Command(), ) } diff --git a/internal/handlers/tigris/msg_update.go b/internal/handlers/tigris/msg_update.go index 67618cf8ba1e..b4da8cf8c76c 100644 --- a/internal/handlers/tigris/msg_update.go +++ b/internal/handlers/tigris/msg_update.go @@ -68,7 +68,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, } doc := u.Filter.DeepCopy() - if _, err = common.UpdateDocument(doc, u.Update); err != nil { + if _, err = common.UpdateDocument(document.Command(), doc, u.Update); err != nil { return nil, err } if !doc.Has("_id") { @@ -95,7 +95,7 @@ func (h *Handler) MsgUpdate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, matched += int32(len(resDocs)) for _, doc := range resDocs { - changed, err := common.UpdateDocument(doc, u.Update) + changed, err := common.UpdateDocument(document.Command(), doc, u.Update) if err != nil { return nil, err } diff --git a/internal/handlers/tigris/msg_whatsmyuri.go b/internal/handlers/tigris/msg_whatsmyuri.go index 56992c910887..e8187c96bddd 100644 --- a/internal/handlers/tigris/msg_whatsmyuri.go +++ b/internal/handlers/tigris/msg_whatsmyuri.go @@ -17,11 +17,11 @@ package tigris import ( "context" - "github.com/FerretDB/FerretDB/internal/handlers/common" + "github.com/FerretDB/FerretDB/internal/handlers/commoncommands" "github.com/FerretDB/FerretDB/internal/wire" ) // MsgWhatsMyURI implements HandlerInterface. func (h *Handler) MsgWhatsMyURI(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, error) { - return common.MsgWhatsMyURI(ctx, msg) + return commoncommands.MsgWhatsMyURI(ctx, msg) } diff --git a/internal/handlers/tigris/tigrisdb/query_iterator.go b/internal/handlers/tigris/tigrisdb/query_iterator.go index 602d6eec5059..047185884fa8 100644 --- a/internal/handlers/tigris/tigrisdb/query_iterator.go +++ b/internal/handlers/tigris/tigrisdb/query_iterator.go @@ -50,7 +50,6 @@ func newQueryIterator(ctx context.Context, titer driver.Iterator, schema *tjson. iter: titer, token: resource.NewToken(), } - resource.Track(iter, iter.token) return iter diff --git a/internal/types/array_iterator.go b/internal/types/array_iterator.go index 404027d1233d..c9156503d049 100644 --- a/internal/types/array_iterator.go +++ b/internal/types/array_iterator.go @@ -34,7 +34,6 @@ func newArrayIterator(array *Array) iterator.Interface[int, any] { arr: array, token: resource.NewToken(), } - resource.Track(iter, iter.token) return iter diff --git a/internal/types/document_iterator.go b/internal/types/document_iterator.go index b05f6b2b294c..3df7f09cce36 100644 --- a/internal/types/document_iterator.go +++ b/internal/types/document_iterator.go @@ -34,7 +34,6 @@ func newDocumentIterator(document *Document) iterator.Interface[string, any] { doc: document, token: resource.NewToken(), } - resource.Track(iter, iter.token) return iter diff --git a/internal/util/contract/contract.go b/internal/util/contract/contract.go new file mode 100644 index 000000000000..035fc58f0a8d --- /dev/null +++ b/internal/util/contract/contract.go @@ -0,0 +1,58 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package contract provides Design by Contract functionality. +package contract + +import ( + "errors" + "fmt" + "reflect" + + "github.com/FerretDB/FerretDB/internal/util/debugbuild" +) + +// EnsureError checks that error is either nil or one of expected errors. +// +// If that is not the case, EnsureError panics in debug builds. +// It does nothing in non-debug builds. +func EnsureError(err error, expected ...error) { + if !debugbuild.Enabled { + return + } + + if err == nil { + return + } + + if reflect.ValueOf(err).IsZero() { + panic(fmt.Sprintf("EnsureError: invalid actual value %#v", err)) + } + + for _, target := range expected { + if target == nil { + panic(fmt.Sprintf("EnsureError: invalid expected value %#v", target)) + } + + if reflect.ValueOf(target).IsZero() { + panic(fmt.Sprintf("EnsureError: invalid expected value %#v", target)) + } + + if errors.Is(err, target) { + return + } + } + + panic(fmt.Sprintf("EnsureError: %#v", err)) +} diff --git a/internal/util/contract/contract_test.go b/internal/util/contract/contract_test.go new file mode 100644 index 000000000000..2ead79724023 --- /dev/null +++ b/internal/util/contract/contract_test.go @@ -0,0 +1,89 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package contract + +import ( + "fmt" + "io/fs" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/FerretDB/FerretDB/internal/util/debugbuild" +) + +var testError = fmt.Errorf("expected error value") + +func TestEnsureError(t *testing.T) { + t.Parallel() + + require.True(t, debugbuild.Enabled) + + assert.NotPanics(t, func() { + EnsureError(nil) + }) + + t.Run("Value", func(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + assert.NotPanics(t, func() { + var err error + EnsureError(err, testError) + }) + + assert.NotPanics(t, func() { + err := testError + EnsureError(err, testError) + }) + + assert.NotPanics(t, func() { + err := fmt.Errorf("wrapped: %w", testError) + EnsureError(err, testError) + }) + }) + + t.Run("Fail", func(t *testing.T) { + t.Parallel() + + assert.PanicsWithValue(t, `EnsureError: &errors.errorString{s:"some other error"}`, func() { + err := fmt.Errorf("some other error") + EnsureError(err, testError) + }) + }) + + t.Run("Invalid", func(t *testing.T) { + t.Parallel() + + assert.PanicsWithValue(t, `EnsureError: invalid actual value (*fs.PathError)(nil)`, func() { + var err *fs.PathError + EnsureError(err, testError) + }) + + assert.PanicsWithValue(t, `EnsureError: invalid expected value `, func() { + err := testError + EnsureError(err, nil) + }) + + assert.PanicsWithValue(t, `EnsureError: invalid expected value (*fs.PathError)(nil)`, func() { + err := testError + EnsureError(err, (*fs.PathError)(nil)) + }) + }) + }) +} diff --git a/internal/util/ctxutil/ctxutil.go b/internal/util/ctxutil/ctxutil.go index b7fe25a5b5f9..bcc15ff42907 100644 --- a/internal/util/ctxutil/ctxutil.go +++ b/internal/util/ctxutil/ctxutil.go @@ -17,6 +17,7 @@ package ctxutil import ( "context" + "fmt" "math" "math/rand" "time" @@ -61,20 +62,32 @@ func SleepWithJitter(ctx context.Context, d time.Duration, attempts int64) { <-sleepCtx.Done() } -// DurationWithJitter returns an exponential backoff duration based on retry with random jitter. -// The maximum sleep is the cap. The minimum duration is at least 100 milliseconds. +// DurationWithJitter returns an exponential backoff duration based on attempt with random "full jitter". +// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ // -// Math/rand is good enough because we don't need the randomness to be cryptographically secure. -func DurationWithJitter(cap time.Duration, retry int64) time.Duration { - const base = time.Millisecond * 100 +// The maximum sleep is the cap. The minimum sleep is at least 3 milliseconds. +// Provided cap must be larger than minimum sleep, and attempt number must be a positive number. +func DurationWithJitter(cap time.Duration, attempt int64) time.Duration { + const base = 100 // ms + const minDuration = 3 // ms - if retry < 1 { - panic("retry must be nonzero positive number") + capDuration := cap.Milliseconds() + + if attempt < 1 { + panic("attempt must be nonzero positive number") + } + + if capDuration <= minDuration { + panic(fmt.Sprintf("cap must be larger than min duration (%dms)", minDuration)) } - maxMilliseconds := float64(base.Milliseconds()) * math.Pow(2, float64(retry)) - capMilliseconds := float64(cap.Milliseconds()) - lowestValue := int64(math.Min(capMilliseconds, maxMilliseconds)) + // calculate base backoff based on base duration and amount of attempts + backoff := float64(base * math.Pow(2, float64(attempt))) + // cap is a max limit of possible durations returned + maxDuration := int64(math.Min(float64(capDuration), backoff)) + + // Math/rand is good enough because we don't need the randomness to be cryptographically secure. + sleep := rand.Int63n(maxDuration-minDuration) + minDuration - return time.Duration(rand.Int63n(lowestValue)) * time.Millisecond + return time.Duration(sleep) * time.Millisecond } diff --git a/internal/util/ctxutil/ctxutil_test.go b/internal/util/ctxutil/ctxutil_test.go index b45c0e7a4ae3..d835225fed1c 100644 --- a/internal/util/ctxutil/ctxutil_test.go +++ b/internal/util/ctxutil/ctxutil_test.go @@ -16,8 +16,11 @@ package ctxutil import ( "fmt" + "math/rand" "os" "path/filepath" + "sync" + "sync/atomic" "testing" "time" @@ -28,31 +31,29 @@ import ( func TestDurationWithJitter(t *testing.T) { t.Parallel() - t.Run("larger or equal then 1ms", func(t *testing.T) { + t.Run("OneRetry", func(t *testing.T) { sleep := DurationWithJitter(time.Second, 1) - assert.GreaterOrEqual(t, sleep, time.Millisecond) + assert.GreaterOrEqual(t, sleep, 3*time.Millisecond) + assert.LessOrEqual(t, sleep, 1*time.Second) }) - t.Run("less or equal then duration input", func(t *testing.T) { + t.Run("ManyRetries", func(t *testing.T) { sleep := DurationWithJitter(time.Second, 100000) + assert.GreaterOrEqual(t, sleep, 3*time.Millisecond) assert.LessOrEqual(t, sleep, time.Second) }) - t.Run("multiple tasks retry multiple times", func(t *testing.T) { - // This test outputs a file for duration it took all nTasks to retry nRetries. - // In reality not all tasks will retry, but this is good enough for visualising it. - - nTasks := 100 - nRetries := 5 - - durations := make([][]time.Duration, nTasks) // task -> retry count -> duration + t.Run("TooLowCap", func(t *testing.T) { + assert.Panics(t, func() { + DurationWithJitter(2*time.Millisecond, 10000) + }) + assert.Panics(t, func() { + DurationWithJitter(3*time.Millisecond, 10000) + }) + }) - for i := 0; i < nTasks; i++ { - durations[i] = make([]time.Duration, nRetries) - for j := 0; j < nRetries; j++ { - durations[i][j] = DurationWithJitter(time.Second, int64(j+1)) - } - } + t.Run("RetryMultipleTimes", func(t *testing.T) { + t.Skip("test used only to generate data") dir := filepath.Join("result") err := os.MkdirAll(dir, 0o777) @@ -64,11 +65,58 @@ func TestDurationWithJitter(t *testing.T) { defer f.Close() - for _, task := range durations { - for j, duration := range task { - // each line has retry count (j+1) and duration waited in milliseconds. - fmt.Fprintln(f, j+1, duration.Milliseconds()) - } + for i := 1; i <= 100; i++ { + t.Logf("simulating %d clients...", i) + total := simulateCompetingClients(i) + fmt.Fprintf(f, "%d\t%d\n", i, total) } }) } + +// simulateCompetingClients simulates amount of clients competing on a single resource. +// They use duration returned by DurationWithJitter before calling the resource again. +// It returns total amount of calls done by all clients. +func simulateCompetingClients(clients int) int64 { + ch := make(chan struct{}) + + go func() { + for { + ch <- struct{}{} + + time.Sleep(time.Duration(rand.Intn(18)+2) * time.Millisecond) + } + }() + + totalCalls := atomic.Int64{} + + call := func() bool { + totalCalls.Add(1) + select { + case <-ch: + return true + default: + return false + } + } + + wg := sync.WaitGroup{} + + for i := 0; i < clients; i++ { + wg.Add(1) + + go func() { + for retry := 1; retry < 1000; retry++ { + if call() { + wg.Done() + return + } + + time.Sleep(DurationWithJitter(200*time.Millisecond, int64(retry))) + } + }() + } + + wg.Wait() + + return totalCalls.Load() +} diff --git a/internal/util/iterator/func.go b/internal/util/iterator/func.go index 42db78acbd25..b7f49cb3776a 100644 --- a/internal/util/iterator/func.go +++ b/internal/util/iterator/func.go @@ -40,7 +40,6 @@ func ForFunc[K, V any](f NextFunc[K, V]) Interface[K, V] { f: f, token: resource.NewToken(), } - resource.Track(iter, iter.token) return iter diff --git a/internal/util/iterator/slice.go b/internal/util/iterator/slice.go index 9ca84d886d41..32dbb3c9c1c7 100644 --- a/internal/util/iterator/slice.go +++ b/internal/util/iterator/slice.go @@ -27,7 +27,6 @@ func ForSlice[V any](s []V) Interface[int, V] { s: s, token: resource.NewToken(), } - resource.Track(res, res.token) return res diff --git a/internal/util/observability/funccall.go b/internal/util/observability/funccall.go new file mode 100644 index 000000000000..a91ba0e7ed57 --- /dev/null +++ b/internal/util/observability/funccall.go @@ -0,0 +1,69 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package observability + +import ( + "context" + "runtime" + "runtime/trace" + + "github.com/FerretDB/FerretDB/internal/util/resource" +) + +// funcCall tracks function calls. +type funcCall struct { + token *resource.Token + region *trace.Region +} + +// leave is called on function exit. +func (fc *funcCall) leave() { + if fc.region != nil { + fc.region.End() + } + + resource.Untrack(fc, fc.token) +} + +// FuncCall adds observability to a function call. +// +// It should be called at the very beginning of the function, +// and returned function should be called at exit. +// The returned function must not be passed or stored. +// The only valid way to use FuncCall is: +// +// func foo(ctx context.Context) { +// defer FuncCall(ctx)() +// // ... +// +// For the Go execution tracer, FuncCall creates a new region for the function call +// and attaches it to the task in the context (or background task). +func FuncCall(ctx context.Context) func() { + fc := &funcCall{ + token: resource.NewToken(), + } + resource.Track(fc, fc.token) + + if trace.IsEnabled() { + pc := make([]uintptr, 1) + runtime.Callers(1, pc) + f, _ := runtime.CallersFrames(pc).Next() + funcName := f.Function + + fc.region = trace.StartRegion(ctx, funcName) + } + + return fc.leave +} diff --git a/internal/util/observability/observability.go b/internal/util/observability/observability.go new file mode 100644 index 000000000000..ce8b6e1b8090 --- /dev/null +++ b/internal/util/observability/observability.go @@ -0,0 +1,16 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package observability provides abstractions for tracing, metrics, etc. +package observability diff --git a/internal/wire/msg_body.go b/internal/wire/msg_body.go index 5f64aa9bffde..ef46598b73f0 100644 --- a/internal/wire/msg_body.go +++ b/internal/wire/msg_body.go @@ -17,8 +17,10 @@ package wire import ( "bufio" "encoding" + "encoding/binary" "errors" "fmt" + "hash/crc32" "io" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -40,6 +42,8 @@ type MsgBody interface { // indicating that connection was closed by the client. var ErrZeroRead = errors.New("zero bytes read") +const kFlagBitSize = 4 + // ReadMessage reads from reader and returns wire header and body. // // Error is (possibly wrapped) ErrZeroRead if zero bytes was read. @@ -64,6 +68,10 @@ func ReadMessage(r *bufio.Reader) (*MsgHeader, MsgBody, error) { return &header, &reply, nil case OpCodeMsg: + if err := containsValidChecksum(&header, b); err != nil { + return &header, nil, lazyerrors.Error(err) + } + var msg OpMsg if err := msg.UnmarshalBinary(b); err != nil { return &header, nil, lazyerrors.Error(err) @@ -113,6 +121,12 @@ func WriteMessage(w *bufio.Writer, header *MsgHeader, msg MsgBody) error { )) } + if header.OpCode == OpCodeMsg { + if err := containsValidChecksum(header, b); err != nil { + return lazyerrors.Error(err) + } + } + if err := header.writeTo(w); err != nil { return lazyerrors.Error(err) } @@ -123,3 +137,48 @@ func WriteMessage(w *bufio.Writer, header *MsgHeader, msg MsgBody) error { return nil } + +// getChecksum returns the checksum attached to an OP_MSG. +func getChecksum(data []byte) (uint32, error) { + // ensure that the length of the body is at least the size of a flagbit + // and a crc32 checksum + n := len(data) + if n < crc32.Size+kFlagBitSize { + return 0, lazyerrors.New("Invalid message size for an OpMsg containing a checksum") + } + + return binary.LittleEndian.Uint32(data[n-crc32.Size:]), nil +} + +func containsValidChecksum(header *MsgHeader, body []byte) error { + if len(body) < kFlagBitSize { + return lazyerrors.New("Message contains illegal flags value") + } + + flagBit := OpMsgFlags(binary.LittleEndian.Uint32(body[:kFlagBitSize])) + if flagBit.FlagSet(OpMsgChecksumPresent) { + want, err := getChecksum(body) + if err != nil { + lazyerrors.Error(err) + } + + // https://datatracker.ietf.org/doc/html/rfc4960#appendix-B + hasher := crc32.New(crc32.MakeTable(crc32.Castagnoli)) + + if err := binary.Write(hasher, binary.LittleEndian, header); err != nil { + return lazyerrors.Error(err) + } + + offset := len(body) - crc32.Size + if err := binary.Write(hasher, binary.LittleEndian, body[:offset]); err != nil { + return lazyerrors.Error(err) + } + + got := hasher.Sum32() + if want != got { + return lazyerrors.New("OP_MSG checksum does not match contents.") + } + } + + return nil +} diff --git a/internal/wire/op_msg.go b/internal/wire/op_msg.go index 58625db11fec..f01f496c8c5b 100644 --- a/internal/wire/op_msg.go +++ b/internal/wire/op_msg.go @@ -205,8 +205,6 @@ func (msg *OpMsg) readFrom(bufr *bufio.Reader) error { if err := binary.Read(bufr, binary.LittleEndian, &msg.checksum); err != nil { return lazyerrors.Error(err) } - - // TODO validate checksum https://github.com/FerretDB/FerretDB/issues/1626 } if _, err := msg.Document(); err != nil { @@ -297,9 +295,6 @@ func (msg *OpMsg) MarshalBinary() ([]byte, error) { } if msg.FlagBits.FlagSet(OpMsgChecksumPresent) { - // TODO validate checksum if present (mainly for tests), update if not - // https://github.com/FerretDB/FerretDB/issues/1626 - if err := binary.Write(bufw, binary.LittleEndian, msg.checksum); err != nil { return nil, lazyerrors.Error(err) } diff --git a/internal/wire/op_msg_test.go b/internal/wire/op_msg_test.go index 837f7f01d70f..38b704b06813 100644 --- a/internal/wire/op_msg_test.go +++ b/internal/wire/op_msg_test.go @@ -389,6 +389,63 @@ var msgTestCases = []testCase{{ }}, }, command: "update", +}, { + name: "InvalidChecksum", + expectedB: []byte{ + 0x77, 0x00, 0x00, 0x00, // MessageLength + 0x0f, 0x00, 0x00, 0x00, // RequestID + 0x00, 0x00, 0x00, 0x00, // ResponseTo + 0xdd, 0x07, 0x00, 0x00, // OpCode + 0x01, 0x00, 0x00, 0x00, // FlagBits + + 0x01, // section kind + 0x2f, 0x00, 0x00, 0x00, // section size + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x00, // section identifier "documents" + 0x21, 0x00, 0x00, 0x00, // document size + 0x07, 0x5f, 0x69, 0x64, 0x00, // ObjectID "_id" + 0x63, 0x8c, 0xec, 0x46, 0xaa, 0x77, 0x8b, 0xf3, 0x70, 0x10, 0x54, 0x29, // ObjectID value + 0x01, 0x61, 0x00, // double "a" + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x40, // 3.0 + 0x00, // end of document + + 0x00, // section kind + 0x2d, 0x00, 0x00, 0x00, // document size + 0x02, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x00, // string "insert" + 0x04, 0x00, 0x00, 0x00, // "foo" length + 0x66, 0x6f, 0x6f, 0x6f, 0x00, // "fooo" + 0x08, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x65, 0x64, 0x00, 0x01, // "ordered" true + 0x02, 0x24, 0x64, 0x62, 0x00, // string "$db" + 0x05, 0x00, 0x00, 0x00, // "test" length + 0x74, 0x65, 0x73, 0x74, 0x00, // "test" + 0x00, // end of document + + 0xe2, 0xb7, 0x90, 0x67, // invalid checksum value + }, + msgHeader: &MsgHeader{ + MessageLength: 119, + RequestID: 15, + ResponseTo: 0, + OpCode: OpCodeMsg, + }, + msgBody: &OpMsg{ + FlagBits: OpMsgFlags(OpMsgChecksumPresent), + checksum: 1737537506, + sections: []OpMsgSection{{ + Kind: 1, + Identifier: "documents", + Documents: []*types.Document{must.NotFail(types.NewDocument( + "_id", types.ObjectID{0x63, 0x8c, 0xec, 0x46, 0xaa, 0x77, 0x8b, 0xf3, 0x70, 0x10, 0x54, 0x29}, + "a", float64(3), + ))}, + }, { + Documents: []*types.Document{must.NotFail(types.NewDocument( + "insert", "fooo", + "ordered", true, + "$db", "test", + ))}, + }}, + }, + err: "OP_MSG checksum does not match contents.", }} func TestMsg(t *testing.T) { diff --git a/internal/wire/records_test.go b/internal/wire/records_test.go index 283ebbddd52e..37291ae0d7dc 100644 --- a/internal/wire/records_test.go +++ b/internal/wire/records_test.go @@ -16,6 +16,7 @@ package wire import ( "bufio" + "encoding/binary" "errors" "io/fs" "math/rand" @@ -97,6 +98,12 @@ func loadRecords(recordsPath string) ([]testCase, error) { return nil, lazyerrors.Errorf("%s: %w", path, err) } + // unset flagBit (if present) + flag := binary.LittleEndian.Uint32(bodyBytes[:kFlagBitSize]) + flag &^= 0x01 // flips the LSB of the flagbit + + binary.LittleEndian.PutUint32(bodyBytes[:kFlagBitSize], flag) + resMsgs = append(resMsgs, testCase{ headerB: headBytes, bodyB: bodyBytes, diff --git a/internal/wire/wire_test.go b/internal/wire/wire_test.go index a3f9c5069829..5bac08729c02 100644 --- a/internal/wire/wire_test.go +++ b/internal/wire/wire_test.go @@ -111,6 +111,11 @@ func testMessages(t *testing.T, testCases []testCase) { var buf bytes.Buffer bufw := bufio.NewWriter(&buf) err := WriteMessage(bufw, tc.msgHeader, tc.msgBody) + if err != nil { + require.Equal(t, tc.err, lastErr(err).Error()) + return + } + require.NoError(t, err) err = bufw.Flush() require.NoError(t, err) diff --git a/tools/cleantool/cleantool.go b/tools/cleantool/cleantool.go new file mode 100644 index 000000000000..7242c8b14e8b --- /dev/null +++ b/tools/cleantool/cleantool.go @@ -0,0 +1,92 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/google/go-github/v41/github" + "golang.org/x/oauth2" +) + +// if the most recent update time of the package was 90 days ago, then mark it as +// stale. +func isPackageToBeClean(p *github.PackageVersion) bool { + daysBack := 90 + toBeClean := false + + if time.Now().After(p.UpdatedAt.Add(time.Duration(daysBack) * 24 * time.Hour)) { + log.Printf("Stale version id: %v , recent update was at %s", p.GetID(), p.UpdatedAt) + + toBeClean = true + } else { + log.Printf("Skip version id: %v , recent update was at %s", p.GetID(), p.UpdatedAt) + } + + return toBeClean +} + +func main() { + ctx := context.Background() + tokenName := "ROBOT_TOKEN" + token := os.Getenv(tokenName) + + if token == "" { + log.Fatalf("env variable %v is not found, please set it before run it", tokenName) + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + packageType := "container" + packageName := "ferretdb-dev" + orgName := "FerretDB" + pageSize := 100 + pageIndex := 1 + + var versions []string + + for { + listOption := github.ListOptions{Page: pageIndex, PerPage: pageSize} + packageListOption := github.PackageListOptions{ListOptions: listOption} + packages, _, err := client.Organizations.PackageGetAllVersions(ctx, orgName, packageType, packageName, &packageListOption) + if err != nil { + log.Printf("Failed to get versions for page %d", pageIndex) + } + + for _, v := range packages { + if isPackageToBeClean(v) { + versions = append(versions, fmt.Sprintf("%v", v.GetID())) + } + } + + if len(packages) < pageSize { + log.Printf("Come to the last page (page %d)", pageIndex) + break + } + + pageIndex++ + } + + staleVersions := strings.Join(versions, ", ") + log.Printf("Found %d stale versions: %v", len(versions), staleVersions) +} diff --git a/tools/go.mod b/tools/go.mod index f256a22a5ccf..41805e79c3fd 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -5,13 +5,15 @@ go 1.20 require ( github.com/BurntSushi/go-sumtype v0.0.0-20221020234012-480526a59796 github.com/go-task/task/v3 v3.24.0 + github.com/google/go-github/v41 v41.0.0 github.com/goreleaser/nfpm/v2 v2.28.0 github.com/quasilyte/go-consistent v0.0.0-20220429160651-4e46040fbc82 github.com/reviewdog/reviewdog v0.14.1 - github.com/stretchr/testify v1.8.2 - golang.org/x/perf v0.0.0-20230227161431-f7320a6d63e8 - golang.org/x/tools v0.8.0 - golang.org/x/vuln v0.0.0-20230419012003-9268f8338db8 + github.com/stretchr/testify v1.8.3 + golang.org/x/oauth2 v0.8.0 + golang.org/x/perf v0.0.0-20230427221525-d343f6398b76 + golang.org/x/tools v0.9.1 + golang.org/x/vuln v0.1.0 mvdan.cc/gofumpt v0.5.0 ) @@ -49,7 +51,6 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-github/v39 v39.2.0 // indirect - github.com/google/go-github/v41 v41.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gax-go/v2 v2.3.0 // indirect @@ -102,11 +103,10 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/net v0.9.0 // indirect - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.7.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/tools/go.sum b/tools/go.sum index a0789eb7444d..bf4166c7ff46 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -427,9 +427,7 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -437,10 +435,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -580,8 +576,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20170207211851-4464e7848382/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -601,11 +597,11 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/perf v0.0.0-20230227161431-f7320a6d63e8 h1:8F1zibFGd22KfifgNSzbIxZTGb6nLKORJ2ZERumnQuw= -golang.org/x/perf v0.0.0-20230227161431-f7320a6d63e8/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc= +golang.org/x/perf v0.0.0-20230427221525-d343f6398b76 h1:cPGZx8Liyx5Pq/yX80/6WMKe2yidT0xvVCQBOGa8WHU= +golang.org/x/perf v0.0.0-20230427221525-d343f6398b76/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -617,8 +613,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -678,12 +674,12 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -756,10 +752,10 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/vuln v0.0.0-20230419012003-9268f8338db8 h1:ypybDHCrQVN869yKbR5P/MGmZ+N6DgymM9xkZK3TAeE= -golang.org/x/vuln v0.0.0-20230419012003-9268f8338db8/go.mod h1:64LpnL2PuSMzFYeCmJjYiRbroOUG9aCZYznINnF5PHE= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/vuln v0.1.0 h1:9GRdj6wAIkDrsMevuolY+SXERPjQPp2P1ysYA0jpZe0= +golang.org/x/vuln v0.1.0/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/website/blog/2023-01-25-ferretdb-fetches-data-query-pushdown.md b/website/blog/2023-01-25-ferretdb-fetches-data-query-pushdown.md index 74147257336f..40f8eda970e8 100644 --- a/website/blog/2023-01-25-ferretdb-fetches-data-query-pushdown.md +++ b/website/blog/2023-01-25-ferretdb-fetches-data-query-pushdown.md @@ -37,7 +37,7 @@ Fortunately, we’ve managed to introduce the query pushdown with [this PR](http Let's jump right into the FerretDB internals and go step by step to see how it handles the sample query! -Let's say we have a collection with data on thousands of customers, and want to check if the one under β€œjohn.doe@example.com” email address has an active account: +Let's say we have a collection with data on thousands of customers, and want to check if the one under `john.doe@example.com` email address has an active account: ```js db.customers.find({ 'email': 'john.doe@example.com' },{ active: 1 }) diff --git a/website/blog/2023-05-09-ferretdb-v-1-1-0-released.md b/website/blog/2023-05-09-ferretdb-v-1-1-0-released.md new file mode 100644 index 000000000000..eef3ab2cfd9a --- /dev/null +++ b/website/blog/2023-05-09-ferretdb-v-1-1-0-released.md @@ -0,0 +1,147 @@ +--- +slug: ferretdb-v-1-1-0-released +title: "FerretDB v1.1.0. Released" +authors: [alex] +description: > + It is our pleasure to announce the release of [FerretDB](https://www.ferretdb.io/) version 1.1.0, which includes the addition of `renameCollection`, support for projection field assignments, and the `$project` pipeline aggregation stage, as well as `create` and `drop` commands in SAP HANA handler. +image: /img/blog/ferretdb-1-1-0-release.png +tags: [release] +--- + +![FerretDB v.1.1.0](/img/blog/ferretdb-1-1-0-release.png) + +It is our pleasure to announce the release of [FerretDB](https://www.ferretdb.io/) version 1.1.0, which includes the addition of `renameCollection`, support for projection field assignments, and the `$project` pipeline aggregation stage, as well as `create` and `drop` commands in SAP HANA handler. + + + +Last month, in the middle of April, we released the [FerretDB 1.0. GA](https://blog.ferretdb.io/ferretdb-1-0-ga-opensource-mongodb-alternative/) to overwhelming success, which has seen us featured and mentioned in several blog posts, podcasts, webinars, and events. +Since then, we’ve had genuinely amazing support from the community as well as a host of new contributors, including [@cooljeanius](https://github.com/cooljeanius), [@j0holo](https://github.com/j0holo), [@AuruTus](https://github.com/AuruTus), [@craigpastro](https://github.com/craigpastro), [@afiskon](https://github.com/afiskon), [@syasyayas](https://github.com/syasyayas), [@raeidish](https://github.com/raeidish), [@polyal](https://github.com/polyal), and [@wqhhust](https://github.com/wqhhust). + +We thank you all! +Your enthusiasm and passion for FerretDB reinforce our belief in the need for a truly open-source document database alternative to MongoDB. + +While this is not a major release, we have some exciting updates and fixes for you. +Let’s find out! + +## New features + +In this release, we’ve added `renameCollection` command, which would enable users to rename an existing FerretDB collection. + +Say you have an `inventory` collection below: + +```js +{ + "_id": 1, + name: "ABC Electronics", + location: "123 Main Street", + category: "Electronics", + inventory: [ + { + product: "Laptop", + price: 1200, + quantity: 10 + } + ] +} +``` + +You can access the `renameCollection` command through the `db.collection.renameCollection()` method within the same database in mongosh, as shown below for a current `inventory` collection. + +```js +db.inventory.renameCollection("store") +``` + +Note that `writeConcern`, `comment`, and `dropTarget` arguments are not currently implemented. + +In addition to the currently available aggregation pipeline stages, we now support `$project` stage, which will enable you to reshape and refine the output of your queries specifying new fields, including or excluding existing fields, and also rearranging the structure of the documents. + +You can include only specific fields from the output documents, such as `category` and `inventory` in a `$project` stage, as shown below. + +```js +db.store.aggregate([ + { + $project: { + category: 1, + inventory: 1 + } + } +]) +``` + +The output document looks like this: + +```js +[ + { + _id: 1, + category: 'Electronics', + inventory: [ { product: 'Laptop', price: 1200, quantity: 10 } ] + } +] +``` + +This outputs the fields specified, together with the default `_id`. +You can suppress the default `_id` by setting it as `0`. + +In the new release, we have added support for field projections assignment. +With this feature, users can now specify which fields to retrieve from the database and assign new values to them in a single query. +For instance, if we have a `users` collection as shown below: + +```js +[ + { + "_id": 1, + name: "John", + age: 30, + email: "john@example.com" + }, + { + "_id": 2, + name: "Jane", + age: 25, + email: "jane@example.com" + } +] +``` + +Suppose we want to retrieve the documents from the `users` collection but only include the name field while assigning a new value of 'Anonymous' to it. + +```js +db.users.find({}, { name: 'Anonymous' }) +``` + +The query will return: + +```js +[ { _id: 1, name: 'Anonymous' }, { _id: 2, name: 'Anonymous' } ] +``` + +Also, thanks to one of our contributors, [@polyal](https://github.com/polyal), we now support `create` and `drop` commands in SAP HANA handler. + +## Bug fixes + +In addition to the new features, we have fixed some of the discovered bugs in the previous release. +For example, in the previous release, there was a bug when using `findandModify` for `$exists` query operations and when it shouldn't allow `$upsert` on existing `_id`. + +Another bug was discovered when using multiple update operators, such as `$set` and `$min` on the same document path. +Normally, it should return an error stating that there is a conflict, which should prevent the update operation, but it didn’t. +This bug has now been resolved. + +Aside from that, we’ve also resolved a bug that occurs when attempting to use dot notation in sorting, especially when using a sort criteria like `{"v.foo", 1}`. + +## Documentation + +For those interested in contributing to FerretDB, we’ve also updated our PR guide in [CONTRIBUTING.md](https://github.com/FerretDB/FerretDB/blob/main/CONTRIBUTING.md), with more details on squash and push, and other information related to PR management. + +In our documentation, you can now discover ways to get Docker and binary executable logs from FerretDB. +[See here for more](https://docs.ferretdb.io/configuration/logging/#docker-logs). +We’ve also documented `createIndexes`, `listIndexes`, and `dropIndexes` commands and how to use them in FerretDB. + +## Conclusion + +Of course, there are several other changes in this release, especially community contributions, and you can find a full list of them here in the [FerretDB version 1.1.0 release notes](https://github.com/FerretDB/FerretDB/releases/tag/v1.1.0). +We appreciate every single word of support, bug discovery, code contributions, and feedback from the community. +Your continuous support showcases the strength and belief in open source. + +Like always, we look forward to your feedback and comments on this new release. +So if you have any questions or find any bugs or suggestions for new features or future improvements, [please feel free to reach out to us](https://docs.ferretdb.io/#community)! diff --git a/website/blog/2023-05-18-ferretdb-meteor-mongodb-alternative.md b/website/blog/2023-05-18-ferretdb-meteor-mongodb-alternative.md new file mode 100644 index 000000000000..42c6ee8c7b07 --- /dev/null +++ b/website/blog/2023-05-18-ferretdb-meteor-mongodb-alternative.md @@ -0,0 +1,88 @@ +--- +slug: ferretdb-meteor-mongodb-alternative +title: "Meteor.js and FerretDB: Using an Open Source MongoDB Alternative for Your Meteor.js Apps" +authors: [alex] +description: > + Here we explore the possible synergy and compatibility of FerretDB in Meteor.js and how you can build your applications without any concern for vendor lock-in. +image: /img/blog/ferretdb-meteor.jpg +keywords: [FerretDB, meteor, mongodb alternative] +tags: [javascript frameworks, compatible applications] +--- + +![Meteor.js and FerretDB](/img/blog/ferretdb-meteor.jpg) + +[Meteor.js](https://www.meteor.com/) has gained immense popularity as a full-stack JavaScript platform for developing modern web and mobile applications, thanks to its seamless data synchronization and full-stack capabilities. +Its flexibility, real-time capabilities, and rich ecosystem have made it a developer's favorite. + + + +However, the default database for Meteor.js applications, MongoDB, presents significant concerns about vendor lock-in, especially since its move away from its open-source roots. +To mitigate this, open-source alternatives like [FerretDB](https://www.ferretdb.io/) offer the opportunity to seamlessly replace MongoDB without compromising the commands and syntax you already know. + +In this article, we'll explore the possible synergy between FerretDB and Meteor.js and how you can build your applications without any concern for lock-in. + +## Brief History of Meteor.js + +Meteor.js is an open-source, all-in-one JavaScript framework, available publicly under the MIT license. +Built with Node.js, Meteor.js uses MongoDB as its core database for applications, Blaze handlebars for efficient templating, a powerful pub/sub method for real-time data synchronization, and seamless integration with various Meteor.js plugins, and `npm` packages you need. + +This extensive ecosystem empowers developers to rapidly create quick cross-platform prototypes or large, complex applications with ease. +With its open-source nature and availability under the MIT license, Meteor.js has gained popularity among not only mid-sized and enterprise companies like [IKEA](https://www.ikea.com/), [Workpop](https://www.betterteam.com/workpop), [Accenture](https://www.accenture.com/us-en) but also individual developers and startups. + +The framework has been responsible for the development of remarkable apps, such as [Rocket.Chat](https://rocket.chat/), [Apify](https://apify.com/), and [Chatra](https://chatra.com/), among others. +Its ability to efficiently handle real-time communication and collaboration make it an ideal platform for applications requiring instant updates and dynamic content. + +Meteor.js uses MongoDB as its standard database. +However, [several concerns about MongoDB](https://blog.ferretdb.io/open-source-is-in-danger/) over the years have led some to seek (or demand) an open-source alternative that sufficiently replaces MongoDB without any hassles. + +Since MongoDB's license switch to SSPL from its former open-source license, there have been growing concerns about the nature of the license and how it may affect developers and companies building their applications with MongoDB. +Several Meteor.js application developers are also in this corner. + +And this is what makes [the announcement of the General Availability version](https://blog.ferretdb.io/ferretdb-1-0-ga-opensource-mongodb-alternative/) of FerretDB – an open-source replacement to MongoDB – so exciting! + +## Introducing FerretDB + +FerretDB is the defacto open-source MongoDB alternative that converts MongoDB wire protocols with a backend on [PostgreSQL](https://www.postgresql.org/). +Besides PostgreSQL, FerretDB is also working to provide support for other database backends, such as [Tigris](https://www.tigrisdata.com/), [SAP Hana](https://www.sap.com/africa/products/technology-platform/hana.html), [SQLite](https://sqlite.org/), and many more to come. + +With the FerretDB GA released in April 2023, users can now leverage FerretDB for their production workloads. +Users familiar with MongoDB can seamlessly transition since it uses the same query language and syntax. +This makes FerretDB a highly attractive option for Meteor.js developers looking to [escape the vendor lock-in challenges associated with MongoDB](https://blog.ferretdb.io/5-ways-to-avoid-database-vendor-lock-in/) while still benefiting from a familiar environment. + +Some of the popular database management tools, like `mongosh`, [MongoDB Compass](https://www.mongodb.com/products/compass), [Studio3T](https://studio3t.com/), [Mingo](https://mingo.io/), and [NoSQLBooster](https://nosqlbooster.com/), are already capable of taking full advantage of the current set of features on FerretDB. + +Apart from these tools, FerretDB is being tested against real-world applications, and some of the major ones are applications built with Meteor.js since they use MongoDB as their default database. + +Some of the benefits of using FerretDB with your Meteor.js applications include the following: + +* **Open-source nature:** With a low barrier to entry, you can get started with FerretDB and take advantage of a community of developers and engineers that's always ready to help and support you every step of the way, without any fear of vendor lock-in. +* **User-friendliness:** FerretDB lets you jump in and use the same syntax you're familiar with, so there's no need to learn a new language or syntax; it's the same familiar environment you're already used to. +* **PostgreSQL backend:** FerretDB's database engine, PostgreSQL, is renowned for its open-source nature, reliability, and robust community, as well as its ability to handle large amounts of data and complex queries, which can be an added advantage. + +## Replacing MongoDB With FerretDB in Your Meteor.js Apps + +FerretDB has made significant strides in ensuring compatibility with Meteor.js apps, and the development team continues to work on enhancing this compatibility further. +As of the latest release, FerretDB demonstrates compatibility with many common MongoDB use cases encountered in Meteor.js applications. + +The current level of compatibility enables Meteor.js developers, already accustomed to MongoDB, to seamlessly transition to FerretDB with minimal friction. +The same query language and syntax can be directly applied to FerretDB, and this compatibility also extends to MongoDB management tools like Compass, Mingo UI, Studio 3T, etc. + +However, please note that while FerretDB strives to achieve comprehensive compatibility with Meteor.js apps, there may be certain edge cases or specific MongoDB features that are not yet fully supported. +The FerretDB team is actively addressing these limitations. +To stay up to date with the compatibility status and ongoing efforts, please refer to the [FerretDB documentation](https://docs.ferretdb.io/reference/supported-commands/), or take a look at this [issue where FerretDB aims to achieve compatibility with Meteor.js](https://github.com/FerretDB/FerretDB/issues/2414) examples. + +While dealing with these compatibility issues, one of the [issues the FerretDB team has grappled with is the challenge of supporting `OpLog` tailing](https://github.com/meteor/meteor/discussions/12150). +The introduction of `ChangeStreams` in MongoDB 3.6 and later allows real-time data access that bypasses the complications associated with `OpLog` tailing. + +Even though Meteor.js traditionally supports MongoDB’s `OpLog` tailing for real-time data sync, Meteor.js falls back to polling when `OpLog` is not available; however, while this is not ideal for many production use cases, it can occasionally do the job. +At this stage, it’s unclear whether the hassles of implementing `OpLog` will be worth it, [especially with a few Meteor.js apps, like Rocket.Chat, moving to `ChangeStreams`](https://github.com/FerretDB/FerretDB/issues/1993#issuecomment-1518978149). + +Some of the other planned feature additions to help improve compatibility with Meteor.js include support for dot notation in query projections, `createIndexes` for unique indexes, support for partial indexes, and many more. +In addition to these feature additions, FerretDB will also be publishing more blog posts that showcase how different applications can use FerretDB in Meteor.js. + +## Get Started With FerretDB and Meteor.js + +As FerretDB continues to evolve and mature, it is poised to become an even more robust and reliable MongoDB alternative for Meteor.js developers. +The FerretDB team is dedicated to ongoing development, bug fixes, and feature implementation to address the needs and feedback of the Meteor.js community. + +Check out the [FerretDB installation guide](https://docs.ferretdb.io/quickstart-guide/) or [contact us to get started](https://docs.ferretdb.io/#community). diff --git a/website/docs/configuration/flags.md b/website/docs/configuration/flags.md index f38a53558c76..e6bc20086859 100644 --- a/website/docs/configuration/flags.md +++ b/website/docs/configuration/flags.md @@ -43,17 +43,23 @@ Some default values are overridden in [our Docker image](quickstart-guide/docker ## Backend handlers + + ### PostgreSQL -PostgreSQL backend can be enabled by `--handler=pg` flag or `FERRETDB_HANDLER=pg` environment variable. +[PostgreSQL backend](../understanding-ferretdb.md#postgresql) can be enabled by +`--handler=pg` flag or `FERRETDB_HANDLER=pg` environment variable. | Flag | Description | Environment Variable | Default Value | | ------------------ | ------------------------------- | ------------------------- | ------------------------------------ | | `--postgresql-url` | PostgreSQL URL for 'pg' handler | `FERRETDB_POSTGRESQL_URL` | `postgres://127.0.0.1:5432/ferretdb` | + + ### Tigris (beta) -Tigris backend can be enabled by `--handler=tigris` flag or `FERRETDB_HANDLER=tigris` environment variable. +[Tigris backend](../understanding-ferretdb.md#tigris-beta) can be enabled by +`--handler=tigris` flag or `FERRETDB_HANDLER=tigris` environment variable. | Flag | Description | Environment Variable | Default Value | | ------------------------ | ------------------------------- | ------------------------------- | ---------------- | diff --git a/website/docs/quickstart-guide/deb.md b/website/docs/quickstart-guide/deb.md index 347aeb2a6038..d884aba43ec7 100644 --- a/website/docs/quickstart-guide/deb.md +++ b/website/docs/quickstart-guide/deb.md @@ -4,40 +4,33 @@ sidebar_position: 2 # DEB package -To install the .deb packages for FerretDB on your Debian, Ubuntu, Linux, and other Unix-like systems, you can use `apt` or `dpkg`. +To install the `.deb` packages for FerretDB on your Debian, Ubuntu, and other `.deb`-based systems, +you can use `dpkg` tool. -Download the latest FerretDB .deb package from [our release pages](https://github.com/FerretDB/FerretDB/releases). - -To install FerretDB .deb package using `dpkg`, run the following command in your terminal: +Download the latest FerretDB `.deb` package from [our release pages](https://github.com/FerretDB/FerretDB/releases/latest), +then run the following command in your terminal: ```sh -$ sudo apt update -# Install using dpkg -$ sudo dpkg -i .deb +sudo dpkg -i ferretdb.deb ``` -It’s important to note that `dpkg` doesn’t address dependencies. -If you encounter any errors related to dependencies when installing, you can resolve them by installing all package dependencies with the following command: +You can check that FerretDB was installed by running ```sh -sudo apt install -f +ferretdb --version ``` -Instead of using `dpkg`, you can use `apt` to manage, install, and resolve all package dependencies automatically. -However, you must specify the full path to the .deb package. -This will stop `apt` from downloading and installing from Ubuntu’s repositories. - -For example, if the file is in your current working directory, indicate the path by prepending "`./`" to the filename. +FerretDB does not automatically installs PostgreSQL or other backends. +To install PostgreSQL, run the following commands: ```sh -$ sudo apt update -# Install via apt -$ sudo apt install ./.deb +sudo apt update +sudo apt install -y postgresql ``` -Once FerretDB is installed, you can start the software. -See [our Docker guide](docker.md) and [Configuration flags and variables](../configuration/flags.md) page for more details. +Currently, our `.deb` package does not provide a SystemD unit for starting FerretDB automatically. +You have to do it manually by running `ferretdb` binary with the [correct flags](../configuration/flags.md). Find out more about: -* [Getting logs](../configuration/logging.md#binary-executable-logs) +* [getting logs](../configuration/logging.md#binary-executable-logs). diff --git a/website/docs/quickstart-guide/docker.md b/website/docs/quickstart-guide/docker.md index 1baa001142c8..0b6a428ae733 100644 --- a/website/docs/quickstart-guide/docker.md +++ b/website/docs/quickstart-guide/docker.md @@ -82,11 +82,11 @@ You can improve that setup by: Find out more about: -* [Getting logs](../configuration/logging.md#docker-logs) +* [getting logs](../configuration/logging.md#docker-logs). ## Development image -The [development image](https://ghcr.io/ferretdb/ferretdb-dev) +The [development image](https://ghcr.io/ferretdb/ferretdb-dev) `ghcr.io/ferretdb/ferretdb-dev` contains the [debug build](https://pkg.go.dev/github.com/FerretDB/FerretDB/build/version#hdr-Debug_builds) of FerretDB with test coverage instrumentation, race detector, and other changes that make it more suitable for debugging problems. diff --git a/website/docs/quickstart-guide/rpm.md b/website/docs/quickstart-guide/rpm.md index 1127c5f61a0c..ad1d8032e49b 100644 --- a/website/docs/quickstart-guide/rpm.md +++ b/website/docs/quickstart-guide/rpm.md @@ -2,6 +2,34 @@ sidebar_position: 3 --- -# RPM package manager +# RPM package -*This section is not currently available. You can help FerretDB by contributing to this section. Click the **Edit this page** link below to get started*. +To install the `.rpm` packages for FerretDB on your RHEL, CentOS, and other `.rpm`-based systems, +you can use `rpm` tool. + +Download the latest FerretDB `.rpm` package from [our release pages](https://github.com/FerretDB/FerretDB/releases/latest), +then run the following command in your terminal: + +```sh +sudo rpm -i ferretdb.rpm +``` + +You can check that FerretDB was installed by running + +```sh +ferretdb --version +``` + +FerretDB does not automatically installs PostgreSQL or other backends. +To install PostgreSQL, run the following commands: + +```sh +sudo yum install -y postgresql +``` + +Currently, our `.rpm` package does not provide a SystemD unit for starting FerretDB automatically. +You have to do it manually by running `ferretdb` binary with the [correct flags](../configuration/flags.md). + +Find out more about: + +* [getting logs](../configuration/logging.md#binary-executable-logs). diff --git a/website/docs/reference/supported-commands.md b/website/docs/reference/supported-commands.md index 0a7882b72534..1b2080ed8e5c 100644 --- a/website/docs/reference/supported-commands.md +++ b/website/docs/reference/supported-commands.md @@ -217,6 +217,9 @@ Related [issue](https://github.com/FerretDB/FerretDB/issues/78). | Command | Argument | Status | Comments | | -------------- | -------- | ------ | --------------------------------------------------------- | | `authenticate` | | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1731) | +| `getnonce` | | ❌ | Deprecated | +| `logout` | | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1750) | +| `saslStart` | | βœ… | | ### Role Management Commands @@ -289,7 +292,7 @@ Related [issue](https://github.com/FerretDB/FerretDB/issues/153). ## Aggregation pipelines -Related [issue](https://github.com/FerretDB/FerretDB/issues/9). +Related [issue](https://github.com/FerretDB/FerretDB/issues/1917). | Command | Argument | Status | Comments | | ----------- | -------- | ------ | -------- | @@ -297,242 +300,223 @@ Related [issue](https://github.com/FerretDB/FerretDB/issues/9). | `count` | | βœ… | | | `distinct` | | βœ… | | - - - -
-Stages and operators - -#### Aggregation collection stages - -```js -db.collection.aggregate() -``` - -| Stage | Status | Comments | -| ------------------------------ | ------ | --------------------------------------------------------- | -| `$addFields`, `$set` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1413) | -| `$bucket`, `$bucketAuto` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1414) | -| `$changeStream` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1415) | -| `$collStats` | ❌ | Fields `count` and `storageStats` are supported | -| `$count` | βœ…οΈ | | -| `$densify` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1418) | -| `$documents` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1419) | -| `$facet` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1420) | -| `$fill` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1421) | -| `$geoNear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1412) | -| `$graphLookup` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1422) | -| `$group` | βœ…οΈ | | -| `$indexStats` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1424) | -| `$limit` | βœ…οΈ | | -| `$listSessions` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1426) | -| `$lookup` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1427) | -| `$match` | βœ… | | -| `$merge` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1429) | -| `$out` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1430) | -| `$planCacheStats` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1431) | -| `$project` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/2382) | -| `$redact` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1433) | -| `$replaceRoot`, `$replaceWith` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1434) | -| `$sample` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1435) | -| `$search`, `$searchMeta` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1436) | -| `$setWindowFields` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1437) | -| `$skip` | βœ…οΈ | | -| `$sort` | βœ…οΈ | | -| `$sortByCount` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1440) | -| `$unionWith` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1441) | -| `$unset` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1432) | -| `$unwind` | βœ…οΈ | | - -#### Aggregation database stages - -```js -db.aggregate() -``` +### Aggregation pipeline stages | Stage | Status | Comments | | -------------------- | ------ | --------------------------------------------------------- | +| `$addFields` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1413) | +| `$bucket` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1414) | +| `$bucketAuto` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1414) | | `$changeStream` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1415) | +| `$changeStream` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1415) | +| `$collStats` | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/2447) | +| `$count` | βœ…οΈ | | | `$currentOp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1444) | -| `$listLocalSessions` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1426) | +| `$densify` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1418) | | `$documents` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1419) | - -#### Aggregation pipeline operators - -| Operator | Status | Comments | -| --------------------------------- | ------ | --------------------------------------------------------- | -| `$abs` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$accumulator` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$acos` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$acosh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$add` (arithmetic operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$add` (date operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$addToSet` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$allElementsTrue` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$and` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | -| `$anyElementTrue` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$arrayElemAt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$arrayToObject` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$asin` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$asinh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$atan` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$atan2` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$atanh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$avg` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$binarySize` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1459) | -| `$bottom` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$bottomN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$bsonSize` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1459) | -| `$ceil` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$cmp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$concat` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$concatArrays` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$cond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | -| `$convert` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$cos` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$cosh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$count` | βœ…οΈ | | -| `$covariancePop` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$covarianceSamp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$dateAdd` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateDiff` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateFromParts` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateSubtract` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateTrunc` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateToParts` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateFromString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dateToString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dayOfMonth` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dayOfWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$dayOfYear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$degreesToRadians` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$denseRank` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$derivative` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$divide` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$documentNumber` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$eq` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$exp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$expMovingAvg` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$filter` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$first` (array operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$first` (accumulator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$firstN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$floor` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$function` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1458) | -| `$getField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1471) | -| `$gt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$gte` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$hour` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$ifNull` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | -| `$in` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$indexOfArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$indexOfBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$indexOfCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$integral` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$isArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$isNumber` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$isoDayOfWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$isoWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$isoWeekYear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$last` (array operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$last` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$lastN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$let` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1469) | -| `$linearFill` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$literal` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1470) | -| `$ln` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$locf` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$log` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$log10` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$lt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$lte` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$ltrim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$map` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$max` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$maxN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$mergeObjects` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$meta` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$min` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$minN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$millisecond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$minute` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$mod` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$month` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$multiply` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$ne` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | -| `$not` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | -| `$objectToArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | -| `$or` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | -| `$pow` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$push` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$radiansToDegrees` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$rand` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/541) | -| `$range` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$rank` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$reduce` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$regexFind` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$regexFindAll` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$regexMatch` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$replaceOne` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$replaceAll` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$reverseArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$round` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$rtrim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$sampleRate` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1472) | -| `$second` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$setDifference` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$setEquals` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$setField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | -| `$setIntersection` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$setIsSubset` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$setUnion` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | -| `$shift` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | -| `$size` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$sin` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$sinh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$slice` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$sortArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | -| `$split` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$sqrt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$stdDevPop` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$stdDevSamp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$strcasecmp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$strLenBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$strLenCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$substr` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$substrBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$substrCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$subtract` (arithmetic operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$subtract` (date operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$sum` | βœ…οΈ | | -| `$switch` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | -| `$tan` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$tanh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | -| `$toBool` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toDate` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$toDecimal` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toDouble` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toInt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toLong` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toObjectId` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$top` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$topN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | -| `$toString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$toLower` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$toUpper` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$trim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | -| `$trunc` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | -| `$tsIncrement` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1464) | -| `$tsSecond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1464) | -| `$type` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | -| `$unsetField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | -| `$week` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$year` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | -| `$zip` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | - -
- - +| `$documents` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1419) | +| `$facet` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1420) | +| `$fill` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1421) | +| `$geoNear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1412) | +| `$graphLookup` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1422) | +| `$group` | βœ…οΈ | | +| `$indexStats` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1424) | +| `$limit` | βœ…οΈ | | +| `$listLocalSessions` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1426) | +| `$listSessions` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1426) | +| `$lookup` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1427) | +| `$match` | βœ… | | +| `$merge` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1429) | +| `$out` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1430) | +| `$planCacheStats` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1431) | +| `$project` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/2382) | +| `$redact` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1433) | +| `$replaceRoot` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1434) | +| `$replaceWith` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1434) | +| `$sample` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1435) | +| `$search` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1436) | +| `$searchMeta` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1436) | +| `$set` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1413) | +| `$setWindowFields` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1437) | +| `$skip` | βœ…οΈ | | +| `$sort` | βœ…οΈ | | +| `$sortByCount` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1440) | +| `$unionWith` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1441) | +| `$unset` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1432) | +| `$unwind` | βœ…οΈ | | + +### Aggregation pipeline operators + +| Operator | Status | Comments | +| ------------------------- | ------ | --------------------------------------------------------- | +| `$abs` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$accumulator` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$acos` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$acosh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$add` (arithmetic) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$add` (date) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$addToSet` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$allElementsTrue` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$and` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | +| `$anyElementTrue` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$arrayElemAt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$arrayToObject` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$asin` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$asinh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$atan` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$atan2` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$atanh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$avg` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$binarySize` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1459) | +| `$bottom` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$bottomN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$bsonSize` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1459) | +| `$ceil` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$cmp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$concat` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$concatArrays` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$cond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | +| `$convert` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$cos` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$cosh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$count` | βœ…οΈ | | +| `$covariancePop` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$covarianceSamp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$dateAdd` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateDiff` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateFromParts` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateFromString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateSubtract` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateToParts` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateToString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dateTrunc` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dayOfMonth` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dayOfWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$dayOfYear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$degreesToRadians` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$denseRank` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$derivative` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$divide` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$documentNumber` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$eq` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$exp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$expMovingAvg` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$filter` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$first` (accumulator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$first` (array operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$firstN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$floor` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$function` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1458) | +| `$getField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1471) | +| `$gt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$gte` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$hour` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$ifNull` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | +| `$in` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$indexOfArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$indexOfBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$indexOfCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$integral` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$isArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$isNumber` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$isoDayOfWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$isoWeek` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$isoWeekYear` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$last` (accumulator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$last` (array operator) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$lastN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$let` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1469) | +| `$linearFill` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$literal` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1470) | +| `$ln` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$locf` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$log` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$log10` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$lt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$lte` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$ltrim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$map` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$max` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$maxN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$mergeObjects` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$meta` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$millisecond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$min` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$minN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$minute` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$mod` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$month` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$multiply` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$ne` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1456) | +| `$not` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | +| `$objectToArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | +| `$or` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1455) | +| `$pow` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$push` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$radiansToDegrees` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$rand` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/541) | +| `$range` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$rank` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$reduce` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$regexFind` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$regexFindAll` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$regexMatch` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$replaceAll` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$replaceOne` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$reverseArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$round` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$rtrim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$sampleRate` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1472) | +| `$second` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$setDifference` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$setEquals` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$setField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | +| `$setIntersection` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$setIsSubset` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$setUnion` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1462) | +| `$shift` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1468) | +| `$sin` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$sinh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$size` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$slice` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$sortArray` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | +| `$split` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$sqrt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$stdDevPop` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$stdDevSamp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$strcasecmp` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$strLenBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$strLenCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$substr` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$substrBytes` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$substrCP` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$subtract` (arithmetic) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$subtract` (date) | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$sum` | βœ…οΈ | | +| `$switch` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1457) | +| `$tan` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$tanh` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1465) | +| `$toBool` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toDate` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$toDecimal` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toDouble` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toInt` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toLong` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toLower` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$toObjectId` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$top` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$topN` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1467) | +| `$toString` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$toUpper` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$trim` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1463) | +| `$trunc` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1453) | +| `$tsIncrement` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1464) | +| `$tsSecond` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1464) | +| `$type` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1466) | +| `$unsetField` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1461) | +| `$week` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$year` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1460) | +| `$zip` | ❌ | [Issue](https://github.com/FerretDB/FerretDB/issues/1454) | ## Administration commands diff --git a/website/docs/security.md b/website/docs/security.md index e985024238ac..2b00a36f98f3 100644 --- a/website/docs/security.md +++ b/website/docs/security.md @@ -33,6 +33,6 @@ The default username and password can be specified in FerretDB's connection stri but the client could use a different user by providing a username and password in MongoDB URI. For example, if the server was started with `postgres://user1:pass1@postgres:5432/ferretdb`, anonymous clients will be authenticated as user1, -but clients that use `mongodb://user2:pass2@ferretdb:27018/?tls=true&authMechanism=PLAIN` MongoDB URI will be authenticated as user2. +but clients that use `mongodb://user2:pass2@ferretdb:27018/ferretdb?tls=true&authMechanism=PLAIN` MongoDB URI will be authenticated as user2. Since usernames and passwords are transferred in plain text, the use of TLS is highly recommended. diff --git a/website/docs/understanding-ferretdb.md b/website/docs/understanding-ferretdb.md index 9bfd4861cb21..3a339172ce7e 100644 --- a/website/docs/understanding-ferretdb.md +++ b/website/docs/understanding-ferretdb.md @@ -158,9 +158,20 @@ As with any database, before moving to production, please verify if it is suitab ### PostgreSQL PostgreSQL backend is our main backend and is fully supported. -MongoDB collections are mapped to PostgreSQL tables, -and MongoDB documents are mapped to rows with a single [JSONB](https://www.postgresql.org/docs/current/datatype-json.html) column. -The physical layout might change as we work on improving compatibility and performance. + +PostgreSQL should be configured with `UTF8` encoding and one of the following locales: +`POSIX`, `C`, `C.UTF8`, `en_US.UTF8`. + +MongoDB databases are mapped to PostgreSQL schemas in a single PostgreSQL database that should be created in advance. +MongoDB collections are mapped to PostgreSQL tables. +MongoDB documents are mapped to rows with a single [JSONB](https://www.postgresql.org/docs/current/datatype-json.html) column. +Those mappings might change as we work on improving compatibility and performance, +but no breaking changes will be introduced without a major version bump. + +### SQLite (alpha) + +We are [working on](https://github.com/FerretDB/FerretDB/issues/2387) SQLite backend. +It is not officially supported yet. ### Tigris (beta) diff --git a/website/static/img/blog/ferretdb-1-1-0-release.png b/website/static/img/blog/ferretdb-1-1-0-release.png new file mode 100644 index 000000000000..2a98d4abb884 --- /dev/null +++ b/website/static/img/blog/ferretdb-1-1-0-release.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a58dc84518f89f93e79d637a23bcb2e87705fa5af1b158dcef955d2f70514e94 +size 697026 diff --git a/website/static/img/blog/ferretdb-meteor.jpg b/website/static/img/blog/ferretdb-meteor.jpg new file mode 100644 index 000000000000..cfa824a0be5d --- /dev/null +++ b/website/static/img/blog/ferretdb-meteor.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cb6499a74fb54f25518a8a71abd6ab6554e14d73d44afcc3a415989dcd21da4 +size 51869