Compare commits
174 Commits
alternator
...
next-2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b252bba4a2 | ||
|
|
a0b9fcc041 | ||
|
|
35c9b675c1 | ||
|
|
d71836fef7 | ||
|
|
f8e150e97c | ||
|
|
10c300f894 | ||
|
|
de1d3e5c6b | ||
|
|
69810c13ca | ||
|
|
9b025a5742 | ||
|
|
74eebc4cab | ||
|
|
9b2ca4ee44 | ||
|
|
773bf45774 | ||
|
|
c6705b4335 | ||
|
|
3997871b4d | ||
|
|
4ff1d731bd | ||
|
|
0e0f9143c9 | ||
|
|
9d809d6ea4 | ||
|
|
630d599c34 | ||
|
|
0933c1a00a | ||
|
|
7a7099fcfb | ||
|
|
50235aacb4 | ||
|
|
e888009f12 | ||
|
|
a19615ee9b | ||
|
|
357ca67fda | ||
|
|
7818c63eb1 | ||
|
|
da10eae18c | ||
|
|
d5292cd3ec | ||
|
|
9cb35361d9 | ||
|
|
3e285248be | ||
|
|
6f10ccb441 | ||
|
|
df420499bc | ||
|
|
d29527b4e1 | ||
|
|
8a90e242e4 | ||
|
|
8a78c0aba9 | ||
|
|
8a2bbcf138 | ||
|
|
22c891e6df | ||
|
|
1841d0c2d9 | ||
|
|
e10107fe5a | ||
|
|
0b3a4679db | ||
|
|
ba60d666a9 | ||
|
|
6ea4d0b75c | ||
|
|
8c5911f312 | ||
|
|
de00d7f5a1 | ||
|
|
e5f9dae4bb | ||
|
|
e13e796290 | ||
|
|
336c771663 | ||
|
|
82968afc25 | ||
|
|
383dcffb53 | ||
|
|
0c2abc007c | ||
|
|
1498c4f150 | ||
|
|
f388992a94 | ||
|
|
310540c11f | ||
|
|
7d833023cc | ||
|
|
d94ac196e0 | ||
|
|
1d7430995e | ||
|
|
b662a7f8a4 | ||
|
|
447ad72882 | ||
|
|
b8485d3bce | ||
|
|
034b0f50db | ||
|
|
12ec0becf3 | ||
|
|
666b19552d | ||
|
|
178f870a03 | ||
|
|
1b18f16dc1 | ||
|
|
28934575e4 | ||
|
|
182cbeefb0 | ||
|
|
b70fc41a90 | ||
|
|
debfc795b2 | ||
|
|
0d094575ec | ||
|
|
20baef69a9 | ||
|
|
1bac88601d | ||
|
|
e581fd1463 | ||
|
|
b366bff998 | ||
|
|
38e6984ba5 | ||
|
|
332f76579e | ||
|
|
315a03cf6c | ||
|
|
1847dc7a6a | ||
|
|
dd11b5987e | ||
|
|
a134e8699a | ||
|
|
bd7dcbb8d2 | ||
|
|
74e61528a6 | ||
|
|
5eb4fde2d5 | ||
|
|
cc0703f8ca | ||
|
|
678283a5bb | ||
|
|
552c0d7641 | ||
|
|
860c06660b | ||
|
|
db733ba075 | ||
|
|
88677d39c8 | ||
|
|
d767dee5ec | ||
|
|
702f6ee1b7 | ||
|
|
473b9aec65 | ||
|
|
b548061257 | ||
|
|
01165a9ae7 | ||
|
|
5cdb963768 | ||
|
|
7c9b9a4e24 | ||
|
|
f475c65ae6 | ||
|
|
687372bc48 | ||
|
|
65c140121c | ||
|
|
ed68ad220f | ||
|
|
35f4b8fbbe | ||
|
|
48012fe418 | ||
|
|
c862ccda91 | ||
|
|
83b1057c4b | ||
|
|
c1cb779dd2 | ||
|
|
b47d18f9fd | ||
|
|
f8713b019e | ||
|
|
cd5e4eace5 | ||
|
|
4fb5403670 | ||
|
|
e9df6c42ce | ||
|
|
5fdf492ccc | ||
|
|
fd2b02a12c | ||
|
|
f8cec2f891 | ||
|
|
e4d6577ef2 | ||
|
|
346027248d | ||
|
|
2cf6191353 | ||
|
|
b52d647de2 | ||
|
|
f7c96a37f1 | ||
|
|
ae71ffdcfd | ||
|
|
a235900388 | ||
|
|
be9f150341 | ||
|
|
2478fa1f6e | ||
|
|
d95ac1826e | ||
|
|
6fc17345e9 | ||
|
|
4bfa0ae247 | ||
|
|
174b7870e6 | ||
|
|
e95b4ee825 | ||
|
|
464305de1c | ||
|
|
3a1a9e1a11 | ||
|
|
90dac5d944 | ||
|
|
e5a83d105c | ||
|
|
9b4a0a2879 | ||
|
|
adad12ddc3 | ||
|
|
a77bb1fe34 | ||
|
|
3c7e6dfdb9 | ||
|
|
fab136ae1d | ||
|
|
a4218f536b | ||
|
|
9f4431ef04 | ||
|
|
66250bf8cc | ||
|
|
88fe3c2694 | ||
|
|
db4c3d3e52 | ||
|
|
ca22a1cd1a | ||
|
|
f9b702764e | ||
|
|
54701bd95c | ||
|
|
30eca5f534 | ||
|
|
cd057d3882 | ||
|
|
c5a5a2265e | ||
|
|
3e482c6c9d | ||
|
|
5b6cadb890 | ||
|
|
9cf8cd6c02 | ||
|
|
b34567b69b | ||
|
|
02b763ed97 | ||
|
|
05500a52d7 | ||
|
|
4afa558e97 | ||
|
|
f3956421f7 | ||
|
|
a17a6ce8f5 | ||
|
|
58a362c1f2 | ||
|
|
361b2dd7a5 | ||
|
|
f6a2bafae2 | ||
|
|
2ec25a55cd | ||
|
|
d3fb7c5515 | ||
|
|
b1ac6a36f2 | ||
|
|
8cba125bce | ||
|
|
f46f9f7533 | ||
|
|
090d991f8e | ||
|
|
ae15a80d01 | ||
|
|
6cf902343a | ||
|
|
d5e59f671c | ||
|
|
38944655c5 | ||
|
|
06e274ff34 | ||
|
|
c24d4a8acb | ||
|
|
5f95b76c65 | ||
|
|
0bdb7e1e7c | ||
|
|
56ea4f3154 | ||
|
|
d9c178063c | ||
|
|
b21b7f73b9 |
@@ -1,3 +0,0 @@
|
||||
.git
|
||||
build
|
||||
seastar/build
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Scylla doesn't use pull-requests, please send a patch to the [mailing list](mailto:scylladb-dev@googlegroups.com) instead.
|
||||
See our [contributing guidelines](../CONTRIBUTING.md) and our [Scylla development guidelines](../HACKING.md) for more information.
|
||||
|
||||
If you have any questions please don't hesitate to send a mail to the [dev list](mailto:scylladb-dev@googlegroups.com).
|
||||
11
.gitmodules
vendored
11
.gitmodules
vendored
@@ -1,17 +1,14 @@
|
||||
[submodule "seastar"]
|
||||
path = seastar
|
||||
url = ../seastar
|
||||
url = ../scylla-seastar
|
||||
ignore = dirty
|
||||
[submodule "swagger-ui"]
|
||||
path = swagger-ui
|
||||
url = ../scylla-swagger-ui
|
||||
ignore = dirty
|
||||
[submodule "dist/ami/files/scylla-ami"]
|
||||
path = dist/ami/files/scylla-ami
|
||||
url = ../scylla-ami
|
||||
[submodule "xxHash"]
|
||||
path = xxHash
|
||||
url = ../xxHash
|
||||
[submodule "libdeflate"]
|
||||
path = libdeflate
|
||||
url = ../libdeflate
|
||||
[submodule "zstd"]
|
||||
path = zstd
|
||||
url = ../zstd
|
||||
|
||||
@@ -138,5 +138,4 @@ target_include_directories(scylla PUBLIC
|
||||
${SEASTAR_INCLUDE_DIRS}
|
||||
${Boost_INCLUDE_DIRS}
|
||||
xxhash
|
||||
libdeflate
|
||||
build/release/gen)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Asking questions or requesting help
|
||||
|
||||
Use the [ScyllaDB user mailing list](https://groups.google.com/forum/#!forum/scylladb-users) or the [Slack workspace](http://slack.scylladb.com) for general questions and help.
|
||||
Use the [ScyllaDB user mailing list](https://groups.google.com/forum/#!forum/scylladb-users) for general questions and help.
|
||||
|
||||
# Reporting an issue
|
||||
|
||||
|
||||
95
HACKING.md
95
HACKING.md
@@ -20,22 +20,11 @@ $ git submodule update --init --recursive
|
||||
|
||||
Scylla depends on the system package manager for its development dependencies.
|
||||
|
||||
Running `./install-dependencies.sh` (as root) installs the appropriate packages based on your Linux distribution.
|
||||
|
||||
On Ubuntu and Debian based Linux distributions, some packages
|
||||
required to build Scylla are missing in the official upstream:
|
||||
|
||||
- libthrift-dev and libthrift
|
||||
- antlr3-c++-dev
|
||||
|
||||
Try running ```sudo ./scripts/scylla_current_repo``` to add Scylla upstream,
|
||||
and get the missing packages from it.
|
||||
Running `./install_dependencies.sh` (as root) installs the appropriate packages based on your Linux distribution.
|
||||
|
||||
### Build system
|
||||
|
||||
**Note**: Compiling Scylla requires, conservatively, 2 GB of memory per native
|
||||
thread, and up to 3 GB per native thread while linking. GCC >= 8.1.1. is
|
||||
required.
|
||||
**Note**: Compiling Scylla requires, conservatively, 2 GB of memory per native thread, and up to 3 GB per native thread while linking.
|
||||
|
||||
Scylla is built with [Ninja](https://ninja-build.org/), a low-level rule-based system. A Python script, `configure.py`, generates a Ninja file (`build.ninja`) based on configuration options.
|
||||
|
||||
@@ -54,7 +43,9 @@ The full suite of options for project configuration is available via
|
||||
$ ./configure.py --help
|
||||
```
|
||||
|
||||
The most important option is:
|
||||
The most important options are:
|
||||
|
||||
- `--mode={release,debug,all}`: Debug mode enables [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) and allows for debugging with tools like GDB. Debugging builds are generally slower and generate much larger object files than release builds.
|
||||
|
||||
- `--{enable,disable}-dpdk`: [DPDK](http://dpdk.org/) is a set of libraries and drivers for fast packet processing. During development, it's not necessary to enable support even if it is supported by your platform.
|
||||
|
||||
@@ -64,30 +55,6 @@ To save time -- for instance, to avoid compiling all unit tests -- you can also
|
||||
|
||||
```bash
|
||||
$ ninja-build build/release/tests/schema_change_test
|
||||
$ ninja-build build/release/service/storage_proxy.o
|
||||
```
|
||||
|
||||
You can also specify a single mode. For example
|
||||
|
||||
```bash
|
||||
$ ninja-build release
|
||||
```
|
||||
|
||||
Will build everytihng in release mode. The valid modes are
|
||||
|
||||
* Debug: Enables [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer)
|
||||
and other sanity checks. It has no optimizations, which allows for debugging with tools like
|
||||
GDB. Debugging builds are generally slower and generate much larger object files than release builds.
|
||||
* Release: Fewer checks and more optimizations. It still has debug info.
|
||||
* Dev: No optimizations or debug info. The objective is to compile and link as fast as possible.
|
||||
This is useful for the first iterations of a patch.
|
||||
|
||||
|
||||
Note that by default unit tests binaries are stripped so they can't be used with gdb or seastar-addr2line.
|
||||
To include debug information in the unit test binary, build the test binary with a `_g` suffix. For example,
|
||||
|
||||
```bash
|
||||
$ ninja-build build/release/tests/schema_change_test_g
|
||||
```
|
||||
|
||||
### Unit testing
|
||||
@@ -116,7 +83,7 @@ The `-c1 -m1G` arguments limit this Seastar-based test to a single system thread
|
||||
|
||||
### Preparing patches
|
||||
|
||||
All changes to Scylla are submitted as patches to the public [mailing list](mailto:scylladb-dev@googlegroups.com). Once a patch is approved by one of the maintainers of the project, it is committed to the maintainers' copy of the repository at https://github.com/scylladb/scylla.
|
||||
All changes to Scylla are submitted as patches to the public mailing list. Once a patch is approved by one of the maintainers of the project, it is committed to the maintainers' copy of the repository at https://github.com/scylladb/scylla.
|
||||
|
||||
Detailed instructions for formatting patches for the mailing list and advice on preparing good patches are available at the [ScyllaDB website](http://docs.scylladb.com/contribute/). There are also some guidelines that can help you make the patch review process smoother:
|
||||
|
||||
@@ -145,8 +112,6 @@ The usual is "Tests: unit (release)", although running debug tests is encouraged
|
||||
|
||||
5. When answering review comments, prefer inline quotes as they make it easier to track the conversation across multiple e-mails.
|
||||
|
||||
6. The Linux kernel's [Submitting Patches](https://www.kernel.org/doc/html/v4.19/process/submitting-patches.html) document offers excellent advice on how to prepare patches and patchsets for review. Since the Scylla development process is derived from the kernel's, almost all of the advice there is directly applicable.
|
||||
|
||||
### Finding a person to review and merge your patches
|
||||
|
||||
You can use the `scripts/find-maintainer` script to find a subsystem maintainer and/or reviewer for your patches. The script accepts a filename in the git source tree as an argument and outputs a list of subsystems the file belongs to and their respective maintainers and reviewers. For example, if you changed the `cql3/statements/create_view_statement.hh` file, run the script as follows:
|
||||
@@ -199,29 +164,6 @@ On a development machine, one might run Scylla as
|
||||
$ SCYLLA_HOME=$HOME/scylla build/release/scylla --overprovisioned --developer-mode=yes
|
||||
```
|
||||
|
||||
To interact with scylla it is recommended to build our versions of
|
||||
cqlsh and nodetool. They are available at
|
||||
https://github.com/scylladb/scylla-tools-java and can be built with
|
||||
|
||||
```bash
|
||||
$ sudo ./install-dependencies.sh
|
||||
$ ant jar
|
||||
```
|
||||
|
||||
cqlsh should work out of the box, but nodetool depends on a running
|
||||
scylla-jmx (https://github.com/scylladb/scylla-jmx). It can be build
|
||||
with
|
||||
|
||||
```bash
|
||||
$ mvn package
|
||||
```
|
||||
|
||||
and must be started with
|
||||
|
||||
```bash
|
||||
$ ./scripts/scylla-jmx
|
||||
```
|
||||
|
||||
### Branches and tags
|
||||
|
||||
Multiple release branches are maintained on the Git repository at https://github.com/scylladb/scylla. Release 1.5, for instance, is tracked on the `branch-1.5` branch.
|
||||
@@ -312,7 +254,7 @@ In this example, `10.0.0.2` will be sent up to 16 jobs and the local machine wil
|
||||
|
||||
When a compilation is in progress, the status of jobs on all remote machines can be visualized in the terminal with `distccmon-text` or graphically as a GTK application with `distccmon-gnome`.
|
||||
|
||||
One thing to keep in mind is that linking object files happens on the coordinating machine, which can be a bottleneck. See the next sections speeding up this process.
|
||||
One thing to keep in mind is that linking object files happens on the coordinating machine, which can be a bottleneck. See the next section speeding up this process.
|
||||
|
||||
### Using the `gold` linker
|
||||
|
||||
@@ -322,24 +264,6 @@ Linking Scylla can be slow. The gold linker can replace GNU ld and often speeds
|
||||
$ sudo alternatives --config ld
|
||||
```
|
||||
|
||||
### Using split dwarf
|
||||
|
||||
With debug info enabled, most of the link time is spent copying and
|
||||
relocating it. It is possible to leave most of the debug info out of
|
||||
the link by writing it to a side .dwo file. This is done by passing
|
||||
`-gsplit-dwarf` to gcc.
|
||||
|
||||
Unfortunately just `-gsplit-dwarf` would slow down `gdb` startup. To
|
||||
avoid that the gold linker can be told to create an index with
|
||||
`--gdb-index`.
|
||||
|
||||
More info at https://gcc.gnu.org/wiki/DebugFission.
|
||||
|
||||
Both options can be enable by passing `--split-dwarf` to configure.py.
|
||||
|
||||
Note that distcc is *not* compatible with it, but icecream
|
||||
(https://github.com/icecc/icecream) is.
|
||||
|
||||
### Testing changes in Seastar with Scylla
|
||||
|
||||
Sometimes Scylla development is closely tied with a feature being developed in Seastar. It can be useful to compile Scylla with a particular check-out of Seastar.
|
||||
@@ -353,8 +277,3 @@ $ git remote add local /home/tsmith/src/seastar
|
||||
$ git remote update
|
||||
$ git checkout -t local/my_local_seastar_branch
|
||||
```
|
||||
|
||||
### Core dump debugging
|
||||
|
||||
Slides:
|
||||
2018.11.20: https://www.slideshare.net/tomekgrabiec/scylla-core-dump-debugging-tools
|
||||
|
||||
21
README.md
21
README.md
@@ -2,23 +2,17 @@
|
||||
|
||||
## Quick-start
|
||||
|
||||
To get the build going quickly, Scylla offers a [frozen toolchain](tools/toolchain/README.md)
|
||||
which would build and run Scylla using a pre-configured Docker image.
|
||||
Using the frozen toolchain will also isolate all of the installed
|
||||
dependencies in a Docker container.
|
||||
Assuming you have met the toolchain prerequisites, which is running
|
||||
Docker in user mode, building and running is as easy as:
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
$ git submodule update --init --recursive
|
||||
$ sudo ./install-dependencies.sh
|
||||
$ ./configure.py --mode=release
|
||||
$ ninja-build -j4 # Assuming 4 system threads.
|
||||
$ ./build/release/scylla
|
||||
$ # Rejoice!
|
||||
```
|
||||
|
||||
Please see [HACKING.md](HACKING.md) for detailed information on building and developing Scylla.
|
||||
|
||||
**Note**: GCC >= 8.1.1 is required to compile Scylla.
|
||||
|
||||
## Running Scylla
|
||||
|
||||
* Run Scylla
|
||||
@@ -81,5 +75,4 @@ docker run -p $(hostname -i):9042:9042 -i -t <image name>
|
||||
|
||||
## Contributing to Scylla
|
||||
|
||||
[Hacking howto](HACKING.md)
|
||||
[Guidelines for contributing](CONTRIBUTING.md)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
PRODUCT=scylla
|
||||
VERSION=666.development
|
||||
VERSION=2.3.6
|
||||
|
||||
if test -f version
|
||||
then
|
||||
@@ -23,4 +22,3 @@ echo "$SCYLLA_VERSION-$SCYLLA_RELEASE"
|
||||
mkdir -p build
|
||||
echo "$SCYLLA_VERSION" > build/SCYLLA-VERSION-FILE
|
||||
echo "$SCYLLA_RELEASE" > build/SCYLLA-RELEASE-FILE
|
||||
echo "$PRODUCT" > build/SCYLLA-PRODUCT-FILE
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
Tests for Alternator that should also pass, identically, against DynamoDB.
|
||||
|
||||
Tests use the boto3 library for AWS API, and the pytest frameworks
|
||||
(both are available from Linux distributions, or with "pip install").
|
||||
|
||||
To run all tests against the local installation of Alternator on
|
||||
http://localhost:8000, just run `pytest`.
|
||||
|
||||
Some additional pytest options:
|
||||
* To run all tests in a single file, do `pytest test_table.py`.
|
||||
* To run a single specific test, do `pytest test_table.py::test_create_table_unsupported_names`.
|
||||
* Additional useful pytest options, especially useful for debugging tests:
|
||||
* -v: show the names of each individual test running instead of just dots.
|
||||
* -s: show the full output of running tests (by default, pytest captures the test's output and only displays it if a test fails)
|
||||
|
||||
Add the `--aws` option to test against AWS instead of the local installation.
|
||||
For example - `pytest --aws test_item.py` or `pytest --aws`.
|
||||
|
||||
If you plan to run tests against AWS and not just a local Scylla installation,
|
||||
the files ~/.aws/credentials should be configured with your AWS key:
|
||||
|
||||
```
|
||||
[default]
|
||||
aws_access_key_id = XXXXXXXXXXXXXXXXXXXX
|
||||
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
and ~/.aws/config with the default region to use in the test:
|
||||
```
|
||||
[default]
|
||||
region = us-east-1
|
||||
```
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This file contains "test fixtures", a pytest concept described in
|
||||
# https://docs.pytest.org/en/latest/fixture.html.
|
||||
# A "fixture" is some sort of setup which an invididual test requires to run.
|
||||
# The fixture has setup code and teardown code, and if multiple tests
|
||||
# require the same fixture, it can be set up only once - while still allowing
|
||||
# the user to run individual tests and automatically set up the fixtures they need.
|
||||
|
||||
import pytest
|
||||
import boto3
|
||||
from util import create_test_table
|
||||
|
||||
# Test that the Boto libraries are new enough. These tests want to test a
|
||||
# large variety of DynamoDB API features, and to do this we need a new-enough
|
||||
# version of the the Boto libraries (boto3 and botocore) so that they can
|
||||
# access all these API features.
|
||||
# In particular, the BillingMode feature was added in botocore 1.12.54.
|
||||
import botocore
|
||||
import sys
|
||||
from distutils.version import LooseVersion
|
||||
if (LooseVersion(botocore.__version__) < LooseVersion('1.12.54')):
|
||||
pytest.exit("Your Boto library is too old. Please upgrade it,\ne.g. using:\n sudo pip{} install --upgrade boto3".format(sys.version_info[0]))
|
||||
|
||||
# By default, tests run against a local Scylla installation on localhost:8080/.
|
||||
# The "--aws" option can be used to run against Amazon DynamoDB in the us-east-1
|
||||
# region.
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--aws", action="store_true",
|
||||
help="run against AWS instead of a local Scylla installation")
|
||||
|
||||
# "dynamodb" fixture: set up client object for communicating with the DynamoDB
|
||||
# API. Currently this chooses either Amazon's DynamoDB in the default region
|
||||
# or a local Alternator installation on http://localhost:8080 - depending on the
|
||||
# existence of the "--aws" option. In the future we should provide options
|
||||
# for choosing other Amazon regions or local installations.
|
||||
# We use scope="session" so that all tests will reuse the same client object.
|
||||
@pytest.fixture(scope="session")
|
||||
def dynamodb(request):
|
||||
if request.config.getoption('aws'):
|
||||
return boto3.resource('dynamodb')
|
||||
else:
|
||||
# Even though we connect to the local installation, Boto3 still
|
||||
# requires us to specify dummy region and credential parameters,
|
||||
# otherwise the user is forced to properly configure ~/.aws even
|
||||
# for local runs.
|
||||
return boto3.resource('dynamodb', endpoint_url='http://localhost:8000',
|
||||
region_name='us-east-1', aws_access_key_id='whatever', aws_secret_access_key='whatever')
|
||||
|
||||
# "test_table" fixture: Create and return a temporary table to be used in tests
|
||||
# that need a table to work on. The table is automatically deleted at the end.
|
||||
# We use scope="session" so that all tests will reuse the same client object.
|
||||
# This "test_table" creates a table which has a specific key schema: both a
|
||||
# partition key and a sort key, and both are strings. Other fixtures (below)
|
||||
# can be used to create different types of tables.
|
||||
#
|
||||
# TODO: Although we are careful about deleting temporary tables when the
|
||||
# fixture is torn down, in some cases (e.g., interrupted tests) we can be left
|
||||
# with some tables not deleted, and they will never be deleted. Because all
|
||||
# our temporary tables have the same test_table_prefix, we can actually find
|
||||
# and remove these old tables with this prefix. We can have a fixture, which
|
||||
# test_table will require, which on teardown will delete all remaining tables
|
||||
# (possibly from an older run). Because the table's name includes the current
|
||||
# time, we can also remove just tables older than a particular age. Such
|
||||
# mechanism will allow running tests in parallel, without the risk of deleting
|
||||
# a parallel run's temporary tables.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
])
|
||||
yield table
|
||||
# We get back here when this fixture is torn down. We ask Dynamo to delete
|
||||
# this table, but not wait for the deletion to complete. The next time
|
||||
# we create a test_table fixture, we'll choose a different table name
|
||||
# anyway.
|
||||
table.delete()
|
||||
|
||||
# The following fixtures test_table_* are similar to test_table but create
|
||||
# tables with different key schemas.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_s(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_b(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'B' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_sb(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'B' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_sn(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'N' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
# "filled_test_table" fixture: Create a temporary table to be used in tests
|
||||
# that involve reading data - GetItem, Scan, etc. The table is filled with
|
||||
# 328 items - each consisting of a partition key, clustering key and two
|
||||
# string attributes. 164 of the items are in a single partition (with the
|
||||
# partition key 'long') and the 164 other items are each in a separate
|
||||
# partition. Finally, a 329th item is added with different attributes.
|
||||
# This table is supposed to be read from, not updated nor overwritten.
|
||||
# This fixture returns both a table object and the description of all items
|
||||
# inserted into it.
|
||||
@pytest.fixture(scope="session")
|
||||
def filled_test_table(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
])
|
||||
count = 164
|
||||
items = [{
|
||||
'p': str(i),
|
||||
'c': str(i),
|
||||
'attribute': "x" * 7,
|
||||
'another': "y" * 16
|
||||
} for i in range(count)]
|
||||
items = items + [{
|
||||
'p': 'long',
|
||||
'c': str(i),
|
||||
'attribute': "x" * (1 + i % 7),
|
||||
'another': "y" * (1 + i % 16)
|
||||
} for i in range(count)]
|
||||
items.append({'p': 'hello', 'c': 'world', 'str': 'and now for something completely different'})
|
||||
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
|
||||
yield table, items
|
||||
table.delete()
|
||||
@@ -1,253 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for batch operations - BatchWriteItem, BatchReadItem.
|
||||
# Note that various other tests in other files also use these operations,
|
||||
# so they are actually tested by other tests as well.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, full_scan, full_query, multiset
|
||||
|
||||
# Test ensuring that items inserted by a batched statement can be properly extracted
|
||||
# via GetItem. Schema has both hash and sort keys.
|
||||
def test_basic_batch_write_item(test_table):
|
||||
count = 7
|
||||
|
||||
with test_table.batch_writer() as batch:
|
||||
for i in range(count):
|
||||
batch.put_item(Item={
|
||||
'p': "batch{}".format(i),
|
||||
'c': "batch_ck{}".format(i),
|
||||
'attribute': str(i),
|
||||
'another': 'xyz'
|
||||
})
|
||||
|
||||
for i in range(count):
|
||||
item = test_table.get_item(Key={'p': "batch{}".format(i), 'c': "batch_ck{}".format(i)}, ConsistentRead=True)['Item']
|
||||
assert item['p'] == "batch{}".format(i)
|
||||
assert item['c'] == "batch_ck{}".format(i)
|
||||
assert item['attribute'] == str(i)
|
||||
assert item['another'] == 'xyz'
|
||||
|
||||
# Test batch write to a table with only a hash key
|
||||
def test_batch_write_hash_only(test_table_s):
|
||||
items = [{'p': random_string(), 'val': random_string()} for i in range(10)]
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for item in items:
|
||||
assert test_table_s.get_item(Key={'p': item['p']}, ConsistentRead=True)['Item'] == item
|
||||
|
||||
# Test batch delete operation (DeleteRequest): We create a bunch of items, and
|
||||
# then delete them all.
|
||||
def test_batch_write_delete(test_table_s):
|
||||
items = [{'p': random_string(), 'val': random_string()} for i in range(10)]
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for item in items:
|
||||
assert test_table_s.get_item(Key={'p': item['p']}, ConsistentRead=True)['Item'] == item
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.delete_item(Key={'p': item['p']})
|
||||
# Verify that all items are now missing:
|
||||
for item in items:
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': item['p']}, ConsistentRead=True)
|
||||
|
||||
# Test the same batch including both writes and delete. Should be fine.
|
||||
def test_batch_write_and_delete(test_table_s):
|
||||
p1 = random_string()
|
||||
p2 = random_string()
|
||||
test_table_s.put_item(Item={'p': p1})
|
||||
assert 'Item' in test_table_s.get_item(Key={'p': p1}, ConsistentRead=True)
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p2}, ConsistentRead=True)
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.put_item({'p': p2})
|
||||
batch.delete_item(Key={'p': p1})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p1}, ConsistentRead=True)
|
||||
assert 'Item' in test_table_s.get_item(Key={'p': p2}, ConsistentRead=True)
|
||||
|
||||
# It is forbidden to update the same key twice in the same batch.
|
||||
# DynamoDB says "Provided list of item keys contains duplicates".
|
||||
def test_batch_write_duplicate_write(test_table_s, test_table):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.put_item({'p': p})
|
||||
batch.put_item({'p': p})
|
||||
c = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
# But it is fine to touch items with one component the same, but the other not.
|
||||
other = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
batch.put_item({'p': p, 'c': other})
|
||||
batch.put_item({'p': other, 'c': c})
|
||||
|
||||
def test_batch_write_duplicate_delete(test_table_s, test_table):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p})
|
||||
batch.delete_item(Key={'p': p})
|
||||
c = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p, 'c': c})
|
||||
batch.delete_item(Key={'p': p, 'c': c})
|
||||
# But it is fine to touch items with one component the same, but the other not.
|
||||
other = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p, 'c': c})
|
||||
batch.delete_item(Key={'p': p, 'c': other})
|
||||
batch.delete_item(Key={'p': other, 'c': c})
|
||||
|
||||
def test_batch_write_duplicate_write_and_delete(test_table_s, test_table):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p})
|
||||
batch.put_item({'p': p})
|
||||
c = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*duplicates'):
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p, 'c': c})
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
# But it is fine to touch items with one component the same, but the other not.
|
||||
other = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.delete_item(Key={'p': p, 'c': c})
|
||||
batch.put_item({'p': p, 'c': other})
|
||||
batch.put_item({'p': other, 'c': c})
|
||||
|
||||
# Test that BatchWriteItem's PutRequest completely replaces an existing item.
|
||||
# It shouldn't merge it with a previously existing value. See also the same
|
||||
# test for PutItem - test_put_item_replace().
|
||||
def test_batch_put_item_replace(test_table_s, test_table):
|
||||
p = random_string()
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.put_item(Item={'p': p, 'a': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hi'}
|
||||
with test_table_s.batch_writer() as batch:
|
||||
batch.put_item(Item={'p': p, 'b': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hello'}
|
||||
c = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item(Item={'p': p, 'c': c, 'a': 'hi'})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'a': 'hi'}
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item(Item={'p': p, 'c': c, 'b': 'hello'})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'b': 'hello'}
|
||||
|
||||
# Test that if one of the batch's operations is invalid, because a key
|
||||
# column is missing or has the wrong type, the entire batch is rejected
|
||||
# before any write is done.
|
||||
def test_batch_write_invalid_operation(test_table_s):
|
||||
# test key attribute with wrong type:
|
||||
p1 = random_string()
|
||||
p2 = random_string()
|
||||
items = [{'p': p1}, {'p': 3}, {'p': p2}]
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for p in [p1, p2]:
|
||||
assert not 'item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# test missing key attribute:
|
||||
p1 = random_string()
|
||||
p2 = random_string()
|
||||
items = [{'p': p1}, {'x': 'whatever'}, {'p': p2}]
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for p in [p1, p2]:
|
||||
assert not 'item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# Basic test for BatchGetItem, reading several entire items.
|
||||
# Schema has both hash and sort keys.
|
||||
def test_batch_get_item(test_table):
|
||||
items = [{'p': random_string(), 'c': random_string(), 'val': random_string()} for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
keys = [{k: x[k] for k in ('p', 'c')} for x in items]
|
||||
# We use the low-level batch_get_item API for lack of a more convenient
|
||||
# API. At least it spares us the need to encode the key's types...
|
||||
reply = test_table.meta.client.batch_get_item(RequestItems = {test_table.name: {'Keys': keys, 'ConsistentRead': True}})
|
||||
print(reply)
|
||||
got_items = reply['Responses'][test_table.name]
|
||||
assert multiset(got_items) == multiset(items)
|
||||
|
||||
# Same, with schema has just hash key.
|
||||
def test_batch_get_item_hash(test_table_s):
|
||||
items = [{'p': random_string(), 'val': random_string()} for i in range(10)]
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
keys = [{k: x[k] for k in ('p')} for x in items]
|
||||
reply = test_table_s.meta.client.batch_get_item(RequestItems = {test_table_s.name: {'Keys': keys, 'ConsistentRead': True}})
|
||||
got_items = reply['Responses'][test_table_s.name]
|
||||
assert multiset(got_items) == multiset(items)
|
||||
|
||||
# Test what do we get if we try to read two *missing* values in addition to
|
||||
# an existing one. It turns out the missing items are simply not returned,
|
||||
# with no sign they are missing.
|
||||
def test_batch_get_item_missing(test_table_s):
|
||||
p = random_string();
|
||||
test_table_s.put_item(Item={'p': p})
|
||||
reply = test_table_s.meta.client.batch_get_item(RequestItems = {test_table_s.name: {'Keys': [{'p': random_string()}, {'p': random_string()}, {'p': p}], 'ConsistentRead': True}})
|
||||
got_items = reply['Responses'][test_table_s.name]
|
||||
assert got_items == [{'p' : p}]
|
||||
|
||||
# If all the keys requested from a particular table are missing, we still
|
||||
# get a response array for that table - it's just empty.
|
||||
def test_batch_get_item_completely_missing(test_table_s):
|
||||
reply = test_table_s.meta.client.batch_get_item(RequestItems = {test_table_s.name: {'Keys': [{'p': random_string()}], 'ConsistentRead': True}})
|
||||
got_items = reply['Responses'][test_table_s.name]
|
||||
assert got_items == []
|
||||
|
||||
# Test GetItem with AttributesToGet
|
||||
def test_batch_get_item_attributes_to_get(test_table):
|
||||
items = [{'p': random_string(), 'c': random_string(), 'val1': random_string(), 'val2': random_string()} for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
keys = [{k: x[k] for k in ('p', 'c')} for x in items]
|
||||
for wanted in [['p'], ['p', 'c'], ['val1'], ['p', 'val2']]:
|
||||
reply = test_table.meta.client.batch_get_item(RequestItems = {test_table.name: {'Keys': keys, 'AttributesToGet': wanted, 'ConsistentRead': True}})
|
||||
got_items = reply['Responses'][test_table.name]
|
||||
expected_items = [{k: item[k] for k in wanted if k in item} for item in items]
|
||||
assert multiset(got_items) == multiset(expected_items)
|
||||
|
||||
# Test GetItem with ProjectionExpression (just a simple one, with
|
||||
# top-level attributes)
|
||||
def test_batch_get_item_projection_expression(test_table):
|
||||
items = [{'p': random_string(), 'c': random_string(), 'val1': random_string(), 'val2': random_string()} for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
keys = [{k: x[k] for k in ('p', 'c')} for x in items]
|
||||
for wanted in [['p'], ['p', 'c'], ['val1'], ['p', 'val2']]:
|
||||
reply = test_table.meta.client.batch_get_item(RequestItems = {test_table.name: {'Keys': keys, 'ProjectionExpression': ",".join(wanted), 'ConsistentRead': True}})
|
||||
got_items = reply['Responses'][test_table.name]
|
||||
expected_items = [{k: item[k] for k in wanted if k in item} for item in items]
|
||||
assert multiset(got_items) == multiset(expected_items)
|
||||
@@ -1,40 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the ConditionExpression parameter
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string
|
||||
|
||||
# Test that ConditionExpression works as expected
|
||||
@pytest.mark.xfail(reason="ConditionExpression not yet implemented")
|
||||
def test_update_condition_expression(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ConditionExpression='b = :oldval',
|
||||
ExpressionAttributeValues={':val1': 6, ':oldval': 4})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ConditionExpression='b = :oldval',
|
||||
ExpressionAttributeValues={':val1': 8, ':oldval': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 6}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Test for the DescribeEndpoints operation
|
||||
|
||||
import boto3
|
||||
|
||||
# Test that the DescribeEndpoints operation works as expected: that it
|
||||
# returns one endpoint (it may return more, but it never does this in
|
||||
# Amazon), and this endpoint can be used to make more requests.
|
||||
def test_describe_endpoints(dynamodb):
|
||||
endpoints = dynamodb.meta.client.describe_endpoints()['Endpoints']
|
||||
# It is not strictly necessary that only a single endpoint be returned,
|
||||
# but this is what Amazon DynamoDB does today (and so does Alternator).
|
||||
assert len(endpoints) == 1
|
||||
for endpoint in endpoints:
|
||||
assert 'CachePeriodInMinutes' in endpoint.keys()
|
||||
address = endpoint['Address']
|
||||
# Check that the address is a valid endpoint by checking that we can
|
||||
# send it another describe_endpoints() request ;-) Note that the
|
||||
# address does not include the "http://" or "https://" prefix, and
|
||||
# we need to choose one manually.
|
||||
url = "http://" + address
|
||||
if address.endswith('.amazonaws.com'):
|
||||
boto3.client('dynamodb',endpoint_url=url).describe_endpoints()
|
||||
else:
|
||||
# Even though we connect to the local installation, Boto3 still
|
||||
# requires us to specify dummy region and credential parameters,
|
||||
# otherwise the user is forced to properly configure ~/.aws even
|
||||
# for local runs.
|
||||
boto3.client('dynamodb',endpoint_url=url, region_name='us-east-1', aws_access_key_id='whatever', aws_secret_access_key='whatever').describe_endpoints()
|
||||
# Nothing to check here - if the above call failed with an exception,
|
||||
# the test would fail.
|
||||
@@ -1,170 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the DescribeTable operation.
|
||||
# Some attributes used only by a specific major feature will be tested
|
||||
# elsewhere:
|
||||
# 1. Tests for describing tables with global or local secondary indexes
|
||||
# (the GlobalSecondaryIndexes and LocalSecondaryIndexes attributes)
|
||||
# are in test_gsi.py and test_lsi.py.
|
||||
# 2. Tests for the stream feature (LatestStreamArn, LatestStreamLabel,
|
||||
# StreamSpecification) will be in the tests devoted to the stream
|
||||
# feature.
|
||||
# 3. Tests for describing a restored table (RestoreSummary, TableId)
|
||||
# will be together with tests devoted to the backup/restore feature.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
import re
|
||||
import time
|
||||
from util import multiset
|
||||
|
||||
# Test that DescribeTable correctly returns the table's name and state
|
||||
def test_describe_table_basic(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert got['TableName'] == test_table.name
|
||||
assert got['TableStatus'] == 'ACTIVE'
|
||||
|
||||
# Test that DescribeTable correctly returns the table's schema, in
|
||||
# AttributeDefinitions and KeySchema attributes
|
||||
@pytest.mark.xfail(reason="DescribeTable does not yet return schema")
|
||||
def test_describe_table_schema(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
expected = { # Copied from test_table()'s fixture
|
||||
'KeySchema': [ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'AttributeDefinitions': [
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
]
|
||||
}
|
||||
assert got['KeySchema'] == expected['KeySchema']
|
||||
# The list of attribute definitions may be arbitrarily reordered
|
||||
assert multiset(got['AttributeDefinitions']) == multiset(expected['AttributeDefinitions'])
|
||||
|
||||
# Test that DescribeTable correctly returns the table's billing mode,
|
||||
# in the BillingModeSummary attribute.
|
||||
def test_describe_table_billing(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert got['BillingModeSummary']['BillingMode'] == 'PAY_PER_REQUEST'
|
||||
# The BillingModeSummary should also contain a
|
||||
# LastUpdateToPayPerRequestDateTime attribute, which is a date.
|
||||
# We don't know what date this is supposed to be, but something we
|
||||
# do know is that the test table was created already with this billing
|
||||
# mode, so the table creation date should be the same as the billing
|
||||
# mode setting date.
|
||||
assert 'LastUpdateToPayPerRequestDateTime' in got['BillingModeSummary']
|
||||
assert got['BillingModeSummary']['LastUpdateToPayPerRequestDateTime'] == got['CreationDateTime']
|
||||
|
||||
# Test that DescribeTable correctly returns the table's creation time.
|
||||
# We don't know what this creation time is supposed to be, so this test
|
||||
# cannot be very thorough... We currently just tests against something we
|
||||
# know to be wrong - returning the *current* time, which changes on every
|
||||
# call.
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return table creation time")
|
||||
def test_describe_table_creation_time(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'CreationDateTime' in got
|
||||
time1 = got['CreationDateTime']
|
||||
time.sleep(1)
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
time2 = got['CreationDateTime']
|
||||
assert time1 == time2
|
||||
|
||||
# Test that DescribeTable returns the table's estimated item count
|
||||
# in the ItemCount attribute. Unfortunately, there's not much we can
|
||||
# really test here... The documentation says that the count can be
|
||||
# delayed by six hours, so the number we get here may have no relation
|
||||
# to the current number of items in the test table. The attribute should exist,
|
||||
# though. This test does NOT verify that ItemCount isn't always returned as
|
||||
# zero - such stub implementation will pass this test.
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return table item count")
|
||||
def test_describe_table_item_count(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'ItemCount' in got
|
||||
|
||||
# Similar test for estimated size in bytes - TableSizeBytes - which again,
|
||||
# may reflect the size as long as six hours ago.
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return table size")
|
||||
def test_describe_table_size(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'TableSizeBytes' in got
|
||||
|
||||
# Test the ProvisionedThroughput attribute returned by DescribeTable.
|
||||
# This is a very partial test: Our test table is configured without
|
||||
# provisioned throughput, so obviously it will not have interesting settings
|
||||
# for it. DynamoDB returns zeros for some of the attributes, even though
|
||||
# the documentation suggests missing values should have been fine too.
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return provisioned throughput")
|
||||
def test_describe_table_provisioned_throughput(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert got['ProvisionedThroughput']['NumberOfDecreasesToday'] == 0
|
||||
assert got['ProvisionedThroughput']['WriteCapacityUnits'] == 0
|
||||
assert got['ProvisionedThroughput']['ReadCapacityUnits'] == 0
|
||||
|
||||
# This is a silly test for the RestoreSummary attribute in DescribeTable -
|
||||
# it should not exist in a table not created by a restore. When testing
|
||||
# the backup/restore feature, we will have more meaninful tests for the
|
||||
# value of this attribute in that case.
|
||||
def test_describe_table_restore_summary(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert not 'RestoreSummary' in got
|
||||
|
||||
# This is a silly test for the SSEDescription attribute in DescribeTable -
|
||||
# by default, a table is encrypted with AWS-owned keys, not using client-
|
||||
# owned keys, and the SSEDescription attribute is not returned at all.
|
||||
def test_describe_table_encryption(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert not 'SSEDescription' in got
|
||||
|
||||
# This is a silly test for the StreamSpecification attribute in DescribeTable -
|
||||
# when there are no streams, this attribute should be missing.
|
||||
def test_describe_table_stream_specification(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert not 'StreamSpecification' in got
|
||||
|
||||
# Test that the table has an ARN, a unique identifier for the table which
|
||||
# includes which zone it is on, which account, and of course the table's
|
||||
# name. The ARN format is described in
|
||||
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return ARN")
|
||||
def test_describe_table_arn(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'TableArn' in got and got['TableArn'].startswith('arn:')
|
||||
|
||||
# Test that the table has a TableId.
|
||||
# TODO: Figure out what is this TableId supposed to be, it is just a
|
||||
# unique id that is created with the table and never changes? Or anything
|
||||
# else?
|
||||
@pytest.mark.xfail(reason="DescribeTable does not return TableId")
|
||||
def test_describe_table_id(test_table):
|
||||
got = test_table.meta.client.describe_table(TableName=test_table.name)['Table']
|
||||
assert 'TableId' in got
|
||||
|
||||
# DescribeTable error path: trying to describe a non-existent table should
|
||||
# result in a ResourceNotFoundException.
|
||||
def test_describe_table_non_existent_table(dynamodb):
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException') as einfo:
|
||||
dynamodb.meta.client.describe_table(TableName='non_existent_table')
|
||||
# As one of the first error-path tests that we wrote, let's test in more
|
||||
# detail that the error reply has the appropriate fields:
|
||||
response = einfo.value.response
|
||||
print(response)
|
||||
err = response['Error']
|
||||
assert err['Code'] == 'ResourceNotFoundException'
|
||||
assert re.match(err['Message'], 'Requested resource not found: Table: non_existent_table not found')
|
||||
@@ -1,369 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the "Expected" parameter used to make certain operations (PutItem,
|
||||
# UpdateItem and DeleteItem) conditional on the existing attribute values.
|
||||
# "Expected" is the older version of ConditionExpression parameter, which
|
||||
# is tested by the separate test_condition_expression.py.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string
|
||||
|
||||
# Most of the tests in this file check that the "Expected" parameter works for
|
||||
# the UpdateItem operation. It should also work the same for the PutItem and
|
||||
# DeleteItem operations, and we'll make a small effort verifying that at
|
||||
# the end of the file.
|
||||
|
||||
# Somewhat pedanticly, DynamoDB forbids using old-style Expected together
|
||||
# with new-style UpdateExpression... Expected can only be used with
|
||||
# AttributeUpdates (and for UpdateExpression, ConditionExpression should be
|
||||
# used).
|
||||
def test_update_expression_and_expected(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :val1',
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
with pytest.raises(ClientError, match='ValidationException.*UpdateExpression'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :val1',
|
||||
ExpressionAttributeValues={':val1': 2},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [1]}}
|
||||
)
|
||||
|
||||
# The following string of tests test the various types of Expected conditions
|
||||
# on a single attribute. This condition is defined using ComparisonOperator
|
||||
# (there are many types of those!) or by Value or Exists, and we need to check
|
||||
# all these types of conditions.
|
||||
#
|
||||
# In each case we have tests for the "true" case of the condition, meaning
|
||||
# that the condition evaluates to true and the update is supposed to happen,
|
||||
# and the "false" case, where the condition evaluates to false, so the update
|
||||
# doesn't happen and we get a ConditionalCheckFailedException instead.
|
||||
|
||||
def test_update_expected_1_eq_true(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
# Case where expected and update are on the same attribute:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [1]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
|
||||
# Case where expected and update are on different attribute:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [2]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2, 'b': 3}
|
||||
# For EQ, AttributeValueList must have a single element
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [2, 3]}}
|
||||
)
|
||||
|
||||
# Check that set equality is checked correctly. Unlike string equality (for
|
||||
# example), it cannot be done with just naive string comparison of the JSON
|
||||
# representation, and we need to allow for any order.
|
||||
@pytest.mark.xfail(reason="bug in EQ test of sets")
|
||||
def test_update_expected_1_eq_set(test_table_s):
|
||||
p = random_string()
|
||||
# Because boto3 sorts the set values we give it, in order to generate a
|
||||
# set with a different order, we need to build it incrementally.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': set(['dog', 'chinchilla']), 'Action': 'PUT'}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
||||
# Sanity check - the attribute contains the set we think it does
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['chinchilla', 'cat', 'dog', 'mouse'])
|
||||
# Now finally check that "Expected"'s equality check knows the equality too.
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [set(['chinchilla', 'cat', 'dog', 'mouse'])]}}
|
||||
)
|
||||
assert 'b' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
||||
|
||||
def test_update_expected_1_eq_false(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [2]}}
|
||||
)
|
||||
# If the compared value has a different type, it results in the
|
||||
# condition failing normally (it's not a validation error).
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': ['dog']}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
||||
|
||||
def test_update_expected_1_begins_with_true(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'}})
|
||||
# Case where expected and update are on different attribute:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'BEGINS_WITH',
|
||||
'AttributeValueList': ['hell']}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello', 'b': 3}
|
||||
# For BEGIN_WITH, AttributeValueList must have a single element
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': ['hell', 'heaven']}}
|
||||
)
|
||||
|
||||
def test_update_expected_1_begins_with_false(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': ['dog']}}
|
||||
)
|
||||
# Although BEGINS_WITH requires String or Binary type, giving it a
|
||||
# number results not with a ValidationException but rather a
|
||||
# failed condition (ConditionalCheckFailedException)
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'ComparisonOperator': 'EQ',
|
||||
'AttributeValueList': [3]}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello'}
|
||||
|
||||
# FIXME: need to test many more ComparisonOperator options... See full list in
|
||||
# description in https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.Expected.html
|
||||
|
||||
# Instead of ComparisonOperator and AttributeValueList, one can specify either
|
||||
# Value or Exists:
|
||||
def test_update_expected_1_value_true(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Value': 1}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2}
|
||||
|
||||
def test_update_expected_1_value_false(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Value': 2}}
|
||||
)
|
||||
# If the expected attribute is completely missing, the condition also fails
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'z': {'Value': 1}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
||||
|
||||
def test_update_expected_1_exists_true(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
# Surprisingly, the "Exists: True" cannot be used to confirm that the
|
||||
# attribute had *any* old value (use the NOT_NULL comparison operator
|
||||
# for that). It can only be used together with "Value", and in that case
|
||||
# doesn't mean a thing.
|
||||
# Only "Exists: False" has an interesting meaning.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True}}
|
||||
)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 1}}
|
||||
)
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'d': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={'z': {'Exists': False}}
|
||||
)
|
||||
# Exists: False cannot be used together with a Value:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'c': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': False, 'Value': 1}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'c': 3, 'd': 4}
|
||||
|
||||
def test_update_expected_1_exists_false(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': False}}
|
||||
)
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 2}}
|
||||
)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
||||
|
||||
# Test that it's not allowed to combine ComparisonOperator and Exists or Value
|
||||
def test_update_expected_operator_clash(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': False, 'ComparisonOperator': 'EQ', 'AttributeValueList': [3]}})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Value': 3, 'ComparisonOperator': 'EQ', 'AttributeValueList': [3]}})
|
||||
|
||||
# All the previous tests involved a single condition on a single attribute.
|
||||
# The following tests involving multiple conditions on multiple attributes.
|
||||
# ConditionalOperator defaults to AND, and can also be set to OR.
|
||||
|
||||
def test_update_expected_multi_true(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
||||
'b': {'Value': 2, 'Action': 'PUT'}})
|
||||
# Test several conditions with default "AND" operator
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 1},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [2]},
|
||||
'c': {'Exists': False}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2, 'z': 3}
|
||||
# Same with explicit "AND" operator
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 1},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [2]},
|
||||
'c': {'Exists': False}},
|
||||
ConditionalOperator="AND")
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2, 'z': 4}
|
||||
# With "OR" operator, it's enough that just one conditions is true
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 5, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 74},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [999]},
|
||||
'c': {'Exists': False}},
|
||||
ConditionalOperator="OR")
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2, 'z': 5}
|
||||
|
||||
def test_update_expected_multi_false(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
||||
'b': {'Value': 2, 'Action': 'PUT'},
|
||||
'c': {'Value': 3, 'Action': 'PUT'}})
|
||||
# Test several conditions, one of them false, with default "AND" operator
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 1},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [3]},
|
||||
'd': {'Exists': False}})
|
||||
# Same with explicit "AND" operator
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 1},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [3]},
|
||||
'd': {'Exists': False}},
|
||||
ConditionalOperator="AND")
|
||||
# With "OR" operator, all the conditions need to be false to fail
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 5, 'Action': 'PUT'}},
|
||||
Expected={'a': {'Exists': True, 'Value': 74},
|
||||
'b': {'ComparisonOperator': 'EQ', 'AttributeValueList': [999]},
|
||||
'c': {'Exists': False}},
|
||||
ConditionalOperator='OR')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2, 'c': 3}
|
||||
|
||||
# Verify the behaviour of an empty Expected parameter:
|
||||
def test_update_expected_empty(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
# An empty Expected array results in a successful update:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 3, 'Action': 'PUT'}},
|
||||
Expected={})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'z': 3}
|
||||
# Trying with ConditionalOperator complains that you can't have
|
||||
# ConditionalOperator without Expected (despite Expected existing, though empty).
|
||||
with pytest.raises(ClientError, match='ValidationException.*ConditionalOperator'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={}, ConditionalOperator='OR')
|
||||
with pytest.raises(ClientError, match='ValidationException.*ConditionalOperator'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'z': {'Value': 4, 'Action': 'PUT'}},
|
||||
Expected={}, ConditionalOperator='AND')
|
||||
|
||||
# All of the above tests tested "Expected" with the UpdateItem operation.
|
||||
# We now want to test that it works also with the PutItem and DeleteItems
|
||||
# operations. We don't need to check again all the different sub-cases tested
|
||||
# above - we can assume that exactly the same code gets used to test the
|
||||
# expected value. So we just need one test for each operation, to verify that
|
||||
# this code actually gets called.
|
||||
|
||||
def test_delete_item_expected(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.delete_item(Key={'p': p}, Expected={'a': {'Value': 2}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
||||
test_table_s.delete_item(Key={'p': p}, Expected={'a': {'Value': 1}})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
def test_put_item_expected(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
||||
test_table_s.put_item(Item={'p': p, 'a': 2}, Expected={'a': {'Value': 1}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
|
||||
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3}, Expected={'a': {'Value': 1}})
|
||||
@@ -1,801 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests of GSI (Global Secondary Indexes)
|
||||
#
|
||||
# Note that many of these tests are slower than usual, because many of them
|
||||
# need to create new tables and/or new GSIs of different types, operations
|
||||
# which are extremely slow in DynamoDB, often taking minutes (!).
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from botocore.exceptions import ClientError, ParamValidationError
|
||||
from util import create_test_table, random_string, full_scan, full_query, multiset, list_tables
|
||||
|
||||
# GSIs only support eventually consistent reads, so tests that involve
|
||||
# writing to a table and then expect to read something from it cannot be
|
||||
# guaranteed to succeed without retrying the read. The following utility
|
||||
# functions make it easy to write such tests.
|
||||
# Note that in practice, there repeated reads are almost never necessary:
|
||||
# Amazon claims that "Changes to the table data are propagated to the global
|
||||
# secondary indexes within a fraction of a second, under normal conditions"
|
||||
# and indeed, in practice, the tests here almost always succeed without a
|
||||
# retry.
|
||||
def assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_scan retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# Although quite silly, it is actually allowed to create an index which is
|
||||
# identical to the base table.
|
||||
def test_gsi_identical(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
items = [{'p': random_string(), 'x': random_string()} for i in range(10)]
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
# Scanning the entire table directly or via the index yields the same
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, IndexName='wrong')
|
||||
table.delete()
|
||||
|
||||
# One of the simplest forms of a non-trivial GSI: The base table has a hash
|
||||
# and sort key, and the index reverses those roles. Other attributes are just
|
||||
# copied.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_1(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
],
|
||||
)
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_gsi_simple(test_table_gsi_1):
|
||||
items = [{'p': random_string(), 'c': random_string(), 'x': random_string()} for i in range(10)]
|
||||
with test_table_gsi_1.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
c = items[0]['c']
|
||||
# The index allows a query on just a specific sort key, which isn't
|
||||
# allowed on the base table.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table_gsi_1, KeyConditions={'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [x for x in items if x['c'] == c]
|
||||
assert_index_query(test_table_gsi_1, 'hello', expected_items,
|
||||
KeyConditions={'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}})
|
||||
# Scanning the entire table directly or via the index yields the same
|
||||
# results (in different order).
|
||||
assert_index_scan(test_table_gsi_1, 'hello', full_scan(test_table_gsi_1))
|
||||
|
||||
def test_gsi_same_key(test_table_gsi_1):
|
||||
c = random_string();
|
||||
# All these items have the same sort key 'c' but different hash key 'p'
|
||||
items = [{'p': random_string(), 'c': c, 'x': random_string()} for i in range(10)]
|
||||
with test_table_gsi_1.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
assert_index_query(test_table_gsi_1, 'hello', items,
|
||||
KeyConditions={'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Check we get an appropriate error when trying to read a non-existing index
|
||||
# of an existing table. Although the documentation specifies that a
|
||||
# ResourceNotFoundException should be returned if "The operation tried to
|
||||
# access a nonexistent table or index", in fact in the specific case that
|
||||
# the table does exist but an index does not - we get a ValidationException.
|
||||
def test_gsi_missing_index(test_table_gsi_1):
|
||||
with pytest.raises(ClientError, match='ValidationException.*wrong_name'):
|
||||
full_query(test_table_gsi_1, IndexName='wrong_name',
|
||||
KeyConditions={'x': {'AttributeValueList': [1], 'ComparisonOperator': 'EQ'}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*wrong_name'):
|
||||
full_scan(test_table_gsi_1, IndexName='wrong_name')
|
||||
|
||||
# Nevertheless, if the table itself does not exist, a query should return
|
||||
# a ResourceNotFoundException, not ValidationException:
|
||||
def test_gsi_missing_table(dynamodb):
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
dynamodb.meta.client.query(TableName='nonexistent_table', IndexName='any_name', KeyConditions={'x': {'AttributeValueList': [1], 'ComparisonOperator': 'EQ'}})
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
dynamodb.meta.client.scan(TableName='nonexistent_table', IndexName='any_name')
|
||||
|
||||
# Verify that strongly-consistent reads on GSI are *not* allowed.
|
||||
@pytest.mark.xfail(reason="GSI strong consistency not checked")
|
||||
def test_gsi_strong_consistency(test_table_gsi_1):
|
||||
with pytest.raises(ClientError, match='ValidationException.*Consistent'):
|
||||
full_query(test_table_gsi_1, KeyConditions={'c': {'AttributeValueList': ['hi'], 'ComparisonOperator': 'EQ'}}, IndexName='hello', ConsistentRead=True)
|
||||
with pytest.raises(ClientError, match='ValidationException.*Consistent'):
|
||||
full_scan(test_table_gsi_1, IndexName='hello', ConsistentRead=True)
|
||||
|
||||
# Verify that a GSI is correctly listed in describe_table
|
||||
@pytest.mark.xfail(reason="DescribeTable provides index names only, no size or item count")
|
||||
def test_gsi_describe(test_table_gsi_1):
|
||||
desc = test_table_gsi_1.meta.client.describe_table(TableName=test_table_gsi_1.name)
|
||||
assert 'Table' in desc
|
||||
assert 'GlobalSecondaryIndexes' in desc['Table']
|
||||
gsis = desc['Table']['GlobalSecondaryIndexes']
|
||||
assert len(gsis) == 1
|
||||
gsi = gsis[0]
|
||||
assert gsi['IndexName'] == 'hello'
|
||||
assert 'IndexSizeBytes' in gsi # actual size depends on content
|
||||
assert 'ItemCount' in gsi
|
||||
assert gsi['Projection'] == {'ProjectionType': 'ALL'}
|
||||
assert gsi['IndexStatus'] == 'ACTIVE'
|
||||
assert gsi['KeySchema'] == [{'KeyType': 'HASH', 'AttributeName': 'c'},
|
||||
{'KeyType': 'RANGE', 'AttributeName': 'p'}]
|
||||
# TODO: check also ProvisionedThroughput, IndexArn
|
||||
|
||||
# When a GSI's key includes an attribute not in the base table's key, we
|
||||
# need to remember to add its type to AttributeDefinitions.
|
||||
def test_gsi_missing_attribute_definition(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*AttributeDefinitions'):
|
||||
create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [ { 'AttributeName': 'c', 'KeyType': 'HASH' } ],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
|
||||
# test_table_gsi_1_hash_only is a variant of test_table_gsi_1: It's another
|
||||
# case where the index doesn't involve non-key attributes. Again the base
|
||||
# table has a hash and sort key, but in this case the index has *only* a
|
||||
# hash key (which is the base's hash key). In the materialized-view-based
|
||||
# implementation, we need to remember the other part of the base key as a
|
||||
# clustering key.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_1_hash_only(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
],
|
||||
)
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_gsi_key_not_in_index(test_table_gsi_1_hash_only):
|
||||
# Test with items with different 'c' values:
|
||||
items = [{'p': random_string(), 'c': random_string(), 'x': random_string()} for i in range(10)]
|
||||
with test_table_gsi_1_hash_only.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
c = items[0]['c']
|
||||
expected_items = [x for x in items if x['c'] == c]
|
||||
assert_index_query(test_table_gsi_1_hash_only, 'hello', expected_items,
|
||||
KeyConditions={'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}})
|
||||
# Test items with the same sort key 'c' but different hash key 'p'
|
||||
c = random_string();
|
||||
items = [{'p': random_string(), 'c': c, 'x': random_string()} for i in range(10)]
|
||||
with test_table_gsi_1_hash_only.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
assert_index_query(test_table_gsi_1_hash_only, 'hello', items,
|
||||
KeyConditions={'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}})
|
||||
# Scanning the entire table directly or via the index yields the same
|
||||
# results (in different order).
|
||||
assert_index_scan(test_table_gsi_1_hash_only, 'hello', full_scan(test_table_gsi_1_hash_only))
|
||||
|
||||
|
||||
# A second scenario of GSI. Base table has just hash key, Index has a
|
||||
# different hash key - one of the non-key attributes from the base table.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_2(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'x', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_gsi_2(test_table_gsi_2):
|
||||
items1 = [{'p': random_string(), 'x': random_string()} for i in range(10)]
|
||||
x1 = items1[0]['x']
|
||||
x2 = random_string()
|
||||
items2 = [{'p': random_string(), 'x': x2} for i in range(10)]
|
||||
items = items1 + items2
|
||||
with test_table_gsi_2.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [i for i in items if i['x'] == x1]
|
||||
assert_index_query(test_table_gsi_2, 'hello', expected_items,
|
||||
KeyConditions={'x': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [i for i in items if i['x'] == x2]
|
||||
assert_index_query(test_table_gsi_2, 'hello', expected_items,
|
||||
KeyConditions={'x': {'AttributeValueList': [x2], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Test that when a table has a GSI, if the indexed attribute is missing, the
|
||||
# item is added to the base table but not the index.
|
||||
def test_gsi_missing_attribute(test_table_gsi_2):
|
||||
p1 = random_string()
|
||||
x1 = random_string()
|
||||
test_table_gsi_2.put_item(Item={'p': p1, 'x': x1})
|
||||
p2 = random_string()
|
||||
test_table_gsi_2.put_item(Item={'p': p2})
|
||||
|
||||
# Both items are now in the base table:
|
||||
assert test_table_gsi_2.get_item(Key={'p': p1})['Item'] == {'p': p1, 'x': x1}
|
||||
assert test_table_gsi_2.get_item(Key={'p': p2})['Item'] == {'p': p2}
|
||||
|
||||
# But only the first item is in the index: It can be found using a
|
||||
# Query, and a scan of the index won't find it (but a scan on the base
|
||||
# will).
|
||||
assert_index_query(test_table_gsi_2, 'hello', [{'p': p1, 'x': x1}],
|
||||
KeyConditions={'x': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
assert any([i['p'] == p1 for i in full_scan(test_table_gsi_2)])
|
||||
# Note: with eventually consistent read, we can't really be sure that
|
||||
# and item will "never" appear in the index. We do this test last,
|
||||
# so if we had a bug and such item did appear, hopefully we had enough
|
||||
# time for the bug to become visible. At least sometimes.
|
||||
assert not any([i['p'] == p2 for i in full_scan(test_table_gsi_2, IndexName='hello')])
|
||||
|
||||
# Test when a table has a GSI, if the indexed attribute has the wrong type,
|
||||
# the update operation is rejected, and is added to neither base table nor
|
||||
# index. This is different from the case of a *missing* attribute, where
|
||||
# the item is added to the base table but not index.
|
||||
# The following three tests test_gsi_wrong_type_attribute_{put,update,batch}
|
||||
# test updates using PutItem, UpdateItem, and BatchWriteItem respectively.
|
||||
def test_gsi_wrong_type_attribute_put(test_table_gsi_2):
|
||||
# PutItem with wrong type for 'x' is rejected, item isn't created even
|
||||
# in the base table.
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
test_table_gsi_2.put_item(Item={'p': p, 'x': 3})
|
||||
assert not 'Item' in test_table_gsi_2.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
def test_gsi_wrong_type_attribute_update(test_table_gsi_2):
|
||||
# An UpdateItem with wrong type for 'x' is also rejected, but naturally
|
||||
# if the item already existed, it remains as it was.
|
||||
p = random_string()
|
||||
x = random_string()
|
||||
test_table_gsi_2.put_item(Item={'p': p, 'x': x})
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
test_table_gsi_2.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': 3, 'Action': 'PUT'}})
|
||||
assert test_table_gsi_2.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'x': x}
|
||||
|
||||
def test_gsi_wrong_type_attribute_batch(test_table_gsi_2):
|
||||
# In a BatchWriteItem, if any update is forbidden, the entire batch is
|
||||
# rejected, and none of the updates happen at all.
|
||||
p1 = random_string()
|
||||
p2 = random_string()
|
||||
p3 = random_string()
|
||||
items = [{'p': p1, 'x': random_string()},
|
||||
{'p': p2, 'x': 3},
|
||||
{'p': p3, 'x': random_string()}]
|
||||
with pytest.raises(ClientError, match='ValidationException.*mismatch'):
|
||||
with test_table_gsi_2.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for p in [p1, p2, p3]:
|
||||
assert not 'Item' in test_table_gsi_2.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# A third scenario of GSI. Index has a hash key and a sort key, both are
|
||||
# non-key attributes from the base table. This scenario may be very
|
||||
# difficult to implement in Alternator because Scylla's materialized-views
|
||||
# implementation only allows one new key column in the view, and here
|
||||
# we need two (which, also, aren't actual columns, but map items).
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_3(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'b', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_gsi_3(test_table_gsi_3):
|
||||
items = [{'p': random_string(), 'a': random_string(), 'b': random_string()} for i in range(10)]
|
||||
with test_table_gsi_3.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
assert_index_query(test_table_gsi_3, 'hello', [items[3]],
|
||||
KeyConditions={'a': {'AttributeValueList': [items[3]['a']], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [items[3]['b']], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
@pytest.mark.xfail(reason="GSI in alternator currently have a bug on updating the second regular base column")
|
||||
def test_gsi_update_second_regular_base_column(test_table_gsi_3):
|
||||
items = [{'p': random_string(), 'a': random_string(), 'b': random_string(), 'd': random_string()} for i in range(10)]
|
||||
with test_table_gsi_3.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
items[3]['b'] = 'updated'
|
||||
test_table_gsi_3.update_item(Key={'p': items[3]['p']}, AttributeUpdates={'b': {'Value': 'updated', 'Action': 'PUT'}})
|
||||
assert_index_query(test_table_gsi_3, 'hello', [items[3]],
|
||||
KeyConditions={'a': {'AttributeValueList': [items[3]['a']], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [items[3]['b']], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
|
||||
# A fourth scenario of GSI. Two GSIs on a single base table.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_4(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello_a',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
},
|
||||
{ 'IndexName': 'hello_b',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'b', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
# Test that a base table with two GSIs updates both as expected.
|
||||
def test_gsi_4(test_table_gsi_4):
|
||||
items = [{'p': random_string(), 'a': random_string(), 'b': random_string()} for i in range(10)]
|
||||
with test_table_gsi_4.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
assert_index_query(test_table_gsi_4, 'hello_a', [items[3]],
|
||||
KeyConditions={'a': {'AttributeValueList': [items[3]['a']], 'ComparisonOperator': 'EQ'}})
|
||||
assert_index_query(test_table_gsi_4, 'hello_b', [items[3]],
|
||||
KeyConditions={'b': {'AttributeValueList': [items[3]['b']], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Verify that describe_table lists the two GSIs.
|
||||
def test_gsi_4_describe(test_table_gsi_4):
|
||||
desc = test_table_gsi_4.meta.client.describe_table(TableName=test_table_gsi_4.name)
|
||||
assert 'Table' in desc
|
||||
assert 'GlobalSecondaryIndexes' in desc['Table']
|
||||
gsis = desc['Table']['GlobalSecondaryIndexes']
|
||||
assert len(gsis) == 2
|
||||
assert multiset([g['IndexName'] for g in gsis]) == multiset(['hello_a', 'hello_b'])
|
||||
|
||||
# A scenario for GSI in which the table has both hash and sort key
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_5(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'x', 'KeyType': 'RANGE' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_gsi_5(test_table_gsi_5):
|
||||
items1 = [{'p': random_string(), 'c': random_string(), 'x': random_string()} for i in range(10)]
|
||||
p1, x1 = items1[0]['p'], items1[0]['x']
|
||||
p2, x2 = random_string(), random_string()
|
||||
items2 = [{'p': p2, 'c': random_string(), 'x': x2} for i in range(10)]
|
||||
items = items1 + items2
|
||||
with test_table_gsi_5.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['x'] == x1]
|
||||
assert_index_query(test_table_gsi_5, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'x': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [i for i in items if i['p'] == p2 and i['x'] == x2]
|
||||
assert_index_query(test_table_gsi_5, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'x': {'AttributeValueList': [x2], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# All tests above involved "ProjectionType: ALL". This test checks how
|
||||
# "ProjectionType:: KEYS_ONLY" works. We note that it projects both
|
||||
# the index's key, *and* the base table's key. So items which had different
|
||||
# base-table keys cannot suddenly become the same item in the index.
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_projection_keys_only(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'x', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'KEYS_ONLY' }
|
||||
}
|
||||
])
|
||||
items = [{'p': random_string(), 'x': random_string(), 'y': random_string()} for i in range(10)]
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
wanted = ['p', 'x']
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert_index_scan(table, 'hello', expected_items)
|
||||
table.delete()
|
||||
|
||||
# Test for "ProjectionType:: INCLUDE". The secondary table includes the
|
||||
# its own and the base's keys (as in KEYS_ONLY) plus the extra keys given
|
||||
# in NonKeyAttributes.
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_projection_include(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'x', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'INCLUDE',
|
||||
'NonKeyAttributes': ['a', 'b'] }
|
||||
}
|
||||
])
|
||||
# Some items have the projected attributes a,b and some don't:
|
||||
items = [{'p': random_string(), 'x': random_string(), 'a': random_string(), 'b': random_string(), 'y': random_string()} for i in range(10)]
|
||||
items = items + [{'p': random_string(), 'x': random_string(), 'y': random_string()} for i in range(10)]
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
wanted = ['p', 'x', 'a', 'b']
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert_index_scan(table, 'hello', expected_items)
|
||||
print(len(expected_items))
|
||||
table.delete()
|
||||
|
||||
# DynamoDB's says the "Projection" argument of GlobalSecondaryIndexes is
|
||||
# mandatory, and indeed Boto3 enforces that it must be passed. The
|
||||
# documentation then goes on to claim that the "ProjectionType" member of
|
||||
# "Projection" is optional - and Boto3 allows it to be missing. But in
|
||||
# fact, it is not allowed to be missing: DynamoDB complains: "Unknown
|
||||
# ProjectionType: null".
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_missing_projection_type(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*ProjectionType'):
|
||||
create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
'Projection': {}
|
||||
}
|
||||
])
|
||||
|
||||
# update_table() for creating a GSI is an asynchronous operation.
|
||||
# The table's TableStatus changes from ACTIVE to UPDATING for a short while
|
||||
# and then goes back to ACTIVE, but the new GSI's IndexStatus appears as
|
||||
# CREATING, until eventually (after a *long* time...) it becomes ACTIVE.
|
||||
# During the CREATING phase, at some point the Backfilling attribute also
|
||||
# appears, until it eventually disappears. We need to wait until all three
|
||||
# markers indicate completion.
|
||||
# Unfortunately, while boto3 has a client.get_waiter('table_exists') to
|
||||
# wait for a table to exists, there is no such function to wait for an
|
||||
# index to come up, so we need to code it ourselves.
|
||||
def wait_for_gsi(table, gsi_name):
|
||||
start_time = time.time()
|
||||
# Surprisingly, even for tiny tables this can take a very long time
|
||||
# on DynamoDB - often many minutes!
|
||||
for i in range(300):
|
||||
time.sleep(1)
|
||||
desc = table.meta.client.describe_table(TableName=table.name)
|
||||
table_status = desc['Table']['TableStatus']
|
||||
if table_status != 'ACTIVE':
|
||||
print('%d Table status still %s' % (i, table_status))
|
||||
continue
|
||||
index_desc = [x for x in desc['Table']['GlobalSecondaryIndexes'] if x['IndexName'] == gsi_name]
|
||||
assert len(index_desc) == 1
|
||||
index_status = index_desc[0]['IndexStatus']
|
||||
if index_status != 'ACTIVE':
|
||||
print('%d Index status still %s' % (i, index_status))
|
||||
continue
|
||||
# When the index is ACTIVE, this must be after backfilling completed
|
||||
assert not 'Backfilling' in index_desc[0]
|
||||
print('wait_for_gsi took %d seconds' % (time.time() - start_time))
|
||||
return
|
||||
raise AssertionError("wait_for_gsi did not complete")
|
||||
|
||||
# Similarly to how wait_for_gsi() waits for a GSI to finish adding,
|
||||
# this function waits for a GSI to be finally deleted.
|
||||
def wait_for_gsi_gone(table, gsi_name):
|
||||
start_time = time.time()
|
||||
for i in range(300):
|
||||
time.sleep(1)
|
||||
desc = table.meta.client.describe_table(TableName=table.name)
|
||||
table_status = desc['Table']['TableStatus']
|
||||
if table_status != 'ACTIVE':
|
||||
print('%d Table status still %s' % (i, table_status))
|
||||
continue
|
||||
if 'GlobalSecondaryIndexes' in desc['Table']:
|
||||
index_desc = [x for x in desc['Table']['GlobalSecondaryIndexes'] if x['IndexName'] == gsi_name]
|
||||
if len(index_desc) != 0:
|
||||
index_status = index_desc[0]['IndexStatus']
|
||||
print('%d Index status still %s' % (i, index_status))
|
||||
continue
|
||||
print('wait_for_gsi_gone took %d seconds' % (time.time() - start_time))
|
||||
return
|
||||
raise AssertionError("wait_for_gsi_gone did not complete")
|
||||
|
||||
# All tests above involved creating a new table with a GSI up-front. This
|
||||
# test will test creating a base table *without* a GSI, putting data in
|
||||
# it, and then adding a GSI with the UpdateTable operation. This starts
|
||||
# a backfilling stage - where data is copied to the index - and when this
|
||||
# stage is done, the index is usable. Items whose indexed column contains
|
||||
# the wrong type are silently ignored and not added to the index (it would
|
||||
# not have been possible to add such items if the GSI was already configured
|
||||
# when they were added).
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_backfill(dynamodb):
|
||||
# First create, and fill, a table without GSI. The items in items1
|
||||
# will have the appropriate string type for 'x' and will later get
|
||||
# indexed. Items in item2 have no value for 'x', and in item3 'x' is in
|
||||
# not a string; So the items in items2 and items3 will be missing
|
||||
# in the index we'll create later.
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
items1 = [{'p': random_string(), 'x': random_string(), 'y': random_string()} for i in range(10)]
|
||||
items2 = [{'p': random_string(), 'y': random_string()} for i in range(10)]
|
||||
items3 = [{'p': random_string(), 'x': i} for i in range(10)]
|
||||
items = items1 + items2 + items3
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
# Now use UpdateTable to create the GSI
|
||||
dynamodb.meta.client.update_table(TableName=table.name,
|
||||
AttributeDefinitions=[{ 'AttributeName': 'x', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexUpdates=[ { 'Create':
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [{ 'AttributeName': 'x', 'KeyType': 'HASH' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}}])
|
||||
# update_table is an asynchronous operation. We need to wait until it
|
||||
# finishes and the table is backfilled.
|
||||
wait_for_gsi(table, 'hello')
|
||||
# As explained above, only items in items1 got copied to the gsi,
|
||||
# and Scan on them works as expected.
|
||||
# Note that we don't need to retry the reads here (i.e., use the
|
||||
# assert_index_scan() or assert_index_query() functions) because after
|
||||
# we waited for backfilling to complete, we know all the pre-existing
|
||||
# data is already in the index.
|
||||
assert multiset(items1) == multiset(full_scan(table, IndexName='hello'))
|
||||
# We can also use Query on the new GSI, to search on the attribute x:
|
||||
assert multiset([items1[3]]) == multiset(full_query(table,
|
||||
IndexName='hello',
|
||||
KeyConditions={'x': {'AttributeValueList': [items1[3]['x']], 'ComparisonOperator': 'EQ'}}))
|
||||
# Let's also test that we cannot add another index with the same name
|
||||
# that already exists
|
||||
with pytest.raises(ClientError, match='ValidationException.*already exists'):
|
||||
dynamodb.meta.client.update_table(TableName=table.name,
|
||||
AttributeDefinitions=[{ 'AttributeName': 'y', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexUpdates=[ { 'Create':
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [{ 'AttributeName': 'y', 'KeyType': 'HASH' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}}])
|
||||
table.delete()
|
||||
|
||||
# Test deleting an existing GSI using UpdateTable
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_delete(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'x', 'KeyType': 'HASH' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
items = [{'p': random_string(), 'x': random_string()} for i in range(10)]
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
# So far, we have the index for "x" and can use it:
|
||||
assert_index_query(table, 'hello', [items[3]],
|
||||
KeyConditions={'x': {'AttributeValueList': [items[3]['x']], 'ComparisonOperator': 'EQ'}})
|
||||
# Now use UpdateTable to delete the GSI for "x"
|
||||
dynamodb.meta.client.update_table(TableName=table.name,
|
||||
GlobalSecondaryIndexUpdates=[{ 'Delete':
|
||||
{ 'IndexName': 'hello' } }])
|
||||
# update_table is an asynchronous operation. We need to wait until it
|
||||
# finishes and the GSI is removed.
|
||||
wait_for_gsi_gone(table, 'hello')
|
||||
# Now index is gone. We cannot query using it.
|
||||
with pytest.raises(ClientError, match='ValidationException.*hello'):
|
||||
full_query(table, IndexName='hello',
|
||||
KeyConditions={'x': {'AttributeValueList': [items[3]['x']], 'ComparisonOperator': 'EQ'}})
|
||||
table.delete()
|
||||
|
||||
# Utility function for creating a new table a GSI with the given name,
|
||||
# and, if creation was successful, delete it. Useful for testing which
|
||||
# GSI names work.
|
||||
def create_gsi(dynamodb, index_name):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': index_name,
|
||||
'KeySchema': [{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
# Verify that the GSI wasn't just ignored, as Scylla originally did ;-)
|
||||
assert 'GlobalSecondaryIndexes' in table.meta.client.describe_table(TableName=table.name)['Table']
|
||||
table.delete()
|
||||
|
||||
# Like table names (tested in test_table.py), index names must must also
|
||||
# be 3-255 characters and match the regex [a-zA-Z0-9._-]+. This test
|
||||
# is similar to test_create_table_unsupported_names(), but for GSI names.
|
||||
# Note that Scylla is actually more limited in the length of the index
|
||||
# names, because both table name and index name, together, have to fit in
|
||||
# 221 characters. But we don't verify here this specific limitation.
|
||||
def test_gsi_unsupported_names(dynamodb):
|
||||
# Unfortunately, the boto library tests for names shorter than the
|
||||
# minimum length (3 characters) immediately, and failure results in
|
||||
# ParamValidationError. But the other invalid names are passed to
|
||||
# DynamoDB, which returns an HTTP response code, which results in a
|
||||
# CientError exception.
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_gsi(dynamodb, 'n')
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_gsi(dynamodb, 'nn')
|
||||
with pytest.raises(ClientError, match='ValidationException.*nnnnn'):
|
||||
create_gsi(dynamodb, 'n' * 256)
|
||||
with pytest.raises(ClientError, match='ValidationException.*nyh'):
|
||||
create_gsi(dynamodb, 'nyh@test')
|
||||
|
||||
# On the other hand, names following the above rules should be accepted. Even
|
||||
# names which the Scylla rules forbid, such as a name starting with .
|
||||
def test_gsi_non_scylla_name(dynamodb):
|
||||
create_gsi(dynamodb, '.alternator_test')
|
||||
|
||||
# Index names with 255 characters are allowed in Dynamo. In Scylla, the
|
||||
# limit is different - the sum of both table and index length cannot
|
||||
# exceed 211 characters. So we test a much shorter limit.
|
||||
# (compare test_create_and_delete_table_very_long_name()).
|
||||
def test_gsi_very_long_name(dynamodb):
|
||||
#create_gsi(dynamodb, 'n' * 255) # works on DynamoDB, but not on Scylla
|
||||
create_gsi(dynamodb, 'n' * 190)
|
||||
|
||||
# Verify that ListTables does not list materialized views used for indexes.
|
||||
# This is hard to test, because we don't really know which table names
|
||||
# should be listed beyond those we created, and don't want to assume that
|
||||
# no other test runs in parallel with us. So the method we chose is to use a
|
||||
# unique random name for an index, and check that no table contains this
|
||||
# name. This assumes that materialized-view names are composed using the
|
||||
# index's name (which is currently what we do).
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_random_name(dynamodb):
|
||||
index_name = random_string()
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': index_name,
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
],
|
||||
)
|
||||
yield [table, index_name]
|
||||
table.delete()
|
||||
|
||||
def test_gsi_list_tables(dynamodb, test_table_gsi_random_name):
|
||||
table, index_name = test_table_gsi_random_name
|
||||
# Check that the random "index_name" isn't a substring of any table name:
|
||||
tables = list_tables(dynamodb)
|
||||
for name in tables:
|
||||
assert not index_name in name
|
||||
# But of course, the table's name should be in the list:
|
||||
assert table.name in tables
|
||||
@@ -1,402 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the CRUD item operations: PutItem, GetItem, UpdateItem, DeleteItem
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from decimal import Decimal
|
||||
from util import random_string, random_bytes
|
||||
|
||||
# Basic test for creating a new item with a random name, and reading it back
|
||||
# with strong consistency.
|
||||
# Only the string type is used for keys and attributes. None of the various
|
||||
# optional PutItem features (Expected, ReturnValues, ReturnConsumedCapacity,
|
||||
# ReturnItemCollectionMetrics, ConditionalOperator, ConditionExpression,
|
||||
# ExpressionAttributeNames, ExpressionAttributeValues) are used, and
|
||||
# for GetItem strong consistency is requested as well as all attributes,
|
||||
# but no other optional features (AttributesToGet, ReturnConsumedCapacity,
|
||||
# ProjectionExpression, ExpressionAttributeNames)
|
||||
def test_basic_string_put_and_get(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
val = random_string()
|
||||
val2 = random_string()
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'attribute': val, 'another': val2})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item['p'] == p
|
||||
assert item['c'] == c
|
||||
assert item['attribute'] == val
|
||||
assert item['another'] == val2
|
||||
|
||||
# Similar to test_basic_string_put_and_get, just uses UpdateItem instead of
|
||||
# PutItem. Because the item does not yet exist, it should work the same.
|
||||
def test_basic_string_update_and_get(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
val = random_string()
|
||||
val2 = random_string()
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'attribute': {'Value': val, 'Action': 'PUT'}, 'another': {'Value': val2, 'Action': 'PUT'}})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item['p'] == p
|
||||
assert item['c'] == c
|
||||
assert item['attribute'] == val
|
||||
assert item['another'] == val2
|
||||
|
||||
# Test put_item and get_item of various types for the *attributes*,
|
||||
# including both scalars as well as nested documents, lists and sets.
|
||||
# The full list of types tested here:
|
||||
# number, boolean, bytes, null, list, map, string set, number set,
|
||||
# binary set.
|
||||
# The keys are still strings.
|
||||
# Note that only top-level attributes are written and read in this test -
|
||||
# this test does not attempt to modify *nested* attributes.
|
||||
# See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/dynamodb.html
|
||||
# on how to pass these various types to Boto3's put_item().
|
||||
def test_put_and_get_attribute_types(test_table):
|
||||
key = {'p': random_string(), 'c': random_string()}
|
||||
test_items = [
|
||||
Decimal("12.345"),
|
||||
42,
|
||||
True,
|
||||
False,
|
||||
b'xyz',
|
||||
None,
|
||||
['hello', 'world', 42],
|
||||
{'hello': 'world', 'life': 42},
|
||||
{'hello': {'test': 'hi', 'hello': True, 'list': [1, 2, 'hi']}},
|
||||
set(['hello', 'world', 'hi']),
|
||||
set([1, 42, Decimal("3.14")]),
|
||||
set([b'xyz', b'hi']),
|
||||
]
|
||||
item = { str(i) : test_items[i] for i in range(len(test_items)) }
|
||||
item.update(key)
|
||||
test_table.put_item(Item=item)
|
||||
got_item = test_table.get_item(Key=key, ConsistentRead=True)['Item']
|
||||
assert item == got_item
|
||||
|
||||
# The test_empty_* tests below verify support for empty items, with no
|
||||
# attributes except the key. This is a difficult case for Scylla, because
|
||||
# for an empty row to exist, Scylla needs to add a "CQL row marker".
|
||||
# There are several ways to create empty items - via PutItem, UpdateItem
|
||||
# and deleting attributes from non-empty items, and we need to check them
|
||||
# all, in several test_empty_* tests:
|
||||
def test_empty_put(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
test_table.put_item(Item={'p': p, 'c': c})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item == {'p': p, 'c': c}
|
||||
def test_empty_put_delete(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'hello': 'world'})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'hello': {'Action': 'DELETE'}})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item == {'p': p, 'c': c}
|
||||
def test_empty_update(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item == {'p': p, 'c': c}
|
||||
def test_empty_update_delete(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'hello': {'Value': 'world', 'Action': 'PUT'}})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'hello': {'Action': 'DELETE'}})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item == {'p': p, 'c': c}
|
||||
|
||||
# Test error handling of UpdateItem passed a bad "Action" field.
|
||||
def test_update_bad_action(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
val = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'attribute': {'Value': val, 'Action': 'NONEXISTENT'}})
|
||||
|
||||
# A more elaborate UpdateItem test, updating different attributes at different
|
||||
# times. Includes PUT and DELETE operations.
|
||||
def test_basic_string_more_update(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
val1 = random_string()
|
||||
val2 = random_string()
|
||||
val3 = random_string()
|
||||
val4 = random_string()
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'a3': {'Value': val1, 'Action': 'PUT'}})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'a1': {'Value': val1, 'Action': 'PUT'}})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'a2': {'Value': val2, 'Action': 'PUT'}})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'a1': {'Value': val3, 'Action': 'PUT'}})
|
||||
test_table.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'a3': {'Action': 'DELETE'}})
|
||||
item = test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert item['p'] == p
|
||||
assert item['c'] == c
|
||||
assert item['a1'] == val3
|
||||
assert item['a2'] == val2
|
||||
assert not 'a3' in item
|
||||
|
||||
# Test that item operations on a non-existant table name fail with correct
|
||||
# error code.
|
||||
def test_item_operations_nonexistent_table(dynamodb):
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
dynamodb.meta.client.put_item(TableName='non_existent_table',
|
||||
Item={'a':{'S':'b'}})
|
||||
|
||||
# Fetching a non-existant item. According to the DynamoDB doc, "If there is no
|
||||
# matching item, GetItem does not return any data and there will be no Item
|
||||
# element in the response."
|
||||
def test_get_item_missing_item(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
assert not "Item" in test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)
|
||||
|
||||
# Test that if we have a table with string hash and sort keys, we can't read
|
||||
# or write items with other key types to it.
|
||||
def test_put_item_wrong_key_type(test_table):
|
||||
b = random_bytes()
|
||||
s = random_string()
|
||||
n = Decimal("3.14")
|
||||
# Should succeed (correct key types)
|
||||
test_table.put_item(Item={'p': s, 'c': s})
|
||||
assert test_table.get_item(Key={'p': s, 'c': s}, ConsistentRead=True)['Item'] == {'p': s, 'c': s}
|
||||
# Should fail (incorrect hash key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'p': b, 'c': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'p': n, 'c': s})
|
||||
# Should fail (incorrect sort key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'p': s, 'c': b})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'p': s, 'c': n})
|
||||
# Should fail (missing hash key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'c': s})
|
||||
# Should fail (missing sort key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.put_item(Item={'p': s})
|
||||
def test_update_item_wrong_key_type(test_table, test_table_s):
|
||||
b = random_bytes()
|
||||
s = random_string()
|
||||
n = Decimal("3.14")
|
||||
# Should succeed (correct key types)
|
||||
test_table.update_item(Key={'p': s, 'c': s}, AttributeUpdates={})
|
||||
assert test_table.get_item(Key={'p': s, 'c': s}, ConsistentRead=True)['Item'] == {'p': s, 'c': s}
|
||||
# Should fail (incorrect hash key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': b, 'c': s}, AttributeUpdates={})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': n, 'c': s}, AttributeUpdates={})
|
||||
# Should fail (incorrect sort key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': s, 'c': b}, AttributeUpdates={})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': s, 'c': n}, AttributeUpdates={})
|
||||
# Should fail (missing hash key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'c': s}, AttributeUpdates={})
|
||||
# Should fail (missing sort key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.update_item(Key={'p': s}, AttributeUpdates={})
|
||||
# Should fail (spurious key columns)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': s, 'c': s, 'spurious': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': s, 'c': s})
|
||||
def test_get_item_wrong_key_type(test_table, test_table_s):
|
||||
b = random_bytes()
|
||||
s = random_string()
|
||||
n = Decimal("3.14")
|
||||
# Should succeed (correct key types) but have empty result
|
||||
assert not "Item" in test_table.get_item(Key={'p': s, 'c': s}, ConsistentRead=True)
|
||||
# Should fail (incorrect hash key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': b, 'c': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': n, 'c': s})
|
||||
# Should fail (incorrect sort key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': s, 'c': b})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': s, 'c': n})
|
||||
# Should fail (missing hash key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'c': s})
|
||||
# Should fail (missing sort key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': s})
|
||||
# Should fail (spurious key columns)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.get_item(Key={'p': s, 'c': s, 'spurious': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': s, 'c': s})
|
||||
def test_delete_item_wrong_key_type(test_table, test_table_s):
|
||||
b = random_bytes()
|
||||
s = random_string()
|
||||
n = Decimal("3.14")
|
||||
# Should succeed (correct key types)
|
||||
test_table.delete_item(Key={'p': s, 'c': s})
|
||||
# Should fail (incorrect hash key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': b, 'c': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': n, 'c': s})
|
||||
# Should fail (incorrect sort key types)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': s, 'c': b})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': s, 'c': n})
|
||||
# Should fail (missing hash key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'c': s})
|
||||
# Should fail (missing sort key)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': s})
|
||||
# Should fail (spurious key columns)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.delete_item(Key={'p': s, 'c': s, 'spurious': s})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.delete_item(Key={'p': s, 'c': s})
|
||||
|
||||
# Most of the tests here arbitrarily used a table with both hash and sort keys
|
||||
# (both strings). Let's check that a table with *only* a hash key works ok
|
||||
# too, for PutItem, GetItem, and UpdateItem.
|
||||
def test_only_hash_key(test_table_s):
|
||||
s = random_string()
|
||||
test_table_s.put_item(Item={'p': s, 'hello': 'world'})
|
||||
assert test_table_s.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'hello': 'world'}
|
||||
test_table_s.update_item(Key={'p': s}, AttributeUpdates={'hi': {'Value': 'there', 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'hello': 'world', 'hi': 'there'}
|
||||
|
||||
# Tests for item operations in tables with non-string hash or sort keys.
|
||||
# These tests focus only on the type of the key - everything else is as
|
||||
# simple as we can (string attributes, no special options for GetItem
|
||||
# and PutItem). These tests also focus on individual items only, and
|
||||
# not about the sort order of sort keys - this should be verified in
|
||||
# test_query.py, for example.
|
||||
def test_bytes_hash_key(test_table_b):
|
||||
# Bytes values are passed using base64 encoding, which has weird cases
|
||||
# depending on len%3 and len%4. So let's try various lengths.
|
||||
for len in range(10,18):
|
||||
p = random_bytes(len)
|
||||
val = random_string()
|
||||
test_table_b.put_item(Item={'p': p, 'attribute': val})
|
||||
assert test_table_b.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'attribute': val}
|
||||
def test_bytes_sort_key(test_table_sb):
|
||||
p = random_string()
|
||||
c = random_bytes()
|
||||
val = random_string()
|
||||
test_table_sb.put_item(Item={'p': p, 'c': c, 'attribute': val})
|
||||
assert test_table_sb.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'attribute': val}
|
||||
|
||||
# Tests for using a large binary blob as hash key, sort key, or attribute.
|
||||
# DynamoDB strictly limits the size of the binary hash key to 2048 bytes,
|
||||
# and binary sort key to 1024 bytes, and refuses anything larger. The total
|
||||
# size of an item is limited to 400KB, which also limits the size of the
|
||||
# largest attributes. For more details on these limits, see
|
||||
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
|
||||
# Alternator currently does *not* have these limitations, and can accept much
|
||||
# larger keys and attributes, but what we do in the following tests is to verify
|
||||
# that items up to DynamoDB's maximum sizes also work well in Alternator.
|
||||
def test_large_blob_hash_key(test_table_b):
|
||||
b = random_bytes(2048)
|
||||
test_table_b.put_item(Item={'p': b})
|
||||
assert test_table_b.get_item(Key={'p': b}, ConsistentRead=True)['Item'] == {'p': b}
|
||||
def test_large_blob_sort_key(test_table_sb):
|
||||
s = random_string()
|
||||
b = random_bytes(1024)
|
||||
test_table_sb.put_item(Item={'p': s, 'c': b})
|
||||
assert test_table_sb.get_item(Key={'p': s, 'c': b}, ConsistentRead=True)['Item'] == {'p': s, 'c': b}
|
||||
def test_large_blob_attribute(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
b = random_bytes(409500) # a bit less than 400KB
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'attribute': b })
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'attribute': b}
|
||||
|
||||
# Checks what it is not allowed to use in a single UpdateItem request both
|
||||
# old-style AttributeUpdates and new-style UpdateExpression.
|
||||
def test_update_item_two_update_methods(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
AttributeUpdates={'a': {'Value': 3, 'Action': 'PUT'}},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
|
||||
# Verify that having neither AttributeUpdates nor UpdateExpression is
|
||||
# allowed, and results in creation of an empty item.
|
||||
def test_update_item_no_update_method(test_table_s):
|
||||
p = random_string()
|
||||
assert not "Item" in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
test_table_s.update_item(Key={'p': p})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
|
||||
# Test GetItem with the AttributesToGet parameter. Result should include the
|
||||
# selected attributes only - if one wants the key attributes as well, one
|
||||
# needs to select them explicitly. When no key attributes are selected,
|
||||
# some items may have *none* of the selected attributes. Those items are
|
||||
# returned too, as empty items - they are not outright missing.
|
||||
def test_getitem_attributes_to_get(dynamodb, test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
item = {'p': p, 'c': c, 'a': 'hello', 'b': 'hi'}
|
||||
test_table.put_item(Item=item)
|
||||
for wanted in [ ['a'], # only non-key attribute
|
||||
['c', 'a'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # Our item doesn't have this
|
||||
]:
|
||||
got_item = test_table.get_item(Key={'p': p, 'c': c}, AttributesToGet=wanted, ConsistentRead=True)['Item']
|
||||
expected_item = {k: item[k] for k in wanted if k in item}
|
||||
assert expected_item == got_item
|
||||
|
||||
# Basic test for DeleteItem, with hash key only
|
||||
def test_delete_item_hash(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p})
|
||||
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
test_table_s.delete_item(Key={'p': p})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# Basic test for DeleteItem, with hash and sort key
|
||||
def test_delete_item_sort(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
key = {'p': p, 'c': c}
|
||||
test_table.put_item(Item=key)
|
||||
assert 'Item' in test_table.get_item(Key=key, ConsistentRead=True)
|
||||
test_table.delete_item(Key=key)
|
||||
assert not 'Item' in test_table.get_item(Key=key, ConsistentRead=True)
|
||||
|
||||
# Test that PutItem completely replaces an existing item. It shouldn't merge
|
||||
# it with a previously existing value, as UpdateItem does!
|
||||
# We test for a table with just hash key, and for a table with both hash and
|
||||
# sort keys.
|
||||
def test_put_item_replace(test_table_s, test_table):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hi'}
|
||||
test_table_s.put_item(Item={'p': p, 'b': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hello'}
|
||||
c = random_string()
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'a': 'hi'})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'a': 'hi'}
|
||||
test_table.put_item(Item={'p': p, 'c': c, 'b': 'hello'})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'b': 'hello'}
|
||||
@@ -1,365 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests of LSI (Local Secondary Indexes)
|
||||
#
|
||||
# Note that many of these tests are slower than usual, because many of them
|
||||
# need to create new tables and/or new LSIs of different types, operations
|
||||
# which are extremely slow in DynamoDB, often taking minutes (!).
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from botocore.exceptions import ClientError, ParamValidationError
|
||||
from util import create_test_table, random_string, full_scan, full_query, multiset, list_tables
|
||||
|
||||
# Currently, Alternator's LSIs only support eventually consistent reads, so tests
|
||||
# that involve writing to a table and then expect to read something from it cannot
|
||||
# be guaranteed to succeed without retrying the read. The following utility
|
||||
# functions make it easy to write such tests.
|
||||
def assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('assert_index_scan retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# Although quite silly, it is actually allowed to create an index which is
|
||||
# identical to the base table.
|
||||
def test_lsi_identical(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'S' }],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [{ 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
items = [{'p': random_string(), 'c': random_string()} for i in range(10)]
|
||||
with table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
# Scanning the entire table directly or via the index yields the same
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, IndexName='wrong')
|
||||
table.delete()
|
||||
|
||||
# Checks that providing a hash key different than the base table is not allowed,
|
||||
# and so is providing duplicated keys or no sort key at all
|
||||
def test_lsi_wrong(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'b', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
table.delete()
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
table.delete()
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
table.delete()
|
||||
|
||||
# A simple scenario for LSI. Base table has just hash key, Index has an
|
||||
# additional sort key - one of the non-key attributes from the base table.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_1(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'b', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_lsi_1(test_table_lsi_1):
|
||||
items1 = [{'p': random_string(), 'c': random_string(), 'b': random_string()} for i in range(10)]
|
||||
p1, b1 = items1[0]['p'], items1[0]['b']
|
||||
p2, b2 = random_string(), random_string()
|
||||
items2 = [{'p': p2, 'c': p2, 'b': b2}]
|
||||
items = items1 + items2
|
||||
with test_table_lsi_1.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['b'] == b1]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [i for i in items if i['p'] == p2 and i['b'] == b2]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# A second scenario of LSI. Base table has both hash and sort keys,
|
||||
# a local index is created on each non-key parameter
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_4(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x1', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x2', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x3', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x4', 'AttributeType': 'S' },
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello_' + column,
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': column, 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
} for column in ['x1','x2','x3','x4']
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
def test_lsi_4(test_table_lsi_4):
|
||||
items1 = [{'p': random_string(), 'c': random_string(),
|
||||
'x1': random_string(), 'x2': random_string(), 'x3': random_string(), 'x4': random_string()} for i in range(10)]
|
||||
i_values = items1[0]
|
||||
i5 = random_string()
|
||||
items2 = [{'p': i5, 'c': i5, 'x1': i5, 'x2': i5, 'x3': i5, 'x4': i5}]
|
||||
items = items1 + items2
|
||||
with test_table_lsi_4.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for column in ['x1', 'x2', 'x3', 'x4']:
|
||||
expected_items = [i for i in items if (i['p'], i[column]) == (i_values['p'], i_values[column])]
|
||||
assert_index_query(test_table_lsi_4, 'hello_' + column, expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [i_values['p']], 'ComparisonOperator': 'EQ'},
|
||||
column: {'AttributeValueList': [i_values[column]], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [i for i in items if (i['p'], i[column]) == (i5, i5)]
|
||||
assert_index_query(test_table_lsi_4, 'hello_' + column, expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'},
|
||||
column: {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
def test_lsi_describe(test_table_lsi_4):
|
||||
desc = test_table_lsi_4.meta.client.describe_table(TableName=test_table_lsi_4.name)
|
||||
assert 'Table' in desc
|
||||
assert 'LocalSecondaryIndexes' in desc['Table']
|
||||
lsis = desc['Table']['LocalSecondaryIndexes']
|
||||
assert(sorted([lsi['IndexName'] for lsi in lsis]) == ['hello_x1', 'hello_x2', 'hello_x3', 'hello_x4'])
|
||||
# TODO: check projection and key params
|
||||
# TODO: check also ProvisionedThroughput, IndexArn
|
||||
|
||||
# A table with selective projection - only keys are projected into the index
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_keys_only(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'b', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'KEYS_ONLY' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
# Check that it's possible to extract a non-projected attribute from the index,
|
||||
# as the documentation promises
|
||||
def test_lsi_get_not_projected_attribute(test_table_lsi_keys_only):
|
||||
items1 = [{'p': random_string(), 'c': random_string(), 'b': random_string(), 'd': random_string()} for i in range(10)]
|
||||
p1, b1, d1 = items1[0]['p'], items1[0]['b'], items1[0]['d']
|
||||
p2, b2, d2 = random_string(), random_string(), random_string()
|
||||
items2 = [{'p': p2, 'c': p2, 'b': b2, 'd': d2}]
|
||||
items = items1 + items2
|
||||
with test_table_lsi_keys_only.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['b'] == b1 and i['d'] == d1]
|
||||
assert_index_query(test_table_lsi_keys_only, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}},
|
||||
Select='ALL_ATTRIBUTES')
|
||||
expected_items = [i for i in items if i['p'] == p2 and i['b'] == b2 and i['d'] == d2]
|
||||
assert_index_query(test_table_lsi_keys_only, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}},
|
||||
Select='ALL_ATTRIBUTES')
|
||||
expected_items = [{'d': i['d']} for i in items if i['p'] == p2 and i['b'] == b2 and i['d'] == d2]
|
||||
assert_index_query(test_table_lsi_keys_only, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}},
|
||||
Select='SPECIFIC_ATTRIBUTES', AttributesToGet=['d'])
|
||||
|
||||
# Check that only projected attributes can be extracted
|
||||
@pytest.mark.xfail(reason="LSI in alternator currently only implement full projections")
|
||||
def test_lsi_get_all_projected_attributes(test_table_lsi_keys_only):
|
||||
items1 = [{'p': random_string(), 'c': random_string(), 'b': random_string(), 'd': random_string()} for i in range(10)]
|
||||
p1, b1, d1 = items1[0]['p'], items1[0]['b'], items1[0]['d']
|
||||
p2, b2, d2 = random_string(), random_string(), random_string()
|
||||
items2 = [{'p': p2, 'c': p2, 'b': b2, 'd': d2}]
|
||||
items = items1 + items2
|
||||
with test_table_lsi_keys_only.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [{'p': i['p'], 'c': i['c'],'b': i['b']} for i in items if i['p'] == p1 and i['b'] == b1]
|
||||
assert_index_query(test_table_lsi_keys_only, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Check that strongly consistent reads are allowed for LSI
|
||||
def test_lsi_consistent_read(test_table_lsi_1):
|
||||
items1 = [{'p': random_string(), 'c': random_string(), 'b': random_string()} for i in range(10)]
|
||||
p1, b1 = items1[0]['p'], items1[0]['b']
|
||||
p2, b2 = random_string(), random_string()
|
||||
items2 = [{'p': p2, 'c': p2, 'b': b2}]
|
||||
items = items1 + items2
|
||||
with test_table_lsi_1.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['b'] == b1]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b1], 'ComparisonOperator': 'EQ'}},
|
||||
ConsistentRead=True)
|
||||
expected_items = [i for i in items if i['p'] == p2 and i['b'] == b2]
|
||||
assert_index_query(test_table_lsi_1, 'hello', expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}},
|
||||
ConsistentRead=True)
|
||||
|
||||
# A table with both gsi and lsi present
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_gsi(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x1', 'AttributeType': 'S' },
|
||||
],
|
||||
GlobalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello_g1',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'x1', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'KEYS_ONLY' }
|
||||
}
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello_l1',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'x1', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'KEYS_ONLY' }
|
||||
}
|
||||
])
|
||||
yield table
|
||||
table.delete()
|
||||
|
||||
# Test that GSI and LSI can coexist, even if they're identical
|
||||
def test_lsi_and_gsi(test_table_lsi_gsi):
|
||||
desc = test_table_lsi_gsi.meta.client.describe_table(TableName=test_table_lsi_gsi.name)
|
||||
assert 'Table' in desc
|
||||
assert 'LocalSecondaryIndexes' in desc['Table']
|
||||
assert 'GlobalSecondaryIndexes' in desc['Table']
|
||||
lsis = desc['Table']['LocalSecondaryIndexes']
|
||||
gsis = desc['Table']['GlobalSecondaryIndexes']
|
||||
assert(sorted([lsi['IndexName'] for lsi in lsis]) == ['hello_l1'])
|
||||
assert(sorted([gsi['IndexName'] for gsi in gsis]) == ['hello_g1'])
|
||||
|
||||
items = [{'p': random_string(), 'c': random_string(), 'x1': random_string()} for i in range(17)]
|
||||
p1, c1, x1 = items[0]['p'], items[0]['c'], items[0]['x1']
|
||||
with test_table_lsi_gsi.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
|
||||
for index in ['hello_g1', 'hello_l1']:
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['x1'] == x1]
|
||||
assert_index_query(test_table_lsi_gsi, index, expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'x1': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
@@ -1,60 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Test for operations on items with *nested* attributes.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string
|
||||
|
||||
# Test that we can write a top-level attribute that is a nested document, and
|
||||
# read it back correctly.
|
||||
def test_nested_document_attribute_write(test_table_s):
|
||||
nested_value = {
|
||||
'a': 3,
|
||||
'b': {'c': 'hello', 'd': ['hi', 'there', {'x': 'y'}, '42']},
|
||||
}
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': nested_value})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': nested_value}
|
||||
|
||||
# Test that if we have a top-level attribute that is a nested document (i.e.,
|
||||
# a dictionary), updating this attribute will replace it entirely by a new
|
||||
# nested document - not merge into the old content with the new content.
|
||||
def test_nested_document_attribute_overwrite(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5}
|
||||
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'a': {'Value': {'c': 5}, 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'c': 5}, 'd': 5}
|
||||
|
||||
# Moreover, we can overwrite an entire nested document by, say, a string,
|
||||
# and that's also fine.
|
||||
def test_nested_document_attribute_overwrite_2(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5}
|
||||
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'a': {'Value': 'hi', 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hi', 'd': 5}
|
||||
|
||||
# Verify that AttributeUpdates cannot be used to update a nested attribute -
|
||||
# trying to use a dot in the name of the attribute, will just create one with
|
||||
# an actual dot in its name.
|
||||
def test_attribute_updates_dot(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'a.b': {'Value': 3, 'Action': 'PUT'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a.b': 3}
|
||||
@@ -1,201 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the various operations (GetItem, Query, Scan) with a
|
||||
# ProjectionExpression parameter.
|
||||
#
|
||||
# ProjectionExpression is an expension of the legacy AttributesToGet
|
||||
# parameter. Both parameters request that only a subset of the attributes
|
||||
# be fetched for each item, instead of all of them. But while AttributesToGet
|
||||
# was limited to top-level attributes, ProjectionExpression can request also
|
||||
# nested attributes.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, full_scan, full_query, multiset
|
||||
|
||||
# Basic test for ProjectionExpression, requesting only top-level attributes.
|
||||
# Result should include the selected attributes only - if one wants the key
|
||||
# attributes as well, one needs to select them explicitly. When no key
|
||||
# attributes are selected, an item may have *none* of the selected
|
||||
# attributes, and returned as an empty item.
|
||||
def test_projection_expression_toplevel(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
item = {'p': p, 'c': c, 'a': 'hello', 'b': 'hi'}
|
||||
test_table.put_item(Item=item)
|
||||
for wanted in [ ['a'], # only non-key attribute
|
||||
['c', 'a'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # Our item doesn't have this
|
||||
]:
|
||||
got_item = test_table.get_item(Key={'p': p, 'c': c}, ProjectionExpression=",".join(wanted), ConsistentRead=True)['Item']
|
||||
expected_item = {k: item[k] for k in wanted if k in item}
|
||||
assert expected_item == got_item
|
||||
|
||||
# Various simple tests for ProjectionExpression's syntax, using only top-evel
|
||||
# attributes.
|
||||
def test_projection_expression_toplevel_syntax(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a')['Item'] == {'a': 'hello'}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a'})['Item'] == {'a': 'hello'}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,b')['Item'] == {'a': 'hello', 'b': 'hi'}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=' a , b ')['Item'] == {'a': 'hello', 'b': 'hi'}
|
||||
# Missing or unused names in ExpressionAttributeNames are errors:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#wrong': 'a'})['Item'] == {'a': 'hello'}
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a', '#unused': 'b'})['Item'] == {'a': 'hello'}
|
||||
# It is not allowed to fetch the same top-level attribute twice (or in
|
||||
# general, list two overlapping attributes). We get an error like
|
||||
# "Invalid ProjectionExpression: Two document paths overlap with each
|
||||
# other; must remove or rewrite one of these paths; path one: [a], path
|
||||
# two: [a]".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,a')['Item']
|
||||
# A comma with nothing after it is a syntax error:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,')['Item']
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=',a')['Item']
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,,b')['Item']
|
||||
# An empty ProjectionExpression is not allowed. DynamoDB recognizes its
|
||||
# syntax, but then writes: "Invalid ProjectionExpression: The expression
|
||||
# can not be empty".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='')['Item']
|
||||
|
||||
# The following two tests are similar to test_projection_expression_toplevel()
|
||||
# which tested the GetItem operation - but these test Scan and Query.
|
||||
# Both test ProjectionExpression with only top-level attributes.
|
||||
def test_projection_expression_scan(filled_test_table):
|
||||
table, items = filled_test_table
|
||||
for wanted in [ ['another'], # only non-key attributes (one item doesn't have it!)
|
||||
['c', 'another'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # none of the items have this attribute!
|
||||
]:
|
||||
got_items = full_scan(table, ProjectionExpression=",".join(wanted))
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
def test_projection_expression_query(test_table):
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': str(i), 'a': str(i*10), 'b': str(i*100) } for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for wanted in [ ['a'], # only non-key attributes
|
||||
['c', 'a'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # none of the items have this attribute!
|
||||
]:
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ProjectionExpression=",".join(wanted))
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# The previous tests all fetched only top-level attributes. They could all
|
||||
# be written using AttributesToGet instead of ProjectionExpression (and,
|
||||
# in fact, we do have similar tests with AttributesToGet in other files),
|
||||
# but the previous test checked that the alternative syntax works correctly.
|
||||
# The following test checks fetching more elaborate attribute paths from
|
||||
# nested documents.
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_projection_expression_path(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={
|
||||
'p': p,
|
||||
'a': {'b': [2, 4, {'x': 'hi', 'y': 'yo'}], 'c': 5},
|
||||
'b': 'hello'
|
||||
})
|
||||
# Fetching the entire nested document "a" works, of course:
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a')['Item'] == {'a': {'b': [2, 4, {'x': 'hi', 'y': 'yo'}], 'c': 5}}
|
||||
# If we fetch a.b, we get only the content of b - but it's still inside
|
||||
# the a dictionary:
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b')['Item'] == {'a': {'b': [2, 4, {'x': 'hi', 'y': 'yo'}]}}
|
||||
# Similarly, fetching a.b[0] gives us a one-element array in a dictionary.
|
||||
# Note that [0] is the first element of an array.
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0]')['Item'] == {'a': {'b': [2]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[2]')['Item'] == {'a': {'b': [{'x': 'hi', 'y': 'yo'}]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[2].y')['Item'] == {'a': {'b': [{'y': 'yo'}]}}
|
||||
# Trying to read any sort of non-existant attribute returns an empty item.
|
||||
# This includes a non-existing top-level attribute, an attempt to read
|
||||
# beyond the end of an array or a non-existant member of a dictionary, as
|
||||
# well as paths which begin with a non-existant prefix.
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='x')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[3]')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.x')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.x.y')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[3].x')['Item'] == {}
|
||||
# We can read multiple paths - the result are merged into one object
|
||||
# structured the same was as in the original item:
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0],a.b[1]')['Item'] == {'a': {'b': [2, 4]}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0],a.c')['Item'] == {'a': {'b': [2], 'c': 5}}
|
||||
# It is not allowed to read the same path multiple times. The error from
|
||||
# DynamoDB looks like: "Invalid ProjectionExpression: Two document paths
|
||||
# overlap with each other; must remove or rewrite one of these paths;
|
||||
# path one: [a, b, [0]], path two: [a, b, [0]]".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0],a.b[0]')['Item']
|
||||
# Two paths are considered to "overlap" if the content of one path
|
||||
# contains the content of the second path. So requesting both "a" and
|
||||
# "a.b[0]" is not allowed.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,a.b[0]')['Item']
|
||||
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_query_projection_expression_path(test_table):
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': str(i), 'a': {'x': str(i*10), 'y': 'hi'}, 'b': 'hello' } for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ProjectionExpression="a.x")
|
||||
expected_items = [{'a': {'x': x['a']['x']}} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
@pytest.mark.xfail(reason="ProjectionExpression does not yet support attribute paths")
|
||||
def test_scan_projection_expression_path(test_table):
|
||||
# This test is similar to test_query_projection_expression_path above,
|
||||
# but uses a scan instead of a query. The scan will generate unrelated
|
||||
# partitions created by other tests (hopefully not too many...) that we
|
||||
# need to ignore. We also need to ask for "p" too, so we can filter by it.
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': str(i), 'a': {'x': str(i*10), 'y': 'hi'}, 'b': 'hello' } for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = [ x for x in full_scan(test_table, ProjectionExpression="p, a.x") if x['p'] == p]
|
||||
expected_items = [{'p': p, 'a': {'x': x['a']['x']}} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# It is not allowed to use both ProjectionExpression and its older cousin,
|
||||
# AttributesToGet, together. If trying to do this, DynamoDB produces an error
|
||||
# like "Can not use both expression and non-expression parameters in the same
|
||||
# request: Non-expression parameters: {AttributesToGet} Expression
|
||||
# parameters: {ProjectionExpression}
|
||||
def test_projection_expression_and_attributes_to_get(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': 'hi'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*both'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a', AttributesToGet=['b'])['Item']
|
||||
with pytest.raises(ClientError, match='ValidationException.*both'):
|
||||
full_scan(test_table_s, ProjectionExpression='a', AttributesToGet=['a'])
|
||||
with pytest.raises(ClientError, match='ValidationException.*both'):
|
||||
full_query(test_table_s, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, ProjectionExpression='a', AttributesToGet=['a'])
|
||||
@@ -1,358 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the Query operation
|
||||
|
||||
import random
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from decimal import Decimal
|
||||
from util import random_string, random_bytes, full_query, multiset
|
||||
from boto3.dynamodb.conditions import Key, Attr
|
||||
|
||||
# Test that scanning works fine with in-stock paginator
|
||||
def test_query_basic_restrictions(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
|
||||
# EQ
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long']) == multiset(got_items)
|
||||
|
||||
# LT
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['12'], 'ComparisonOperator': 'LT'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] < '12']) == multiset(got_items)
|
||||
|
||||
# LE
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['14'], 'ComparisonOperator': 'LE'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] <= '14']) == multiset(got_items)
|
||||
|
||||
# GT
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['15'], 'ComparisonOperator': 'GT'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] > '15']) == multiset(got_items)
|
||||
|
||||
# GE
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['14'], 'ComparisonOperator': 'GE'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] >= '14']) == multiset(got_items)
|
||||
|
||||
# BETWEEN
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['155', '164'], 'ComparisonOperator': 'BETWEEN'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] >= '155' and item['c'] <= '164']) == multiset(got_items)
|
||||
|
||||
# BEGINS_WITH
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': ['11'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
print([item for item in items if item['p'] == 'long' and item['c'].startswith('11')])
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'].startswith('11')]) == multiset(got_items)
|
||||
|
||||
# Test that KeyConditionExpression parameter is supported
|
||||
@pytest.mark.xfail(reason="KeyConditionExpression not supported yet")
|
||||
def test_query_key_condition_expression(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditionExpression=Key("p").eq("long") & Key("c").lt("12")):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['c'] < '12']) == multiset(got_items)
|
||||
|
||||
def test_begins_with(dynamodb, test_table):
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
items = [{'p': 'unorthodox_chars', 'c': sort_key, 'str': 'a'} for sort_key in [u'ÿÿÿ', u'cÿbÿ', u'cÿbÿÿabg'] ]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
|
||||
# TODO(sarna): Once bytes type is supported, /xFF character should be tested
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [u'ÿÿ'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert sorted([d['c'] for d in got_items]) == sorted([d['c'] for d in items if d['c'].startswith(u'ÿÿ')])
|
||||
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [u'cÿbÿ'], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
got_items += page['Items']
|
||||
print(got_items)
|
||||
assert sorted([d['c'] for d in got_items]) == sorted([d['c'] for d in items if d['c'].startswith(u'cÿbÿ')])
|
||||
|
||||
def test_begins_with_wrong_type(dynamodb, test_table_sn):
|
||||
paginator = dynamodb.meta.client.get_paginator('query')
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
for page in paginator.paginate(TableName=test_table_sn.name, KeyConditions={
|
||||
'p' : {'AttributeValueList': ['unorthodox_chars'], 'ComparisonOperator': 'EQ'},
|
||||
'c' : {'AttributeValueList': [17], 'ComparisonOperator': 'BEGINS_WITH'}
|
||||
}):
|
||||
pass
|
||||
|
||||
# Items returned by Query should be sorted by the sort key. The following
|
||||
# tests verify that this is indeed the case, for the three allowed key types:
|
||||
# strings, binary, and numbers. These tests test not just the Query operation,
|
||||
# but inherently that the sort-key sorting works.
|
||||
def test_query_sort_order_string(test_table):
|
||||
# Insert a lot of random items in one new partition:
|
||||
# str(i) has a non-obvious sort order (e.g., "100" comes before "2") so is a nice test.
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': str(i)} for i in range(128)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
assert len(items) == len(got_items)
|
||||
# Extract just the sort key ("c") from the items
|
||||
sort_keys = [x['c'] for x in items]
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
# Verify that got_sort_keys are already sorted (in string order)
|
||||
assert sorted(got_sort_keys) == got_sort_keys
|
||||
# Verify that got_sort_keys are a sorted version of the expected sort_keys
|
||||
assert sorted(sort_keys) == got_sort_keys
|
||||
def test_query_sort_order_bytes(test_table_sb):
|
||||
# Insert a lot of random items in one new partition:
|
||||
# We arbitrarily use random_bytes with a random length.
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': random_bytes(10)} for i in range(128)]
|
||||
with test_table_sb.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = full_query(test_table_sb, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
assert len(items) == len(got_items)
|
||||
sort_keys = [x['c'] for x in items]
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
# Boto3's "Binary" objects are sorted as if bytes are signed integers.
|
||||
# This isn't the order that DynamoDB itself uses (byte 0 should be first,
|
||||
# not byte -128). Sorting the byte array ".value" works.
|
||||
assert sorted(got_sort_keys, key=lambda x: x.value) == got_sort_keys
|
||||
assert sorted(sort_keys) == got_sort_keys
|
||||
def test_query_sort_order_number(test_table_sn):
|
||||
# This is a list of numbers, sorted in correct order, and each suitable
|
||||
# for accurate representation by Alternator's number type.
|
||||
numbers = [
|
||||
Decimal("-2e10"),
|
||||
Decimal("-7.1e2"),
|
||||
Decimal("-4.1"),
|
||||
Decimal("-0.1"),
|
||||
Decimal("-1e-5"),
|
||||
Decimal("0"),
|
||||
Decimal("2e-5"),
|
||||
Decimal("0.15"),
|
||||
Decimal("1"),
|
||||
Decimal("1.00000000000000000000000001"),
|
||||
Decimal("3.14159"),
|
||||
Decimal("3.1415926535897932384626433832795028841"),
|
||||
Decimal("31.4"),
|
||||
Decimal("1.4e10"),
|
||||
]
|
||||
# Insert these numbers, in random order, into one partition:
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': num} for num in random.sample(numbers, len(numbers))]
|
||||
with test_table_sn.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
# Finally, verify that we get back exactly the same numbers (with identical
|
||||
# precision), and in their original sorted order.
|
||||
got_items = full_query(test_table_sn, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
got_sort_keys = [x['c'] for x in got_items]
|
||||
assert got_sort_keys == numbers
|
||||
|
||||
def test_query_filtering_attributes_equality(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
query_filter = {
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "xxxx" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx']) == multiset(got_items)
|
||||
|
||||
query_filter = {
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "xxxx" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"another" : {
|
||||
"AttributeValueList" : [ "yy" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx' and item['another'] == 'yy']) == multiset(got_items)
|
||||
|
||||
# Test that FilterExpression works as expected
|
||||
@pytest.mark.xfail(reason="FilterExpression not supported yet")
|
||||
def test_query_filter_expression(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, FilterExpression=Attr("attribute").eq("xxxx"))
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx']) == multiset(got_items)
|
||||
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, FilterExpression=Attr("attribute").eq("xxxx") & Attr("another").eq("yy"))
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if item['p'] == 'long' and item['attribute'] == 'xxxx' and item['another'] == 'yy']) == multiset(got_items)
|
||||
|
||||
# QueryFilter can only contain non-key attributes in order to be compatible
|
||||
def test_query_filtering_key_equality(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
query_filter = {
|
||||
"c" : {
|
||||
"AttributeValueList" : [ "5" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
query_filter = {
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "x" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"p" : {
|
||||
"AttributeValueList" : [ "5" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': ['long'], 'ComparisonOperator': 'EQ'}}, QueryFilter=query_filter)
|
||||
print(got_items)
|
||||
|
||||
# Test Query with the AttributesToGet parameter. Result should include the
|
||||
# selected attributes only - if one wants the key attributes as well, one
|
||||
# needs to select them explicitly. When no key attributes are selected,
|
||||
# some items may have *none* of the selected attributes. Those items are
|
||||
# returned too, as empty items - they are not outright missing.
|
||||
def test_query_attributes_to_get(dynamodb, test_table):
|
||||
p = random_string()
|
||||
items = [{'p': p, 'c': str(i), 'a': str(i*10), 'b': str(i*100) } for i in range(10)]
|
||||
with test_table.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
for wanted in [ ['a'], # only non-key attributes
|
||||
['c', 'a'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # none of the items have this attribute!
|
||||
]:
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}}, AttributesToGet=wanted)
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Test that in a table with both hash key and sort key, which keys we can
|
||||
# Query by: We can Query by the hash key, by a combination of both hash and
|
||||
# sort keys, but *cannot* query by just the sort key, and obviously not
|
||||
# by any non-key column.
|
||||
def test_query_which_key(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
p2 = random_string()
|
||||
c2 = random_string()
|
||||
item1 = {'p': p, 'c': c}
|
||||
item2 = {'p': p, 'c': c2}
|
||||
item3 = {'p': p2, 'c': c}
|
||||
for i in [item1, item2, item3]:
|
||||
test_table.put_item(Item=i)
|
||||
# Query by hash key only:
|
||||
got_items = full_query(test_table, KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}})
|
||||
expected_items = [item1, item2]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
# Query by hash key *and* sort key (this is basically a GetItem):
|
||||
got_items = full_query(test_table, KeyConditions={
|
||||
'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'},
|
||||
'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}
|
||||
})
|
||||
expected_items = [item1]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
# Query by sort key alone is not allowed. DynamoDB reports:
|
||||
# "Query condition missed key schema element: p".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table, KeyConditions={
|
||||
'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}
|
||||
})
|
||||
# Query by a non-key isn't allowed, for the same reason - that the
|
||||
# actual hash key (p) is missing in the query:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table, KeyConditions={
|
||||
'z': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}
|
||||
})
|
||||
# If we try both p and a non-key we get a complaint that the sort
|
||||
# key is missing: "Query condition missed key schema element: c"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table, KeyConditions={
|
||||
'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'},
|
||||
'z': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}
|
||||
})
|
||||
# If we try p, c and another key, we get an error that
|
||||
# "Conditions can be of length 1 or 2 only".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_query(test_table, KeyConditions={
|
||||
'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'},
|
||||
'c': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'},
|
||||
'z': {'AttributeValueList': [c], 'ComparisonOperator': 'EQ'}
|
||||
})
|
||||
@@ -1,191 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the Scan operation
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, full_scan, multiset
|
||||
from boto3.dynamodb.conditions import Attr
|
||||
|
||||
# Test that scanning works fine with/without pagination
|
||||
def test_scan_basic(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
for limit in [None,1,2,4,33,50,100,9007,16*1024*1024]:
|
||||
pos = None
|
||||
got_items = []
|
||||
while True:
|
||||
if limit:
|
||||
response = test_table.scan(Limit=limit, ExclusiveStartKey=pos) if pos else test_table.scan(Limit=limit)
|
||||
assert len(response['Items']) <= limit
|
||||
else:
|
||||
response = test_table.scan(ExclusiveStartKey=pos) if pos else test_table.scan()
|
||||
pos = response.get('LastEvaluatedKey', None)
|
||||
got_items += response['Items']
|
||||
if not pos:
|
||||
break
|
||||
|
||||
assert len(items) == len(got_items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
def test_scan_with_paginator(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('scan')
|
||||
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name):
|
||||
got_items += page['Items']
|
||||
|
||||
assert len(items) == len(got_items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
for page_size in [1, 17, 1234]:
|
||||
got_items = []
|
||||
for page in paginator.paginate(TableName=test_table.name, PaginationConfig={'PageSize': page_size}):
|
||||
got_items += page['Items']
|
||||
|
||||
assert len(items) == len(got_items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
# Although partitions are scanned in seemingly-random order, inside a
|
||||
# partition items must be returned by Scan sorted in sort-key order.
|
||||
# This test verifies this, for string sort key. We'll need separate
|
||||
# tests for the other sort-key types (number and binary)
|
||||
def test_scan_sort_order_string(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
got_items = full_scan(test_table)
|
||||
assert len(items) == len(got_items)
|
||||
# Extract just the sort key ("c") from the partition "long"
|
||||
items_long = [x['c'] for x in items if x['p'] == 'long']
|
||||
got_items_long = [x['c'] for x in got_items if x['p'] == 'long']
|
||||
# Verify that got_items_long are already sorted (in string order)
|
||||
assert sorted(got_items_long) == got_items_long
|
||||
# Verify that got_items_long are a sorted version of the expected items_long
|
||||
assert sorted(items_long) == got_items_long
|
||||
|
||||
# Test Scan with the AttributesToGet parameter. Result should include the
|
||||
# selected attributes only - if one wants the key attributes as well, one
|
||||
# needs to select them explicitly. When no key attributes are selected,
|
||||
# some items may have *none* of the selected attributes. Those items are
|
||||
# returned too, as empty items - they are not outright missing.
|
||||
def test_scan_attributes_to_get(dynamodb, filled_test_table):
|
||||
table, items = filled_test_table
|
||||
for wanted in [ ['another'], # only non-key attributes (one item doesn't have it!)
|
||||
['c', 'another'], # a key attribute (sort key) and non-key
|
||||
['p', 'c'], # entire key
|
||||
['nonexistent'] # none of the items have this attribute!
|
||||
]:
|
||||
print(wanted)
|
||||
got_items = full_scan(table, AttributesToGet=wanted)
|
||||
expected_items = [{k: x[k] for k in wanted if k in x} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
def test_scan_with_attribute_equality_filtering(dynamodb, filled_test_table):
|
||||
table, items = filled_test_table
|
||||
scan_filter = {
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "xxxxx" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
|
||||
got_items = full_scan(table, ScanFilter=scan_filter)
|
||||
expected_items = [item for item in items if "attribute" in item.keys() and item["attribute"] == "xxxxx" ]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
scan_filter = {
|
||||
"another" : {
|
||||
"AttributeValueList" : [ "y" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "xxxxx" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
|
||||
got_items = full_scan(table, ScanFilter=scan_filter)
|
||||
expected_items = [item for item in items if "attribute" in item.keys() and item["attribute"] == "xxxxx" and item["another"] == "y" ]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Test that FilterExpression works as expected
|
||||
@pytest.mark.xfail(reason="FilterExpression not supported yet")
|
||||
def test_scan_filter_expression(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
|
||||
got_items = full_scan(test_table, FilterExpression=Attr("attribute").eq("xxxx"))
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if 'attribute' in item.keys() and item['attribute'] == 'xxxx']) == multiset(got_items)
|
||||
|
||||
got_items = full_scan(test_table, FilterExpression=Attr("attribute").eq("xxxx") & Attr("another").eq("yy"))
|
||||
print(got_items)
|
||||
assert multiset([item for item in items if 'attribute' in item.keys() and 'another' in item.keys() and item['attribute'] == 'xxxx' and item['another'] == 'yy']) == multiset(got_items)
|
||||
|
||||
def test_scan_with_key_equality_filtering(dynamodb, filled_test_table):
|
||||
table, items = filled_test_table
|
||||
scan_filter_p = {
|
||||
"p" : {
|
||||
"AttributeValueList" : [ "7" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
scan_filter_c = {
|
||||
"c" : {
|
||||
"AttributeValueList" : [ "9" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
scan_filter_p_and_attribute = {
|
||||
"p" : {
|
||||
"AttributeValueList" : [ "7" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"attribute" : {
|
||||
"AttributeValueList" : [ "x"*7 ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
scan_filter_c_and_another = {
|
||||
"c" : {
|
||||
"AttributeValueList" : [ "9" ],
|
||||
"ComparisonOperator": "EQ"
|
||||
},
|
||||
"another" : {
|
||||
"AttributeValueList" : [ "y"*16 ],
|
||||
"ComparisonOperator": "EQ"
|
||||
}
|
||||
}
|
||||
|
||||
# Filtering on the hash key
|
||||
got_items = full_scan(table, ScanFilter=scan_filter_p)
|
||||
expected_items = [item for item in items if "p" in item.keys() and item["p"] == "7" ]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Filtering on the sort key
|
||||
got_items = full_scan(table, ScanFilter=scan_filter_c)
|
||||
expected_items = [item for item in items if "c" in item.keys() and item["c"] == "9"]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Filtering on the hash key and an attribute
|
||||
got_items = full_scan(table, ScanFilter=scan_filter_p_and_attribute)
|
||||
expected_items = [item for item in items if "p" in item.keys() and "another" in item.keys() and item["p"] == "7" and item["another"] == "y"*16]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# Filtering on the sort key and an attribute
|
||||
got_items = full_scan(table, ScanFilter=scan_filter_c_and_another)
|
||||
expected_items = [item for item in items if "c" in item.keys() and "another" in item.keys() and item["c"] == "9" and item["another"] == "y"*16]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
@@ -1,276 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for basic table operations: CreateTable, DeleteTable, ListTables.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import list_tables, test_table_name, create_test_table, random_string
|
||||
|
||||
# Utility function for create a table with a given name and some valid
|
||||
# schema.. This function initiates the table's creation, but doesn't
|
||||
# wait for the table to actually become ready.
|
||||
def create_table(dynamodb, name, BillingMode='PAY_PER_REQUEST', **kwargs):
|
||||
return dynamodb.create_table(
|
||||
TableName=name,
|
||||
BillingMode=BillingMode,
|
||||
KeySchema=[
|
||||
{
|
||||
'AttributeName': 'p',
|
||||
'KeyType': 'HASH'
|
||||
},
|
||||
{
|
||||
'AttributeName': 'c',
|
||||
'KeyType': 'RANGE'
|
||||
}
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{
|
||||
'AttributeName': 'p',
|
||||
'AttributeType': 'S'
|
||||
},
|
||||
{
|
||||
'AttributeName': 'c',
|
||||
'AttributeType': 'S'
|
||||
},
|
||||
],
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Utility function for creating a table with a given name, and then deleting
|
||||
# it immediately, waiting for these operations to complete. Since the wait
|
||||
# uses DescribeTable, this function requires all of CreateTable, DescribeTable
|
||||
# and DeleteTable to work correctly.
|
||||
# Note that in DynamoDB, table deletion takes a very long time, so tests
|
||||
# successfully using this function are very slow.
|
||||
def create_and_delete_table(dynamodb, name, **kwargs):
|
||||
table = create_table(dynamodb, name, **kwargs)
|
||||
table.meta.client.get_waiter('table_exists').wait(TableName=name)
|
||||
table.delete()
|
||||
table.meta.client.get_waiter('table_not_exists').wait(TableName=name)
|
||||
|
||||
##############################################################################
|
||||
|
||||
# Test creating a table, and then deleting it, waiting for each operation
|
||||
# to have completed before proceeding. Since the wait uses DescribeTable,
|
||||
# this tests requires all of CreateTable, DescribeTable and DeleteTable to
|
||||
# function properly in their basic use cases.
|
||||
# Unfortunately, this test is extremely slow with DynamoDB because deleting
|
||||
# a table is extremely slow until it really happens.
|
||||
def test_create_and_delete_table(dynamodb):
|
||||
create_and_delete_table(dynamodb, 'alternator_test')
|
||||
|
||||
# DynamoDB documentation specifies that table names must be 3-255 characters,
|
||||
# and match the regex [a-zA-Z0-9._-]+. Names not matching these rules should
|
||||
# be rejected, and no table be created.
|
||||
def test_create_table_unsupported_names(dynamodb):
|
||||
from botocore.exceptions import ParamValidationError, ClientError
|
||||
# Intererstingly, the boto library tests for names shorter than the
|
||||
# minimum length (3 characters) immediately, and failure results in
|
||||
# ParamValidationError. But the other invalid names are passed to
|
||||
# DynamoDB, which returns an HTTP response code, which results in a
|
||||
# CientError exception.
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_table(dynamodb, 'n')
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_table(dynamodb, 'nn')
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, 'n' * 256)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, 'nyh@test')
|
||||
|
||||
# On the other hand, names following the above rules should be accepted. Even
|
||||
# names which the Scylla rules forbid, such as a name starting with .
|
||||
def test_create_and_delete_table_non_scylla_name(dynamodb):
|
||||
create_and_delete_table(dynamodb, '.alternator_test')
|
||||
|
||||
# names with 255 characters are allowed in Dynamo, but they are not currently
|
||||
# supported in Scylla because we create a directory whose name is the table's
|
||||
# name followed by 33 bytes (underscore and UUID). So currently, we only
|
||||
# correctly support names with length up to 222.
|
||||
def test_create_and_delete_table_very_long_name(dynamodb):
|
||||
# In the future, this should work:
|
||||
#create_and_delete_table(dynamodb, 'n' * 255)
|
||||
# But for now, only 222 works:
|
||||
create_and_delete_table(dynamodb, 'n' * 222)
|
||||
# We cannot test the following on DynamoDB because it will succeed
|
||||
# (DynamoDB allows up to 255 bytes)
|
||||
#with pytest.raises(ClientError, match='ValidationException'):
|
||||
# create_table(dynamodb, 'n' * 223)
|
||||
|
||||
# Tests creating a table with an invalid schema should return a
|
||||
# ValidationException error.
|
||||
def test_create_table_invalid_schema(dynamodb):
|
||||
# The name of the table "created" by this test shouldn't matter, the
|
||||
# creation should not succeed anyway.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'p', 'KeyType': 'RANGE' },
|
||||
{ 'AttributeName': 'z', 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'z', 'AttributeType': 'S' }
|
||||
],
|
||||
)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'z', 'AttributeType': 'S' }
|
||||
],
|
||||
)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(
|
||||
TableName='name_doesnt_matter',
|
||||
BillingMode='PAY_PER_REQUEST',
|
||||
KeySchema=[
|
||||
{ 'AttributeName': 'k', 'KeyType': 'HASH' },
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'k', 'AttributeType': 'Q' }
|
||||
],
|
||||
)
|
||||
|
||||
# Test that trying to create a table that already exists fails in the
|
||||
# appropriate way (ResourceInUseException)
|
||||
def test_create_table_already_exists(dynamodb, test_table):
|
||||
with pytest.raises(ClientError, match='ResourceInUseException'):
|
||||
create_table(dynamodb, test_table.name)
|
||||
|
||||
# Test that BillingMode error path works as expected - only the values
|
||||
# PROVISIONED or PAY_PER_REQUEST are allowed. The former requires
|
||||
# ProvisionedThroughput to be set, the latter forbids it.
|
||||
# If BillingMode is outright missing, it defaults (as original
|
||||
# DynamoDB did) to PROVISIONED so ProvisionedThroughput is allowed.
|
||||
def test_create_table_billing_mode_errors(dynamodb, test_table):
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, test_table_name(), BillingMode='unknown')
|
||||
# billing mode is case-sensitive
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, test_table_name(), BillingMode='pay_per_request')
|
||||
# PAY_PER_REQUEST cannot come with a ProvisionedThroughput:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, test_table_name(),
|
||||
BillingMode='PAY_PER_REQUEST', ProvisionedThroughput={'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10})
|
||||
# On the other hand, PROVISIONED requires ProvisionedThroughput:
|
||||
# By the way, ProvisionedThroughput not only needs to appear, it must
|
||||
# have both ReadCapacityUnits and WriteCapacityUnits - but we can't test
|
||||
# this with boto3, because boto3 has its own verification that if
|
||||
# ProvisionedThroughput is given, it must have the correct form.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, test_table_name(), BillingMode='PROVISIONED')
|
||||
# If BillingMode is completely missing, it defaults to PROVISIONED, so
|
||||
# ProvisionedThroughput is required
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.create_table(TableName=test_table_name(),
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }])
|
||||
|
||||
# Our first implementation had a special column name called "attrs" where
|
||||
# we stored a map for all non-key columns. If the user tried to name one
|
||||
# of the key columns with this same name, the result was a disaster - Scylla
|
||||
# goes into a bad state after trying to write data with two updates to same-
|
||||
# named columns.
|
||||
special_column_name1 = 'attrs'
|
||||
special_column_name2 = ':attrs'
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_special_column_name(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[
|
||||
{ 'AttributeName': special_column_name1, 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': special_column_name2, 'KeyType': 'RANGE' }
|
||||
],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': special_column_name1, 'AttributeType': 'S' },
|
||||
{ 'AttributeName': special_column_name2, 'AttributeType': 'S' },
|
||||
],
|
||||
)
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.mark.xfail(reason="special attrs column not yet hidden correctly")
|
||||
def test_create_table_special_column_name(test_table_special_column_name):
|
||||
s = random_string()
|
||||
c = random_string()
|
||||
h = random_string()
|
||||
expected = {special_column_name1: s, special_column_name2: c, 'hello': h}
|
||||
test_table_special_column_name.put_item(Item=expected)
|
||||
got = test_table_special_column_name.get_item(Key={special_column_name1: s, special_column_name2: c}, ConsistentRead=True)['Item']
|
||||
assert got == expected
|
||||
|
||||
# Test that all tables we create are listed, and pagination works properly.
|
||||
# Note that the DyanamoDB setup we run this against may have hundreds of
|
||||
# other tables, for all we know. We just need to check that the tables we
|
||||
# created are indeed listed.
|
||||
def test_list_tables_paginated(dynamodb, test_table, test_table_s, test_table_b):
|
||||
my_tables_set = {table.name for table in [test_table, test_table_s, test_table_b]}
|
||||
for limit in [1, 2, 3, 4, 50, 100]:
|
||||
print("testing limit={}".format(limit))
|
||||
list_tables_set = set(list_tables(dynamodb, limit))
|
||||
assert my_tables_set.issubset(list_tables_set)
|
||||
|
||||
# Test that pagination limit is validated
|
||||
def test_list_tables_wrong_limit(dynamodb):
|
||||
# lower limit (min. 1) is imposed by boto3 library checks
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
dynamodb.meta.client.list_tables(Limit=101)
|
||||
@@ -1,854 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for the UpdateItem operations with an UpdateExpression parameter
|
||||
|
||||
import random
|
||||
import string
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from decimal import Decimal
|
||||
from util import random_string
|
||||
|
||||
# The simplest test of using UpdateExpression to set a top-level attribute,
|
||||
# instead of the older AttributeUpdates parameter.
|
||||
# Checks only one "SET" action in an UpdateExpression.
|
||||
def test_update_expression_set(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 4}
|
||||
|
||||
# An empty UpdateExpression is NOT allowed, and generates a "The expression
|
||||
# can not be empty" error. This contrasts with an empty AttributeUpdates which
|
||||
# is allowed, and results in the creation of an empty item if it didn't exist
|
||||
# yet (see test_empty_update()).
|
||||
def test_update_expression_empty(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='')
|
||||
|
||||
# A basic test with multiple SET actions in one expression
|
||||
def test_update_expression_set_multi(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET x = :val1, y = :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'x': 4, 'y': 4}
|
||||
|
||||
# SET can be used to copy an existing attribute to a new one
|
||||
def test_update_expression_set_copy(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello'}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET b = a')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello', 'b': 'hello'}
|
||||
# Copying an non-existing attribute generates an error
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c = z')
|
||||
# It turns out that attributes to be copied are read before the SET
|
||||
# starts to write, so "SET x = :val1, y = x" does not work...
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET x = :val1, y = x', ExpressionAttributeValues={':val1': 4})
|
||||
# SET z=z does nothing if z exists, or fails if it doesn't
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = a')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello', 'b': 'hello'}
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET z = z')
|
||||
# We can also use name references in either LHS or RHS of SET, e.g.,
|
||||
# SET #one = #two. We need to also take the references used in the RHS
|
||||
# when we want to complain about unused names in ExpressionAttributeNames.
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #one = #two',
|
||||
ExpressionAttributeNames={'#one': 'c', '#two': 'a'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello', 'b': 'hello', 'c': 'hello'}
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #one = #two',
|
||||
ExpressionAttributeNames={'#one': 'c', '#two': 'a', '#three': 'z'})
|
||||
|
||||
# Test for read-before-write action where the value to be read is nested inside a - operator
|
||||
def test_update_expression_set_nested_copy(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #n = :two',
|
||||
ExpressionAttributeNames={'#n': 'n'}, ExpressionAttributeValues={':two': 2})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #nn = :seven - #n',
|
||||
ExpressionAttributeNames={'#nn': 'nn', '#n': 'n'}, ExpressionAttributeValues={':seven': 7})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'n': 2, 'nn': 5}
|
||||
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #nnn = :nnn',
|
||||
ExpressionAttributeNames={'#nnn': 'nnn'}, ExpressionAttributeValues={':nnn': [2,4]})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #nnnn = list_append(:val1, #nnn)',
|
||||
ExpressionAttributeNames={'#nnnn': 'nnnn', '#nnn': 'nnn'}, ExpressionAttributeValues={':val1': [1,3]})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'n': 2, 'nn': 5, 'nnn': [2,4], 'nnnn': [1,3,2,4]}
|
||||
|
||||
# Test for getting a key value with read-before-write
|
||||
def test_update_expression_set_key(test_table_sn):
|
||||
p = random_string()
|
||||
test_table_sn.update_item(Key={'p': p, 'c': 7});
|
||||
test_table_sn.update_item(Key={'p': p, 'c': 7}, UpdateExpression='SET #n = #p',
|
||||
ExpressionAttributeNames={'#n': 'n', '#p': 'p'})
|
||||
test_table_sn.update_item(Key={'p': p, 'c': 7}, UpdateExpression='SET #nn = #c + #c',
|
||||
ExpressionAttributeNames={'#nn': 'nn', '#c': 'c'})
|
||||
assert test_table_sn.get_item(Key={'p': p, 'c': 7}, ConsistentRead=True)['Item'] == {'p': p, 'c': 7, 'n': p, 'nn': 14}
|
||||
|
||||
# Simple test for the "REMOVE" action
|
||||
def test_update_expression_remove(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello', 'b': 'hi'}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hi'}
|
||||
|
||||
# Demonstrate that although all DynamoDB examples give UpdateExpression
|
||||
# action names in uppercase - e.g., "SET", it can actually be any case.
|
||||
def test_update_expression_action_case(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET b = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 3}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='set b = :val1', ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 4}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='sEt b = :val1', ExpressionAttributeValues={':val1': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 5}
|
||||
|
||||
# Demonstrate that whitespace is ignored in UpdateExpression parsing.
|
||||
def test_update_expression_action_whitespace(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='set b = :val1', ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 4}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression=' set b=:val1 ', ExpressionAttributeValues={':val1': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 5}
|
||||
|
||||
# In UpdateExpression, the attribute name can appear directly in the expression
|
||||
# (without a "#placeholder" notation) only if it is a single "token" as
|
||||
# determined by DynamoDB's lexical analyzer rules: Such token is composed of
|
||||
# alphanumeric characters whose first character must be alphabetic. Other
|
||||
# names cause the parser to see multiple tokens, and produce syntax errors.
|
||||
def test_update_expression_name_token(test_table_s):
|
||||
p = random_string()
|
||||
# Alphanumeric names starting with an alphabetical character work
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET alnum = :val1', ExpressionAttributeValues={':val1': 1})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['alnum'] == 1
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET Alpha_Numeric_123 = :val1', ExpressionAttributeValues={':val1': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['Alpha_Numeric_123'] == 2
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET A123_ = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['A123_'] == 3
|
||||
# But alphanumeric names cannot start with underscore or digits.
|
||||
# DynamoDB's lexical analyzer doesn't recognize them, and produces
|
||||
# a ValidationException looking like:
|
||||
# Invalid UpdateExpression: Syntax error; token: "_", near: "SET _123"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET _123 = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET _abc = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET 123a = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET 123 = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
# Various other non-alpha-numeric characters, split a token and NOT allowed
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET hi-there = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET hi$there = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET "hithere" = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET !hithere = :val1', ExpressionAttributeValues={':val1': 3})
|
||||
|
||||
# In addition to the literal names, DynamoDB also allows references to any
|
||||
# name, using the "#reference" syntax. It turns out the reference name is
|
||||
# also a token following the rules as above, with one interesting point:
|
||||
# since "#" already started the token, the next character may be any
|
||||
# alphanumeric and doesn't need to be only alphabetical.
|
||||
# Note that the reference target - the actual attribute name - can include
|
||||
# absolutely any characters, and we use silly_name below as an example
|
||||
silly_name = '3can include any character!.#='
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #Alpha_Numeric_123 = :val1', ExpressionAttributeValues={':val1': 4}, ExpressionAttributeNames={'#Alpha_Numeric_123': silly_name})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'][silly_name] == 4
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #123a = :val1', ExpressionAttributeValues={':val1': 5}, ExpressionAttributeNames={'#123a': silly_name})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'][silly_name] == 5
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #123 = :val1', ExpressionAttributeValues={':val1': 6}, ExpressionAttributeNames={'#123': silly_name})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'][silly_name] == 6
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #_ = :val1', ExpressionAttributeValues={':val1': 7}, ExpressionAttributeNames={'#_': silly_name})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'][silly_name] == 7
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #hi-there = :val1', ExpressionAttributeValues={':val1': 7}, ExpressionAttributeNames={'#hi-there': silly_name})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #!hi = :val1', ExpressionAttributeValues={':val1': 7}, ExpressionAttributeNames={'#!hi': silly_name})
|
||||
# Just a "#" is not enough as a token. Interestingly, DynamoDB will
|
||||
# find the bad name in ExpressionAttributeNames before it actually tries
|
||||
# to parse UpdateExpression, but we can verify the parse fails too by
|
||||
# using a valid but irrelevant name in ExpressionAttributeNames:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET # = :val1', ExpressionAttributeValues={':val1': 7}, ExpressionAttributeNames={'#': silly_name})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET # = :val1', ExpressionAttributeValues={':val1': 7}, ExpressionAttributeNames={'#a': silly_name})
|
||||
|
||||
# There is also the value references, ":reference", for the right-hand
|
||||
# side of an assignment. These have similar naming rules like "#reference".
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :Alpha_Numeric_123', ExpressionAttributeValues={':Alpha_Numeric_123': 8})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 8
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :123a', ExpressionAttributeValues={':123a': 9})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 9
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :123', ExpressionAttributeValues={':123': 10})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 10
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :_', ExpressionAttributeValues={':_': 11})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 11
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :hi!there', ExpressionAttributeValues={':hi!there': 12})
|
||||
# Just a ":" is not enough as a token.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :', ExpressionAttributeValues={':': 7})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :', ExpressionAttributeValues={':a': 7})
|
||||
# Trying to use a :reference on the left-hand side of an assignment will
|
||||
# not work. In DynamoDB, it's a different type of token (and generates
|
||||
# syntax error).
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET :a = :b', ExpressionAttributeValues={':a': 1, ':b': 2})
|
||||
|
||||
# Multiple actions are allowed in one expression, but actions are divided
|
||||
# into clauses (SET, REMOVE, DELETE, ADD) and each of those can only appear
|
||||
# once.
|
||||
def test_update_expression_multi(test_table_s):
|
||||
p = random_string()
|
||||
# We can have two SET actions in one SET clause:
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val1, b = :val2', ExpressionAttributeValues={':val1': 1, ':val2': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1, 'b': 2}
|
||||
# But not two SET clauses - we get error "The "SET" section can only be used once in an update expression"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val1 SET b = :val2', ExpressionAttributeValues={':val1': 1, ':val2': 2})
|
||||
# We can have a REMOVE and a SET clause (note no comma between clauses):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a SET b = :val2', ExpressionAttributeValues={':val2': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 3}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c = :val2 REMOVE b', ExpressionAttributeValues={':val2': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'c': 3}
|
||||
# The same clause (e.g., SET) cannot be used twice, even if interleaved with something else
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val1 REMOVE a SET b = :val2', ExpressionAttributeValues={':val1': 1, ':val2': 2})
|
||||
|
||||
# Trying to modify the same item twice in the same update is forbidden.
|
||||
# For "SET a=:v REMOVE a" DynamoDB says: "Invalid UpdateExpression: Two
|
||||
# document paths overlap with each other; must remove or rewrite one of
|
||||
# these paths; path one: [a], path two: [a]".
|
||||
# It is actually good for Scylla that such updates are forbidden, because had
|
||||
# we allowed "SET a=:v REMOVE a" the result would be surprising - because data
|
||||
# wins over a delete with the same timestamp, so "a" would be set despite the
|
||||
# REMOVE command appearing later in the command line.
|
||||
def test_update_expression_multi_overlap(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello'}
|
||||
# Neither "REMOVE a SET a = :v" nor "SET a = :v REMOVE a" are allowed:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a SET a = :v', ExpressionAttributeValues={':v': 'hi'})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :v REMOVE a', ExpressionAttributeValues={':v': 'yo'})
|
||||
# It's also not allowed to set a twice in the same clause
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :v1, a = :v2', ExpressionAttributeValues={':v1': 'yo', ':v2': 'he'})
|
||||
# Obviously, the paths are compared after the name references are evaluated
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #a1 = :v1, #a2 = :v2', ExpressionAttributeValues={':v1': 'yo', ':v2': 'he'}, ExpressionAttributeNames={'#a1': 'a', '#a2': 'a'})
|
||||
|
||||
# The problem isn't just with identical paths - we can't modify two paths that
|
||||
# "overlap" in the sense that one is the ancestor of the other.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_multi_overlap_nested(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*overlap'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val1, a.b = :val2',
|
||||
ExpressionAttributeValues={':val1': {'b': 7}, ':val2': 'there'})
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': {'c': 2}}})
|
||||
with pytest.raises(ClientError, match='ValidationException.*overlap'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.b = :val1, a.b.c = :val2',
|
||||
ExpressionAttributeValues={':val1': 'hi', ':val2': 'there'})
|
||||
|
||||
# In the previous test we saw that *modifying* the same item twice in the same
|
||||
# update is forbidden; But it is allowed to *read* an item in the same update
|
||||
# that also modifies it, and we check this here.
|
||||
def test_update_expression_multi_with_copy(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hello'}
|
||||
# "REMOVE a SET b = a" works: as noted in test_update_expression_set_copy()
|
||||
# the value of 'a' is read before the actual REMOVE operation happens.
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a SET b = a')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hello'}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c = b REMOVE b')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'c': 'hello'}
|
||||
|
||||
|
||||
# Test case where a :val1 is referenced, without being defined
|
||||
def test_update_expression_set_missing_value(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1',
|
||||
ExpressionAttributeValues={':val2': 4})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1')
|
||||
|
||||
# It is forbidden for ExpressionAttributeValues to contain values not used
|
||||
# by the expression. DynamoDB produces an error like: "Value provided in
|
||||
# ExpressionAttributeValues unused in expressions: keys: {:val1}"
|
||||
def test_update_expression_spurious_value(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val1',
|
||||
ExpressionAttributeValues={':val1': 3, ':val2': 4})
|
||||
|
||||
# Test case where a #name is referenced, without being defined
|
||||
def test_update_expression_set_missing_name(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name = :val1',
|
||||
ExpressionAttributeValues={':val2': 4},
|
||||
ExpressionAttributeNames={'#wrongname': 'hello'})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name = :val1',
|
||||
ExpressionAttributeValues={':val2': 4})
|
||||
|
||||
# It is forbidden for ExpressionAttributeNames to contain names not used
|
||||
# by the expression. DynamoDB produces an error like: "Value provided in
|
||||
# ExpressionAttributeNames unused in expressions: keys: {#b}"
|
||||
def test_update_expression_spurious_name(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #a = :val1',
|
||||
ExpressionAttributeNames={'#a': 'hello', '#b': 'hi'},
|
||||
ExpressionAttributeValues={':val1': 3, ':val2': 4})
|
||||
|
||||
# Test that the key attributes (hash key or sort key) cannot be modified
|
||||
# by an update
|
||||
def test_update_expression_cannot_modify_key(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='SET p = :val1', ExpressionAttributeValues={':val1': 4})
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='SET c = :val1', ExpressionAttributeValues={':val1': 4})
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c}, UpdateExpression='REMOVE p')
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c}, UpdateExpression='REMOVE c')
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='ADD p :val1', ExpressionAttributeValues={':val1': 4})
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='ADD c :val1', ExpressionAttributeValues={':val1': 4})
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='DELETE p :val1', ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
||||
with pytest.raises(ClientError, match='ValidationException.*key'):
|
||||
test_table.update_item(Key={'p': p, 'c': c},
|
||||
UpdateExpression='DELETE c :val1', ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
||||
# As sanity check, verify we *can* modify a non-key column
|
||||
test_table.update_item(Key={'p': p, 'c': c}, UpdateExpression='SET a = :val1', ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c, 'a': 4}
|
||||
test_table.update_item(Key={'p': p, 'c': c}, UpdateExpression='REMOVE a')
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c}
|
||||
|
||||
# Test that trying to start an expression with some nonsense like HELLO
|
||||
# instead of SET, REMOVE, ADD or DELETE, fails.
|
||||
def test_update_expression_non_existant_clause(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='HELLO b = :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
|
||||
# Test support for "SET a = :val1 + :val2", "SET a = :val1 - :val2"
|
||||
# Only exactly these combinations work - e.g., it's a syntax error to
|
||||
# try to add three. Trying to add a string fails.
|
||||
def test_update_expression_plus_basic(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': 4, ':val2': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 7}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 - :val2',
|
||||
ExpressionAttributeValues={':val1': 5, ':val2': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 3}
|
||||
# Only the addition of exactly two values is supported!
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2 + :val3',
|
||||
ExpressionAttributeValues={':val1': 4, ':val2': 3, ':val3': 2})
|
||||
# Only numeric values can be added - other things like strings or lists
|
||||
# cannot be added, and we get an error like "Incorrect operand type for
|
||||
# operator or function; operator or function: +, operand type: S".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': 'dog', ':val2': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': ['a', 'b'], ':val2': ['1', '2']})
|
||||
|
||||
# While most of the Alternator code just saves high-precision numbers
|
||||
# unchanged, the "+" and "-" operations need to calculate with them, and
|
||||
# we should check the calculation isn't done with some lower-precision
|
||||
# representation, e.g., double
|
||||
def test_update_expression_plus_precision(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + :val2',
|
||||
ExpressionAttributeValues={':val1': Decimal("1"), ':val2': Decimal("10000000000000000000000")})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': Decimal("10000000000000000000001")}
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val2 - :val1',
|
||||
ExpressionAttributeValues={':val1': Decimal("1"), ':val2': Decimal("10000000000000000000000")})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': Decimal("9999999999999999999999")}
|
||||
|
||||
# Test support for "SET a = b + :val2" et al., i.e., a version of the
|
||||
# above test_update_expression_plus_basic with read before write.
|
||||
def test_update_expression_plus_rmw(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = a + :val1',
|
||||
ExpressionAttributeValues={':val1': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 5
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :val1 + a',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 9
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = :val1 + a',
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 10
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = b + a')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 19
|
||||
|
||||
# Test the list_append() function in SET, for the most basic use case of
|
||||
# concatenating two value references. Because this is the first test of
|
||||
# functions in SET, we also test some generic features of how functions
|
||||
# are parsed.
|
||||
def test_update_expression_list_append_basic(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1, :val2)',
|
||||
ExpressionAttributeValues={':val1': [4, 'hello'], ':val2': ['hi', 7]})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': [4, 'hello', 'hi', 7]}
|
||||
# Unlike the operation name "SET", function names are case-sensitive!
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = LIST_APPEND(:val1, :val2)',
|
||||
ExpressionAttributeValues={':val1': [4, 'hello'], ':val2': ['hi', 7]})
|
||||
# As usual, spaces are ignored by the parser
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1, :val2)',
|
||||
ExpressionAttributeValues={':val1': ['a'], ':val2': ['b']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['a', 'b']}
|
||||
# The list_append function only allows two parameters. The parser can
|
||||
# correctly parse fewer or more, but then an error is generated: "Invalid
|
||||
# UpdateExpression: Incorrect number of operands for operator or function;
|
||||
# operator or function: list_append, number of operands: 1".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1)',
|
||||
ExpressionAttributeValues={':val1': ['a']})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1, :val2, :val3)',
|
||||
ExpressionAttributeValues={':val1': [4, 'hello'], ':val2': [7], ':val3': ['a']})
|
||||
# If list_append is used on value which isn't a list, we get
|
||||
# error: "Invalid UpdateExpression: Incorrect operand type for operator
|
||||
# or function; operator or function: list_append, operand type: S"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1, :val2)',
|
||||
ExpressionAttributeValues={':val1': [4, 'hello'], ':val2': 'hi'})
|
||||
|
||||
# Additional list_append() tests, also using attribute paths as parameters
|
||||
# (i.e., read-modify-write).
|
||||
def test_update_expression_list_append(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :val1',
|
||||
ExpressionAttributeValues={':val1': ['hi', 2]})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] ==['hi', 2]
|
||||
# Often, list_append is used to append items to a list attribute
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(a, :val1)',
|
||||
ExpressionAttributeValues={':val1': [4, 'hello']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == ['hi', 2, 4, 'hello']
|
||||
# But it can also be used to just concatenate in other ways:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(:val1, a)',
|
||||
ExpressionAttributeValues={':val1': ['dog']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == ['dog', 'hi', 2, 4, 'hello']
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = list_append(a, :val1)',
|
||||
ExpressionAttributeValues={':val1': ['cat']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == ['dog', 'hi', 2, 4, 'hello', 'cat']
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = list_append(a, b)')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == ['dog', 'hi', 2, 4, 'hello', 'dog', 'hi', 2, 4, 'hello', 'cat']
|
||||
# As usual, #references are allowed instead of inline names:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET #name1 = list_append(#name2,:val1)',
|
||||
ExpressionAttributeValues={':val1': [8]},
|
||||
ExpressionAttributeNames={'#name1': 'a', '#name2': 'a'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == ['dog', 'hi', 2, 4, 'hello', 8]
|
||||
|
||||
# Test the "if_not_exists" function in SET
|
||||
# The test also checks additional features of function-call parsing.
|
||||
def test_update_expression_if_not_exists(test_table_s):
|
||||
p = random_string()
|
||||
# Since attribute a doesn't exist, set it:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = if_not_exists(a, :val1)',
|
||||
ExpressionAttributeValues={':val1': 2})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
||||
# Now the attribute does exist, so set does nothing:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = if_not_exists(a, :val1)',
|
||||
ExpressionAttributeValues={':val1': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
||||
# if_not_exists can also be used to check one attribute and set another,
|
||||
# but note that if_not_exists(a, :val) means a's value if it exists,
|
||||
# otherwise :val!
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(c, :val1)',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 4
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(c, :val1)',
|
||||
ExpressionAttributeValues={':val1': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 5
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(a, :val1)',
|
||||
ExpressionAttributeValues={':val1': 6})
|
||||
# note how because 'a' does exist, its value is copied, overwriting b's
|
||||
# value:
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 2
|
||||
# The parser expects function parameters to be value references, paths,
|
||||
# or nested call to functions. Other crap will cause syntax errors:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(non@sense, :val1)',
|
||||
ExpressionAttributeValues={':val1': 6})
|
||||
# if_not_exists() requires that the first parameter be a path. However,
|
||||
# the parser doesn't know this, and allows for a function parameter
|
||||
# also a value reference or a function call. If try one of these other
|
||||
# things the parser succeeds, but we get a later error, looking like:
|
||||
# "Invalid UpdateExpression: Operator or function requires a document
|
||||
# path; operator or function: if_not_exists"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(if_not_exists(a, :val2), :val1)',
|
||||
ExpressionAttributeValues={':val1': 6, ':val2': 3})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(:val2, :val1)',
|
||||
ExpressionAttributeValues={':val1': 6, ':val2': 3})
|
||||
# Surprisingly, if the wrong argument is a :val value reference, the
|
||||
# parser first tries to look it up in ExpressionAttributeValues (and
|
||||
# fails if it's missing), before realizing any value reference would be
|
||||
# wrong... So the following fails like the above does - but with a
|
||||
# different error message (which we do not check here): "Invalid
|
||||
# UpdateExpression: An expression attribute value used in expression
|
||||
# is not defined; attribute value: :val2"
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(:val2, :val1)',
|
||||
ExpressionAttributeValues={':val1': 6})
|
||||
|
||||
# When the expression parser parses a function call f(value, value), each
|
||||
# value may itself be a function call - ad infinitum. So expressions like
|
||||
# list_append(if_not_exists(a, :val1), :val2) are legal and so is deeper
|
||||
# nesting.
|
||||
@pytest.mark.xfail(reason="SET functions not yet implemented")
|
||||
def test_update_expression_function_nesting(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(if_not_exists(a, :val1), :val2)',
|
||||
ExpressionAttributeValues={':val1': ['a', 'b'], ':val2': ['cat', 'dog']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == ['a', 'b', 'cat', 'dog']
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(if_not_exists(a, :val1), :val2)',
|
||||
ExpressionAttributeValues={':val1': ['a', 'b'], ':val2': ['1', '2']})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == ['a', 'b', 'cat', 'dog', '1', '2']
|
||||
# I don't understand why the following expression isn't accepted, but it
|
||||
# isn't! It produces a "Invalid UpdateExpression: The function is not
|
||||
# allowed to be used this way in an expression; function: list_append".
|
||||
# I don't know how to explain it. In any case, the *parsing* works -
|
||||
# this is not a syntax error - the failure is in some verification later.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(list_append(:val1, :val2), :val3)',
|
||||
ExpressionAttributeValues={':val1': ['a'], ':val2': ['1'], ':val3': ['hi']})
|
||||
# Ditto, the following passes the parser but fails some later check with
|
||||
# the same error message as above.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = list_append(list_append(list_append(:val1, :val2), :val3), :val4)',
|
||||
ExpressionAttributeValues={':val1': ['a'], ':val2': ['1'], ':val3': ['hi'], ':val4': ['yo']})
|
||||
|
||||
# Verify how in SET expressions, "+" (or "-") nests with functions.
|
||||
# We discover that f(x)+f(y) works but f(x+y) does NOT (results in a syntax
|
||||
# error on the "+"). This means that the parser has two separate rules:
|
||||
# 1. set_action: SET path = value + value
|
||||
# 2. value: VALREF | NAME | NAME (value, ...)
|
||||
def test_update_expression_function_plus_nesting(test_table_s):
|
||||
p = random_string()
|
||||
# As explained above, this - with "+" outside the expression, works:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = if_not_exists(b, :val1)+:val2',
|
||||
ExpressionAttributeValues={':val1': 2, ':val2': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 5
|
||||
# ...but this - with the "+" inside an expression parameter, is a syntax
|
||||
# error:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET c = if_not_exists(c, :val1+:val2)',
|
||||
ExpressionAttributeValues={':val1': 5, ':val2': 4})
|
||||
|
||||
# This test tries to use an undefined function "f". This, obviously, fails,
|
||||
# but where we to actually print the error we would see "Invalid
|
||||
# UpdateExpression: Invalid function name; function: f". Not a syntax error.
|
||||
# This means that the parser accepts any alphanumeric name as a function
|
||||
# name, and only later use of this function fails because it's not one of
|
||||
# the supported file.
|
||||
def test_update_expression_unknown_function(test_table_s):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*f'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = f(b,c,d)')
|
||||
with pytest.raises(ClientError, match='ValidationException.*f123_hi'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = f123_hi(b,c,d)')
|
||||
# Just like unreferenced column names parsed by the DynamoDB parser,
|
||||
# function names must also start with an alphabetic character. Trying
|
||||
# to use _f as a function name will result with an actual syntax error,
|
||||
# on the "_" token.
|
||||
with pytest.raises(ClientError, match='ValidationException.*yntax error'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = _f(b,c,d)')
|
||||
|
||||
# Test "ADD" operation for numbers
|
||||
def test_update_expression_add_numbers(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 3, 'b': 'hi'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': 4})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 7
|
||||
# If the value to be added isn't a number, we get an error like "Invalid
|
||||
# UpdateExpression: Incorrect operand type for operator or function;
|
||||
# operator: ADD, operand type: STRING".
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
# Similarly, if the attribute we're adding to isn't a number, we get an
|
||||
# error like "An operand in the update expression has an incorrect data
|
||||
# type"
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
|
||||
# Test "ADD" operation for sets
|
||||
def test_update_expression_add_sets(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog', 'cat', 'mouse']), 'b': 'hi'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['pig'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['dog', 'cat', 'mouse', 'pig'])
|
||||
|
||||
# TODO: right now this test won't detect duplicated values in the returned result,
|
||||
# because boto3 parses a set out of the returned JSON anyway. This check should leverage
|
||||
# lower level API (if exists) to ensure that the JSON contains no duplicates
|
||||
# in the set representation. It has been verified manually.
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['beaver', 'lynx', 'coati']), 'b': 'hi'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['coati', 'beaver', 'badger'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['beaver', 'badger', 'lynx', 'coati'])
|
||||
|
||||
# The value to be added needs to be a set of the same type - it can't
|
||||
# be a single element or anything else. If the value has the wrong type,
|
||||
# we get an error like "Invalid UpdateExpression: Incorrect operand type
|
||||
# for operator or function; operator: ADD, operand type: STRING".
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
|
||||
# Test "DELETE" operation for sets
|
||||
def test_update_expression_delete_sets(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog', 'cat', 'mouse']), 'b': 'hi'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['dog'])
|
||||
# Deleting an element not present in the set is not an error - it just
|
||||
# does nothing
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['pig'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['dog'])
|
||||
# The value to be deleted must be a set of the same type - it can't
|
||||
# be a single element or anything else. If the value has the wrong type,
|
||||
# we get an error like "Invalid UpdateExpression: Incorrect operand type
|
||||
# for operator or function; operator: DELETE, operand type: STRING".
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
|
||||
######## Tests for paths and nested attribute updates:
|
||||
|
||||
# A dot inside a name in ExpressionAttributeNames is a literal dot, and
|
||||
# results in a top-level attribute with an actual dot in its name - not
|
||||
# a nested attribute path.
|
||||
def test_update_expression_dot_in_name(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #a = :val1',
|
||||
ExpressionAttributeValues={':val1': 3},
|
||||
ExpressionAttributeNames={'#a': 'a.b'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a.b': 3}
|
||||
|
||||
# A basic test for direct update of a nested attribute: One of the top-level
|
||||
# attributes is itself a document, and we update only one of that document's
|
||||
# nested attributes.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_nested_attribute_dot(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 4}, 'd': 5}
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 7}, 'd': 5}
|
||||
# Of course we can also add new nested attributes, not just modify
|
||||
# existing ones:
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.d = :val1',
|
||||
ExpressionAttributeValues={':val1': 3})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 7, 'd': 3}, 'd': 5}
|
||||
|
||||
# Similar test, for a list: one of the top-level attributes is a list, we
|
||||
# can update one of its items.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_nested_attribute_index(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': ['one', 'two', 'three']})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[1] = :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['one', 'hello', 'three']}
|
||||
|
||||
# Test that just like happens in top-level attributes, also in nested
|
||||
# attributes, setting them replaces the old value - potentially an entire
|
||||
# nested document, by the whole value (which may have a different type)
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_nested_different_type(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': {'one': 1, 'two': 2}}})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': {'b': 3, 'c': 7}}
|
||||
|
||||
# Yet another test of a nested attribute update. This one uses deeper
|
||||
# level of nesting (dots and indexes), adds #name references to the mix.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_nested_deep(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': ['hi', {'x': {'y': [3, 5, 7]}}]}})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.c[1].#name.y[1] = :val1',
|
||||
ExpressionAttributeValues={':val1': 9}, ExpressionAttributeNames={'#name': 'x'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == {'b': 3, 'c': ['hi', {'x': {'y': [3, 9, 7]}}]}
|
||||
# A deep path can also appear on the right-hand-side of an assignment
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.z = a.c[1].#name.y[1]',
|
||||
ExpressionAttributeNames={'#name': 'x'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a']['z'] == 9
|
||||
|
||||
# A REMOVE operation can be used to remove nested attributes, and also
|
||||
# individual list items.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_update_expression_nested_remove(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': ['hi', {'x': {'y': [3, 5, 7]}, 'q': 2}]}})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a.c[1].x.y[1], a.c[1].q')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == {'b': 3, 'c': ['hi', {'x': {'y': [3, 7]}}]}
|
||||
|
||||
# The DynamoDB documentation specifies: "When you use SET to update a list
|
||||
# element, the contents of that element are replaced with the new data that
|
||||
# you specify. If the element does not already exist, SET will append the
|
||||
# new element at the end of the list."
|
||||
# So if we take a three-element list a[7], and set a[7], the new element
|
||||
# will be put at the end of the list, not position 7 specifically.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_nested_attribute_update_array_out_of_bounds(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': ['one', 'two', 'three']})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[7] = :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['one', 'two', 'three', 'hello']}
|
||||
# The DynamoDB documentation also says: "If you add multiple elements
|
||||
# in a single SET operation, the elements are sorted in order by element
|
||||
# number.
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[84] = :val1, a[37] = :val2',
|
||||
ExpressionAttributeValues={':val1': 'a1', ':val2': 'a2'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['one', 'two', 'three', 'hello', 'a2', 'a1']}
|
||||
|
||||
# Test what happens if we try to write to a.b, which would only make sense if
|
||||
# a were a nested document, but a doesn't exist, or exists and is NOT a nested
|
||||
# document but rather a scalar or list or something.
|
||||
# DynamoDB actually detects this case and prints an error:
|
||||
# ClientError: An error occurred (ValidationException) when calling the
|
||||
# UpdateItem operation: The document path provided in the update expression
|
||||
# is invalid for update
|
||||
# Because Scylla doesn't read before write, it cannot detect this as an error,
|
||||
# so we'll probably want to allow for that possibility as well.
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_nested_attribute_update_bad_path_dot(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': ['hi']})
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET b.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
|
||||
|
||||
# Similarly for other types of bad paths - using [0] on something which
|
||||
# isn't an array,
|
||||
@pytest.mark.xfail(reason="nested updates not yet implemented")
|
||||
def test_nested_attribute_update_bad_path_array(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[0] = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
@@ -1,121 +0,0 @@
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
# Scylla is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Scylla is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Various utility functions which are useful for multiple tests
|
||||
|
||||
import string
|
||||
import random
|
||||
import collections
|
||||
import time
|
||||
|
||||
def random_string(length=10, chars=string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(length))
|
||||
|
||||
def random_bytes(length=10):
|
||||
return bytearray(random.getrandbits(8) for _ in range(length))
|
||||
|
||||
# Utility functions for scan and query into an array of items:
|
||||
# TODO: add to full_scan and full_query by default ConsistentRead=True, as
|
||||
# it's not useful for tests without it!
|
||||
def full_scan(table, **kwargs):
|
||||
response = table.scan(**kwargs)
|
||||
items = response['Items']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.scan(ExclusiveStartKey=response['LastEvaluatedKey'], **kwargs)
|
||||
items.extend(response['Items'])
|
||||
return items
|
||||
|
||||
# Utility function for fetching the entire results of a query into an array of items
|
||||
def full_query(table, **kwargs):
|
||||
response = table.query(**kwargs)
|
||||
items = response['Items']
|
||||
while 'LastEvaluatedKey' in response:
|
||||
response = table.query(ExclusiveStartKey=response['LastEvaluatedKey'], **kwargs)
|
||||
items.extend(response['Items'])
|
||||
return items
|
||||
|
||||
# To compare two lists of items (each is a dict) without regard for order,
|
||||
# "==" is not good enough because it will fail if the order is different.
|
||||
# The following function, multiset() converts the list into a multiset
|
||||
# (set with duplicates) where order doesn't matter, so the multisets can
|
||||
# be compared.
|
||||
|
||||
def freeze(item):
|
||||
if isinstance(item, dict):
|
||||
return frozenset((key, freeze(value)) for key, value in item.items())
|
||||
elif isinstance(item, list):
|
||||
return tuple(freeze(value) for value in item)
|
||||
return item
|
||||
|
||||
def multiset(items):
|
||||
return collections.Counter([freeze(item) for item in items])
|
||||
|
||||
|
||||
test_table_prefix = 'alternator_test_'
|
||||
def test_table_name():
|
||||
current_ms = int(round(time.time() * 1000))
|
||||
# In the off chance that test_table_name() is called twice in the same millisecond...
|
||||
if test_table_name.last_ms >= current_ms:
|
||||
current_ms = test_table_name.last_ms + 1
|
||||
test_table_name.last_ms = current_ms
|
||||
return test_table_prefix + str(current_ms)
|
||||
test_table_name.last_ms = 0
|
||||
|
||||
def create_test_table(dynamodb, **kwargs):
|
||||
name = test_table_name()
|
||||
print("fixture creating new table {}".format(name))
|
||||
table = dynamodb.create_table(TableName=name,
|
||||
BillingMode='PAY_PER_REQUEST', **kwargs)
|
||||
waiter = table.meta.client.get_waiter('table_exists')
|
||||
# recheck every second instead of the default, lower, frequency. This can
|
||||
# save a few seconds on AWS with its very slow table creation, but can
|
||||
# more on tests on Scylla with its faster table creation turnaround.
|
||||
waiter.config.delay = 1
|
||||
waiter.config.max_attempts = 200
|
||||
waiter.wait(TableName=name)
|
||||
return table
|
||||
|
||||
# DynamoDB's ListTables request returns up to a single page of table names
|
||||
# (e.g., up to 100) and it is up to the caller to call it again and again
|
||||
# to get the next page. This is a utility function which calls it repeatedly
|
||||
# as much as necessary to get the entire list.
|
||||
# We deliberately return a list and not a set, because we want the caller
|
||||
# to be able to recognize bugs in ListTables which causes the same table
|
||||
# to be returned twice.
|
||||
def list_tables(dynamodb, limit=100):
|
||||
ret = []
|
||||
pos = None
|
||||
while True:
|
||||
if pos:
|
||||
page = dynamodb.meta.client.list_tables(Limit=limit, ExclusiveStartTableName=pos);
|
||||
else:
|
||||
page = dynamodb.meta.client.list_tables(Limit=limit);
|
||||
results = page.get('TableNames', None)
|
||||
assert(results)
|
||||
ret = ret + results
|
||||
newpos = page.get('LastEvaluatedTableName', None)
|
||||
if not newpos:
|
||||
break;
|
||||
# It doesn't make sense for Dynamo to tell us we need more pages, but
|
||||
# not send anything in *this* page!
|
||||
assert len(results) > 0
|
||||
assert newpos != pos
|
||||
# Note that we only checked that we got back tables, not that we got
|
||||
# any new tables not already in ret. So a buggy implementation might
|
||||
# still cause an endless loop getting the same tables again and again.
|
||||
pos = newpos
|
||||
return ret
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// The DynamoAPI dictates that "binary" (a.k.a. "bytes" or "blob") values
|
||||
// be encoded in the JSON API as base64-encoded strings. This is code to
|
||||
// convert byte arrays to base64-encoded strings, and back.
|
||||
|
||||
#include "base64.hh"
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
|
||||
// Arrays for quickly converting to and from an integer between 0 and 63,
|
||||
// and the character used in base64 encoding to represent it.
|
||||
static class base64_chars {
|
||||
public:
|
||||
static constexpr const char* to =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int8_t from[255];
|
||||
base64_chars() {
|
||||
static_assert(strlen(to) == 64);
|
||||
for (int i = 0; i < 255; i++) {
|
||||
from[i] = 255; // signal invalid character
|
||||
}
|
||||
for (int i = 0; i < 64; i++) {
|
||||
from[(unsigned) to[i]] = i;
|
||||
}
|
||||
}
|
||||
} base64_chars;
|
||||
|
||||
std::string base64_encode(bytes_view in) {
|
||||
std::string ret;
|
||||
ret.reserve(((4 * in.size() / 3) + 3) & ~3);
|
||||
int i = 0;
|
||||
unsigned char chunk3[3]; // chunk of input
|
||||
for (auto byte : in) {
|
||||
chunk3[i++] = byte;
|
||||
if (i == 3) {
|
||||
ret += base64_chars.to[ (chunk3[0] & 0xfc) >> 2 ];
|
||||
ret += base64_chars.to[ ((chunk3[0] & 0x03) << 4) + ((chunk3[1] & 0xf0) >> 4) ];
|
||||
ret += base64_chars.to[ ((chunk3[1] & 0x0f) << 2) + ((chunk3[2] & 0xc0) >> 6) ];
|
||||
ret += base64_chars.to[ chunk3[2] & 0x3f ];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
// i can be 1 or 2.
|
||||
for(int j = i; j < 3; j++)
|
||||
chunk3[j] = '\0';
|
||||
ret += base64_chars.to[ ( chunk3[0] & 0xfc) >> 2 ];
|
||||
ret += base64_chars.to[ ((chunk3[0] & 0x03) << 4) + ((chunk3[1] & 0xf0) >> 4) ];
|
||||
if (i == 2) {
|
||||
ret += base64_chars.to[ ((chunk3[1] & 0x0f) << 2) + ((chunk3[2] & 0xc0) >> 6) ];
|
||||
} else {
|
||||
ret += '=';
|
||||
}
|
||||
ret += '=';
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bytes base64_decode(std::string_view in) {
|
||||
int i = 0;
|
||||
int8_t chunk4[4]; // chunk of input, each byte converted to 0..63;
|
||||
std::string ret;
|
||||
ret.reserve(in.size() * 3 / 4);
|
||||
for (unsigned char c : in) {
|
||||
uint8_t dc = base64_chars.from[c];
|
||||
if (dc == 255) {
|
||||
// Any unexpected character, include the "=" character usually
|
||||
// used for padding, signals the end of the decode.
|
||||
break;
|
||||
}
|
||||
chunk4[i++] = dc;
|
||||
if (i == 4) {
|
||||
ret += (chunk4[0] << 2) + ((chunk4[1] & 0x30) >> 4);
|
||||
ret += ((chunk4[1] & 0xf) << 4) + ((chunk4[2] & 0x3c) >> 2);
|
||||
ret += ((chunk4[2] & 0x3) << 6) + chunk4[3];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
// i can be 2 or 3, meaning 1 or 2 more output characters
|
||||
if (i>=2)
|
||||
ret += (chunk4[0] << 2) + ((chunk4[1] & 0x30) >> 4);
|
||||
if (i==3)
|
||||
ret += ((chunk4[1] & 0xf) << 4) + ((chunk4[2] & 0x3c) >> 2);
|
||||
}
|
||||
// FIXME: This copy is sad. The problem is we need back "bytes"
|
||||
// but "bytes" doesn't have efficient append and std::string.
|
||||
// To fix this we need to use bytes' "uninitialized" feature.
|
||||
return bytes(ret.begin(), ret.end());
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "bytes.hh"
|
||||
|
||||
std::string base64_encode(bytes_view);
|
||||
bytes base64_decode(std::string_view);
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include "alternator/conditions.hh"
|
||||
#include "alternator/error.hh"
|
||||
#include "cql3/constants.hh"
|
||||
#include <unordered_map>
|
||||
#include "rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
static logging::logger clogger("alternator-conditions");
|
||||
|
||||
comparison_operator_type get_comparison_operator(const rjson::value& comparison_operator) {
|
||||
static std::unordered_map<std::string, comparison_operator_type> ops = {
|
||||
{"EQ", comparison_operator_type::EQ},
|
||||
{"LE", comparison_operator_type::LE},
|
||||
{"LT", comparison_operator_type::LT},
|
||||
{"GE", comparison_operator_type::GE},
|
||||
{"GT", comparison_operator_type::GT},
|
||||
{"BETWEEN", comparison_operator_type::BETWEEN},
|
||||
{"BEGINS_WITH", comparison_operator_type::BEGINS_WITH},
|
||||
}; //TODO(sarna): NE, IN, CONTAINS, NULL, NOT_NULL
|
||||
if (!comparison_operator.IsString()) {
|
||||
throw api_error("ValidationException", format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
||||
}
|
||||
std::string op = comparison_operator.GetString();
|
||||
auto it = ops.find(op);
|
||||
if (it == ops.end()) {
|
||||
throw api_error("ValidationException", format("Unsupported comparison operator {}", op));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static ::shared_ptr<cql3::restrictions::single_column_restriction::contains> make_map_element_restriction(const column_definition& cdef, std::string_view key, const rjson::value& value) {
|
||||
bytes raw_key = utf8_type->from_string(sstring_view(key.data(), key.size()));
|
||||
auto key_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_key)));
|
||||
bytes raw_value = serialize_item(value);
|
||||
auto entry_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_value)));
|
||||
return make_shared<cql3::restrictions::single_column_restriction::contains>(cdef, std::move(key_value), std::move(entry_value));
|
||||
}
|
||||
|
||||
static ::shared_ptr<cql3::restrictions::single_column_restriction::EQ> make_key_eq_restriction(const column_definition& cdef, const rjson::value& value) {
|
||||
bytes raw_value = get_key_from_typed_value(value, cdef, type_to_string(cdef.type));
|
||||
auto restriction_value = ::make_shared<cql3::constants::value>(cql3::raw_value::make_value(std::move(raw_value)));
|
||||
return make_shared<cql3::restrictions::single_column_restriction::EQ>(cdef, std::move(restriction_value));
|
||||
}
|
||||
|
||||
::shared_ptr<cql3::restrictions::statement_restrictions> get_filtering_restrictions(schema_ptr schema, const column_definition& attrs_col, const rjson::value& query_filter) {
|
||||
clogger.trace("Getting filtering restrictions for: {}", rjson::print(query_filter));
|
||||
auto filtering_restrictions = ::make_shared<cql3::restrictions::statement_restrictions>(schema, true);
|
||||
for (auto it = query_filter.MemberBegin(); it != query_filter.MemberEnd(); ++it) {
|
||||
std::string_view column_name(it->name.GetString(), it->name.GetStringLength());
|
||||
const rjson::value& condition = it->value;
|
||||
|
||||
const rjson::value& comp_definition = rjson::get(condition, "ComparisonOperator");
|
||||
const rjson::value& attr_list = rjson::get(condition, "AttributeValueList");
|
||||
comparison_operator_type op = get_comparison_operator(comp_definition);
|
||||
|
||||
if (op != comparison_operator_type::EQ) {
|
||||
throw api_error("ValidationException", "Filtering is currently implemented for EQ operator only");
|
||||
}
|
||||
if (attr_list.Size() != 1) {
|
||||
throw api_error("ValidationException", format("EQ restriction needs exactly 1 attribute value: {}", rjson::print(attr_list)));
|
||||
}
|
||||
if (const column_definition* cdef = schema->get_column_definition(to_bytes(column_name.data()))) {
|
||||
// Primary key restriction
|
||||
filtering_restrictions->add_restriction(make_key_eq_restriction(*cdef, attr_list[0]), false, true);
|
||||
} else {
|
||||
// Regular column restriction
|
||||
filtering_restrictions->add_restriction(make_map_element_restriction(attrs_col, column_name, attr_list[0]), false, true);
|
||||
}
|
||||
|
||||
}
|
||||
return filtering_restrictions;
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the EQ relation
|
||||
static bool check_EQ(const rjson::value& v1, const rjson::value& v2) {
|
||||
return v1 == v2;
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the BEGINS_WITH relation
|
||||
static bool check_BEGINS_WITH(const rjson::value& v1, const rjson::value& v2) {
|
||||
// BEGINS_WITH only supports comparing two strings or two binaries -
|
||||
// any other combinations of types, or other malformed values, return
|
||||
// false (no match).
|
||||
if (!v1.IsObject() || v1.MemberCount() != 1 || !v2.IsObject() || v2.MemberCount() != 1) {
|
||||
return false;
|
||||
}
|
||||
auto it1 = v1.MemberBegin();
|
||||
auto it2 = v2.MemberBegin();
|
||||
if (it1->name != it2->name) {
|
||||
return false;
|
||||
}
|
||||
if (it1->name != "S" && it1->name != "B") {
|
||||
return false;
|
||||
}
|
||||
std::string_view val1(it1->value.GetString(), it1->value.GetStringLength());
|
||||
std::string_view val2(it2->value.GetString(), it2->value.GetStringLength());
|
||||
return val1.substr(0, val2.size()) == val2;
|
||||
}
|
||||
|
||||
// Verify one Expect condition on one attribute (whose content is "got")
|
||||
// for the verify_expected() below.
|
||||
// This function returns true or false depending on whether the condition
|
||||
// succeeded - it does not throw ConditionalCheckFailedException.
|
||||
// However, it may throw ValidationException on input validation errors.
|
||||
static bool verify_expected_one(const rjson::value& condition, const rjson::value* got) {
|
||||
const rjson::value* comparison_operator = rjson::find(condition, "ComparisonOperator");
|
||||
const rjson::value* attribute_value_list = rjson::find(condition, "AttributeValueList");
|
||||
const rjson::value* value = rjson::find(condition, "Value");
|
||||
const rjson::value* exists = rjson::find(condition, "Exists");
|
||||
// There are three types of conditions that Expected supports:
|
||||
// A value, not-exists, and a comparison of some kind. Each allows
|
||||
// and requires a different combinations of parameters in the request
|
||||
if (value) {
|
||||
if (exists && (!exists->IsBool() || exists->GetBool() != true)) {
|
||||
throw api_error("ValidationException", "Cannot combine Value with Exists!=true");
|
||||
}
|
||||
if (comparison_operator) {
|
||||
throw api_error("ValidationException", "Cannot combine Value with ComparisonOperator");
|
||||
}
|
||||
return got && check_EQ(*got, *value);
|
||||
} else if (exists) {
|
||||
if (comparison_operator) {
|
||||
throw api_error("ValidationException", "Cannot combine Exists with ComparisonOperator");
|
||||
}
|
||||
if (!exists->IsBool() || exists->GetBool() != false) {
|
||||
throw api_error("ValidationException", "Exists!=false requires Value");
|
||||
}
|
||||
// Remember Exists=false, so we're checking that the attribute does *not* exist:
|
||||
return !got;
|
||||
} else {
|
||||
if (!comparison_operator) {
|
||||
throw api_error("ValidationException", "Missing ComparisonOperator, Value or Exists");
|
||||
}
|
||||
if (!attribute_value_list || !attribute_value_list->IsArray()) {
|
||||
throw api_error("ValidationException", "With ComparisonOperator, AttributeValueList must be given and an array");
|
||||
}
|
||||
comparison_operator_type op = get_comparison_operator(*comparison_operator);
|
||||
switch (op) {
|
||||
case comparison_operator_type::EQ:
|
||||
if (attribute_value_list->Size() != 1) {
|
||||
throw api_error("ValidationException", "EQ operator requires one element in AttributeValueList");
|
||||
}
|
||||
if (got) {
|
||||
const rjson::value& expected = (*attribute_value_list)[0];
|
||||
return check_EQ(*got, expected);
|
||||
}
|
||||
return false;
|
||||
case comparison_operator_type::BEGINS_WITH:
|
||||
if (attribute_value_list->Size() != 1) {
|
||||
throw api_error("ValidationException", "BEGINS_WITH operator requires one element in AttributeValueList");
|
||||
}
|
||||
if (got) {
|
||||
const rjson::value& expected = (*attribute_value_list)[0];
|
||||
return check_BEGINS_WITH(*got, expected);
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
// FIXME: implement all the missing types, so there will be no default here.
|
||||
throw api_error("ValidationException", format("ComparisonOperator {} is not yet supported", *comparison_operator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the existing values of the item (previous_item) match the
|
||||
// conditions given by the Expected and ConditionalOperator parameters
|
||||
// (if they exist) in the request (an UpdateItem, PutItem or DeleteItem).
|
||||
// This function will throw a ConditionalCheckFailedException API error
|
||||
// if the values do not match the condition, or ValidationException if there
|
||||
// are errors in the format of the condition itself.
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item) {
|
||||
const rjson::value* expected = rjson::find(req, "Expected");
|
||||
if (!expected) {
|
||||
return;
|
||||
}
|
||||
if (!expected->IsObject()) {
|
||||
throw api_error("ValidationException", "'Expected' parameter, if given, must be an object");
|
||||
}
|
||||
// ConditionalOperator can be "AND" for requiring all conditions, or
|
||||
// "OR" for requiring one condition, and defaults to "AND" if missing.
|
||||
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
||||
bool require_all = true;
|
||||
if (conditional_operator) {
|
||||
if (!conditional_operator->IsString()) {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter, if given, must be a string");
|
||||
}
|
||||
std::string_view s(conditional_operator->GetString(), conditional_operator->GetStringLength());
|
||||
if (s == "AND") {
|
||||
// require_all is already true
|
||||
} else if (s == "OR") {
|
||||
require_all = false;
|
||||
} else {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter must be AND, OR or missing");
|
||||
}
|
||||
if (expected->GetObject().ObjectEmpty()) {
|
||||
throw api_error("ValidationException", "'ConditionalOperator' parameter cannot be specified for empty Expression");
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = expected->MemberBegin(); it != expected->MemberEnd(); ++it) {
|
||||
const rjson::value* got = nullptr;
|
||||
if (previous_item && previous_item->IsObject() && previous_item->HasMember("Item")) {
|
||||
got = rjson::find((*previous_item)["Item"], rjson::string_ref_type(it->name.GetString()));
|
||||
}
|
||||
bool success = verify_expected_one(it->value, got);
|
||||
if (success && !require_all) {
|
||||
// When !require_all, one success is enough!
|
||||
return;
|
||||
} else if (!success && require_all) {
|
||||
// When require_all, one failure is enough!
|
||||
throw api_error("ConditionalCheckFailedException", "Failed condition.");
|
||||
}
|
||||
}
|
||||
// If we got here and require_all, none of the checks failed, so succeed.
|
||||
// If we got here and !require_all, all of the checks failed, so fail.
|
||||
if (!require_all) {
|
||||
throw api_error("ConditionalCheckFailedException", "None of ORed Expect conditions were successful.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file contains definitions and functions related to placing conditions
|
||||
* on Alternator queries (equivalent of CQL's restrictions).
|
||||
*
|
||||
* With conditions, it's possible to add criteria to selection requests (Scan, Query)
|
||||
* and use them for narrowing down the result set, by means of filtering or indexing.
|
||||
*
|
||||
* Ref: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cql3/restrictions/statement_restrictions.hh"
|
||||
#include "serialization.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
enum class comparison_operator_type {
|
||||
EQ, NE, LE, LT, GE, GT, IN, BETWEEN, CONTAINS, IS_NULL, NOT_NULL, BEGINS_WITH
|
||||
};
|
||||
|
||||
comparison_operator_type get_comparison_operator(const rjson::value& comparison_operator);
|
||||
|
||||
::shared_ptr<cql3::restrictions::statement_restrictions> get_filtering_restrictions(schema_ptr schema, const column_definition& attrs_col, const rjson::value& query_filter);
|
||||
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item);
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// DynamoDB's error messages are described in detail in
|
||||
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html
|
||||
// Ah An error message has a "type", e.g., "ResourceNotFoundException", a coarser
|
||||
// HTTP code (almost always, 400), and a human readable message. Eventually these
|
||||
// will be wrapped into a JSON object returned to the client.
|
||||
class api_error : public std::exception {
|
||||
public:
|
||||
using status_type = httpd::reply::status_type;
|
||||
status_type _http_code;
|
||||
std::string _type;
|
||||
std::string _msg;
|
||||
api_error(std::string type, std::string msg, status_type http_code = status_type::bad_request)
|
||||
: _http_code(std::move(http_code))
|
||||
, _type(std::move(type))
|
||||
, _msg(std::move(msg))
|
||||
{ }
|
||||
api_error() = default;
|
||||
virtual const char* what() const noexcept override { return _msg.c_str(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/json/json_elements.hh>
|
||||
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "service/migration_manager.hh"
|
||||
#include "service/client_state.hh"
|
||||
|
||||
#include "stats.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class executor {
|
||||
service::storage_proxy& _proxy;
|
||||
service::migration_manager& _mm;
|
||||
|
||||
public:
|
||||
using client_state = service::client_state;
|
||||
stats _stats;
|
||||
static constexpr auto ATTRS_COLUMN_NAME = ":attrs";
|
||||
static constexpr auto KEYSPACE_NAME = "alternator";
|
||||
|
||||
executor(service::storage_proxy& proxy, service::migration_manager& mm) : _proxy(proxy), _mm(mm) {}
|
||||
|
||||
future<json::json_return_type> create_table(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> describe_table(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> delete_table(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> put_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> get_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> delete_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> update_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> list_tables(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> scan(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> describe_endpoints(client_state& client_state, std::string content, std::string host_header);
|
||||
future<json::json_return_type> batch_write_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> batch_get_item(client_state& client_state, std::string content);
|
||||
future<json::json_return_type> query(client_state& client_state, std::string content);
|
||||
|
||||
future<> start();
|
||||
future<> stop() { return make_ready_future<>(); }
|
||||
|
||||
future<> maybe_create_keyspace();
|
||||
|
||||
static void maybe_trace_query(client_state& client_state, sstring_view op, sstring_view query);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "expressions.hh"
|
||||
#include "alternator/expressionsLexer.hpp"
|
||||
#include "alternator/expressionsParser.hpp"
|
||||
|
||||
#include <seastarx.hh>
|
||||
|
||||
#include <seastar/core/print.hh>
|
||||
#include <seastar/util/log.hh>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
template <typename Func, typename Result = std::result_of_t<Func(expressionsParser&)>>
|
||||
Result do_with_parser(std::string input, Func&& f) {
|
||||
expressionsLexer::InputStreamType input_stream{
|
||||
reinterpret_cast<const ANTLR_UINT8*>(input.data()),
|
||||
ANTLR_ENC_UTF8,
|
||||
static_cast<ANTLR_UINT32>(input.size()),
|
||||
nullptr };
|
||||
expressionsLexer lexer(&input_stream);
|
||||
expressionsParser::TokenStreamType tstream(ANTLR_SIZE_HINT, lexer.get_tokSource());
|
||||
expressionsParser parser(&tstream);
|
||||
|
||||
auto result = f(parser);
|
||||
return result;
|
||||
}
|
||||
|
||||
parsed::update_expression
|
||||
parse_update_expression(std::string query) {
|
||||
try {
|
||||
return do_with_parser(query, std::mem_fn(&expressionsParser::update_expression));
|
||||
} catch (...) {
|
||||
throw expressions_syntax_error(format("Failed parsing UpdateExpression '{}': {}", query, std::current_exception()));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<parsed::path>
|
||||
parse_projection_expression(std::string query) {
|
||||
try {
|
||||
return do_with_parser(query, std::mem_fn(&expressionsParser::projection_expression));
|
||||
} catch (...) {
|
||||
throw expressions_syntax_error(format("Failed parsing ProjectionExpression '{}': {}", query, std::current_exception()));
|
||||
}
|
||||
}
|
||||
|
||||
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
|
||||
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
namespace parsed {
|
||||
|
||||
void update_expression::add(update_expression::action a) {
|
||||
std::visit(overloaded {
|
||||
[&] (action::set&) { seen_set = true; },
|
||||
[&] (action::remove&) { seen_remove = true; },
|
||||
[&] (action::add&) { seen_add = true; },
|
||||
[&] (action::del&) { seen_del = true; }
|
||||
}, a._action);
|
||||
_actions.push_back(std::move(a));
|
||||
}
|
||||
|
||||
void update_expression::append(update_expression other) {
|
||||
if ((seen_set && other.seen_set) ||
|
||||
(seen_remove && other.seen_remove) ||
|
||||
(seen_add && other.seen_add) ||
|
||||
(seen_del && other.seen_del)) {
|
||||
throw expressions_syntax_error("Each of SET, REMOVE, ADD, DELETE may only appear once in UpdateExpression");
|
||||
}
|
||||
std::move(other._actions.begin(), other._actions.end(), std::back_inserter(_actions));
|
||||
seen_set |= other.seen_set;
|
||||
seen_remove |= other.seen_remove;
|
||||
seen_add |= other.seen_add;
|
||||
seen_del |= other.seen_del;
|
||||
}
|
||||
|
||||
} // namespace parsed
|
||||
} // namespace alternator
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*
|
||||
* This file is part of Scylla. See the LICENSE.PROPRIETARY file in the
|
||||
* top-level directory for licensing information.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The DynamoDB protocol is based on JSON, and most DynamoDB requests
|
||||
* describe the operation and its parameters via JSON objects such as maps
|
||||
* and lists. Nevertheless, in some types of requests an "expression" is
|
||||
* passed as a single string, and we need to parse this string. These
|
||||
* cases include:
|
||||
* 1. Attribute paths, such as "a[3].b.c", are used in projection
|
||||
* expressions as well as inside other expressions described below.
|
||||
* 2. Condition expressions, such as "(NOT (a=b OR c=d)) AND e=f",
|
||||
* used in conditional updates, filters, and other places.
|
||||
* 3. Update expressions, such as "SET #a.b = :x, c = :y DELETE d"
|
||||
*
|
||||
* All these expression syntaxes are very simple: Most of them could be
|
||||
* parsed as regular expressions, and the parenthesized condition expression
|
||||
* could be done with a simple hand-written lexical analyzer and recursive-
|
||||
* descent parser. Nevertheless, we decided to specify these parsers in the
|
||||
* ANTLR3 language already used in the Scylla project, hopefully making these
|
||||
* parsers easier to reason about, and easier to change if needed - and
|
||||
* reducing the amount of boiler-plate code.
|
||||
*/
|
||||
|
||||
grammar expressions;
|
||||
|
||||
options {
|
||||
language = Cpp;
|
||||
}
|
||||
|
||||
@parser::namespace{alternator}
|
||||
@lexer::namespace{alternator}
|
||||
|
||||
/* TODO: explain what these traits things are. I haven't seen them explained
|
||||
* in any document... Compilation fails without these fail because a definition
|
||||
* of "expressionsLexerTraits" and "expressionParserTraits" is needed.
|
||||
*/
|
||||
@lexer::traits {
|
||||
class expressionsLexer;
|
||||
class expressionsParser;
|
||||
typedef antlr3::Traits<expressionsLexer, expressionsParser> expressionsLexerTraits;
|
||||
}
|
||||
@parser::traits {
|
||||
typedef expressionsLexerTraits expressionsParserTraits;
|
||||
}
|
||||
|
||||
@lexer::header {
|
||||
#include "alternator/expressions.hh"
|
||||
// ANTLR generates a bunch of unused variables and functions. Yuck...
|
||||
#pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
}
|
||||
@parser::header {
|
||||
#include "expressionsLexer.hpp"
|
||||
}
|
||||
|
||||
/* By default, ANTLR3 composes elaborate syntax-error messages, saying which
|
||||
* token was unexpected, where, and so on on, but then dutifully writes these
|
||||
* error messages to the standard error, and returns from the parser as if
|
||||
* everything was fine, with a half-constructed output object! If we define
|
||||
* the "displayRecognitionError" method, it will be called upon to build this
|
||||
* error message, and we can instead throw an exception to stop the parsing
|
||||
* immediately. This is good enough for now, for our simple needs, but if
|
||||
* we ever want to show more information about the syntax error, Cql3.g
|
||||
* contains an elaborate implementation (it would be nice if we could reuse
|
||||
* it, not duplicate it).
|
||||
* Unfortunately, we have to repeat the same definition twice - once for the
|
||||
* parser, and once for the lexer.
|
||||
*/
|
||||
@parser::context {
|
||||
void displayRecognitionError(ANTLR_UINT8** token_names, ExceptionBaseType* ex) {
|
||||
throw expressions_syntax_error("syntax error");
|
||||
}
|
||||
}
|
||||
@lexer::context {
|
||||
void displayRecognitionError(ANTLR_UINT8** token_names, ExceptionBaseType* ex) {
|
||||
throw expressions_syntax_error("syntax error");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Lexical analysis phase, i.e., splitting the input up to tokens.
|
||||
* Lexical analyzer rules have names starting in capital letters.
|
||||
* "fragment" rules do not generate tokens, and are just aliases used to
|
||||
* make other rules more readable.
|
||||
* Characters *not* listed here, e.g., '=', '(', etc., will be handled
|
||||
* as individual tokens on their own right.
|
||||
* Whitespace spans are skipped, so do not generate tokens.
|
||||
*/
|
||||
WHITESPACE: (' ' | '\t' | '\n' | '\r')+ { skip(); };
|
||||
|
||||
/* shortcuts for case-insensitive keywords */
|
||||
fragment A:('a'|'A');
|
||||
fragment B:('b'|'B');
|
||||
fragment C:('c'|'C');
|
||||
fragment D:('d'|'D');
|
||||
fragment E:('e'|'E');
|
||||
fragment F:('f'|'F');
|
||||
fragment G:('g'|'G');
|
||||
fragment H:('h'|'H');
|
||||
fragment I:('i'|'I');
|
||||
fragment J:('j'|'J');
|
||||
fragment K:('k'|'K');
|
||||
fragment L:('l'|'L');
|
||||
fragment M:('m'|'M');
|
||||
fragment N:('n'|'N');
|
||||
fragment O:('o'|'O');
|
||||
fragment P:('p'|'P');
|
||||
fragment Q:('q'|'Q');
|
||||
fragment R:('r'|'R');
|
||||
fragment S:('s'|'S');
|
||||
fragment T:('t'|'T');
|
||||
fragment U:('u'|'U');
|
||||
fragment V:('v'|'V');
|
||||
fragment W:('w'|'W');
|
||||
fragment X:('x'|'X');
|
||||
fragment Y:('y'|'Y');
|
||||
fragment Z:('z'|'Z');
|
||||
/* These keywords must be appear before the generic NAME token below,
|
||||
* because NAME matches too, and the first to match wins.
|
||||
*/
|
||||
SET: S E T;
|
||||
REMOVE: R E M O V E;
|
||||
ADD: A D D;
|
||||
DELETE: D E L E T E;
|
||||
|
||||
fragment ALPHA: 'A'..'Z' | 'a'..'z';
|
||||
fragment DIGIT: '0'..'9';
|
||||
fragment ALNUM: ALPHA | DIGIT | '_';
|
||||
INTEGER: DIGIT+;
|
||||
NAME: ALPHA ALNUM*;
|
||||
NAMEREF: '#' ALNUM+;
|
||||
VALREF: ':' ALNUM+;
|
||||
|
||||
/*
|
||||
* Parsing phase - parsing the string of tokens generated by the lexical
|
||||
* analyzer defined above.
|
||||
*/
|
||||
|
||||
path_component: NAME | NAMEREF;
|
||||
path returns [parsed::path p]:
|
||||
root=path_component { $p.set_root($root.text); }
|
||||
( '.' name=path_component { $p.add_dot($name.text); }
|
||||
| '[' INTEGER ']' { $p.add_index(std::stoi($INTEGER.text)); }
|
||||
)*;
|
||||
|
||||
update_expression_set_value returns [parsed::value v]:
|
||||
VALREF { $v.set_valref($VALREF.text); }
|
||||
| path { $v.set_path($path.p); }
|
||||
| NAME { $v.set_func_name($NAME.text); }
|
||||
'(' x=update_expression_set_value { $v.add_func_parameter($x.v); }
|
||||
(',' x=update_expression_set_value { $v.add_func_parameter($x.v); })*
|
||||
')'
|
||||
;
|
||||
|
||||
update_expression_set_rhs returns [parsed::set_rhs rhs]:
|
||||
v=update_expression_set_value { $rhs.set_value(std::move($v.v)); }
|
||||
( '+' v=update_expression_set_value { $rhs.set_plus(std::move($v.v)); }
|
||||
| '-' v=update_expression_set_value { $rhs.set_minus(std::move($v.v)); }
|
||||
)?
|
||||
;
|
||||
|
||||
update_expression_set_action returns [parsed::update_expression::action a]:
|
||||
path '=' rhs=update_expression_set_rhs { $a.assign_set($path.p, $rhs.rhs); };
|
||||
|
||||
update_expression_remove_action returns [parsed::update_expression::action a]:
|
||||
path { $a.assign_remove($path.p); };
|
||||
|
||||
update_expression_add_action returns [parsed::update_expression::action a]:
|
||||
path VALREF { $a.assign_add($path.p, $VALREF.text); };
|
||||
|
||||
update_expression_delete_action returns [parsed::update_expression::action a]:
|
||||
path VALREF { $a.assign_del($path.p, $VALREF.text); };
|
||||
|
||||
update_expression_clause returns [parsed::update_expression e]:
|
||||
SET s=update_expression_set_action { $e.add(s); }
|
||||
(',' s=update_expression_set_action { $e.add(s); })*
|
||||
| REMOVE r=update_expression_remove_action { $e.add(r); }
|
||||
(',' r=update_expression_remove_action { $e.add(r); })*
|
||||
| ADD a=update_expression_add_action { $e.add(a); }
|
||||
(',' a=update_expression_add_action { $e.add(a); })*
|
||||
| DELETE d=update_expression_delete_action { $e.add(d); }
|
||||
(',' d=update_expression_delete_action { $e.add(d); })*
|
||||
;
|
||||
|
||||
// Note the "EOF" token at the end of the update expression. We want to the
|
||||
// parser to match the entire string given to it - not just its beginning!
|
||||
update_expression returns [parsed::update_expression e]:
|
||||
(update_expression_clause { e.append($update_expression_clause.e); })* EOF;
|
||||
|
||||
projection_expression returns [std::vector<parsed::path> v]:
|
||||
p=path { $v.push_back(std::move($p.p)); }
|
||||
(',' p=path { $v.push_back(std::move($p.p)); } )* EOF;
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include "expressions_types.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class expressions_syntax_error : public std::runtime_error {
|
||||
public:
|
||||
using runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
parsed::update_expression parse_update_expression(std::string query);
|
||||
std::vector<parsed::path> parse_projection_expression(std::string query);
|
||||
|
||||
|
||||
} /* namespace alternator */
|
||||
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
/*
|
||||
* Parsed representation of expressions and their components.
|
||||
*
|
||||
* Types in alternator::parse namespace are used for holding the parse
|
||||
* tree - objects generated by the Antlr rules after parsing an expression.
|
||||
* Because of the way Antlr works, all these objects are default-constructed
|
||||
* first, and then assigned when the rule is completed, so all these types
|
||||
* have only default constructors - but setter functions to set them later.
|
||||
*/
|
||||
|
||||
namespace alternator {
|
||||
namespace parsed {
|
||||
|
||||
// "path" is an attribute's path in a document, e.g., a.b[3].c.
|
||||
class path {
|
||||
// All paths have a "root", a top-level attribute, and any number of
|
||||
// "dereference operators" - each either an index (e.g., "[2]") or a
|
||||
// dot (e.g., ".xyz").
|
||||
std::string _root;
|
||||
std::vector<std::variant<std::string, unsigned>> _operators;
|
||||
public:
|
||||
void set_root(std::string root) {
|
||||
_root = std::move(root);
|
||||
}
|
||||
void add_index(unsigned i) {
|
||||
_operators.emplace_back(i);
|
||||
}
|
||||
void add_dot(std::string(name)) {
|
||||
_operators.emplace_back(std::move(name));
|
||||
}
|
||||
const std::string& root() const {
|
||||
return _root;
|
||||
}
|
||||
bool has_operators() const {
|
||||
return !_operators.empty();
|
||||
}
|
||||
};
|
||||
|
||||
// "value" is is a value used in the right hand side of an assignment
|
||||
// expression, "SET a = ...". It can be a reference to a value included in
|
||||
// the request (":val"), a path to an attribute from the existing item
|
||||
// (e.g., "a.b[3].c"), or a function of other such values.
|
||||
// Note that the real right-hand-side of an assignment is actually a bit
|
||||
// more general - it allows either a value, or a value+value or value-value -
|
||||
// see class set_rhs below.
|
||||
struct value {
|
||||
struct function_call {
|
||||
std::string _function_name;
|
||||
std::vector<value> _parameters;
|
||||
};
|
||||
std::variant<std::string, path, function_call> _value;
|
||||
void set_valref(std::string s) {
|
||||
_value = std::move(s);
|
||||
}
|
||||
void set_path(path p) {
|
||||
_value = std::move(p);
|
||||
}
|
||||
void set_func_name(std::string s) {
|
||||
_value = function_call {std::move(s), {}};
|
||||
}
|
||||
void add_func_parameter(value v) {
|
||||
std::get<function_call>(_value)._parameters.emplace_back(std::move(v));
|
||||
}
|
||||
};
|
||||
|
||||
// The right-hand-side of a SET in an update expression can be either a
|
||||
// single value (see above), or value+value, or value-value.
|
||||
class set_rhs {
|
||||
public:
|
||||
char _op; // '+', '-', or 'v''
|
||||
value _v1;
|
||||
value _v2;
|
||||
void set_value(value&& v1) {
|
||||
_op = 'v';
|
||||
_v1 = std::move(v1);
|
||||
}
|
||||
void set_plus(value&& v2) {
|
||||
_op = '+';
|
||||
_v2 = std::move(v2);
|
||||
}
|
||||
void set_minus(value&& v2) {
|
||||
_op = '-';
|
||||
_v2 = std::move(v2);
|
||||
}
|
||||
};
|
||||
|
||||
class update_expression {
|
||||
public:
|
||||
struct action {
|
||||
path _path;
|
||||
struct set {
|
||||
set_rhs _rhs;
|
||||
};
|
||||
struct remove {
|
||||
};
|
||||
struct add {
|
||||
std::string _valref;
|
||||
};
|
||||
struct del {
|
||||
std::string _valref;
|
||||
};
|
||||
std::variant<set, remove, add, del> _action;
|
||||
|
||||
void assign_set(path p, set_rhs rhs) {
|
||||
_path = std::move(p);
|
||||
_action = set { std::move(rhs) };
|
||||
}
|
||||
void assign_remove(path p) {
|
||||
_path = std::move(p);
|
||||
_action = remove { };
|
||||
}
|
||||
void assign_add(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = add { std::move(v) };
|
||||
}
|
||||
void assign_del(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = del { std::move(v) };
|
||||
}
|
||||
};
|
||||
private:
|
||||
std::vector<action> _actions;
|
||||
bool seen_set = false;
|
||||
bool seen_remove = false;
|
||||
bool seen_add = false;
|
||||
bool seen_del = false;
|
||||
public:
|
||||
void add(action a);
|
||||
void append(update_expression other);
|
||||
bool empty() const {
|
||||
return _actions.empty();
|
||||
}
|
||||
const std::vector<action>& actions() const {
|
||||
return _actions;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace parsed
|
||||
} // namespace alternator
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "rjson.hh"
|
||||
#include "error.hh"
|
||||
#include <seastar/core/print.hh>
|
||||
|
||||
namespace rjson {
|
||||
|
||||
static allocator the_allocator;
|
||||
|
||||
std::string print(const rjson::value& value) {
|
||||
string_buffer buffer;
|
||||
writer writer(buffer);
|
||||
value.Accept(writer);
|
||||
return std::string(buffer.GetString());
|
||||
}
|
||||
|
||||
rjson::value copy(const rjson::value& value) {
|
||||
return rjson::value(value, the_allocator);
|
||||
}
|
||||
|
||||
rjson::value parse(const std::string& str) {
|
||||
return parse_raw(str.c_str(), str.size());
|
||||
}
|
||||
|
||||
rjson::value parse_raw(const char* c_str, size_t size) {
|
||||
rjson::document d;
|
||||
d.Parse(c_str, size);
|
||||
if (d.HasParseError()) {
|
||||
throw rjson::error(format("Parsing JSON failed: {}", GetParseError_En(d.GetParseError())));
|
||||
}
|
||||
rjson::value& v = d;
|
||||
return std::move(v);
|
||||
}
|
||||
|
||||
rjson::value& get(rjson::value& value, rjson::string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
if (member_it != value.MemberEnd())
|
||||
return member_it->value;
|
||||
else {
|
||||
throw rjson::error(format("JSON parameter {} not found", name));
|
||||
}
|
||||
}
|
||||
|
||||
const rjson::value& get(const rjson::value& value, rjson::string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
if (member_it != value.MemberEnd())
|
||||
return member_it->value;
|
||||
else {
|
||||
throw rjson::error(format("JSON parameter {} not found", name));
|
||||
}
|
||||
}
|
||||
|
||||
rjson::value from_string(const std::string& str) {
|
||||
return rjson::value(str.c_str(), str.size(), the_allocator);
|
||||
}
|
||||
|
||||
rjson::value from_string(const sstring& str) {
|
||||
return rjson::value(str.c_str(), str.size(), the_allocator);
|
||||
}
|
||||
|
||||
rjson::value from_string(const char* str, size_t size) {
|
||||
return rjson::value(str, size, the_allocator);
|
||||
}
|
||||
|
||||
const rjson::value* find(const rjson::value& value, string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
return member_it != value.MemberEnd() ? &member_it->value : nullptr;
|
||||
}
|
||||
|
||||
rjson::value* find(rjson::value& value, string_ref_type name) {
|
||||
auto member_it = value.FindMember(name);
|
||||
return member_it != value.MemberEnd() ? &member_it->value : nullptr;
|
||||
}
|
||||
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::value&& member) {
|
||||
base.AddMember(rjson::value(name.c_str(), name.size(), the_allocator), std::move(member), the_allocator);
|
||||
}
|
||||
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::string_ref_type member) {
|
||||
base.AddMember(rjson::value(name.c_str(), name.size(), the_allocator), rjson::value(member), the_allocator);
|
||||
}
|
||||
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::value&& member) {
|
||||
base.AddMember(name, std::move(member), the_allocator);
|
||||
}
|
||||
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::string_ref_type member) {
|
||||
base.AddMember(name, rjson::value(member), the_allocator);
|
||||
}
|
||||
|
||||
void push_back(rjson::value& base_array, rjson::value&& item) {
|
||||
base_array.PushBack(std::move(item), the_allocator);
|
||||
|
||||
}
|
||||
|
||||
} // end namespace rjson
|
||||
|
||||
std::ostream& std::operator<<(std::ostream& os, const rjson::value& v) {
|
||||
return os << rjson::print(v);
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* rjson is a wrapper over rapidjson library, providing fast JSON parsing and generation.
|
||||
*
|
||||
* rapidjson has strict copy elision policies, which, among other things, involves
|
||||
* using provided char arrays without copying them and allows copying objects only explicitly.
|
||||
* As such, one should be careful when passing strings with limited liveness
|
||||
* (e.g. data underneath local std::strings) to rjson functions, because created JSON objects
|
||||
* may end up relying on dangling char pointers. All rjson functions that create JSONs from strings
|
||||
* by rjson have both APIs for string_ref_type (more optimal, used when the string is known to live
|
||||
* at least as long as the object, e.g. a static char array) and for std::strings. The more optimal
|
||||
* variants should be used *only* if the liveness of the string is guaranteed, otherwise it will
|
||||
* result in undefined behaviour.
|
||||
* Also, bear in mind that methods exposed by rjson::value are generic, but some of them
|
||||
* work fine only for specific types. In case the type does not match, an rjson::error will be thrown.
|
||||
* Examples of such mismatched usages is calling MemberCount() on a JSON value not of object type
|
||||
* or calling Size() on a non-array value.
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace rjson {
|
||||
class error : public std::exception {
|
||||
std::string _msg;
|
||||
public:
|
||||
error() = default;
|
||||
error(const std::string& msg) : _msg(msg) {}
|
||||
|
||||
virtual const char* what() const noexcept override { return _msg.c_str(); }
|
||||
};
|
||||
}
|
||||
|
||||
// rapidjson configuration macros
|
||||
#define RAPIDJSON_HAS_STDSTRING 1
|
||||
// Default rjson policy is to use assert() - which is dangerous for two reasons:
|
||||
// 1. assert() can be turned off with -DNDEBUG
|
||||
// 2. assert() crashes a program
|
||||
// Fortunately, the default policy can be overridden, and so rapidjson errors will
|
||||
// throw an rjson::error exception instead.
|
||||
#define RAPIDJSON_ASSERT(x) do { if (!(x)) throw rjson::error(std::string("JSON error: condition not met: ") + #x); } while (0)
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/writer.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/error/en.h>
|
||||
#include <seastar/core/sstring.hh>
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace rjson {
|
||||
|
||||
using allocator = rapidjson::CrtAllocator;
|
||||
using encoding = rapidjson::UTF8<>;
|
||||
using document = rapidjson::GenericDocument<encoding, allocator>;
|
||||
using value = rapidjson::GenericValue<encoding, allocator>;
|
||||
using string_ref_type = value::StringRefType;
|
||||
using string_buffer = rapidjson::GenericStringBuffer<encoding>;
|
||||
using writer = rapidjson::Writer<string_buffer, encoding>;
|
||||
using type = rapidjson::Type;
|
||||
|
||||
// Returns an object representing JSON's null
|
||||
inline rjson::value null_value() {
|
||||
return rjson::value(rapidjson::kNullType);
|
||||
}
|
||||
|
||||
// Returns an empty JSON object - {}
|
||||
inline rjson::value empty_object() {
|
||||
return rjson::value(rapidjson::kObjectType);
|
||||
}
|
||||
|
||||
// Returns an empty JSON array - []
|
||||
inline rjson::value empty_array() {
|
||||
return rjson::value(rapidjson::kArrayType);
|
||||
}
|
||||
|
||||
// Returns an empty JSON string - ""
|
||||
inline rjson::value empty_string() {
|
||||
return rjson::value(rapidjson::kStringType);
|
||||
}
|
||||
|
||||
// Convert the JSON value to a string with JSON syntax, the opposite of parse().
|
||||
// The representation is dense - without any redundant indentation.
|
||||
std::string print(const rjson::value& value);
|
||||
|
||||
// Copies given JSON value - involves allocation
|
||||
rjson::value copy(const rjson::value& value);
|
||||
|
||||
// Parses a JSON value from given string or raw character array.
|
||||
// The string/char array liveness does not need to be persisted,
|
||||
// as both parse() and parse_raw() will allocate member names and values.
|
||||
// Throws rjson::error if parsing failed.
|
||||
rjson::value parse(const std::string& str);
|
||||
rjson::value parse_raw(const char* c_str, size_t size);
|
||||
|
||||
// Creates a JSON value (of JSON string type) out of internal string representations.
|
||||
// The string value is copied, so str's liveness does not need to be persisted.
|
||||
rjson::value from_string(const std::string& str);
|
||||
rjson::value from_string(const sstring& str);
|
||||
rjson::value from_string(const char* str, size_t size);
|
||||
|
||||
// Returns a pointer to JSON member if it exists, nullptr otherwise
|
||||
rjson::value* find(rjson::value& value, rjson::string_ref_type name);
|
||||
const rjson::value* find(const rjson::value& value, rjson::string_ref_type name);
|
||||
|
||||
// Returns a reference to JSON member if it exists, throws otherwise
|
||||
rjson::value& get(rjson::value& value, rjson::string_ref_type name);
|
||||
const rjson::value& get(const rjson::value& value, rjson::string_ref_type name);
|
||||
|
||||
// Sets a member in given JSON object by moving the member - allocates the name.
|
||||
// Throws if base is not a JSON object.
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::value&& member);
|
||||
|
||||
// Sets a string member in given JSON object by assigning its reference - allocates the name.
|
||||
// NOTICE: member string liveness must be ensured to be at least as long as base's.
|
||||
// Throws if base is not a JSON object.
|
||||
void set_with_string_name(rjson::value& base, const std::string& name, rjson::string_ref_type member);
|
||||
|
||||
// Sets a member in given JSON object by moving the member.
|
||||
// NOTICE: name liveness must be ensured to be at least as long as base's.
|
||||
// Throws if base is not a JSON object.
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::value&& member);
|
||||
|
||||
// Sets a string member in given JSON object by assigning its reference.
|
||||
// NOTICE: name liveness must be ensured to be at least as long as base's.
|
||||
// NOTICE: member liveness must be ensured to be at least as long as base's.
|
||||
// Throws if base is not a JSON object.
|
||||
void set(rjson::value& base, rjson::string_ref_type name, rjson::string_ref_type member);
|
||||
|
||||
// Adds a value to a JSON list by moving the item to its end.
|
||||
// Throws if base_array is not a JSON array.
|
||||
void push_back(rjson::value& base_array, rjson::value&& item);
|
||||
|
||||
} // end namespace rjson
|
||||
|
||||
namespace std {
|
||||
std::ostream& operator<<(std::ostream& os, const rjson::value& v);
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "base64.hh"
|
||||
#include "log.hh"
|
||||
#include "serialization.hh"
|
||||
#include "error.hh"
|
||||
#include "rapidjson/writer.h"
|
||||
#include "concrete_types.hh"
|
||||
|
||||
static logging::logger slogger("alternator-serialization");
|
||||
|
||||
namespace alternator {
|
||||
|
||||
type_info type_info_from_string(std::string type) {
|
||||
static thread_local const std::unordered_map<std::string, type_info> type_infos = {
|
||||
{"S", {alternator_type::S, utf8_type}},
|
||||
{"B", {alternator_type::B, bytes_type}},
|
||||
{"BOOL", {alternator_type::BOOL, boolean_type}},
|
||||
{"N", {alternator_type::N, decimal_type}}, //FIXME: Replace with custom Alternator type when implemented
|
||||
};
|
||||
auto it = type_infos.find(type);
|
||||
if (it == type_infos.end()) {
|
||||
return {alternator_type::NOT_SUPPORTED_YET, utf8_type};
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
type_representation represent_type(alternator_type atype) {
|
||||
static thread_local const std::unordered_map<alternator_type, type_representation> type_representations = {
|
||||
{alternator_type::S, {"S", utf8_type}},
|
||||
{alternator_type::B, {"B", bytes_type}},
|
||||
{alternator_type::BOOL, {"BOOL", boolean_type}},
|
||||
{alternator_type::N, {"N", decimal_type}}, //FIXME: Replace with custom Alternator type when implemented
|
||||
};
|
||||
auto it = type_representations.find(atype);
|
||||
if (it == type_representations.end()) {
|
||||
throw std::runtime_error(format("Unknown alternator type {}", int8_t(atype)));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
struct from_json_visitor {
|
||||
const rjson::value& v;
|
||||
bytes_ostream& bo;
|
||||
|
||||
void operator()(const reversed_type_impl& t) const { visit(*t.underlying_type(), from_json_visitor{v, bo}); };
|
||||
void operator()(const string_type_impl& t) {
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
void operator()(const bytes_type_impl& t) const {
|
||||
bo.write(base64_decode(std::string_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
void operator()(const boolean_type_impl& t) const {
|
||||
bo.write(boolean_type->decompose(v.GetBool()));
|
||||
}
|
||||
void operator()(const decimal_type_impl& t) const {
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
bo.write(t.from_json_object(Json::Value(rjson::print(v)), cql_serialization_format::internal()));
|
||||
}
|
||||
};
|
||||
|
||||
bytes serialize_item(const rjson::value& item) {
|
||||
if (item.IsNull() || item.MemberCount() != 1) {
|
||||
throw api_error("ValidationException", format("An item can contain only one attribute definition: {}", item));
|
||||
}
|
||||
auto it = item.MemberBegin();
|
||||
type_info type_info = type_info_from_string(it->name.GetString()); // JSON keys are guaranteed to be strings
|
||||
|
||||
if (type_info.atype == alternator_type::NOT_SUPPORTED_YET) {
|
||||
slogger.trace("Non-optimal serialization of type {}", it->name.GetString());
|
||||
return bytes{int8_t(type_info.atype)} + to_bytes(rjson::print(item));
|
||||
}
|
||||
|
||||
bytes_ostream bo;
|
||||
bo.write(bytes{int8_t(type_info.atype)});
|
||||
visit(*type_info.dtype, from_json_visitor{it->value, bo});
|
||||
|
||||
return bytes(bo.linearize());
|
||||
}
|
||||
|
||||
struct to_json_visitor {
|
||||
rjson::value& deserialized;
|
||||
const std::string& type_ident;
|
||||
bytes_view bv;
|
||||
|
||||
void operator()(const reversed_type_impl& t) const { visit(*t.underlying_type(), to_json_visitor{deserialized, type_ident, bv}); };
|
||||
void operator()(const decimal_type_impl& t) const {
|
||||
auto s = decimal_type->to_json_string(bytes(bv));
|
||||
//FIXME(sarna): unnecessary copy
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::from_string(s));
|
||||
}
|
||||
void operator()(const string_type_impl& t) {
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::from_string(reinterpret_cast<const char *>(bv.data()), bv.size()));
|
||||
}
|
||||
void operator()(const bytes_type_impl& t) const {
|
||||
std::string b64 = base64_encode(bv);
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::from_string(b64));
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::parse(t.to_string(bytes(bv))));
|
||||
}
|
||||
};
|
||||
|
||||
rjson::value deserialize_item(bytes_view bv) {
|
||||
rjson::value deserialized(rapidjson::kObjectType);
|
||||
if (bv.empty()) {
|
||||
throw api_error("ValidationException", "Serialized value empty");
|
||||
}
|
||||
|
||||
alternator_type atype = alternator_type(bv[0]);
|
||||
bv.remove_prefix(1);
|
||||
|
||||
if (atype == alternator_type::NOT_SUPPORTED_YET) {
|
||||
slogger.trace("Non-optimal deserialization of alternator type {}", int8_t(atype));
|
||||
return rjson::parse_raw(reinterpret_cast<const char *>(bv.data()), bv.size());
|
||||
}
|
||||
type_representation type_representation = represent_type(atype);
|
||||
visit(*type_representation.dtype, to_json_visitor{deserialized, type_representation.ident, bv});
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
std::string type_to_string(data_type type) {
|
||||
static thread_local std::unordered_map<data_type, std::string> types = {
|
||||
{utf8_type, "S"},
|
||||
{bytes_type, "B"},
|
||||
{boolean_type, "BOOL"},
|
||||
{decimal_type, "N"}, // FIXME: use a specialized Alternator number type instead of the general decimal_type
|
||||
};
|
||||
auto it = types.find(type);
|
||||
if (it == types.end()) {
|
||||
throw std::runtime_error(format("Unknown type {}", type->name()));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column) {
|
||||
std::string column_name = column.name_as_text();
|
||||
std::string expected_type = type_to_string(column.type);
|
||||
|
||||
const rjson::value& key_typed_value = rjson::get(item, rjson::value::StringRefType(column_name.c_str()));
|
||||
if (!key_typed_value.IsObject() || key_typed_value.MemberCount() != 1) {
|
||||
throw api_error("ValidationException",
|
||||
format("Missing or invalid value object for key column {}: {}", column_name, item));
|
||||
}
|
||||
return get_key_from_typed_value(key_typed_value, column, expected_type);
|
||||
}
|
||||
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column, const std::string& expected_type) {
|
||||
auto it = key_typed_value.MemberBegin();
|
||||
if (it->name.GetString() != expected_type) {
|
||||
throw api_error("ValidationException",
|
||||
format("Type mismatch: expected type {} for key column {}, got type {}",
|
||||
expected_type, column.name_as_text(), it->name.GetString()));
|
||||
}
|
||||
if (column.type == bytes_type) {
|
||||
return base64_decode(it->value.GetString());
|
||||
} else {
|
||||
return column.type->from_string(it->value.GetString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rjson::value json_key_column_value(bytes_view cell, const column_definition& column) {
|
||||
if (column.type == bytes_type) {
|
||||
std::string b64 = base64_encode(cell);
|
||||
return rjson::from_string(b64);
|
||||
} if (column.type == utf8_type) {
|
||||
return rjson::from_string(std::string(reinterpret_cast<const char*>(cell.data()), cell.size()));
|
||||
} else if (column.type == decimal_type) {
|
||||
// FIXME: use specialized Alternator number type, not the more
|
||||
// general "decimal_type". A dedicated type can be more efficient
|
||||
// in storage space and in parsing speed.
|
||||
auto s = decimal_type->to_json_string(bytes(cell));
|
||||
return rjson::from_string(s);
|
||||
} else {
|
||||
// We shouldn't get here, we shouldn't see such key columns.
|
||||
throw std::runtime_error(format("Unexpected key type: {}", column.type->name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
partition_key pk_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
std::vector<bytes> raw_pk;
|
||||
// FIXME: this is a loop, but we really allow only one partition key column.
|
||||
for (const column_definition& cdef : schema->partition_key_columns()) {
|
||||
bytes raw_value = get_key_column_value(item, cdef);
|
||||
raw_pk.push_back(std::move(raw_value));
|
||||
}
|
||||
return partition_key::from_exploded(raw_pk);
|
||||
}
|
||||
|
||||
clustering_key ck_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
if (schema->clustering_key_size() == 0) {
|
||||
return clustering_key::make_empty();
|
||||
}
|
||||
std::vector<bytes> raw_ck;
|
||||
// FIXME: this is a loop, but we really allow only one clustering key column.
|
||||
for (const column_definition& cdef : schema->clustering_key_columns()) {
|
||||
bytes raw_value = get_key_column_value(item, cdef);
|
||||
raw_ck.push_back(std::move(raw_value));
|
||||
}
|
||||
|
||||
return clustering_key::from_exploded(raw_ck);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "types.hh"
|
||||
#include "schema.hh"
|
||||
#include "keys.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
enum class alternator_type : int8_t {
|
||||
S, B, BOOL, N, NOT_SUPPORTED_YET
|
||||
};
|
||||
|
||||
struct type_info {
|
||||
alternator_type atype;
|
||||
data_type dtype;
|
||||
};
|
||||
|
||||
struct type_representation {
|
||||
std::string ident;
|
||||
data_type dtype;
|
||||
};
|
||||
|
||||
type_info type_info_from_string(std::string type);
|
||||
type_representation represent_type(alternator_type atype);
|
||||
|
||||
bytes serialize_item(const rjson::value& item);
|
||||
rjson::value deserialize_item(bytes_view bv);
|
||||
|
||||
std::string type_to_string(data_type type);
|
||||
|
||||
bytes get_key_column_value(const rjson::value& item, const column_definition& column);
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column, const std::string& expected_type);
|
||||
rjson::value json_key_column_value(bytes_view cell, const column_definition& column);
|
||||
|
||||
partition_key pk_from_json(const rjson::value& item, schema_ptr schema);
|
||||
clustering_key ck_from_json(const rjson::value& item, schema_ptr schema);
|
||||
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "alternator/server.hh"
|
||||
#include "log.hh"
|
||||
#include <seastar/http/function_handlers.hh>
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastarx.hh>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include "error.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
static logging::logger slogger("alternator-server");
|
||||
|
||||
using namespace httpd;
|
||||
|
||||
namespace alternator {
|
||||
|
||||
static constexpr auto TARGET = "X-Amz-Target";
|
||||
|
||||
inline std::vector<sstring> split(const sstring& text, const char* separator) {
|
||||
if (text == "") {
|
||||
return std::vector<sstring>();
|
||||
}
|
||||
std::vector<sstring> tokens;
|
||||
return boost::split(tokens, text, boost::is_any_of(separator));
|
||||
}
|
||||
|
||||
// DynamoDB HTTP error responses are structured as follows
|
||||
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html
|
||||
// Our handlers throw an exception to report an error. If the exception
|
||||
// is of type alternator::api_error, it unwrapped and properly reported to
|
||||
// the user directly. Other exceptions are unexpected, and reported as
|
||||
// Internal Server Error.
|
||||
class api_handler : public handler_base {
|
||||
public:
|
||||
api_handler(const future_json_function& _handle) : _f_handle(
|
||||
[_handle](std::unique_ptr<request> req, std::unique_ptr<reply> rep) {
|
||||
return seastar::futurize_apply(_handle, std::move(req)).then_wrapped([rep = std::move(rep)](future<json::json_return_type> resf) mutable {
|
||||
if (resf.failed()) {
|
||||
// Exceptions of type api_error are wrapped as JSON and
|
||||
// returned to the client as expected. Other types of
|
||||
// exceptions are unexpected, and returned to the user
|
||||
// as an internal server error:
|
||||
api_error ret;
|
||||
try {
|
||||
resf.get();
|
||||
} catch (api_error &ae) {
|
||||
ret = ae;
|
||||
} catch (rjson::error & re) {
|
||||
ret = api_error("ValidationException", re.what());
|
||||
} catch (...) {
|
||||
ret = api_error(
|
||||
"Internal Server Error",
|
||||
format("Internal server error: {}", std::current_exception()),
|
||||
reply::status_type::internal_server_error);
|
||||
}
|
||||
// FIXME: what is this version number?
|
||||
rep->_content += "{\"__type\":\"com.amazonaws.dynamodb.v20120810#" + ret._type + "\"," +
|
||||
"\"message\":\"" + ret._msg + "\"}";
|
||||
rep->_status = ret._http_code;
|
||||
slogger.trace("api_handler error case: {}", rep->_content);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
slogger.trace("api_handler success case");
|
||||
auto res = resf.get0();
|
||||
if (res._body_writer) {
|
||||
rep->write_body("json", std::move(res._body_writer));
|
||||
} else {
|
||||
rep->_content += res._res;
|
||||
}
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
}), _type("json") { }
|
||||
|
||||
api_handler(const api_handler&) = default;
|
||||
future<std::unique_ptr<reply>> handle(const sstring& path,
|
||||
std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
return _f_handle(std::move(req), std::move(rep)).then(
|
||||
[this](std::unique_ptr<reply> rep) {
|
||||
rep->done(_type);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
future_handler_function _f_handle;
|
||||
sstring _type;
|
||||
};
|
||||
|
||||
void server::set_routes(routes& r) {
|
||||
using alternator_callback = std::function<future<json::json_return_type>(executor&, executor::client_state&, std::unique_ptr<request>)>;
|
||||
std::unordered_map<std::string, alternator_callback> routes{
|
||||
{"CreateTable", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) {
|
||||
return e.maybe_create_keyspace().then([&e, &client_state, req = std::move(req)] { return e.create_table(client_state, req->content); }); }
|
||||
},
|
||||
{"DescribeTable", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.describe_table(client_state, req->content); }},
|
||||
{"DeleteTable", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.delete_table(client_state, req->content); }},
|
||||
{"PutItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.put_item(client_state, req->content); }},
|
||||
{"UpdateItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.update_item(client_state, req->content); }},
|
||||
{"GetItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.get_item(client_state, req->content); }},
|
||||
{"DeleteItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.delete_item(client_state, req->content); }},
|
||||
{"ListTables", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.list_tables(client_state, req->content); }},
|
||||
{"Scan", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.scan(client_state, req->content); }},
|
||||
{"DescribeEndpoints", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.describe_endpoints(client_state, req->content, req->get_header("Host")); }},
|
||||
{"BatchWriteItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.batch_write_item(client_state, req->content); }},
|
||||
{"BatchGetItem", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.batch_get_item(client_state, req->content); }},
|
||||
{"Query", [] (executor& e, executor::client_state& client_state, std::unique_ptr<request> req) { return e.query(client_state, req->content); }},
|
||||
};
|
||||
|
||||
api_handler* handler = new api_handler([this, routes = std::move(routes)](std::unique_ptr<request> req) -> future<json::json_return_type> {
|
||||
_executor.local()._stats.total_operations++;
|
||||
sstring target = req->get_header(TARGET);
|
||||
std::vector<sstring> split_target = split(target, ".");
|
||||
//NOTICE(sarna): Target consists of Dynamo API version folllowed by a dot '.' and operation type (e.g. CreateTable)
|
||||
sstring op = split_target.empty() ? sstring() : split_target.back();
|
||||
slogger.trace("Request: {} {}", op, req->content);
|
||||
auto callback_it = routes.find(op);
|
||||
if (callback_it == routes.end()) {
|
||||
_executor.local()._stats.unsupported_operations++;
|
||||
throw api_error("UnknownOperationException",
|
||||
format("Unsupported operation {}", op));
|
||||
}
|
||||
//FIXME: Client state can provide more context, e.g. client's endpoint address
|
||||
return do_with(executor::client_state::for_internal_calls(), [this, callback_it = std::move(callback_it), op = std::move(op), req = std::move(req)] (executor::client_state& client_state) mutable {
|
||||
client_state.set_raw_keyspace(executor::KEYSPACE_NAME);
|
||||
executor::maybe_trace_query(client_state, op, req->content);
|
||||
tracing::trace(client_state.get_trace_state(), op);
|
||||
return callback_it->second(_executor.local(), client_state, std::move(req));
|
||||
});
|
||||
});
|
||||
|
||||
r.add(operation_type::POST, url("/"), handler);
|
||||
}
|
||||
|
||||
future<> server::init(net::inet_address addr, uint16_t port) {
|
||||
return _executor.invoke_on_all([] (executor& e) {
|
||||
return e.start();
|
||||
}).then([this] {
|
||||
return _control.start();
|
||||
}).then([this] {
|
||||
return _control.set_routes(std::bind(&server::set_routes, this, std::placeholders::_1));
|
||||
}).then([this, addr, port] {
|
||||
return _control.listen(socket_address{addr, port});
|
||||
}).then([addr, port] {
|
||||
slogger.info("Alternator HTTP server listening on {} port {}", addr, port);
|
||||
}).handle_exception([addr, port] (std::exception_ptr e) {
|
||||
slogger.warn("Failed to set up Alternator HTTP server on {} port {}: {}", addr, port, e);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "alternator/executor.hh"
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/http/httpd.hh>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class server {
|
||||
seastar::httpd::http_server_control _control;
|
||||
seastar::sharded<executor>& _executor;
|
||||
public:
|
||||
server(seastar::sharded<executor>& executor) : _executor(executor) {}
|
||||
|
||||
seastar::future<> init(net::inet_address addr, uint16_t port);
|
||||
private:
|
||||
void set_routes(seastar::httpd::routes& r);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "stats.hh"
|
||||
|
||||
#include <seastar/core/metrics.hh>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
const char* ALTERNATOR_METRICS = "alternator";
|
||||
|
||||
stats::stats() : api_operations{} {
|
||||
// Register the
|
||||
seastar::metrics::label op("op");
|
||||
|
||||
_metrics.add_group("alternator", {
|
||||
#define OPERATION(name, CamelCaseName) \
|
||||
seastar::metrics::make_total_operations("operation", api_operations.name, \
|
||||
seastar::metrics::description("number of operations via Alternator API"), {op(CamelCaseName)}),
|
||||
#define OPERATION_LATENCY(name, CamelCaseName) \
|
||||
seastar::metrics::make_histogram("op_latency", \
|
||||
seastar::metrics::description("Latency histogram of an operation via Alternator API"), {op(CamelCaseName)}, [this]{return api_operations.name.get_histogram(1,20);}),
|
||||
OPERATION(batch_write_item, "BatchWriteItem")
|
||||
OPERATION(create_backup, "CreateBackup")
|
||||
OPERATION(create_global_table, "CreateGlobalTable")
|
||||
OPERATION(create_table, "CreateTable")
|
||||
OPERATION(delete_backup, "DeleteBackup")
|
||||
OPERATION(delete_item, "DeleteItem")
|
||||
OPERATION(delete_table, "DeleteTable")
|
||||
OPERATION(describe_backup, "DescribeBackup")
|
||||
OPERATION(describe_continuous_backups, "DescribeContinuousBackups")
|
||||
OPERATION(describe_endpoints, "DescribeEndpoints")
|
||||
OPERATION(describe_global_table, "DescribeGlobalTable")
|
||||
OPERATION(describe_global_table_settings, "DescribeGlobalTableSettings")
|
||||
OPERATION(describe_limits, "DescribeLimits")
|
||||
OPERATION(describe_table, "DescribeTable")
|
||||
OPERATION(describe_time_to_live, "DescribeTimeToLive")
|
||||
OPERATION(get_item, "GetItem")
|
||||
OPERATION(list_backups, "ListBackups")
|
||||
OPERATION(list_global_tables, "ListGlobalTables")
|
||||
OPERATION(list_tables, "ListTables")
|
||||
OPERATION(list_tags_of_resource, "ListTagsOfResource")
|
||||
OPERATION(put_item, "PutItem")
|
||||
OPERATION(query, "Query")
|
||||
OPERATION(restore_table_from_backup, "RestoreTableFromBackup")
|
||||
OPERATION(restore_table_to_point_in_time, "RestoreTableToPointInTime")
|
||||
OPERATION(scan, "Scan")
|
||||
OPERATION(tag_resource, "TagResource")
|
||||
OPERATION(transact_get_items, "TransactGetItems")
|
||||
OPERATION(transact_write_items, "TransactWriteItems")
|
||||
OPERATION(untag_resource, "UntagResource")
|
||||
OPERATION(update_continuous_backups, "UpdateContinuousBackups")
|
||||
OPERATION(update_global_table, "UpdateGlobalTable")
|
||||
OPERATION(update_global_table_settings, "UpdateGlobalTableSettings")
|
||||
OPERATION(update_item, "UpdateItem")
|
||||
OPERATION(update_table, "UpdateTable")
|
||||
OPERATION(update_time_to_live, "UpdateTimeToLive")
|
||||
OPERATION_LATENCY(put_item_latency, "PutItem")
|
||||
OPERATION_LATENCY(get_item_latency, "GetItem")
|
||||
OPERATION_LATENCY(delete_item_latency, "DeleteItem")
|
||||
OPERATION_LATENCY(update_item_latency, "UpdateItem")
|
||||
});
|
||||
_metrics.add_group("alternator", {
|
||||
seastar::metrics::make_total_operations("unsupported_operations", unsupported_operations,
|
||||
seastar::metrics::description("number of unsupported operations via Alternator API")),
|
||||
seastar::metrics::make_total_operations("total_operations", total_operations,
|
||||
seastar::metrics::description("number of total operations via Alternator API")),
|
||||
seastar::metrics::make_total_operations("reads_before_write", reads_before_write,
|
||||
seastar::metrics::description("number of performed read-before-write operations")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_read_total", cql_stats.filtered_rows_read_total,
|
||||
seastar::metrics::description("number of rows read during filtering operations")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_matched_total", cql_stats.filtered_rows_matched_total,
|
||||
seastar::metrics::description("number of rows read and matched during filtering operations")),
|
||||
seastar::metrics::make_total_operations("filtered_rows_dropped_total", [this] { return cql_stats.filtered_rows_read_total - cql_stats.filtered_rows_matched_total; },
|
||||
seastar::metrics::description("number of rows read and dropped during filtering operations")),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <seastar/core/metrics_registration.hh>
|
||||
#include "seastarx.hh"
|
||||
#include "utils/estimated_histogram.hh"
|
||||
#include "cql3/stats.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// Object holding per-shard statistics related to Alternator.
|
||||
// While this object is alive, these metrics are also registered to be
|
||||
// visible by the metrics REST API, with the "alternator" prefix.
|
||||
class stats {
|
||||
public:
|
||||
stats();
|
||||
// Count of DynamoDB API operations by types
|
||||
struct {
|
||||
uint64_t batch_get_item = 0;
|
||||
uint64_t batch_write_item = 0;
|
||||
uint64_t create_backup = 0;
|
||||
uint64_t create_global_table = 0;
|
||||
uint64_t create_table = 0;
|
||||
uint64_t delete_backup = 0;
|
||||
uint64_t delete_item = 0;
|
||||
uint64_t delete_table = 0;
|
||||
uint64_t describe_backup = 0;
|
||||
uint64_t describe_continuous_backups = 0;
|
||||
uint64_t describe_endpoints = 0;
|
||||
uint64_t describe_global_table = 0;
|
||||
uint64_t describe_global_table_settings = 0;
|
||||
uint64_t describe_limits = 0;
|
||||
uint64_t describe_table = 0;
|
||||
uint64_t describe_time_to_live = 0;
|
||||
uint64_t get_item = 0;
|
||||
uint64_t list_backups = 0;
|
||||
uint64_t list_global_tables = 0;
|
||||
uint64_t list_tables = 0;
|
||||
uint64_t list_tags_of_resource = 0;
|
||||
uint64_t put_item = 0;
|
||||
uint64_t query = 0;
|
||||
uint64_t restore_table_from_backup = 0;
|
||||
uint64_t restore_table_to_point_in_time = 0;
|
||||
uint64_t scan = 0;
|
||||
uint64_t tag_resource = 0;
|
||||
uint64_t transact_get_items = 0;
|
||||
uint64_t transact_write_items = 0;
|
||||
uint64_t untag_resource = 0;
|
||||
uint64_t update_continuous_backups = 0;
|
||||
uint64_t update_global_table = 0;
|
||||
uint64_t update_global_table_settings = 0;
|
||||
uint64_t update_item = 0;
|
||||
uint64_t update_table = 0;
|
||||
uint64_t update_time_to_live = 0;
|
||||
|
||||
utils::estimated_histogram put_item_latency;
|
||||
utils::estimated_histogram get_item_latency;
|
||||
utils::estimated_histogram delete_item_latency;
|
||||
utils::estimated_histogram update_item_latency;
|
||||
} api_operations;
|
||||
// Miscellaneous event counters
|
||||
uint64_t total_operations = 0;
|
||||
uint64_t unsupported_operations = 0;
|
||||
uint64_t reads_before_write = 0;
|
||||
// CQL-derived stats
|
||||
cql3::cql_stats cql_stats;
|
||||
private:
|
||||
// The metric_groups object holds this stat object's metrics registered
|
||||
// as long as the stats object is alive.
|
||||
seastar::metrics::metric_groups _metrics;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -611,54 +611,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/column_family/toppartitions/{name}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Toppartitions query",
|
||||
"type":"toppartitions_query_results",
|
||||
"nickname":"toppartitions",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The column family name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
},
|
||||
{
|
||||
"name":"duration",
|
||||
"description":"Duration (in milliseconds) of monitoring operation",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"list_size",
|
||||
"description":"number of the top partitions to list",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"capacity",
|
||||
"description":"capacity of stream summary: determines amount of resources used in query processing",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/column_family/metrics/memtable_columns_count/",
|
||||
"operations":[
|
||||
@@ -2864,44 +2816,6 @@
|
||||
"description":"The column family type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"toppartitions_record":{
|
||||
"id":"toppartitions_record",
|
||||
"description":"nodetool toppartitions query record",
|
||||
"properties":{
|
||||
"partition":{
|
||||
"type":"string",
|
||||
"description":"Partition key"
|
||||
},
|
||||
"count":{
|
||||
"type":"long",
|
||||
"description":"Number of read/write operations"
|
||||
},
|
||||
"error":{
|
||||
"type":"long",
|
||||
"description":"Indication of inaccuracy in counting PKs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"toppartitions_query_results":{
|
||||
"id":"toppartitions_query_results",
|
||||
"description":"nodetool toppartitions query results",
|
||||
"properties":{
|
||||
"read":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"toppartitions_record"
|
||||
},
|
||||
"description":"Read results"
|
||||
},
|
||||
"write":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"toppartitions_record"
|
||||
},
|
||||
"description":"Write results"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,24 +127,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/compaction_manager/metrics/pending_tasks_by_table",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get pending tasks by table name",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "pending_compaction"
|
||||
},
|
||||
"nickname": "get_pending_tasks_by_table",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/compaction_manager/metrics/completed_tasks",
|
||||
"operations": [
|
||||
@@ -262,23 +244,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pending_compaction": {
|
||||
"id": "pending_compaction",
|
||||
"properties": {
|
||||
"cf": {
|
||||
"type": "string",
|
||||
"description": "The column family name"
|
||||
},
|
||||
"ks": {
|
||||
"type":"string",
|
||||
"description": "The keyspace name"
|
||||
},
|
||||
"task": {
|
||||
"type":"long",
|
||||
"description": "The number of pending tasks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"id":"history",
|
||||
"description":"Compaction history information",
|
||||
|
||||
@@ -791,36 +791,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_read/moving_average_histogram",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get CAS read rate and latency histogram",
|
||||
"$ref": "#/utils/rate_moving_average_and_histogram",
|
||||
"nickname": "get_cas_read_metrics_latency_histogram",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/view_write/moving_average_histogram",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get view write rate and latency histogram",
|
||||
"$ref": "#/utils/rate_moving_average_and_histogram",
|
||||
"nickname": "get_view_write_metrics_latency_histogram",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/range/moving_average_histogram",
|
||||
"operations": [
|
||||
@@ -986,21 +956,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_write/moving_average_histogram",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get CAS write rate and latency histogram",
|
||||
"$ref": "#/utils/rate_moving_average_and_histogram",
|
||||
"nickname": "get_cas_write_metrics_latency_histogram",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_proxy/metrics/read/estimated_histogram/",
|
||||
"operations":[
|
||||
|
||||
@@ -2164,42 +2164,7 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/sstable_info",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"SSTable information",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"table_sstables"
|
||||
},
|
||||
"nickname":"sstable_info",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"The keyspace",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"column family name",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"models":{
|
||||
"mapper":{
|
||||
@@ -2359,92 +2324,6 @@
|
||||
"description":"The endpoint details"
|
||||
}
|
||||
}
|
||||
},
|
||||
"named_maps":{
|
||||
"id":"named_maps",
|
||||
"properties":{
|
||||
"group":{
|
||||
"type":"string"
|
||||
},
|
||||
"attributes":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"mapper"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sstable":{
|
||||
"id":"sstable",
|
||||
"properties":{
|
||||
"size":{
|
||||
"type":"long",
|
||||
"description":"Total size in bytes of sstable"
|
||||
},
|
||||
"data_size":{
|
||||
"type":"long",
|
||||
"description":"The size in bytes on disk of data"
|
||||
},
|
||||
"index_size":{
|
||||
"type":"long",
|
||||
"description":"The size in bytes on disk of index"
|
||||
},
|
||||
"filter_size":{
|
||||
"type":"long",
|
||||
"description":"The size in bytes on disk of filter"
|
||||
},
|
||||
"timestamp":{
|
||||
"type":"datetime",
|
||||
"description":"File creation time"
|
||||
},
|
||||
"generation":{
|
||||
"type":"long",
|
||||
"description":"SSTable generation"
|
||||
},
|
||||
"level":{
|
||||
"type":"long",
|
||||
"description":"SSTable level"
|
||||
},
|
||||
"version":{
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"ka", "la", "mc"
|
||||
],
|
||||
"description":"SSTable version"
|
||||
},
|
||||
"properties":{
|
||||
"type":"array",
|
||||
"description":"SSTable attributes",
|
||||
"items":{
|
||||
"type":"mapper"
|
||||
}
|
||||
},
|
||||
"extended_properties":{
|
||||
"type":"array",
|
||||
"description":"SSTable extended attributes",
|
||||
"items":{
|
||||
"type":"named_maps"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"table_sstables":{
|
||||
"id":"table_sstables",
|
||||
"description":"Per-table SSTable info and attributes",
|
||||
"properties":{
|
||||
"keyspace":{
|
||||
"type":"string"
|
||||
},
|
||||
"table":{
|
||||
"type":"string"
|
||||
},
|
||||
"sstables":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"$ref":"sstable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
api/api.cc
10
api/api.cc
@@ -20,9 +20,9 @@
|
||||
*/
|
||||
|
||||
#include "api.hh"
|
||||
#include <seastar/http/file_handler.hh>
|
||||
#include <seastar/http/transformers.hh>
|
||||
#include <seastar/http/api_docs.hh>
|
||||
#include "http/file_handler.hh"
|
||||
#include "http/transformers.hh"
|
||||
#include "http/api_docs.hh"
|
||||
#include "storage_service.hh"
|
||||
#include "commitlog.hh"
|
||||
#include "gossiper.hh"
|
||||
@@ -36,13 +36,11 @@
|
||||
#include "endpoint_snitch.hh"
|
||||
#include "compaction_manager.hh"
|
||||
#include "hinted_handoff.hh"
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "stream_manager.hh"
|
||||
#include "system.hh"
|
||||
#include "api/config.hh"
|
||||
|
||||
logging::logger apilog("api");
|
||||
|
||||
namespace api {
|
||||
|
||||
static std::unique_ptr<reply> exception_reply(std::exception_ptr eptr) {
|
||||
|
||||
44
api/api.hh
44
api/api.hh
@@ -21,15 +21,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <type_traits>
|
||||
#include "json/json_elements.hh"
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/units/detail/utility.hpp>
|
||||
#include "api/api-doc/utils.json.hh"
|
||||
#include "utils/histogram.hh"
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "api_init.hh"
|
||||
#include "seastarx.hh"
|
||||
|
||||
@@ -218,42 +216,4 @@ std::vector<T> concat(std::vector<T> a, std::vector<T>&& b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
template <class T, class Base = T>
|
||||
class req_param {
|
||||
public:
|
||||
sstring name;
|
||||
sstring param;
|
||||
T value;
|
||||
|
||||
req_param(const request& req, sstring name, T default_val) : name(name) {
|
||||
param = req.get_query_param(name);
|
||||
if (param.empty()) {
|
||||
value = default_val;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// boost::lexical_cast does not use boolalpha. Converting a
|
||||
// true/false throws exceptions. We don't want that.
|
||||
if constexpr (std::is_same_v<Base, bool>) {
|
||||
// Cannot use boolalpha because we (probably) want to
|
||||
// accept 1 and 0 as well as true and false. And True. And fAlse.
|
||||
std::transform(param.begin(), param.end(), param.begin(), ::tolower);
|
||||
if (param == "true" || param == "1") {
|
||||
value = T(true);
|
||||
} else if (param == "false" || param == "0") {
|
||||
value = T(false);
|
||||
} else {
|
||||
throw boost::bad_lexical_cast{};
|
||||
}
|
||||
} else {
|
||||
value = T{boost::lexical_cast<Base>(param)};
|
||||
}
|
||||
} catch (boost::bad_lexical_cast&) {
|
||||
throw bad_param_exception(format("{} ({}): type error - should be {}", name, param, boost::units::detail::demangle(typeid(Base).name())));
|
||||
}
|
||||
}
|
||||
|
||||
operator T() const { return value; }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "database_fwd.hh"
|
||||
#include "database.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "http/httpd.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
#include "collectd.hh"
|
||||
#include "api/api-doc/collectd.json.hh"
|
||||
#include <seastar/core/scollectd.hh>
|
||||
#include <seastar/core/scollectd_api.hh>
|
||||
#include "core/scollectd.hh"
|
||||
#include "core/scollectd_api.hh"
|
||||
#include "endian.h"
|
||||
#include <boost/range/irange.hpp>
|
||||
#include <regex>
|
||||
|
||||
@@ -22,15 +22,11 @@
|
||||
#include "column_family.hh"
|
||||
#include "api/api-doc/column_family.json.hh"
|
||||
#include <vector>
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "sstables/sstables.hh"
|
||||
#include "utils/estimated_histogram.hh"
|
||||
#include <algorithm>
|
||||
|
||||
#include "db/data_listeners.hh"
|
||||
|
||||
extern logging::logger apilog;
|
||||
|
||||
namespace api {
|
||||
using namespace httpd;
|
||||
|
||||
@@ -38,7 +34,7 @@ using namespace std;
|
||||
using namespace json;
|
||||
namespace cf = httpd::column_family_json;
|
||||
|
||||
std::tuple<sstring, sstring> parse_fully_qualified_cf_name(sstring name) {
|
||||
const utils::UUID& get_uuid(const sstring& name, const database& db) {
|
||||
auto pos = name.find("%3A");
|
||||
size_t end;
|
||||
if (pos == sstring::npos) {
|
||||
@@ -50,15 +46,11 @@ std::tuple<sstring, sstring> parse_fully_qualified_cf_name(sstring name) {
|
||||
} else {
|
||||
end = pos + 3;
|
||||
}
|
||||
return std::make_tuple(name.substr(0, pos), name.substr(end));
|
||||
}
|
||||
|
||||
const utils::UUID& get_uuid(const sstring& name, const database& db) {
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name);
|
||||
try {
|
||||
return db.find_uuid(ks, cf);
|
||||
return db.find_uuid(name.substr(0, pos), name.substr(end));
|
||||
} catch (std::out_of_range& e) {
|
||||
throw bad_param_exception(format("Column family '{}:{}' not found", ks, cf));
|
||||
throw bad_param_exception("Column family '" + name.substr(0, pos) + ":"
|
||||
+ name.substr(end) + "' not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,27 +166,27 @@ static future<json::json_return_type> get_cf_unleveled_sstables(http_context& ct
|
||||
}, std::plus<int64_t>());
|
||||
}
|
||||
|
||||
static int64_t min_partition_size(column_family& cf) {
|
||||
static int64_t min_row_size(column_family& cf) {
|
||||
int64_t res = INT64_MAX;
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res = std::min(res, i->get_stats_metadata().estimated_partition_size.min());
|
||||
res = std::min(res, i->get_stats_metadata().estimated_row_size.min());
|
||||
}
|
||||
return (res == INT64_MAX) ? 0 : res;
|
||||
}
|
||||
|
||||
static int64_t max_partition_size(column_family& cf) {
|
||||
static int64_t max_row_size(column_family& cf) {
|
||||
int64_t res = 0;
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res = std::max(i->get_stats_metadata().estimated_partition_size.max(), res);
|
||||
res = std::max(i->get_stats_metadata().estimated_row_size.max(), res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static integral_ratio_holder mean_partition_size(column_family& cf) {
|
||||
static integral_ratio_holder mean_row_size(column_family& cf) {
|
||||
integral_ratio_holder res;
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
auto c = i->get_stats_metadata().estimated_partition_size.count();
|
||||
res.sub += i->get_stats_metadata().estimated_partition_size.mean() * c;
|
||||
auto c = i->get_stats_metadata().estimated_row_size.count();
|
||||
res.sub += i->get_stats_metadata().estimated_row_size.mean() * c;
|
||||
res.total += c;
|
||||
}
|
||||
return res;
|
||||
@@ -411,24 +403,22 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return get_cf_stats(ctx, &column_family::stats::memtable_switch_count);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_estimated_row_size_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
utils::estimated_histogram res(0);
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res.merge(i->get_stats_metadata().estimated_partition_size);
|
||||
res.merge(i->get_stats_metadata().estimated_row_size);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_estimated_row_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], int64_t(0), [](column_family& cf) {
|
||||
uint64_t res = 0;
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res += i->get_stats_metadata().estimated_partition_size.count();
|
||||
res += i->get_stats_metadata().estimated_row_size.count();
|
||||
}
|
||||
return res;
|
||||
},
|
||||
@@ -556,36 +546,30 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return sum_sstable(ctx, true);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_min_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], INT64_MAX, min_partition_size, min_int64);
|
||||
return map_reduce_cf(ctx, req->param["name"], INT64_MAX, min_row_size, min_int64);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_all_min_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, INT64_MAX, min_partition_size, min_int64);
|
||||
return map_reduce_cf(ctx, INT64_MAX, min_row_size, min_int64);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_max_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], int64_t(0), max_partition_size, max_int64);
|
||||
return map_reduce_cf(ctx, req->param["name"], int64_t(0), max_row_size, max_int64);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_all_max_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, int64_t(0), max_partition_size, max_int64);
|
||||
return map_reduce_cf(ctx, int64_t(0), max_row_size, max_int64);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_mean_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
// Cassandra 3.x mean values are truncated as integrals.
|
||||
return map_reduce_cf(ctx, req->param["name"], integral_ratio_holder(), mean_partition_size, std::plus<integral_ratio_holder>());
|
||||
return map_reduce_cf(ctx, req->param["name"], integral_ratio_holder(), mean_row_size, std::plus<integral_ratio_holder>());
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_all_mean_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
// Cassandra 3.x mean values are truncated as integrals.
|
||||
return map_reduce_cf(ctx, integral_ratio_holder(), mean_partition_size, std::plus<integral_ratio_holder>());
|
||||
return map_reduce_cf(ctx, integral_ratio_holder(), mean_row_size, std::plus<integral_ratio_holder>());
|
||||
});
|
||||
|
||||
cf::get_bloom_filter_false_positives.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -936,45 +920,5 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(container_to_vec(res));
|
||||
});
|
||||
});
|
||||
|
||||
cf::toppartitions.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
auto name_param = req->param["name"];
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name_param);
|
||||
|
||||
api::req_param<std::chrono::milliseconds, unsigned> duration{*req, "duration", 1000ms};
|
||||
api::req_param<unsigned> capacity(*req, "capacity", 256);
|
||||
api::req_param<unsigned> list_size(*req, "list_size", 10);
|
||||
|
||||
apilog.info("toppartitions query: name={} duration={} list_size={} capacity={}",
|
||||
name_param, duration.param, list_size.param, capacity.param);
|
||||
|
||||
return seastar::do_with(db::toppartitions_query(ctx.db, ks, cf, duration.value, list_size, capacity), [&ctx](auto& q) {
|
||||
return q.scatter().then([&q] {
|
||||
return sleep(q.duration()).then([&q] {
|
||||
return q.gather(q.capacity()).then([&q] (auto topk_results) {
|
||||
apilog.debug("toppartitions query: processing results");
|
||||
cf::toppartitions_query_results results;
|
||||
|
||||
for (auto& d: topk_results.read.top(q.list_size())) {
|
||||
cf::toppartitions_record r;
|
||||
r.partition = sstring(d.item);
|
||||
r.count = d.count;
|
||||
r.error = d.error;
|
||||
results.read.push(r);
|
||||
}
|
||||
for (auto& d: topk_results.write.top(q.list_size())) {
|
||||
cf::toppartitions_record r;
|
||||
r.partition = sstring(d.item);
|
||||
r.count = d.count;
|
||||
r.error = d.error;
|
||||
results.write.push(r);
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "api.hh"
|
||||
#include "api/api-doc/column_family.json.hh"
|
||||
#include "database.hh"
|
||||
#include <seastar/core/future-util.hh>
|
||||
#include <any>
|
||||
|
||||
namespace api {
|
||||
@@ -39,14 +38,14 @@ template<class Mapper, class I, class Reducer>
|
||||
future<I> map_reduce_cf_raw(http_context& ctx, const sstring& name, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
auto uuid = get_uuid(name, ctx.db.local());
|
||||
using mapper_type = std::function<std::unique_ptr<std::any>(database&)>;
|
||||
using reducer_type = std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)>;
|
||||
using mapper_type = std::function<std::any (database&)>;
|
||||
using reducer_type = std::function<std::any (std::any, std::any)>;
|
||||
return ctx.db.map_reduce0(mapper_type([mapper, uuid](database& db) {
|
||||
return std::make_unique<std::any>(I(mapper(db.find_column_family(uuid))));
|
||||
}), std::make_unique<std::any>(std::move(init)), reducer_type([reducer = std::move(reducer)] (std::unique_ptr<std::any> a, std::unique_ptr<std::any> b) mutable {
|
||||
return std::make_unique<std::any>(I(reducer(std::any_cast<I>(std::move(*a)), std::any_cast<I>(std::move(*b)))));
|
||||
})).then([] (std::unique_ptr<std::any> r) {
|
||||
return std::any_cast<I>(std::move(*r));
|
||||
return I(mapper(db.find_column_family(uuid)));
|
||||
}), std::any(std::move(init)), reducer_type([reducer = std::move(reducer)] (std::any a, std::any b) mutable {
|
||||
return I(reducer(std::any_cast<I>(std::move(a)), std::any_cast<I>(std::move(b))));
|
||||
})).then([] (std::any r) {
|
||||
return std::any_cast<I>(std::move(r));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,32 +69,30 @@ future<json::json_return_type> map_reduce_cf(http_context& ctx, const sstring& n
|
||||
|
||||
struct map_reduce_column_families_locally {
|
||||
std::any init;
|
||||
std::function<std::unique_ptr<std::any>(column_family&)> mapper;
|
||||
std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)> reducer;
|
||||
future<std::unique_ptr<std::any>> operator()(database& db) const {
|
||||
auto res = seastar::make_lw_shared<std::unique_ptr<std::any>>(std::make_unique<std::any>(init));
|
||||
return do_for_each(db.get_column_families(), [res, this](const std::pair<utils::UUID, seastar::lw_shared_ptr<table>>& i) {
|
||||
*res = std::move(reducer(std::move(*res), mapper(*i.second.get())));
|
||||
}).then([res] {
|
||||
return std::move(*res);
|
||||
});
|
||||
std::function<std::any (column_family&)> mapper;
|
||||
std::function<std::any (std::any, std::any)> reducer;
|
||||
std::any operator()(database& db) const {
|
||||
auto res = init;
|
||||
for (auto i : db.get_column_families()) {
|
||||
res = reducer(res, mapper(*i.second.get()));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
template<class Mapper, class I, class Reducer>
|
||||
future<I> map_reduce_cf_raw(http_context& ctx, I init,
|
||||
Mapper mapper, Reducer reducer) {
|
||||
using mapper_type = std::function<std::unique_ptr<std::any>(column_family&)>;
|
||||
using reducer_type = std::function<std::unique_ptr<std::any>(std::unique_ptr<std::any>, std::unique_ptr<std::any>)>;
|
||||
using mapper_type = std::function<std::any (column_family&)>;
|
||||
using reducer_type = std::function<std::any (std::any, std::any)>;
|
||||
auto wrapped_mapper = mapper_type([mapper = std::move(mapper)] (column_family& cf) mutable {
|
||||
return std::make_unique<std::any>(I(mapper(cf)));
|
||||
return I(mapper(cf));
|
||||
});
|
||||
auto wrapped_reducer = reducer_type([reducer = std::move(reducer)] (std::unique_ptr<std::any> a, std::unique_ptr<std::any> b) mutable {
|
||||
return std::make_unique<std::any>(I(reducer(std::any_cast<I>(std::move(*a)), std::any_cast<I>(std::move(*b)))));
|
||||
auto wrapped_reducer = reducer_type([reducer = std::move(reducer)] (std::any a, std::any b) mutable {
|
||||
return I(reducer(std::any_cast<I>(std::move(a)), std::any_cast<I>(std::move(b))));
|
||||
});
|
||||
return ctx.db.map_reduce0(map_reduce_column_families_locally{init,
|
||||
std::move(wrapped_mapper), wrapped_reducer}, std::make_unique<std::any>(init), wrapped_reducer).then([] (std::unique_ptr<std::any> res) {
|
||||
return std::any_cast<I>(std::move(*res));
|
||||
return ctx.db.map_reduce0(map_reduce_column_families_locally{init, std::move(wrapped_mapper), wrapped_reducer}, std::any(init), wrapped_reducer).then([] (std::any res) {
|
||||
return std::any_cast<I>(std::move(res));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,15 @@
|
||||
#include "commitlog.hh"
|
||||
#include <db/commitlog/commitlog.hh>
|
||||
#include "api/api-doc/commitlog.json.hh"
|
||||
#include "database.hh"
|
||||
#include <vector>
|
||||
|
||||
namespace api {
|
||||
|
||||
template<typename T>
|
||||
static auto acquire_cl_metric(http_context& ctx, std::function<T (db::commitlog*)> func) {
|
||||
typedef T ret_type;
|
||||
template<typename Func>
|
||||
static auto acquire_cl_metric(http_context& ctx, Func&& func) {
|
||||
typedef std::result_of_t<Func(db::commitlog *)> ret_type;
|
||||
|
||||
return ctx.db.map_reduce0([func = std::move(func)](database& db) {
|
||||
return ctx.db.map_reduce0([func = std::forward<Func>(func)](database& db) {
|
||||
if (db.commitlog() == nullptr) {
|
||||
return make_ready_future<ret_type>();
|
||||
}
|
||||
@@ -64,15 +63,15 @@ void set_commitlog(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
httpd::commitlog_json::get_completed_tasks.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return acquire_cl_metric<uint64_t>(ctx, std::bind(&db::commitlog::get_completed_tasks, std::placeholders::_1));
|
||||
return acquire_cl_metric(ctx, std::bind(&db::commitlog::get_completed_tasks, std::placeholders::_1));
|
||||
});
|
||||
|
||||
httpd::commitlog_json::get_pending_tasks.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return acquire_cl_metric<uint64_t>(ctx, std::bind(&db::commitlog::get_pending_tasks, std::placeholders::_1));
|
||||
return acquire_cl_metric(ctx, std::bind(&db::commitlog::get_pending_tasks, std::placeholders::_1));
|
||||
});
|
||||
|
||||
httpd::commitlog_json::get_total_commit_log_size.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return acquire_cl_metric<uint64_t>(ctx, std::bind(&db::commitlog::get_total_size, std::placeholders::_1));
|
||||
return acquire_cl_metric(ctx, std::bind(&db::commitlog::get_total_size, std::placeholders::_1));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "api/api-doc/compaction_manager.json.hh"
|
||||
#include "db/system_keyspace.hh"
|
||||
#include "column_family.hh"
|
||||
#include <utility>
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -39,16 +38,6 @@ static future<json::json_return_type> get_cm_stats(http_context& ctx,
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
}
|
||||
static std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash> sum_pending_tasks(std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>&& a,
|
||||
const std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>& b) {
|
||||
for (auto&& i : b) {
|
||||
if (i.second) {
|
||||
a[i.first] += i.second;
|
||||
}
|
||||
}
|
||||
return std::move(a);
|
||||
}
|
||||
|
||||
|
||||
void set_compaction_manager(http_context& ctx, routes& r) {
|
||||
cm::get_compactions.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -58,8 +47,8 @@ void set_compaction_manager(http_context& ctx, routes& r) {
|
||||
|
||||
for (const auto& c : cm.get_compactions()) {
|
||||
cm::summary s;
|
||||
s.ks = c->ks_name;
|
||||
s.cf = c->cf_name;
|
||||
s.ks = c->ks;
|
||||
s.cf = c->cf;
|
||||
s.unit = "keys";
|
||||
s.task_type = sstables::compaction_name(c->type);
|
||||
s.completed = c->total_keys_written;
|
||||
@@ -72,31 +61,6 @@ void set_compaction_manager(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
cm::get_pending_tasks_by_table.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return ctx.db.map_reduce0([&ctx](database& db) {
|
||||
std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash> tasks;
|
||||
return do_for_each(db.get_column_families(), [&tasks](const std::pair<utils::UUID, seastar::lw_shared_ptr<table>>& i) {
|
||||
table& cf = *i.second.get();
|
||||
tasks[std::make_pair(cf.schema()->ks_name(), cf.schema()->cf_name())] = cf.get_compaction_strategy().estimated_pending_compactions(cf);
|
||||
return make_ready_future<>();
|
||||
}).then([&tasks] {
|
||||
return tasks;
|
||||
});
|
||||
}, std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>(), sum_pending_tasks).then(
|
||||
[](const std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>& task_map) {
|
||||
std::vector<cm::pending_compaction> res;
|
||||
res.reserve(task_map.size());
|
||||
for (auto i : task_map) {
|
||||
cm::pending_compaction task;
|
||||
task.ks = i.first.first;
|
||||
task.cf = i.first.second;
|
||||
task.task = i.second;
|
||||
res.emplace_back(std::move(task));
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
cm::force_user_defined_compaction.set(r, [] (std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
// FIXME
|
||||
@@ -139,37 +103,29 @@ void set_compaction_manager(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cm::get_compaction_history.set(r, [] (std::unique_ptr<request> req) {
|
||||
std::function<future<>(output_stream<char>&&)> f = [](output_stream<char>&& s) {
|
||||
return do_with(output_stream<char>(std::move(s)), true, [] (output_stream<char>& s, bool& first){
|
||||
return s.write("[").then([&s, &first] {
|
||||
return db::system_keyspace::get_compaction_history([&s, &first](const db::system_keyspace::compaction_history_entry& entry) mutable {
|
||||
cm::history h;
|
||||
h.id = entry.id.to_sstring();
|
||||
h.ks = std::move(entry.ks);
|
||||
h.cf = std::move(entry.cf);
|
||||
h.compacted_at = entry.compacted_at;
|
||||
h.bytes_in = entry.bytes_in;
|
||||
h.bytes_out = entry.bytes_out;
|
||||
for (auto it : entry.rows_merged) {
|
||||
httpd::compaction_manager_json::row_merged e;
|
||||
e.key = it.first;
|
||||
e.value = it.second;
|
||||
h.rows_merged.push(std::move(e));
|
||||
}
|
||||
auto fut = first ? make_ready_future<>() : s.write(", ");
|
||||
first = false;
|
||||
return fut.then([&s, h = std::move(h)] {
|
||||
return formatter::write(s, h);
|
||||
});
|
||||
}).then([&s] {
|
||||
return s.write("]").then([&s] {
|
||||
return s.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
return make_ready_future<json::json_return_type>(std::move(f));
|
||||
return db::system_keyspace::get_compaction_history().then([] (std::vector<db::system_keyspace::compaction_history_entry> history) {
|
||||
std::vector<cm::history> res;
|
||||
res.reserve(history.size());
|
||||
|
||||
for (auto& entry : history) {
|
||||
cm::history h;
|
||||
h.id = entry.id.to_sstring();
|
||||
h.ks = std::move(entry.ks);
|
||||
h.cf = std::move(entry.cf);
|
||||
h.compacted_at = entry.compacted_at;
|
||||
h.bytes_in = entry.bytes_in;
|
||||
h.bytes_out = entry.bytes_out;
|
||||
for (auto it : entry.rows_merged) {
|
||||
httpd::compaction_manager_json::row_merged e;
|
||||
e.key = it.first;
|
||||
e.value = it.second;
|
||||
h.rows_merged.push(std::move(e));
|
||||
}
|
||||
res.push_back(std::move(h));
|
||||
}
|
||||
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
cm::get_compaction_info.set(r, [] (std::unique_ptr<request> req) {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "api/config.hh"
|
||||
#include "api/api-doc/config.json.hh"
|
||||
#include "db/config.hh"
|
||||
#include "database.hh"
|
||||
#include <sstream>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
|
||||
@@ -44,14 +43,14 @@ json::json_return_type get_json_return_type(const db::seed_provider_type& val) {
|
||||
return json::json_return_type(val.class_name);
|
||||
}
|
||||
|
||||
std::string_view format_type(std::string_view type) {
|
||||
std::string format_type(const std::string& type) {
|
||||
if (type == "int") {
|
||||
return "integer";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
future<> get_config_swagger_entry(std::string_view name, const std::string& description, std::string_view type, bool& first, output_stream<char>& os) {
|
||||
future<> get_config_swagger_entry(const std::string& name, const std::string& description, const std::string& type, bool& first, output_stream<char>& os) {
|
||||
std::stringstream ss;
|
||||
if (first) {
|
||||
first=false;
|
||||
@@ -88,29 +87,23 @@ future<> get_config_swagger_entry(std::string_view name, const std::string& desc
|
||||
}
|
||||
|
||||
namespace cs = httpd::config_json;
|
||||
#define _get_config_value(name, type, deflt, status, desc, ...) if (id == #name) {return get_json_return_type(ctx.db.local().get_config().name());}
|
||||
|
||||
|
||||
#define _get_config_description(name, type, deflt, status, desc, ...) f = f.then([&os, &first] {return get_config_swagger_entry(#name, desc, #type, first, os);});
|
||||
|
||||
void set_config(std::shared_ptr < api_registry_builder20 > rb, http_context& ctx, routes& r) {
|
||||
rb->register_function(r, [&ctx] (output_stream<char>& os) {
|
||||
return do_with(true, [&os, &ctx] (bool& first) {
|
||||
rb->register_function(r, [] (output_stream<char>& os) {
|
||||
return do_with(true, [&os] (bool& first) {
|
||||
auto f = make_ready_future();
|
||||
for (auto&& cfg_ref : ctx.db.local().get_config().values()) {
|
||||
auto&& cfg = cfg_ref.get();
|
||||
f = f.then([&os, &first, &cfg] {
|
||||
return get_config_swagger_entry(cfg.name(), std::string(cfg.desc()), cfg.type_name(), first, os);
|
||||
});
|
||||
}
|
||||
_make_config_values(_get_config_description)
|
||||
return f;
|
||||
});
|
||||
});
|
||||
|
||||
cs::find_config_id.set(r, [&ctx] (const_req r) {
|
||||
auto id = r.param["id"];
|
||||
for (auto&& cfg_ref : ctx.db.local().get_config().values()) {
|
||||
auto&& cfg = cfg_ref.get();
|
||||
if (id == cfg.name()) {
|
||||
return cfg.value_as_json();
|
||||
}
|
||||
}
|
||||
_make_config_values(_get_config_value)
|
||||
throw bad_param_exception(sstring("No such config entry: ") + id);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include "api/lsa.hh"
|
||||
#include "api/api.hh"
|
||||
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "utils/logalloc.hh"
|
||||
#include "log.hh"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "messaging_service.hh"
|
||||
#include "message/messaging_service.hh"
|
||||
#include <seastar/rpc/rpc_types.hh>
|
||||
#include "rpc/rpc_types.hh"
|
||||
#include "api/api-doc/messaging_service.json.hh"
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
@@ -76,7 +76,7 @@ future_json_function get_server_getter(std::function<uint64_t(const rpc::stats&)
|
||||
auto get_shard_map = [f](messaging_service& ms) {
|
||||
std::unordered_map<gms::inet_address, unsigned long> map;
|
||||
ms.foreach_server_connection_stats([&map, f] (const rpc::client_info& info, const rpc::stats& stats) mutable {
|
||||
map[gms::inet_address(info.addr.addr())] = f(stats);
|
||||
map[gms::inet_address(net::ipv4_address(info.addr))] = f(stats);
|
||||
});
|
||||
return map;
|
||||
};
|
||||
@@ -139,7 +139,7 @@ void set_messaging_service(http_context& ctx, routes& r) {
|
||||
messaging_verb v = i; // for type safety we use messaging_verb values
|
||||
auto idx = static_cast<uint32_t>(v);
|
||||
if (idx >= map->size()) {
|
||||
throw std::runtime_error(format("verb index out of bounds: {:d}, map size: {:d}", idx, map->size()));
|
||||
throw std::runtime_error(sprint("verb index out of bounds: %lu, map size: %lu", idx, map->size()));
|
||||
}
|
||||
if ((*map)[idx] > 0) {
|
||||
c.count = (*map)[idx];
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "service/storage_service.hh"
|
||||
#include "db/config.hh"
|
||||
#include "utils/histogram.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -47,10 +46,6 @@ static future<json::json_return_type> sum_timed_rate_as_obj(distributed<proxy>&
|
||||
});
|
||||
}
|
||||
|
||||
httpd::utils_json::rate_moving_average_and_histogram get_empty_moving_average() {
|
||||
return timer_to_json(utils::rate_moving_average_and_histogram());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> sum_timed_rate_as_long(distributed<proxy>& d, utils::timed_rate_moving_average proxy::stats::*f) {
|
||||
return sum_timed_rate(d, f).then([](const utils::rate_moving_average& val) {
|
||||
return make_ready_future<json::json_return_type>(val.count);
|
||||
@@ -381,29 +376,6 @@ void set_storage_proxy(http_context& ctx, routes& r) {
|
||||
sp::get_write_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::write);
|
||||
});
|
||||
sp::get_cas_write_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
// FIXME
|
||||
// cas is not supported yet, so just return empty moving average
|
||||
|
||||
return make_ready_future<json::json_return_type>(get_empty_moving_average());
|
||||
});
|
||||
|
||||
sp::get_cas_read_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
// FIXME
|
||||
// cas is not supported yet, so just return empty moving average
|
||||
|
||||
return make_ready_future<json::json_return_type>(get_empty_moving_average());
|
||||
});
|
||||
|
||||
sp::get_view_write_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
// FIXME
|
||||
// No View metrics are available, so just return empty moving average
|
||||
|
||||
return make_ready_future<json::json_return_type>(get_empty_moving_average());
|
||||
});
|
||||
|
||||
sp::get_read_metrics_latency_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return sum_timer_stats(ctx.sp, &proxy::stats::read);
|
||||
|
||||
@@ -22,26 +22,19 @@
|
||||
#include "storage_service.hh"
|
||||
#include "api/api-doc/storage_service.json.hh"
|
||||
#include "db/config.hh"
|
||||
#include <optional>
|
||||
#include <time.h>
|
||||
#include <boost/range/adaptor/map.hpp>
|
||||
#include <boost/range/adaptor/filtered.hpp>
|
||||
#include "service/storage_service.hh"
|
||||
#include "db/commitlog/commitlog.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include "db/system_keyspace.hh"
|
||||
#include "seastar/http/exception.hh"
|
||||
#include <service/storage_service.hh>
|
||||
#include <db/commitlog/commitlog.hh>
|
||||
#include <gms/gossiper.hh>
|
||||
#include <db/system_keyspace.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "repair/repair.hh"
|
||||
#include "locator/snitch_base.hh"
|
||||
#include "column_family.hh"
|
||||
#include "log.hh"
|
||||
#include "release.hh"
|
||||
#include "sstables/compaction_manager.hh"
|
||||
#include "sstables/sstables.hh"
|
||||
#include "database.hh"
|
||||
#include "db/extensions.hh"
|
||||
|
||||
sstables::sstable::version_types get_highest_supported_format();
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -55,6 +48,7 @@ static sstring validate_keyspace(http_context& ctx, const parameters& param) {
|
||||
throw bad_param_exception("Keyspace " + param["keyspace"] + " Does not exist");
|
||||
}
|
||||
|
||||
|
||||
static std::vector<ss::token_range> describe_ring(const sstring& keyspace) {
|
||||
std::vector<ss::token_range> res;
|
||||
for (auto d : service::get_local_storage_service().describe_ring(keyspace)) {
|
||||
@@ -78,36 +72,21 @@ static std::vector<ss::token_range> describe_ring(const sstring& keyspace) {
|
||||
}
|
||||
|
||||
void set_storage_service(http_context& ctx, routes& r) {
|
||||
using ks_cf_func = std::function<future<json::json_return_type>(std::unique_ptr<request>, sstring, std::vector<sstring>)>;
|
||||
|
||||
auto wrap_ks_cf = [&ctx](ks_cf_func f) {
|
||||
return [&ctx, f = std::move(f)](std::unique_ptr<request> req) {
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_families = split_cf(req->get_query_param("cf"));
|
||||
if (column_families.empty()) {
|
||||
column_families = map_keys(ctx.db.local().find_keyspace(keyspace).metadata().get()->cf_meta_data());
|
||||
}
|
||||
return f(std::move(req), std::move(keyspace), std::move(column_families));
|
||||
};
|
||||
};
|
||||
|
||||
ss::local_hostid.set(r, [](std::unique_ptr<request> req) {
|
||||
return db::system_keyspace::get_local_host_id().then([](const utils::UUID& id) {
|
||||
return make_ready_future<json::json_return_type>(id.to_sstring());
|
||||
});
|
||||
});
|
||||
|
||||
ss::get_tokens.set(r, [] (std::unique_ptr<request> req) {
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().get_token_metadata().sorted_tokens(), [](const dht::token& i) {
|
||||
return boost::lexical_cast<std::string>(i);
|
||||
}));
|
||||
ss::get_tokens.set(r, [] (const_req req) {
|
||||
auto tokens = service::get_local_storage_service().get_token_metadata().sorted_tokens();
|
||||
return container_to_vec(tokens);
|
||||
});
|
||||
|
||||
ss::get_node_tokens.set(r, [] (std::unique_ptr<request> req) {
|
||||
gms::inet_address addr(req->param["endpoint"]);
|
||||
return make_ready_future<json::json_return_type>(stream_range_as_array(service::get_local_storage_service().get_token_metadata().get_tokens(addr), [](const dht::token& i) {
|
||||
return boost::lexical_cast<std::string>(i);
|
||||
}));
|
||||
ss::get_node_tokens.set(r, [] (const_req req) {
|
||||
gms::inet_address addr(req.param["endpoint"]);
|
||||
auto tokens = service::get_local_storage_service().get_token_metadata().get_tokens(addr);
|
||||
return container_to_vec(tokens);
|
||||
});
|
||||
|
||||
ss::get_commitlog.set(r, [&ctx](const_req req) {
|
||||
@@ -128,7 +107,11 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
ss::get_moving_nodes.set(r, [](const_req req) {
|
||||
auto points = service::get_local_storage_service().get_token_metadata().get_moving_endpoints();
|
||||
std::unordered_set<sstring> addr;
|
||||
for (auto i: points) {
|
||||
addr.insert(boost::lexical_cast<std::string>(i.second));
|
||||
}
|
||||
return container_to_vec(addr);
|
||||
});
|
||||
|
||||
@@ -318,44 +301,24 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
ss::scrub.set(r, wrap_ks_cf([&ctx](std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
// TODO: respect this
|
||||
ss::scrub.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_family = req->get_query_param("cf");
|
||||
auto disable_snapshot = req->get_query_param("disable_snapshot");
|
||||
auto skip_corrupted = req->get_query_param("skip_corrupted");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
auto f = make_ready_future<>();
|
||||
if (!req_param<bool>(*req, "disable_snapshot", false)) {
|
||||
auto tag = format("pre-scrub-{:d}", db_clock::now().time_since_epoch().count());
|
||||
f = parallel_for_each(column_families, [keyspace, tag](sstring cf) {
|
||||
return service::get_local_storage_service().take_column_family_snapshot(keyspace, cf, tag);
|
||||
});
|
||||
}
|
||||
|
||||
return f.then([&ctx, keyspace, column_families] {
|
||||
return ctx.db.invoke_on_all([=] (database& db) {
|
||||
return do_for_each(column_families, [=, &db](sstring cfname) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
auto& cf = db.find_column_family(keyspace, cfname);
|
||||
return cm.perform_sstable_scrub(&cf);
|
||||
});
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
}));
|
||||
|
||||
ss::upgrade_sstables.set(r, wrap_ks_cf([&ctx](std::unique_ptr<request> req, sstring keyspace, std::vector<sstring> column_families) {
|
||||
bool exclude_current_version = req_param<bool>(*req, "exclude_current_version", false);
|
||||
|
||||
return ctx.db.invoke_on_all([=] (database& db) {
|
||||
return do_for_each(column_families, [=, &db](sstring cfname) {
|
||||
auto& cm = db.get_compaction_manager();
|
||||
auto& cf = db.find_column_family(keyspace, cfname);
|
||||
return cm.perform_sstable_upgrade(&cf, exclude_current_version);
|
||||
});
|
||||
}).then([]{
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
}));
|
||||
ss::upgrade_sstables.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
auto column_family = req->get_query_param("cf");
|
||||
auto exclude_current_version = req->get_query_param("exclude_current_version");
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
|
||||
ss::force_keyspace_flush.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
auto keyspace = validate_keyspace(ctx, req->param);
|
||||
@@ -493,7 +456,7 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
return service::get_storage_service().map_reduce(adder<service::storage_service::drain_progress>(), [] (auto& ss) {
|
||||
return ss.get_drain_progress();
|
||||
}).then([] (auto&& progress) {
|
||||
auto progress_str = format("Drained {}/{} ColumnFamilies", progress.remaining_cfs, progress.total_cfs);
|
||||
auto progress_str = sprint("Drained %s/%s ColumnFamilies", progress.remaining_cfs, progress.total_cfs);
|
||||
return make_ready_future<json::json_return_type>(std::move(progress_str));
|
||||
});
|
||||
});
|
||||
@@ -704,11 +667,7 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
auto coordinator = std::hash<sstring>()(cf) % smp::count;
|
||||
return service::get_storage_service().invoke_on(coordinator, [ks = std::move(ks), cf = std::move(cf)] (service::storage_service& s) {
|
||||
return s.load_new_sstables(ks, cf);
|
||||
}).then_wrapped([] (auto&& f) {
|
||||
if (f.failed()) {
|
||||
auto msg = fmt::format("Failed to load new sstables: {}", f.get_exception());
|
||||
return make_exception_future<json::json_return_type>(httpd::server_error_exception(msg));
|
||||
}
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
@@ -742,7 +701,7 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
} catch (std::out_of_range& e) {
|
||||
throw httpd::bad_param_exception(e.what());
|
||||
} catch (std::invalid_argument&){
|
||||
throw httpd::bad_param_exception(format("Bad format in a probability value: \"{}\"", probability.c_str()));
|
||||
throw httpd::bad_param_exception(sprint("Bad format in a probability value: \"%s\"", probability.c_str()));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -778,7 +737,7 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
} catch (...) {
|
||||
throw httpd::bad_param_exception(format("Bad format value: "));
|
||||
throw httpd::bad_param_exception(sprint("Bad format value: "));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -902,133 +861,6 @@ void set_storage_service(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(map_to_key_value(std::move(status), res));
|
||||
});
|
||||
});
|
||||
|
||||
ss::sstable_info.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
auto ks = api::req_param<sstring>(*req, "keyspace", {}).value;
|
||||
auto cf = api::req_param<sstring>(*req, "cf", {}).value;
|
||||
|
||||
// The size of this vector is bound by ks::cf. I.e. it is as most Nks + Ncf long
|
||||
// which is not small, but not huge either.
|
||||
using table_sstables_list = std::vector<ss::table_sstables>;
|
||||
|
||||
return do_with(table_sstables_list{}, [ks, cf, &ctx](table_sstables_list& dst) {
|
||||
return service::get_local_storage_service().db().map_reduce([&dst](table_sstables_list&& res) {
|
||||
for (auto&& t : res) {
|
||||
auto i = std::find_if(dst.begin(), dst.end(), [&t](const ss::table_sstables& t2) {
|
||||
return t.keyspace() == t2.keyspace() && t.table() == t2.table();
|
||||
});
|
||||
if (i == dst.end()) {
|
||||
dst.emplace_back(std::move(t));
|
||||
continue;
|
||||
}
|
||||
auto& ssd = i->sstables;
|
||||
for (auto&& sd : t.sstables._elements) {
|
||||
auto j = std::find_if(ssd._elements.begin(), ssd._elements.end(), [&sd](const ss::sstable& s) {
|
||||
return s.generation() == sd.generation();
|
||||
});
|
||||
if (j == ssd._elements.end()) {
|
||||
i->sstables.push(std::move(sd));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ks, cf](const database& db) {
|
||||
// see above
|
||||
table_sstables_list res;
|
||||
|
||||
auto& ext = db.get_config().extensions();
|
||||
|
||||
for (auto& t : db.get_column_families() | boost::adaptors::map_values) {
|
||||
auto& schema = t->schema();
|
||||
if ((ks.empty() || ks == schema->ks_name()) && (cf.empty() || cf == schema->cf_name())) {
|
||||
// at most Nsstables long
|
||||
ss::table_sstables tst;
|
||||
tst.keyspace = schema->ks_name();
|
||||
tst.table = schema->cf_name();
|
||||
|
||||
for (auto sstable : *t->get_sstables_including_compacted_undeleted()) {
|
||||
auto ts = db_clock::to_time_t(sstable->data_file_write_time());
|
||||
::tm t;
|
||||
::gmtime_r(&ts, &t);
|
||||
|
||||
ss::sstable info;
|
||||
|
||||
info.timestamp = t;
|
||||
info.generation = sstable->generation();
|
||||
info.level = sstable->get_sstable_level();
|
||||
info.size = sstable->bytes_on_disk();
|
||||
info.data_size = sstable->ondisk_data_size();
|
||||
info.index_size = sstable->index_size();
|
||||
info.filter_size = sstable->filter_size();
|
||||
info.version = sstable->get_version();
|
||||
|
||||
if (sstable->has_component(sstables::component_type::CompressionInfo)) {
|
||||
auto& c = sstable->get_compression();
|
||||
auto cp = sstables::get_sstable_compressor(c);
|
||||
|
||||
ss::named_maps nm;
|
||||
nm.group = "compression_parameters";
|
||||
for (auto& p : cp->options()) {
|
||||
ss::mapper e;
|
||||
e.key = p.first;
|
||||
e.value = p.second;
|
||||
nm.attributes.push(std::move(e));
|
||||
}
|
||||
if (!cp->options().count(compression_parameters::SSTABLE_COMPRESSION)) {
|
||||
ss::mapper e;
|
||||
e.key = compression_parameters::SSTABLE_COMPRESSION;
|
||||
e.value = cp->name();
|
||||
nm.attributes.push(std::move(e));
|
||||
}
|
||||
info.extended_properties.push(std::move(nm));
|
||||
}
|
||||
|
||||
sstables::file_io_extension::attr_value_map map;
|
||||
|
||||
for (auto* ep : ext.sstable_file_io_extensions()) {
|
||||
map.merge(ep->get_attributes(*sstable));
|
||||
}
|
||||
|
||||
for (auto& p : map) {
|
||||
struct {
|
||||
const sstring& key;
|
||||
ss::sstable& info;
|
||||
void operator()(const std::map<sstring, sstring>& map) const {
|
||||
ss::named_maps nm;
|
||||
nm.group = key;
|
||||
for (auto& p : map) {
|
||||
ss::mapper e;
|
||||
e.key = p.first;
|
||||
e.value = p.second;
|
||||
nm.attributes.push(std::move(e));
|
||||
}
|
||||
info.extended_properties.push(std::move(nm));
|
||||
}
|
||||
void operator()(const sstring& value) const {
|
||||
ss::mapper e;
|
||||
e.key = key;
|
||||
e.value = value;
|
||||
info.properties.push(std::move(e));
|
||||
}
|
||||
} v{p.first, info};
|
||||
|
||||
std::visit(v, p.second);
|
||||
}
|
||||
|
||||
tst.sstables.push(std::move(info));
|
||||
}
|
||||
res.emplace_back(std::move(tst));
|
||||
}
|
||||
}
|
||||
std::sort(res.begin(), res.end(), [](const ss::table_sstables& t1, const ss::table_sstables& t2) {
|
||||
return t1.keyspace() < t2.keyspace() || (t1.keyspace() == t2.keyspace() && t1.table() < t2.table());
|
||||
});
|
||||
return res;
|
||||
}).then([&dst] {
|
||||
return make_ready_future<json::json_return_type>(stream_object(dst));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "api/api-doc/system.json.hh"
|
||||
#include "api/api.hh"
|
||||
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "http/exception.hh"
|
||||
#include "log.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "atomic_cell.hh"
|
||||
#include "atomic_cell_or_collection.hh"
|
||||
#include "types.hh"
|
||||
#include "types/collection.hh"
|
||||
|
||||
/// LSA mirator for cells with irrelevant type
|
||||
///
|
||||
@@ -48,23 +47,6 @@ atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_typ
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_type timestamp, ser::buffer_view<bytes_ostream::fragment_iterator> value, atomic_cell::collection_member cm) {
|
||||
auto& imr_data = type.imr_state();
|
||||
return atomic_cell(
|
||||
imr_data.type_info(),
|
||||
imr_object_type::make(data::cell::make_live(imr_data.type_info(), timestamp, value, bool(cm)), &imr_data.lsa_migrator())
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_type timestamp, const fragmented_temporary_buffer::view& value, collection_member cm)
|
||||
{
|
||||
auto& imr_data = type.imr_state();
|
||||
return atomic_cell(
|
||||
imr_data.type_info(),
|
||||
imr_object_type::make(data::cell::make_live(imr_data.type_info(), timestamp, value, bool(cm)), &imr_data.lsa_migrator())
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_type timestamp, bytes_view value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, atomic_cell::collection_member cm) {
|
||||
auto& imr_data = type.imr_state();
|
||||
@@ -74,25 +56,6 @@ atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_typ
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_type timestamp, ser::buffer_view<bytes_ostream::fragment_iterator> value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, atomic_cell::collection_member cm) {
|
||||
auto& imr_data = type.imr_state();
|
||||
return atomic_cell(
|
||||
imr_data.type_info(),
|
||||
imr_object_type::make(data::cell::make_live(imr_data.type_info(), timestamp, value, expiry, ttl, bool(cm)), &imr_data.lsa_migrator())
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live(const abstract_type& type, api::timestamp_type timestamp, const fragmented_temporary_buffer::view& value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, collection_member cm)
|
||||
{
|
||||
auto& imr_data = type.imr_state();
|
||||
return atomic_cell(
|
||||
imr_data.type_info(),
|
||||
imr_object_type::make(data::cell::make_live(imr_data.type_info(), timestamp, value, expiry, ttl, bool(cm)), &imr_data.lsa_migrator())
|
||||
);
|
||||
}
|
||||
|
||||
atomic_cell atomic_cell::make_live_counter_update(api::timestamp_type timestamp, int64_t value) {
|
||||
auto& imr_data = no_type_imr_descriptor();
|
||||
return atomic_cell(
|
||||
@@ -192,20 +155,20 @@ bool atomic_cell_or_collection::equals(const abstract_type& type, const atomic_c
|
||||
if (a.timestamp() != b.timestamp()) {
|
||||
return false;
|
||||
}
|
||||
if (a.is_live() != b.is_live()) {
|
||||
return false;
|
||||
}
|
||||
if (a.is_live()) {
|
||||
if (a.is_counter_update() != b.is_counter_update()) {
|
||||
if (!b.is_live()) {
|
||||
return false;
|
||||
}
|
||||
if (a.is_counter_update()) {
|
||||
if (!b.is_counter_update()) {
|
||||
return false;
|
||||
}
|
||||
return a.counter_update_value() == b.counter_update_value();
|
||||
}
|
||||
if (a.is_live_and_has_ttl() != b.is_live_and_has_ttl()) {
|
||||
return false;
|
||||
}
|
||||
if (a.is_live_and_has_ttl()) {
|
||||
if (!b.is_live_and_has_ttl()) {
|
||||
return false;
|
||||
}
|
||||
if (a.ttl() != b.ttl() || a.expiry() != b.expiry()) {
|
||||
return false;
|
||||
}
|
||||
@@ -244,18 +207,16 @@ size_t atomic_cell_or_collection::external_memory_usage(const abstract_type& t)
|
||||
+ imr_object_type::size_overhead + external_value_size;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const atomic_cell_or_collection::printer& p) {
|
||||
if (!p._cell._data.get()) {
|
||||
std::ostream& operator<<(std::ostream& os, const atomic_cell_or_collection& c) {
|
||||
if (!c._data.get()) {
|
||||
return os << "{ null atomic_cell_or_collection }";
|
||||
}
|
||||
using dc = data::cell;
|
||||
os << "{ ";
|
||||
if (dc::structure::get_member<dc::tags::flags>(p._cell._data.get()).get<dc::tags::collection>()) {
|
||||
os << "collection ";
|
||||
auto cmv = p._cell.as_collection_mutation();
|
||||
os << to_hex(cmv.data.linearize());
|
||||
if (dc::structure::get_member<dc::tags::flags>(c._data.get()).get<dc::tags::collection>()) {
|
||||
os << "collection";
|
||||
} else {
|
||||
os << p._cell.as_atomic_cell(p._cdef);
|
||||
os << "atomic cell";
|
||||
}
|
||||
return os << " }";
|
||||
return os << " @" << static_cast<const void*>(c._data.get()) << " }";
|
||||
}
|
||||
|
||||
@@ -26,16 +26,13 @@
|
||||
#include "tombstone.hh"
|
||||
#include "gc_clock.hh"
|
||||
#include "utils/managed_bytes.hh"
|
||||
#include <seastar/net//byteorder.hh>
|
||||
#include "net/byteorder.hh"
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <seastar/util/gcc6-concepts.hh>
|
||||
#include "data/cell.hh"
|
||||
#include "data/schema_info.hh"
|
||||
#include "imr/utils.hh"
|
||||
#include "utils/fragmented_temporary_buffer.hh"
|
||||
|
||||
#include "serializer.hh"
|
||||
|
||||
class abstract_type;
|
||||
class collection_type_impl;
|
||||
@@ -189,10 +186,6 @@ public:
|
||||
static atomic_cell make_dead(api::timestamp_type timestamp, gc_clock::time_point deletion_time);
|
||||
static atomic_cell make_live(const abstract_type& type, api::timestamp_type timestamp, bytes_view value,
|
||||
collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type& type, api::timestamp_type timestamp, ser::buffer_view<bytes_ostream::fragment_iterator> value,
|
||||
collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type& type, api::timestamp_type timestamp, const fragmented_temporary_buffer::view& value,
|
||||
collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type& type, api::timestamp_type timestamp, const bytes& value,
|
||||
collection_member cm = collection_member::no) {
|
||||
return make_live(type, timestamp, bytes_view(value), cm);
|
||||
@@ -200,10 +193,6 @@ public:
|
||||
static atomic_cell make_live_counter_update(api::timestamp_type timestamp, int64_t value);
|
||||
static atomic_cell make_live(const abstract_type&, api::timestamp_type timestamp, bytes_view value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type&, api::timestamp_type timestamp, ser::buffer_view<bytes_ostream::fragment_iterator> value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type&, api::timestamp_type timestamp, const fragmented_temporary_buffer::view& value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, collection_member = collection_member::no);
|
||||
static atomic_cell make_live(const abstract_type& type, api::timestamp_type timestamp, const bytes& value,
|
||||
gc_clock::time_point expiry, gc_clock::duration ttl, collection_member cm = collection_member::no)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
// Not part of atomic_cell.hh to avoid cyclic dependency between types.hh and atomic_cell.hh
|
||||
|
||||
#include "types.hh"
|
||||
#include "types/collection.hh"
|
||||
#include "atomic_cell.hh"
|
||||
#include "atomic_cell_or_collection.hh"
|
||||
#include "hashing.hh"
|
||||
|
||||
@@ -67,19 +67,7 @@ public:
|
||||
bytes_view serialize() const;
|
||||
bool equals(const abstract_type& type, const atomic_cell_or_collection& other) const;
|
||||
size_t external_memory_usage(const abstract_type&) const;
|
||||
|
||||
class printer {
|
||||
const column_definition& _cdef;
|
||||
const atomic_cell_or_collection& _cell;
|
||||
public:
|
||||
printer(const column_definition& cdef, const atomic_cell_or_collection& cell)
|
||||
: _cdef(cdef), _cell(cell) { }
|
||||
printer(const printer&) = delete;
|
||||
printer(printer&&) = delete;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream&, const printer&);
|
||||
};
|
||||
friend std::ostream& operator<<(std::ostream&, const printer&);
|
||||
friend std::ostream& operator<<(std::ostream&, const atomic_cell_or_collection&);
|
||||
};
|
||||
|
||||
namespace std {
|
||||
|
||||
@@ -72,19 +72,19 @@ public:
|
||||
return make_ready_future<authenticated_user>(anonymous_user());
|
||||
}
|
||||
|
||||
virtual future<> create(std::string_view, const authentication_options& options) const override {
|
||||
virtual future<> create(stdx::string_view, const authentication_options& options) const override {
|
||||
return make_ready_future();
|
||||
}
|
||||
|
||||
virtual future<> alter(std::string_view, const authentication_options& options) const override {
|
||||
virtual future<> alter(stdx::string_view, const authentication_options& options) const override {
|
||||
return make_ready_future();
|
||||
}
|
||||
|
||||
virtual future<> drop(std::string_view) const override {
|
||||
virtual future<> drop(stdx::string_view) const override {
|
||||
return make_ready_future();
|
||||
}
|
||||
|
||||
virtual future<custom_options> query_custom_options(std::string_view role_name) const override {
|
||||
virtual future<custom_options> query_custom_options(stdx::string_view role_name) const override {
|
||||
return make_ready_future<custom_options>();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "auth/authorizer.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace cql3 {
|
||||
class query_processor;
|
||||
@@ -57,12 +58,12 @@ public:
|
||||
return make_ready_future<permission_set>(permissions::ALL);
|
||||
}
|
||||
|
||||
virtual future<> grant(std::string_view, permission_set, const resource&) const override {
|
||||
virtual future<> grant(stdx::string_view, permission_set, const resource&) const override {
|
||||
return make_exception_future<>(
|
||||
unsupported_authorization_operation("GRANT operation is not supported by AllowAllAuthorizer"));
|
||||
}
|
||||
|
||||
virtual future<> revoke(std::string_view, permission_set, const resource&) const override {
|
||||
virtual future<> revoke(stdx::string_view, permission_set, const resource&) const override {
|
||||
return make_exception_future<>(
|
||||
unsupported_authorization_operation("REVOKE operation is not supported by AllowAllAuthorizer"));
|
||||
}
|
||||
@@ -73,7 +74,7 @@ public:
|
||||
"LIST PERMISSIONS operation is not supported by AllowAllAuthorizer"));
|
||||
}
|
||||
|
||||
virtual future<> revoke_all(std::string_view) const override {
|
||||
virtual future<> revoke_all(stdx::string_view) const override {
|
||||
return make_exception_future(
|
||||
unsupported_authorization_operation("REVOKE operation is not supported by AllowAllAuthorizer"));
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
namespace auth {
|
||||
|
||||
authenticated_user::authenticated_user(std::string_view name)
|
||||
authenticated_user::authenticated_user(stdx::string_view name)
|
||||
: name(sstring(name)) {
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <functional>
|
||||
#include <iosfwd>
|
||||
#include <optional>
|
||||
@@ -49,6 +49,7 @@
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -66,7 +67,7 @@ public:
|
||||
/// An anonymous user.
|
||||
///
|
||||
authenticated_user() = default;
|
||||
explicit authenticated_user(std::string_view name);
|
||||
explicit authenticated_user(stdx::string_view name);
|
||||
};
|
||||
|
||||
///
|
||||
|
||||
@@ -57,7 +57,7 @@ inline bool any_authentication_options(const authentication_options& aos) noexce
|
||||
class unsupported_authentication_option : public std::invalid_argument {
|
||||
public:
|
||||
explicit unsupported_authentication_option(authentication_option k)
|
||||
: std::invalid_argument(format("The {} option is not supported.", k)) {
|
||||
: std::invalid_argument(sprint("The %s option is not supported.", k)) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "auth/common.hh"
|
||||
#include "auth/password_authenticator.hh"
|
||||
#include "cql3/query_processor.hh"
|
||||
#include "db/config.hh"
|
||||
#include "utils/class_registrator.hh"
|
||||
|
||||
const sstring auth::authenticator::USERNAME_KEY("username");
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
@@ -55,10 +55,10 @@
|
||||
|
||||
#include "auth/authentication_options.hh"
|
||||
#include "auth/resource.hh"
|
||||
#include "auth/sasl_challenge.hh"
|
||||
#include "bytes.hh"
|
||||
#include "enum_set.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace db {
|
||||
class config;
|
||||
@@ -122,7 +122,7 @@ public:
|
||||
///
|
||||
/// The options provided must be a subset of `supported_options()`.
|
||||
///
|
||||
virtual future<> create(std::string_view role_name, const authentication_options& options) const = 0;
|
||||
virtual future<> create(stdx::string_view role_name, const authentication_options& options) const = 0;
|
||||
|
||||
///
|
||||
/// Alter the authentication record of an existing user.
|
||||
@@ -131,25 +131,39 @@ public:
|
||||
///
|
||||
/// Callers must ensure that the specification of `alterable_options()` is adhered to.
|
||||
///
|
||||
virtual future<> alter(std::string_view role_name, const authentication_options& options) const = 0;
|
||||
virtual future<> alter(stdx::string_view role_name, const authentication_options& options) const = 0;
|
||||
|
||||
///
|
||||
/// Delete the authentication record for a user. This will disallow the user from logging in.
|
||||
///
|
||||
virtual future<> drop(std::string_view role_name) const = 0;
|
||||
virtual future<> drop(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// Query for custom options (those corresponding to \ref authentication_options::options).
|
||||
///
|
||||
/// If no options are set the result is an empty container.
|
||||
///
|
||||
virtual future<custom_options> query_custom_options(std::string_view role_name) const = 0;
|
||||
virtual future<custom_options> query_custom_options(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// System resources used internally as part of the implementation. These are made inaccessible to users.
|
||||
///
|
||||
virtual const resource_set& protected_resources() const = 0;
|
||||
|
||||
///
|
||||
/// A stateful SASL challenge which supports many authentication schemes (depending on the implementation).
|
||||
///
|
||||
class sasl_challenge {
|
||||
public:
|
||||
virtual ~sasl_challenge() = default;
|
||||
|
||||
virtual bytes evaluate_response(bytes_view client_response) = 0;
|
||||
|
||||
virtual bool is_complete() const = 0;
|
||||
|
||||
virtual future<authenticated_user> get_authenticated_user() const = 0;
|
||||
};
|
||||
|
||||
virtual ::shared_ptr<sasl_challenge> new_sasl_challenge() const = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "auth/permission.hh"
|
||||
#include "auth/resource.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -116,14 +117,14 @@ public:
|
||||
///
|
||||
/// \throws \ref unsupported_authorization_operation if granting permissions is not supported.
|
||||
///
|
||||
virtual future<> grant(std::string_view role_name, permission_set, const resource&) const = 0;
|
||||
virtual future<> grant(stdx::string_view role_name, permission_set, const resource&) const = 0;
|
||||
|
||||
///
|
||||
/// Revoke a set of permissions from a role for a particular \ref resource.
|
||||
///
|
||||
/// \throws \ref unsupported_authorization_operation if revoking permissions is not supported.
|
||||
///
|
||||
virtual future<> revoke(std::string_view role_name, permission_set, const resource&) const = 0;
|
||||
virtual future<> revoke(stdx::string_view role_name, permission_set, const resource&) const = 0;
|
||||
|
||||
///
|
||||
/// Query for all directly granted permissions.
|
||||
@@ -137,7 +138,7 @@ public:
|
||||
///
|
||||
/// \throws \ref unsupported_authorization_operation if revoking permissions is not supported.
|
||||
///
|
||||
virtual future<> revoke_all(std::string_view role_name) const = 0;
|
||||
virtual future<> revoke_all(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// Revoke all permissions granted to any role for a particular resource.
|
||||
|
||||
@@ -48,9 +48,9 @@ future<> do_after_system_ready(seastar::abort_source& as, seastar::noncopyable_f
|
||||
struct empty_state { };
|
||||
return delay_until_system_ready(as).then([&as, func = std::move(func)] () mutable {
|
||||
return exponential_backoff_retry::do_until_value(1s, 1min, as, [func = std::move(func)] {
|
||||
return func().then_wrapped([] (auto&& f) -> std::optional<empty_state> {
|
||||
return func().then_wrapped([] (auto&& f) -> stdx::optional<empty_state> {
|
||||
if (f.failed()) {
|
||||
auth_log.debug("Auth task failed with error, rescheduling: {}", f.get_exception());
|
||||
auth_log.info("Auth task failed with error, rescheduling: {}", f.get_exception());
|
||||
return { };
|
||||
}
|
||||
return { empty_state() };
|
||||
@@ -60,14 +60,16 @@ future<> do_after_system_ready(seastar::abort_source& as, seastar::noncopyable_f
|
||||
}
|
||||
|
||||
future<> create_metadata_table_if_missing(
|
||||
std::string_view table_name,
|
||||
stdx::string_view table_name,
|
||||
cql3::query_processor& qp,
|
||||
std::string_view cql,
|
||||
stdx::string_view cql,
|
||||
::service::migration_manager& mm) {
|
||||
static auto ignore_existing = [] (seastar::noncopyable_function<future<>()> func) {
|
||||
return futurize_apply(std::move(func)).handle_exception_type([] (exceptions::already_exists_exception& ignored) { });
|
||||
};
|
||||
auto& db = qp.db();
|
||||
auto& db = qp.db().local();
|
||||
|
||||
if (db.has_schema(meta::AUTH_KS, sstring(table_name))) {
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
auto parsed_statement = static_pointer_cast<cql3::statements::raw::cf_statement>(
|
||||
cql3::query_processor::parse_statement(cql));
|
||||
|
||||
@@ -76,29 +78,20 @@ future<> create_metadata_table_if_missing(
|
||||
auto statement = static_pointer_cast<cql3::statements::create_table_statement>(
|
||||
parsed_statement->prepare(db, qp.get_cql_stats())->statement);
|
||||
|
||||
const auto schema = statement->get_cf_meta_data(qp.db());
|
||||
const auto schema = statement->get_cf_meta_data(qp.db().local());
|
||||
const auto uuid = generate_legacy_id(schema->ks_name(), schema->cf_name());
|
||||
|
||||
schema_builder b(schema);
|
||||
b.set_uuid(uuid);
|
||||
schema_ptr table = b.build();
|
||||
return ignore_existing([&mm, table = std::move(table)] () {
|
||||
return mm.announce_new_column_family(table, false);
|
||||
});
|
||||
|
||||
return mm.announce_new_column_family(b.build(), false);
|
||||
}
|
||||
|
||||
future<> wait_for_schema_agreement(::service::migration_manager& mm, const database& db, seastar::abort_source& as) {
|
||||
future<> wait_for_schema_agreement(::service::migration_manager& mm, const database& db) {
|
||||
static const auto pause = [] { return sleep(std::chrono::milliseconds(500)); };
|
||||
|
||||
return do_until([&db, &as] {
|
||||
as.check();
|
||||
return db.get_version() != database::empty_version;
|
||||
}, pause).then([&mm, &as] {
|
||||
return do_until([&mm, &as] {
|
||||
as.check();
|
||||
return mm.have_schema_agreement();
|
||||
}, pause);
|
||||
return do_until([&db] { return db.get_version() != database::empty_version; }, pause).then([&mm] {
|
||||
return do_until([&mm] { return mm.have_schema_agreement(); }, pause);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/abort_source.hh>
|
||||
@@ -76,12 +76,12 @@ inline future<> delay_until_system_ready(seastar::abort_source& as) {
|
||||
future<> do_after_system_ready(seastar::abort_source& as, seastar::noncopyable_function<future<>()> func);
|
||||
|
||||
future<> create_metadata_table_if_missing(
|
||||
std::string_view table_name,
|
||||
stdx::string_view table_name,
|
||||
cql3::query_processor&,
|
||||
std::string_view cql,
|
||||
stdx::string_view cql,
|
||||
::service::migration_manager&);
|
||||
|
||||
future<> wait_for_schema_agreement(::service::migration_manager&, const database&, seastar::abort_source&);
|
||||
future<> wait_for_schema_agreement(::service::migration_manager&, const database&);
|
||||
|
||||
///
|
||||
/// Time-outs for internal, non-local CQL queries.
|
||||
|
||||
@@ -61,7 +61,6 @@ extern "C" {
|
||||
#include "cql3/untyped_result_set.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "log.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -95,11 +94,11 @@ default_authorizer::~default_authorizer() {
|
||||
static const sstring legacy_table_name{"permissions"};
|
||||
|
||||
bool default_authorizer::legacy_metadata_exists() const {
|
||||
return _qp.db().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
return _qp.db().local().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
}
|
||||
|
||||
future<bool> default_authorizer::any_granted() const {
|
||||
static const sstring query = format("SELECT * FROM {}.{} LIMIT 1", meta::AUTH_KS, PERMISSIONS_CF);
|
||||
static const sstring query = sprint("SELECT * FROM %s.%s LIMIT 1", meta::AUTH_KS, PERMISSIONS_CF);
|
||||
|
||||
return _qp.process(
|
||||
query,
|
||||
@@ -113,7 +112,7 @@ future<bool> default_authorizer::any_granted() const {
|
||||
|
||||
future<> default_authorizer::migrate_legacy_metadata() const {
|
||||
alogger.info("Starting migration of legacy permissions metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
static const sstring query = sprint("SELECT * FROM %s.%s", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.process(
|
||||
query,
|
||||
@@ -161,7 +160,7 @@ future<> default_authorizer::start() {
|
||||
_migration_manager).then([this] {
|
||||
_finished = do_after_system_ready(_as, [this] {
|
||||
return async([this] {
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db(), _as).get0();
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db().local()).get0();
|
||||
|
||||
if (legacy_metadata_exists()) {
|
||||
if (!any_granted().get0()) {
|
||||
@@ -179,7 +178,7 @@ future<> default_authorizer::start() {
|
||||
|
||||
future<> default_authorizer::stop() {
|
||||
_as.request_abort();
|
||||
return _finished.handle_exception_type([](const sleep_aborted&) {}).handle_exception_type([](const abort_requested_exception&) {});
|
||||
return _finished.handle_exception_type([](const sleep_aborted&) {});
|
||||
}
|
||||
|
||||
future<permission_set>
|
||||
@@ -188,7 +187,8 @@ default_authorizer::authorize(const role_or_anonymous& maybe_role, const resourc
|
||||
return make_ready_future<permission_set>(permissions::NONE);
|
||||
}
|
||||
|
||||
static const sstring query = format("SELECT {} FROM {}.{} WHERE {} = ? AND {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"SELECT %s FROM %s.%s WHERE %s = ? AND %s = ?",
|
||||
PERMISSIONS_NAME,
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
@@ -210,12 +210,13 @@ default_authorizer::authorize(const role_or_anonymous& maybe_role, const resourc
|
||||
|
||||
future<>
|
||||
default_authorizer::modify(
|
||||
std::string_view role_name,
|
||||
stdx::string_view role_name,
|
||||
permission_set set,
|
||||
const resource& resource,
|
||||
std::string_view op) const {
|
||||
stdx::string_view op) const {
|
||||
return do_with(
|
||||
format("UPDATE {}.{} SET {} = {} {} ? WHERE {} = ? AND {} = ?",
|
||||
sprint(
|
||||
"UPDATE %s.%s SET %s = %s %s ? WHERE %s = ? AND %s = ?",
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
PERMISSIONS_NAME,
|
||||
@@ -233,16 +234,17 @@ default_authorizer::modify(
|
||||
}
|
||||
|
||||
|
||||
future<> default_authorizer::grant(std::string_view role_name, permission_set set, const resource& resource) const {
|
||||
future<> default_authorizer::grant(stdx::string_view role_name, permission_set set, const resource& resource) const {
|
||||
return modify(role_name, std::move(set), resource, "+");
|
||||
}
|
||||
|
||||
future<> default_authorizer::revoke(std::string_view role_name, permission_set set, const resource& resource) const {
|
||||
future<> default_authorizer::revoke(stdx::string_view role_name, permission_set set, const resource& resource) const {
|
||||
return modify(role_name, std::move(set), resource, "-");
|
||||
}
|
||||
|
||||
future<std::vector<permission_details>> default_authorizer::list_all() const {
|
||||
static const sstring query = format("SELECT {}, {}, {} FROM {}.{}",
|
||||
static const sstring query = sprint(
|
||||
"SELECT %s, %s, %s FROM %s.%s",
|
||||
ROLE_NAME,
|
||||
RESOURCE_NAME,
|
||||
PERMISSIONS_NAME,
|
||||
@@ -270,8 +272,9 @@ future<std::vector<permission_details>> default_authorizer::list_all() const {
|
||||
});
|
||||
}
|
||||
|
||||
future<> default_authorizer::revoke_all(std::string_view role_name) const {
|
||||
static const sstring query = format("DELETE FROM {}.{} WHERE {} = ?",
|
||||
future<> default_authorizer::revoke_all(stdx::string_view role_name) const {
|
||||
static const sstring query = sprint(
|
||||
"DELETE FROM %s.%s WHERE %s = ?",
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
ROLE_NAME);
|
||||
@@ -290,7 +293,8 @@ future<> default_authorizer::revoke_all(std::string_view role_name) const {
|
||||
}
|
||||
|
||||
future<> default_authorizer::revoke_all(const resource& resource) const {
|
||||
static const sstring query = format("SELECT {} FROM {}.{} WHERE {} = ? ALLOW FILTERING",
|
||||
static const sstring query = sprint(
|
||||
"SELECT %s FROM %s.%s WHERE %s = ? ALLOW FILTERING",
|
||||
ROLE_NAME,
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
@@ -307,7 +311,8 @@ future<> default_authorizer::revoke_all(const resource& resource) const {
|
||||
res->begin(),
|
||||
res->end(),
|
||||
[this, res, resource](const cql3::untyped_result_set::row& r) {
|
||||
static const sstring query = format("DELETE FROM {}.{} WHERE {} = ? AND {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"DELETE FROM %s.%s WHERE %s = ? AND %s = ?",
|
||||
meta::AUTH_KS,
|
||||
PERMISSIONS_CF,
|
||||
ROLE_NAME,
|
||||
|
||||
@@ -77,13 +77,13 @@ public:
|
||||
|
||||
virtual future<permission_set> authorize(const role_or_anonymous&, const resource&) const override;
|
||||
|
||||
virtual future<> grant(std::string_view, permission_set, const resource&) const override;
|
||||
virtual future<> grant(stdx::string_view, permission_set, const resource&) const override;
|
||||
|
||||
virtual future<> revoke( std::string_view, permission_set, const resource&) const override;
|
||||
virtual future<> revoke( stdx::string_view, permission_set, const resource&) const override;
|
||||
|
||||
virtual future<std::vector<permission_details>> list_all() const override;
|
||||
|
||||
virtual future<> revoke_all(std::string_view) const override;
|
||||
virtual future<> revoke_all(stdx::string_view) const override;
|
||||
|
||||
virtual future<> revoke_all(const resource&) const override;
|
||||
|
||||
@@ -96,7 +96,7 @@ private:
|
||||
|
||||
future<> migrate_legacy_metadata() const;
|
||||
|
||||
future<> modify(std::string_view, permission_set, const resource&, std::string_view) const;
|
||||
future<> modify(stdx::string_view, permission_set, const resource&, stdx::string_view) const;
|
||||
};
|
||||
|
||||
} /* namespace auth */
|
||||
|
||||
@@ -41,24 +41,25 @@
|
||||
|
||||
#include "auth/password_authenticator.hh"
|
||||
|
||||
extern "C" {
|
||||
#include <crypt.h>
|
||||
#include <unistd.h>
|
||||
}
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
#include <optional>
|
||||
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
#include <seastar/core/reactor.hh>
|
||||
|
||||
#include "auth/authenticated_user.hh"
|
||||
#include "auth/common.hh"
|
||||
#include "auth/passwords.hh"
|
||||
#include "auth/roles-metadata.hh"
|
||||
#include "cql3/untyped_result_set.hh"
|
||||
#include "log.hh"
|
||||
#include "service/migration_manager.hh"
|
||||
#include "utils/class_registrator.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -81,8 +82,6 @@ static const class_registrator<
|
||||
cql3::query_processor&,
|
||||
::service::migration_manager&> password_auth_reg("org.apache.cassandra.auth.PasswordAuthenticator");
|
||||
|
||||
static thread_local auto rng_for_salt = std::default_random_engine(std::random_device{}());
|
||||
|
||||
password_authenticator::~password_authenticator() {
|
||||
}
|
||||
|
||||
@@ -92,11 +91,84 @@ password_authenticator::password_authenticator(cql3::query_processor& qp, ::serv
|
||||
, _stopped(make_ready_future<>()) {
|
||||
}
|
||||
|
||||
// TODO: blowfish
|
||||
// Origin uses Java bcrypt library, i.e. blowfish salt
|
||||
// generation and hashing, which is arguably a "better"
|
||||
// password hash than sha/md5 versions usually available in
|
||||
// crypt_r. Otoh, glibc 2.7+ uses a modified sha512 algo
|
||||
// which should be the same order of safe, so the only
|
||||
// real issue should be salted hash compatibility with
|
||||
// origin if importing system tables from there.
|
||||
//
|
||||
// Since bcrypt/blowfish is _not_ (afaict) not available
|
||||
// as a dev package/lib on most linux distros, we'd have to
|
||||
// copy and compile for example OWL crypto
|
||||
// (http://cvsweb.openwall.com/cgi/cvsweb.cgi/Owl/packages/glibc/crypt_blowfish/)
|
||||
// to be fully bit-compatible.
|
||||
//
|
||||
// Until we decide this is needed, let's just use crypt_r,
|
||||
// and some old-fashioned random salt generation.
|
||||
|
||||
static constexpr size_t rand_bytes = 16;
|
||||
static thread_local crypt_data tlcrypt = { 0, };
|
||||
|
||||
static sstring hashpw(const sstring& pass, const sstring& salt) {
|
||||
auto res = crypt_r(pass.c_str(), salt.c_str(), &tlcrypt);
|
||||
if (res == nullptr) {
|
||||
throw std::system_error(errno, std::system_category());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool checkpw(const sstring& pass, const sstring& salted_hash) {
|
||||
auto tmp = hashpw(pass, salted_hash);
|
||||
return tmp == salted_hash;
|
||||
}
|
||||
|
||||
static sstring gensalt() {
|
||||
static sstring prefix;
|
||||
|
||||
std::random_device rd;
|
||||
std::default_random_engine e1(rd());
|
||||
std::uniform_int_distribution<char> dist;
|
||||
|
||||
sstring valid_salt = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./";
|
||||
sstring input(rand_bytes, 0);
|
||||
|
||||
for (char&c : input) {
|
||||
c = valid_salt[dist(e1) % valid_salt.size()];
|
||||
}
|
||||
|
||||
sstring salt;
|
||||
|
||||
if (!prefix.empty()) {
|
||||
return prefix + input;
|
||||
}
|
||||
|
||||
// Try in order:
|
||||
// blowfish 2011 fix, blowfish, sha512, sha256, md5
|
||||
for (sstring pfx : { "$2y$", "$2a$", "$6$", "$5$", "$1$" }) {
|
||||
salt = pfx + input;
|
||||
const char* e = crypt_r("fisk", salt.c_str(), &tlcrypt);
|
||||
|
||||
if (e && (e[0] != '*')) {
|
||||
prefix = pfx;
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Could not initialize hashing algorithm");
|
||||
}
|
||||
|
||||
static sstring hashpw(const sstring& pass) {
|
||||
return hashpw(pass, gensalt());
|
||||
}
|
||||
|
||||
static bool has_salted_hash(const cql3::untyped_result_set_row& row) {
|
||||
return !row.get_or<sstring>(SALTED_HASH, "").empty();
|
||||
}
|
||||
|
||||
static const sstring update_row_query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
static const sstring update_row_query = sprint(
|
||||
"UPDATE %s SET %s = ? WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
SALTED_HASH,
|
||||
meta::roles_table::role_col_name);
|
||||
@@ -104,12 +176,12 @@ static const sstring update_row_query = format("UPDATE {} SET {} = ? WHERE {} =
|
||||
static const sstring legacy_table_name{"credentials"};
|
||||
|
||||
bool password_authenticator::legacy_metadata_exists() const {
|
||||
return _qp.db().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
return _qp.db().local().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
}
|
||||
|
||||
future<> password_authenticator::migrate_legacy_metadata() const {
|
||||
plogger.info("Starting migration of legacy authentication metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
static const sstring query = sprint("SELECT * FROM %s.%s", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.process(
|
||||
query,
|
||||
@@ -140,7 +212,7 @@ future<> password_authenticator::create_default_if_missing() const {
|
||||
update_row_query,
|
||||
db::consistency_level::QUORUM,
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(DEFAULT_USER_PASSWORD, rng_for_salt), DEFAULT_USER_NAME}).then([](auto&&) {
|
||||
{hashpw(DEFAULT_USER_PASSWORD), DEFAULT_USER_NAME}).then([](auto&&) {
|
||||
plogger.info("Created default superuser authentication record.");
|
||||
});
|
||||
}
|
||||
@@ -151,6 +223,8 @@ future<> password_authenticator::create_default_if_missing() const {
|
||||
|
||||
future<> password_authenticator::start() {
|
||||
return once_among_shards([this] {
|
||||
gensalt(); // do this once to determine usable hashing
|
||||
|
||||
auto f = create_metadata_table_if_missing(
|
||||
meta::roles_table::name,
|
||||
_qp,
|
||||
@@ -159,7 +233,7 @@ future<> password_authenticator::start() {
|
||||
|
||||
_stopped = do_after_system_ready(_as, [this] {
|
||||
return async([this] {
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db(), _as).get0();
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db().local()).get0();
|
||||
|
||||
if (any_nondefault_role_row_satisfies(_qp, &has_salted_hash).get0()) {
|
||||
if (legacy_metadata_exists()) {
|
||||
@@ -184,10 +258,10 @@ future<> password_authenticator::start() {
|
||||
|
||||
future<> password_authenticator::stop() {
|
||||
_as.request_abort();
|
||||
return _stopped.handle_exception_type([] (const sleep_aborted&) { }).handle_exception_type([](const abort_requested_exception&) {});
|
||||
return _stopped.handle_exception_type([] (const sleep_aborted&) { });
|
||||
}
|
||||
|
||||
db::consistency_level password_authenticator::consistency_for_user(std::string_view role_name) {
|
||||
db::consistency_level password_authenticator::consistency_for_user(stdx::string_view role_name) {
|
||||
if (role_name == DEFAULT_USER_NAME) {
|
||||
return db::consistency_level::QUORUM;
|
||||
}
|
||||
@@ -213,10 +287,10 @@ authentication_option_set password_authenticator::alterable_options() const {
|
||||
future<authenticated_user> password_authenticator::authenticate(
|
||||
const credentials_map& credentials) const {
|
||||
if (!credentials.count(USERNAME_KEY)) {
|
||||
throw exceptions::authentication_exception(format("Required key '{}' is missing", USERNAME_KEY));
|
||||
throw exceptions::authentication_exception(sprint("Required key '%s' is missing", USERNAME_KEY));
|
||||
}
|
||||
if (!credentials.count(PASSWORD_KEY)) {
|
||||
throw exceptions::authentication_exception(format("Required key '{}' is missing", PASSWORD_KEY));
|
||||
throw exceptions::authentication_exception(sprint("Required key '%s' is missing", PASSWORD_KEY));
|
||||
}
|
||||
|
||||
auto& username = credentials.at(USERNAME_KEY);
|
||||
@@ -228,7 +302,8 @@ future<authenticated_user> password_authenticator::authenticate(
|
||||
// Rely on query processing caching statements instead, and lets assume
|
||||
// that a map lookup string->statement is not gonna kill us much.
|
||||
return futurize_apply([this, username, password] {
|
||||
static const sstring query = format("SELECT {} FROM {} WHERE {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"SELECT %s FROM %s WHERE %s = ?",
|
||||
SALTED_HASH,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
@@ -242,11 +317,11 @@ future<authenticated_user> password_authenticator::authenticate(
|
||||
}).then_wrapped([=](future<::shared_ptr<cql3::untyped_result_set>> f) {
|
||||
try {
|
||||
auto res = f.get0();
|
||||
auto salted_hash = std::optional<sstring>();
|
||||
auto salted_hash = std::experimental::optional<sstring>();
|
||||
if (!res->empty()) {
|
||||
salted_hash = res->one().get_opt<sstring>(SALTED_HASH);
|
||||
}
|
||||
if (!salted_hash || !passwords::check(password, *salted_hash)) {
|
||||
if (!salted_hash || !checkpw(password, *salted_hash)) {
|
||||
throw exceptions::authentication_exception("Username and/or password are incorrect");
|
||||
}
|
||||
return make_ready_future<authenticated_user>(username);
|
||||
@@ -254,15 +329,13 @@ future<authenticated_user> password_authenticator::authenticate(
|
||||
std::throw_with_nested(exceptions::authentication_exception("Could not verify password"));
|
||||
} catch (exceptions::request_execution_exception& e) {
|
||||
std::throw_with_nested(exceptions::authentication_exception(e.what()));
|
||||
} catch (exceptions::authentication_exception& e) {
|
||||
std::throw_with_nested(e);
|
||||
} catch (...) {
|
||||
std::throw_with_nested(exceptions::authentication_exception("authentication failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
future<> password_authenticator::create(std::string_view role_name, const authentication_options& options) const {
|
||||
future<> password_authenticator::create(stdx::string_view role_name, const authentication_options& options) const {
|
||||
if (!options.password) {
|
||||
return make_ready_future<>();
|
||||
}
|
||||
@@ -271,15 +344,16 @@ future<> password_authenticator::create(std::string_view role_name, const authen
|
||||
update_row_query,
|
||||
consistency_for_user(role_name),
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result();
|
||||
{hashpw(*options.password), sstring(role_name)}).discard_result();
|
||||
}
|
||||
|
||||
future<> password_authenticator::alter(std::string_view role_name, const authentication_options& options) const {
|
||||
future<> password_authenticator::alter(stdx::string_view role_name, const authentication_options& options) const {
|
||||
if (!options.password) {
|
||||
return make_ready_future<>();
|
||||
}
|
||||
|
||||
static const sstring query = format("UPDATE {} SET {} = ? WHERE {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"UPDATE %s SET %s = ? WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
SALTED_HASH,
|
||||
meta::roles_table::role_col_name);
|
||||
@@ -288,11 +362,12 @@ future<> password_authenticator::alter(std::string_view role_name, const authent
|
||||
query,
|
||||
consistency_for_user(role_name),
|
||||
internal_distributed_timeout_config(),
|
||||
{passwords::hash(*options.password, rng_for_salt), sstring(role_name)}).discard_result();
|
||||
{hashpw(*options.password), sstring(role_name)}).discard_result();
|
||||
}
|
||||
|
||||
future<> password_authenticator::drop(std::string_view name) const {
|
||||
static const sstring query = format("DELETE {} FROM {} WHERE {} = ?",
|
||||
future<> password_authenticator::drop(stdx::string_view name) const {
|
||||
static const sstring query = sprint(
|
||||
"DELETE %s FROM %s WHERE %s = ?",
|
||||
SALTED_HASH,
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
@@ -303,7 +378,7 @@ future<> password_authenticator::drop(std::string_view name) const {
|
||||
{sstring(name)}).discard_result();
|
||||
}
|
||||
|
||||
future<custom_options> password_authenticator::query_custom_options(std::string_view role_name) const {
|
||||
future<custom_options> password_authenticator::query_custom_options(stdx::string_view role_name) const {
|
||||
return make_ready_future<custom_options>();
|
||||
}
|
||||
|
||||
@@ -312,13 +387,75 @@ const resource_set& password_authenticator::protected_resources() const {
|
||||
return resources;
|
||||
}
|
||||
|
||||
::shared_ptr<sasl_challenge> password_authenticator::new_sasl_challenge() const {
|
||||
return ::make_shared<plain_sasl_challenge>([this](std::string_view username, std::string_view password) {
|
||||
credentials_map credentials{};
|
||||
credentials[USERNAME_KEY] = sstring(username);
|
||||
credentials[PASSWORD_KEY] = sstring(password);
|
||||
return this->authenticate(credentials);
|
||||
});
|
||||
::shared_ptr<authenticator::sasl_challenge> password_authenticator::new_sasl_challenge() const {
|
||||
class plain_text_password_challenge : public sasl_challenge {
|
||||
const password_authenticator& _self;
|
||||
|
||||
public:
|
||||
plain_text_password_challenge(const password_authenticator& self) : _self(self) {
|
||||
}
|
||||
|
||||
/**
|
||||
* SASL PLAIN mechanism specifies that credentials are encoded in a
|
||||
* sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL).
|
||||
* The form is : {code}authzId<NUL>authnId<NUL>password<NUL>{code}
|
||||
* authzId is optional, and in fact we don't care about it here as we'll
|
||||
* set the authzId to match the authnId (that is, there is no concept of
|
||||
* a user being authorized to act on behalf of another).
|
||||
*
|
||||
* @param bytes encoded credentials string sent by the client
|
||||
* @return map containing the username/password pairs in the form an IAuthenticator
|
||||
* would expect
|
||||
* @throws javax.security.sasl.SaslException
|
||||
*/
|
||||
bytes evaluate_response(bytes_view client_response) override {
|
||||
plogger.debug("Decoding credentials from client token");
|
||||
|
||||
sstring username, password;
|
||||
|
||||
auto b = client_response.crbegin();
|
||||
auto e = client_response.crend();
|
||||
auto i = b;
|
||||
|
||||
while (i != e) {
|
||||
if (*i == 0) {
|
||||
sstring tmp(i.base(), b.base());
|
||||
if (password.empty()) {
|
||||
password = std::move(tmp);
|
||||
} else if (username.empty()) {
|
||||
username = std::move(tmp);
|
||||
}
|
||||
b = ++i;
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
if (username.empty()) {
|
||||
throw exceptions::authentication_exception("Authentication ID must not be null");
|
||||
}
|
||||
if (password.empty()) {
|
||||
throw exceptions::authentication_exception("Password must not be null");
|
||||
}
|
||||
|
||||
_credentials[USERNAME_KEY] = std::move(username);
|
||||
_credentials[PASSWORD_KEY] = std::move(password);
|
||||
_complete = true;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool is_complete() const override {
|
||||
return _complete;
|
||||
}
|
||||
|
||||
future<authenticated_user> get_authenticated_user() const override {
|
||||
return _self.authenticate(_credentials);
|
||||
}
|
||||
private:
|
||||
credentials_map _credentials;
|
||||
bool _complete = false;
|
||||
};
|
||||
return ::make_shared<plain_text_password_challenge>(*this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class password_authenticator : public authenticator {
|
||||
seastar::abort_source _as;
|
||||
|
||||
public:
|
||||
static db::consistency_level consistency_for_user(std::string_view role_name);
|
||||
static db::consistency_level consistency_for_user(stdx::string_view role_name);
|
||||
|
||||
password_authenticator(cql3::query_processor&, ::service::migration_manager&);
|
||||
|
||||
@@ -81,13 +81,13 @@ public:
|
||||
|
||||
virtual future<authenticated_user> authenticate(const credentials_map& credentials) const override;
|
||||
|
||||
virtual future<> create(std::string_view role_name, const authentication_options& options) const override;
|
||||
virtual future<> create(stdx::string_view role_name, const authentication_options& options) const override;
|
||||
|
||||
virtual future<> alter(std::string_view role_name, const authentication_options& options) const override;
|
||||
virtual future<> alter(stdx::string_view role_name, const authentication_options& options) const override;
|
||||
|
||||
virtual future<> drop(std::string_view role_name) const override;
|
||||
virtual future<> drop(stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<custom_options> query_custom_options(std::string_view role_name) const override;
|
||||
virtual future<custom_options> query_custom_options(stdx::string_view role_name) const override;
|
||||
|
||||
virtual const resource_set& protected_resources() const override;
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "auth/passwords.hh"
|
||||
|
||||
#include <cerrno>
|
||||
#include <optional>
|
||||
|
||||
extern "C" {
|
||||
#include <crypt.h>
|
||||
#include <unistd.h>
|
||||
}
|
||||
|
||||
namespace auth::passwords {
|
||||
|
||||
static thread_local crypt_data tlcrypt = { 0, };
|
||||
|
||||
namespace detail {
|
||||
|
||||
scheme identify_best_supported_scheme() {
|
||||
const auto all_schemes = { scheme::bcrypt_y, scheme::bcrypt_a, scheme::sha_512, scheme::sha_256, scheme::md5 };
|
||||
// "Random", for testing schemes.
|
||||
const sstring random_part_of_salt = "aaaabbbbccccdddd";
|
||||
|
||||
for (scheme c : all_schemes) {
|
||||
const sstring salt = sstring(prefix_for_scheme(c)) + random_part_of_salt;
|
||||
const char* e = crypt_r("fisk", salt.c_str(), &tlcrypt);
|
||||
|
||||
if (e && (e[0] != '*')) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
throw no_supported_schemes();
|
||||
}
|
||||
|
||||
sstring hash_with_salt(const sstring& pass, const sstring& salt) {
|
||||
auto res = crypt_r(pass.c_str(), salt.c_str(), &tlcrypt);
|
||||
if (!res || (res[0] == '*')) {
|
||||
throw std::system_error(errno, std::system_category());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const char* prefix_for_scheme(scheme c) noexcept {
|
||||
switch (c) {
|
||||
case scheme::bcrypt_y: return "$2y$";
|
||||
case scheme::bcrypt_a: return "$2a$";
|
||||
case scheme::sha_512: return "$6$";
|
||||
case scheme::sha_256: return "$5$";
|
||||
case scheme::md5: return "$1$";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
no_supported_schemes::no_supported_schemes()
|
||||
: std::runtime_error("No allowed hashing schemes are supported on this system") {
|
||||
}
|
||||
|
||||
bool check(const sstring& pass, const sstring& salted_hash) {
|
||||
return detail::hash_with_salt(pass, salted_hash) == salted_hash;
|
||||
}
|
||||
|
||||
} // namespace auth::paswords
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace auth::passwords {
|
||||
|
||||
class no_supported_schemes : public std::runtime_error {
|
||||
public:
|
||||
no_supported_schemes();
|
||||
};
|
||||
|
||||
///
|
||||
/// Apache Cassandra uses a library to provide the bcrypt scheme. Many Linux implementations do not support bcrypt, so
|
||||
/// we support alternatives. The cost is loss of direct compatibility with Apache Cassandra system tables.
|
||||
///
|
||||
enum class scheme {
|
||||
bcrypt_y,
|
||||
bcrypt_a,
|
||||
sha_512,
|
||||
sha_256,
|
||||
md5
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename RandomNumberEngine>
|
||||
sstring generate_random_salt_bytes(RandomNumberEngine& g) {
|
||||
static const sstring valid_bytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./";
|
||||
static constexpr std::size_t num_bytes = 16;
|
||||
std::uniform_int_distribution<std::size_t> dist(0, valid_bytes.size() - 1);
|
||||
sstring result(num_bytes, 0);
|
||||
|
||||
for (char& c : result) {
|
||||
c = valid_bytes[dist(g)];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
///
|
||||
/// Test each allowed hashing scheme and report the best supported one on the current system.
|
||||
///
|
||||
/// \throws \ref no_supported_schemes when none of the known schemes is supported.
|
||||
///
|
||||
scheme identify_best_supported_scheme();
|
||||
|
||||
const char* prefix_for_scheme(scheme) noexcept;
|
||||
|
||||
///
|
||||
/// Generate a implementation-specific salt string for hashing passwords.
|
||||
///
|
||||
/// The `RandomNumberEngine` is used to generate the string, which is an implementation-specific length.
|
||||
///
|
||||
/// \throws \ref no_supported_schemes when no known hashing schemes are supported on the system.
|
||||
///
|
||||
template <typename RandomNumberEngine>
|
||||
sstring generate_salt(RandomNumberEngine& g) {
|
||||
static const scheme scheme = identify_best_supported_scheme();
|
||||
static const sstring prefix = sstring(prefix_for_scheme(scheme));
|
||||
return prefix + generate_random_salt_bytes(g);
|
||||
}
|
||||
|
||||
///
|
||||
/// Hash a password combined with an implementation-specific salt string.
|
||||
///
|
||||
/// \throws \ref std::system_error when an unexpected implementation-specific error occurs.
|
||||
///
|
||||
sstring hash_with_salt(const sstring& pass, const sstring& salt);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
///
|
||||
/// Run a one-way hashing function on cleartext to produce encrypted text.
|
||||
///
|
||||
/// Prior to applying the hashing function, random salt is amended to the cleartext. The random salt bytes are generated
|
||||
/// according to the random number engine `g`.
|
||||
///
|
||||
/// The result is the encrypted cyphertext, and also the salt used but in a implementation-specific format.
|
||||
///
|
||||
/// \throws \ref std::system_error when the implementation-specific implementation fails to hash the cleartext.
|
||||
///
|
||||
template <typename RandomNumberEngine>
|
||||
sstring hash(const sstring& pass, RandomNumberEngine& g) {
|
||||
return detail::hash_with_salt(pass, detail::generate_salt(g));
|
||||
}
|
||||
|
||||
///
|
||||
/// Check that cleartext matches previously hashed cleartext with salt.
|
||||
///
|
||||
/// \ref salted_hash is the result of invoking \ref hash, which is the implementation-specific combination of the hashed
|
||||
/// password and the salt that was generated for it.
|
||||
///
|
||||
/// \returns `true` if the cleartext matches the salted hash.
|
||||
///
|
||||
/// \throws \ref std::system_error when an unexpected implementation-specific error occurs.
|
||||
///
|
||||
bool check(const sstring& pass, const sstring& salted_hash);
|
||||
|
||||
} // namespace auth::passwords
|
||||
@@ -24,9 +24,19 @@
|
||||
#include "auth/authorizer.hh"
|
||||
#include "auth/common.hh"
|
||||
#include "auth/service.hh"
|
||||
#include "db/config.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
permissions_cache_config permissions_cache_config::from_db_config(const db::config& dc) {
|
||||
permissions_cache_config c;
|
||||
c.max_entries = dc.permissions_cache_max_entries();
|
||||
c.validity_period = std::chrono::milliseconds(dc.permissions_validity_in_ms());
|
||||
c.update_period = std::chrono::milliseconds(dc.permissions_update_interval_in_ms());
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
permissions_cache::permissions_cache(const permissions_cache_config& c, service& ser, logging::logger& log)
|
||||
: _cache(c.max_entries, c.validity_period, c.update_period, log, [&ser, &log](const key_type& k) {
|
||||
log.debug("Refreshing permissions for {}", k.first);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "auth/resource.hh"
|
||||
#include "auth/role_or_anonymous.hh"
|
||||
#include "log.hh"
|
||||
#include "stdx.hh"
|
||||
#include "utils/hash.hh"
|
||||
#include "utils/loading_cache.hh"
|
||||
|
||||
@@ -58,6 +59,8 @@ namespace auth {
|
||||
class service;
|
||||
|
||||
struct permissions_cache_config final {
|
||||
static permissions_cache_config from_db_config(const db::config&);
|
||||
|
||||
std::size_t max_entries;
|
||||
std::chrono::milliseconds validity_period;
|
||||
std::chrono::milliseconds update_period;
|
||||
|
||||
@@ -61,7 +61,7 @@ std::ostream& operator<<(std::ostream& os, resource_kind kind) {
|
||||
return os;
|
||||
}
|
||||
|
||||
static const std::unordered_map<resource_kind, std::string_view> roots{
|
||||
static const std::unordered_map<resource_kind, stdx::string_view> roots{
|
||||
{resource_kind::data, "data"},
|
||||
{resource_kind::role, "roles"}};
|
||||
|
||||
@@ -101,25 +101,24 @@ static permission_set applicable_permissions(const role_resource_view& rv) {
|
||||
permission::DESCRIBE>();
|
||||
}
|
||||
|
||||
resource::resource(resource_kind kind) : _kind(kind) {
|
||||
_parts.emplace_back(roots.at(kind));
|
||||
resource::resource(resource_kind kind) : _kind(kind), _parts{sstring(roots.at(kind))} {
|
||||
}
|
||||
|
||||
resource::resource(resource_kind kind, utils::small_vector<sstring, 3> parts) : resource(kind) {
|
||||
resource::resource(resource_kind kind, std::vector<sstring> parts) : resource(kind) {
|
||||
_parts.reserve(parts.size() + 1);
|
||||
_parts.insert(_parts.end(), std::make_move_iterator(parts.begin()), std::make_move_iterator(parts.end()));
|
||||
}
|
||||
|
||||
resource::resource(data_resource_t, std::string_view keyspace) : resource(resource_kind::data) {
|
||||
_parts.emplace_back(keyspace);
|
||||
resource::resource(data_resource_t, stdx::string_view keyspace)
|
||||
: resource(resource_kind::data, std::vector<sstring>{sstring(keyspace)}) {
|
||||
}
|
||||
|
||||
resource::resource(data_resource_t, std::string_view keyspace, std::string_view table) : resource(resource_kind::data) {
|
||||
_parts.emplace_back(keyspace);
|
||||
_parts.emplace_back(table);
|
||||
resource::resource(data_resource_t, stdx::string_view keyspace, stdx::string_view table)
|
||||
: resource(resource_kind::data, std::vector<sstring>{sstring(keyspace), sstring(table)}) {
|
||||
}
|
||||
|
||||
resource::resource(role_resource_t, std::string_view role) : resource(resource_kind::role) {
|
||||
_parts.emplace_back(role);
|
||||
resource::resource(role_resource_t, stdx::string_view role)
|
||||
: resource(resource_kind::role, std::vector<sstring>{sstring(role)}) {
|
||||
}
|
||||
|
||||
sstring resource::name() const {
|
||||
@@ -174,7 +173,7 @@ data_resource_view::data_resource_view(const resource& r) : _resource(r) {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string_view> data_resource_view::keyspace() const {
|
||||
std::optional<stdx::string_view> data_resource_view::keyspace() const {
|
||||
if (_resource._parts.size() == 1) {
|
||||
return {};
|
||||
}
|
||||
@@ -182,7 +181,7 @@ std::optional<std::string_view> data_resource_view::keyspace() const {
|
||||
return _resource._parts[1];
|
||||
}
|
||||
|
||||
std::optional<std::string_view> data_resource_view::table() const {
|
||||
std::optional<stdx::string_view> data_resource_view::table() const {
|
||||
if (_resource._parts.size() <= 2) {
|
||||
return {};
|
||||
}
|
||||
@@ -211,7 +210,7 @@ role_resource_view::role_resource_view(const resource& r) : _resource(r) {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string_view> role_resource_view::role() const {
|
||||
std::optional<stdx::string_view> role_resource_view::role() const {
|
||||
if (_resource._parts.size() == 1) {
|
||||
return {};
|
||||
}
|
||||
@@ -231,9 +230,9 @@ std::ostream& operator<<(std::ostream& os, const role_resource_view& v) {
|
||||
return os;
|
||||
}
|
||||
|
||||
resource parse_resource(std::string_view name) {
|
||||
static const std::unordered_map<std::string_view, resource_kind> reverse_roots = [] {
|
||||
std::unordered_map<std::string_view, resource_kind> result;
|
||||
resource parse_resource(stdx::string_view name) {
|
||||
static const std::unordered_map<stdx::string_view, resource_kind> reverse_roots = [] {
|
||||
std::unordered_map<stdx::string_view, resource_kind> result;
|
||||
|
||||
for (const auto& pair : roots) {
|
||||
result.emplace(pair.second, pair.first);
|
||||
@@ -242,7 +241,7 @@ resource parse_resource(std::string_view name) {
|
||||
return result;
|
||||
}();
|
||||
|
||||
utils::small_vector<sstring, 3> parts;
|
||||
std::vector<sstring> parts;
|
||||
boost::split(parts, name, [](char ch) { return ch == '/'; });
|
||||
|
||||
if (parts.empty()) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -54,15 +54,15 @@
|
||||
|
||||
#include "auth/permission.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
#include "utils/hash.hh"
|
||||
#include "utils/small_vector.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
class invalid_resource_name : public std::invalid_argument {
|
||||
public:
|
||||
explicit invalid_resource_name(std::string_view name)
|
||||
: std::invalid_argument(format("The resource name '{}' is invalid.", name)) {
|
||||
explicit invalid_resource_name(stdx::string_view name)
|
||||
: std::invalid_argument(sprint("The resource name '%s' is invalid.", name)) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,16 +98,16 @@ struct role_resource_t final {};
|
||||
class resource final {
|
||||
resource_kind _kind;
|
||||
|
||||
utils::small_vector<sstring, 3> _parts;
|
||||
std::vector<sstring> _parts;
|
||||
|
||||
public:
|
||||
///
|
||||
/// A root resource of a particular kind.
|
||||
///
|
||||
explicit resource(resource_kind);
|
||||
resource(data_resource_t, std::string_view keyspace);
|
||||
resource(data_resource_t, std::string_view keyspace, std::string_view table);
|
||||
resource(role_resource_t, std::string_view role);
|
||||
resource(data_resource_t, stdx::string_view keyspace);
|
||||
resource(data_resource_t, stdx::string_view keyspace, stdx::string_view table);
|
||||
resource(role_resource_t, stdx::string_view role);
|
||||
|
||||
resource_kind kind() const noexcept {
|
||||
return _kind;
|
||||
@@ -123,7 +123,7 @@ public:
|
||||
permission_set applicable_permissions() const;
|
||||
|
||||
private:
|
||||
resource(resource_kind, utils::small_vector<sstring, 3> parts);
|
||||
resource(resource_kind, std::vector<sstring> parts);
|
||||
|
||||
friend class std::hash<resource>;
|
||||
friend class data_resource_view;
|
||||
@@ -131,7 +131,7 @@ private:
|
||||
|
||||
friend bool operator<(const resource&, const resource&);
|
||||
friend bool operator==(const resource&, const resource&);
|
||||
friend resource parse_resource(std::string_view);
|
||||
friend resource parse_resource(stdx::string_view);
|
||||
};
|
||||
|
||||
bool operator<(const resource&, const resource&);
|
||||
@@ -150,7 +150,7 @@ class resource_kind_mismatch : public std::invalid_argument {
|
||||
public:
|
||||
explicit resource_kind_mismatch(resource_kind expected, resource_kind actual)
|
||||
: std::invalid_argument(
|
||||
format("This resource has kind '{}', but was expected to have kind '{}'.", actual, expected)) {
|
||||
sprint("This resource has kind '%s', but was expected to have kind '%s'.", actual, expected)) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,9 +166,9 @@ public:
|
||||
///
|
||||
explicit data_resource_view(const resource& r);
|
||||
|
||||
std::optional<std::string_view> keyspace() const;
|
||||
std::optional<stdx::string_view> keyspace() const;
|
||||
|
||||
std::optional<std::string_view> table() const;
|
||||
std::optional<stdx::string_view> table() const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream&, const data_resource_view&);
|
||||
@@ -187,7 +187,7 @@ public:
|
||||
///
|
||||
explicit role_resource_view(const resource&);
|
||||
|
||||
std::optional<std::string_view> role() const;
|
||||
std::optional<stdx::string_view> role() const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream&, const role_resource_view&);
|
||||
@@ -197,20 +197,20 @@ std::ostream& operator<<(std::ostream&, const role_resource_view&);
|
||||
///
|
||||
/// \throws \ref invalid_resource_name when the name is malformed.
|
||||
///
|
||||
resource parse_resource(std::string_view name);
|
||||
resource parse_resource(stdx::string_view name);
|
||||
|
||||
const resource& root_data_resource();
|
||||
|
||||
inline resource make_data_resource(std::string_view keyspace) {
|
||||
inline resource make_data_resource(stdx::string_view keyspace) {
|
||||
return resource(data_resource_t{}, keyspace);
|
||||
}
|
||||
inline resource make_data_resource(std::string_view keyspace, std::string_view table) {
|
||||
inline resource make_data_resource(stdx::string_view keyspace, stdx::string_view table) {
|
||||
return resource(data_resource_t{}, keyspace, table);
|
||||
}
|
||||
|
||||
const resource& root_role_resource();
|
||||
|
||||
inline resource make_role_resource(std::string_view role) {
|
||||
inline resource make_role_resource(stdx::string_view role) {
|
||||
return resource(role_resource_t{}, role);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#include "auth/resource.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -59,31 +60,31 @@ public:
|
||||
|
||||
class role_already_exists : public roles_argument_exception {
|
||||
public:
|
||||
explicit role_already_exists(std::string_view role_name)
|
||||
: roles_argument_exception(format("Role {} already exists.", role_name)) {
|
||||
explicit role_already_exists(stdx::string_view role_name)
|
||||
: roles_argument_exception(sprint("Role %s already exists.", role_name)) {
|
||||
}
|
||||
};
|
||||
|
||||
class nonexistant_role : public roles_argument_exception {
|
||||
public:
|
||||
explicit nonexistant_role(std::string_view role_name)
|
||||
: roles_argument_exception(format("Role {} doesn't exist.", role_name)) {
|
||||
explicit nonexistant_role(stdx::string_view role_name)
|
||||
: roles_argument_exception(sprint("Role %s doesn't exist.", role_name)) {
|
||||
}
|
||||
};
|
||||
|
||||
class role_already_included : public roles_argument_exception {
|
||||
public:
|
||||
role_already_included(std::string_view grantee_name, std::string_view role_name)
|
||||
role_already_included(stdx::string_view grantee_name, stdx::string_view role_name)
|
||||
: roles_argument_exception(
|
||||
format("{} already includes role {}.", grantee_name, role_name)) {
|
||||
sprint("%s already includes role %s.", grantee_name, role_name)) {
|
||||
}
|
||||
};
|
||||
|
||||
class revoke_ungranted_role : public roles_argument_exception {
|
||||
public:
|
||||
revoke_ungranted_role(std::string_view revokee_name, std::string_view role_name)
|
||||
revoke_ungranted_role(stdx::string_view revokee_name, stdx::string_view role_name)
|
||||
: roles_argument_exception(
|
||||
format("{} was not granted role {}, so it cannot be revoked.", revokee_name, role_name)) {
|
||||
sprint("%s was not granted role %s, so it cannot be revoked.", revokee_name, role_name)) {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,7 +104,7 @@ class role_manager {
|
||||
public:
|
||||
virtual ~role_manager() = default;
|
||||
|
||||
virtual std::string_view qualified_java_name() const noexcept = 0;
|
||||
virtual stdx::string_view qualified_java_name() const noexcept = 0;
|
||||
|
||||
virtual const resource_set& protected_resources() const = 0;
|
||||
|
||||
@@ -114,17 +115,17 @@ public:
|
||||
///
|
||||
/// \returns an exceptional future with \ref role_already_exists for a role that has previously been created.
|
||||
///
|
||||
virtual future<> create(std::string_view role_name, const role_config&) const = 0;
|
||||
virtual future<> create(stdx::string_view role_name, const role_config&) const = 0;
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
virtual future<> drop(std::string_view role_name) const = 0;
|
||||
virtual future<> drop(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
virtual future<> alter(std::string_view role_name, const role_config_update&) const = 0;
|
||||
virtual future<> alter(stdx::string_view role_name, const role_config_update&) const = 0;
|
||||
|
||||
///
|
||||
/// Grant `role_name` to `grantee_name`.
|
||||
@@ -134,7 +135,7 @@ public:
|
||||
/// \returns an exceptional future with \ref role_already_included if granting the role would be redundant, or
|
||||
/// create a cycle.
|
||||
///
|
||||
virtual future<> grant(std::string_view grantee_name, std::string_view role_name) const = 0;
|
||||
virtual future<> grant(stdx::string_view grantee_name, stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// Revoke `role_name` from `revokee_name`.
|
||||
@@ -143,26 +144,26 @@ public:
|
||||
///
|
||||
/// \returns an exceptional future with \ref revoke_ungranted_role if the role was not granted.
|
||||
///
|
||||
virtual future<> revoke(std::string_view revokee_name, std::string_view role_name) const = 0;
|
||||
virtual future<> revoke(stdx::string_view revokee_name, stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
virtual future<role_set> query_granted(std::string_view grantee, recursive_role_query) const = 0;
|
||||
virtual future<role_set> query_granted(stdx::string_view grantee, recursive_role_query) const = 0;
|
||||
|
||||
virtual future<role_set> query_all() const = 0;
|
||||
|
||||
virtual future<bool> exists(std::string_view role_name) const = 0;
|
||||
virtual future<bool> exists(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
virtual future<bool> is_superuser(std::string_view role_name) const = 0;
|
||||
virtual future<bool> is_superuser(stdx::string_view role_name) const = 0;
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
virtual future<bool> can_login(std::string_view role_name) const = 0;
|
||||
virtual future<bool> can_login(stdx::string_view role_name) const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <functional>
|
||||
#include <iosfwd>
|
||||
#include <optional>
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -37,7 +38,7 @@ public:
|
||||
std::optional<sstring> name{};
|
||||
|
||||
role_or_anonymous() = default;
|
||||
role_or_anonymous(std::string_view name) : name(name) {
|
||||
role_or_anonymous(stdx::string_view name) : name(name) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace meta {
|
||||
|
||||
namespace roles_table {
|
||||
|
||||
std::string_view creation_query() {
|
||||
stdx::string_view creation_query() {
|
||||
static const sstring instance = sprint(
|
||||
"CREATE TABLE %s ("
|
||||
" %s text PRIMARY KEY,"
|
||||
@@ -51,7 +51,7 @@ std::string_view creation_query() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
std::string_view qualified_name() noexcept {
|
||||
stdx::string_view qualified_name() noexcept {
|
||||
static const sstring instance = AUTH_KS + "." + sstring(name);
|
||||
return instance;
|
||||
}
|
||||
@@ -63,7 +63,8 @@ std::string_view qualified_name() noexcept {
|
||||
future<bool> default_role_row_satisfies(
|
||||
cql3::query_processor& qp,
|
||||
std::function<bool(const cql3::untyped_result_set_row&)> p) {
|
||||
static const sstring query = format("SELECT * FROM {} WHERE {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"SELECT * FROM %s WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
@@ -97,7 +98,7 @@ future<bool> default_role_row_satisfies(
|
||||
future<bool> any_nondefault_role_row_satisfies(
|
||||
cql3::query_processor& qp,
|
||||
std::function<bool(const cql3::untyped_result_set_row&)> p) {
|
||||
static const sstring query = format("SELECT * FROM {}", meta::roles_table::qualified_name());
|
||||
static const sstring query = sprint("SELECT * FROM %s", meta::roles_table::qualified_name());
|
||||
|
||||
return do_with(std::move(p), [&qp](const auto& p) {
|
||||
return qp.process(
|
||||
|
||||
@@ -21,12 +21,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <functional>
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace cql3 {
|
||||
class query_processor;
|
||||
@@ -39,13 +40,13 @@ namespace meta {
|
||||
|
||||
namespace roles_table {
|
||||
|
||||
std::string_view creation_query();
|
||||
stdx::string_view creation_query();
|
||||
|
||||
constexpr std::string_view name{"roles", 5};
|
||||
constexpr stdx::string_view name{"roles", 5};
|
||||
|
||||
std::string_view qualified_name() noexcept;
|
||||
stdx::string_view qualified_name() noexcept;
|
||||
|
||||
constexpr std::string_view role_col_name{"role", 4};
|
||||
constexpr stdx::string_view role_col_name{"role", 4};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 ScyllaDB
|
||||
*
|
||||
* Modified by ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "auth/sasl_challenge.hh"
|
||||
|
||||
#include "exceptions/exceptions.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
/**
|
||||
* SASL PLAIN mechanism specifies that credentials are encoded in a
|
||||
* sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL).
|
||||
* The form is : {code}authzId<NUL>authnId<NUL>password<NUL>{code}
|
||||
* authzId is optional, and in fact we don't care about it here as we'll
|
||||
* set the authzId to match the authnId (that is, there is no concept of
|
||||
* a user being authorized to act on behalf of another).
|
||||
*
|
||||
* @param bytes encoded credentials string sent by the client
|
||||
* @return map containing the username/password pairs in the form an IAuthenticator
|
||||
* would expect
|
||||
* @throws javax.security.sasl.SaslException
|
||||
*/
|
||||
bytes plain_sasl_challenge::evaluate_response(bytes_view client_response) {
|
||||
sstring username, password;
|
||||
|
||||
auto b = client_response.crbegin();
|
||||
auto e = client_response.crend();
|
||||
auto i = b;
|
||||
|
||||
while (i != e) {
|
||||
if (*i == 0) {
|
||||
sstring tmp(i.base(), b.base());
|
||||
if (password.empty()) {
|
||||
password = std::move(tmp);
|
||||
} else if (username.empty()) {
|
||||
username = std::move(tmp);
|
||||
}
|
||||
b = ++i;
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
if (username.empty()) {
|
||||
throw exceptions::authentication_exception("Authentication ID must not be null");
|
||||
}
|
||||
if (password.empty()) {
|
||||
throw exceptions::authentication_exception("Password must not be null");
|
||||
}
|
||||
|
||||
_username = std::move(username);
|
||||
_password = std::move(password);
|
||||
return {};
|
||||
}
|
||||
|
||||
bool plain_sasl_challenge::is_complete() const {
|
||||
return _username && _password;
|
||||
}
|
||||
|
||||
future<authenticated_user> plain_sasl_challenge::get_authenticated_user() const {
|
||||
return _when_complete(*_username, *_password);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 ScyllaDB
|
||||
*
|
||||
* Modified by ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
#include "auth/authenticated_user.hh"
|
||||
#include "bytes.hh"
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
///
|
||||
/// A stateful SASL challenge which supports many authentication schemes (depending on the implementation).
|
||||
///
|
||||
class sasl_challenge {
|
||||
public:
|
||||
virtual ~sasl_challenge() = default;
|
||||
|
||||
virtual bytes evaluate_response(bytes_view client_response) = 0;
|
||||
|
||||
virtual bool is_complete() const = 0;
|
||||
|
||||
virtual future<authenticated_user> get_authenticated_user() const = 0;
|
||||
};
|
||||
|
||||
class plain_sasl_challenge : public sasl_challenge {
|
||||
public:
|
||||
using completion_callback = std::function<future<authenticated_user>(std::string_view, std::string_view)>;
|
||||
|
||||
explicit plain_sasl_challenge(completion_callback f) : _when_complete(std::move(f)) {
|
||||
}
|
||||
|
||||
virtual bytes evaluate_response(bytes_view) override;
|
||||
|
||||
virtual bool is_complete() const override;
|
||||
|
||||
virtual future<authenticated_user> get_authenticated_user() const override;
|
||||
|
||||
private:
|
||||
std::optional<sstring> _username, _password;
|
||||
completion_callback _when_complete;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -36,12 +36,12 @@
|
||||
#include "auth/standard_role_manager.hh"
|
||||
#include "cql3/query_processor.hh"
|
||||
#include "cql3/untyped_result_set.hh"
|
||||
#include "db/config.hh"
|
||||
#include "db/consistency_level_type.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "log.hh"
|
||||
#include "service/migration_listener.hh"
|
||||
#include "utils/class_registrator.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -97,7 +97,7 @@ private:
|
||||
void on_drop_view(const sstring& ks_name, const sstring& view_name) override {}
|
||||
};
|
||||
|
||||
static future<> validate_role_exists(const service& ser, std::string_view role_name) {
|
||||
static future<> validate_role_exists(const service& ser, stdx::string_view role_name) {
|
||||
return ser.underlying_role_manager().exists(role_name).then([role_name](bool exists) {
|
||||
if (!exists) {
|
||||
throw nonexistant_role(role_name);
|
||||
@@ -105,6 +105,19 @@ static future<> validate_role_exists(const service& ser, std::string_view role_n
|
||||
});
|
||||
}
|
||||
|
||||
service_config service_config::from_db_config(const db::config& dc) {
|
||||
const qualified_name qualified_authorizer_name(meta::AUTH_PACKAGE_NAME, dc.authorizer());
|
||||
const qualified_name qualified_authenticator_name(meta::AUTH_PACKAGE_NAME, dc.authenticator());
|
||||
const qualified_name qualified_role_manager_name(meta::AUTH_PACKAGE_NAME, dc.role_manager());
|
||||
|
||||
service_config c;
|
||||
c.authorizer_java_name = qualified_authorizer_name;
|
||||
c.authenticator_java_name = qualified_authenticator_name;
|
||||
c.role_manager_java_name = qualified_role_manager_name;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
service::service(
|
||||
permissions_cache_config c,
|
||||
cql3::query_processor& qp,
|
||||
@@ -126,7 +139,8 @@ service::service(
|
||||
if ((_authenticator->qualified_java_name() == password_authenticator_name())
|
||||
&& (_role_manager->qualified_java_name() != standard_role_manager_name())) {
|
||||
throw incompatible_module_combination(
|
||||
format("The {} authenticator must be loaded alongside the {} role-manager.",
|
||||
sprint(
|
||||
"The %s authenticator must be loaded alongside the %s role-manager.",
|
||||
password_authenticator_name(),
|
||||
standard_role_manager_name()));
|
||||
}
|
||||
@@ -147,7 +161,7 @@ service::service(
|
||||
}
|
||||
|
||||
future<> service::create_keyspace_if_missing() const {
|
||||
auto& db = _qp.db();
|
||||
auto& db = _qp.db().local();
|
||||
|
||||
if (!db.has_keyspace(meta::AUTH_KS)) {
|
||||
std::map<sstring, sstring> opts{{"replication_factor", "1"}};
|
||||
@@ -170,9 +184,7 @@ future<> service::start() {
|
||||
return once_among_shards([this] {
|
||||
return create_keyspace_if_missing();
|
||||
}).then([this] {
|
||||
return _role_manager->start().then([this] {
|
||||
return when_all_succeed(_authorizer->start(), _authenticator->start());
|
||||
});
|
||||
return when_all_succeed(_role_manager->start(), _authorizer->start(), _authenticator->start());
|
||||
}).then([this] {
|
||||
_permissions_cache = std::make_unique<permissions_cache>(_permissions_cache_config, *this, log);
|
||||
}).then([this] {
|
||||
@@ -194,16 +206,18 @@ future<> service::stop() {
|
||||
}
|
||||
|
||||
future<bool> service::has_existing_legacy_users() const {
|
||||
if (!_qp.db().has_schema(meta::AUTH_KS, meta::USERS_CF)) {
|
||||
if (!_qp.db().local().has_schema(meta::AUTH_KS, meta::USERS_CF)) {
|
||||
return make_ready_future<bool>(false);
|
||||
}
|
||||
|
||||
static const sstring default_user_query = format("SELECT * FROM {}.{} WHERE {} = ?",
|
||||
static const sstring default_user_query = sprint(
|
||||
"SELECT * FROM %s.%s WHERE %s = ?",
|
||||
meta::AUTH_KS,
|
||||
meta::USERS_CF,
|
||||
meta::user_name_col_name);
|
||||
|
||||
static const sstring all_users_query = format("SELECT * FROM {}.{} LIMIT 1",
|
||||
static const sstring all_users_query = sprint(
|
||||
"SELECT * FROM %s.%s LIMIT 1",
|
||||
meta::AUTH_KS,
|
||||
meta::USERS_CF);
|
||||
|
||||
@@ -246,7 +260,7 @@ service::get_uncached_permissions(const role_or_anonymous& maybe_role, const res
|
||||
return _authorizer->authorize(maybe_role, r);
|
||||
}
|
||||
|
||||
const std::string_view role_name = *maybe_role.name;
|
||||
const stdx::string_view role_name = *maybe_role.name;
|
||||
|
||||
return has_superuser(role_name).then([this, role_name, &r](bool superuser) {
|
||||
if (superuser) {
|
||||
@@ -260,7 +274,7 @@ service::get_uncached_permissions(const role_or_anonymous& maybe_role, const res
|
||||
return do_with(permission_set(), [this, role_name, &r](auto& all_perms) {
|
||||
return get_roles(role_name).then([this, &r, &all_perms](role_set all_roles) {
|
||||
return do_with(std::move(all_roles), [this, &r, &all_perms](const auto& all_roles) {
|
||||
return parallel_for_each(all_roles, [this, &r, &all_perms](std::string_view role_name) {
|
||||
return parallel_for_each(all_roles, [this, &r, &all_perms](stdx::string_view role_name) {
|
||||
return _authorizer->authorize(role_name, r).then([&all_perms](permission_set perms) {
|
||||
all_perms = permission_set::from_mask(all_perms.mask() | perms.mask());
|
||||
});
|
||||
@@ -277,7 +291,7 @@ future<permission_set> service::get_permissions(const role_or_anonymous& maybe_r
|
||||
return _permissions_cache->get(maybe_role, r);
|
||||
}
|
||||
|
||||
future<bool> service::has_superuser(std::string_view role_name) const {
|
||||
future<bool> service::has_superuser(stdx::string_view role_name) const {
|
||||
return this->get_roles(std::move(role_name)).then([this](role_set roles) {
|
||||
return do_with(std::move(roles), [this](const role_set& roles) {
|
||||
return do_with(false, roles.begin(), [this, &roles](bool& any_super, auto& iter) {
|
||||
@@ -295,7 +309,7 @@ future<bool> service::has_superuser(std::string_view role_name) const {
|
||||
});
|
||||
}
|
||||
|
||||
future<role_set> service::get_roles(std::string_view role_name) const {
|
||||
future<role_set> service::get_roles(stdx::string_view role_name) const {
|
||||
//
|
||||
// We may wish to cache this information in the future (as Apache Cassandra does).
|
||||
//
|
||||
@@ -306,7 +320,7 @@ future<role_set> service::get_roles(std::string_view role_name) const {
|
||||
future<bool> service::exists(const resource& r) const {
|
||||
switch (r.kind()) {
|
||||
case resource_kind::data: {
|
||||
const auto& db = _qp.db();
|
||||
const auto& db = _qp.db().local();
|
||||
|
||||
data_resource_view v(r);
|
||||
const auto keyspace = v.keyspace();
|
||||
@@ -401,7 +415,7 @@ static void validate_authentication_options_are_supported(
|
||||
|
||||
future<> create_role(
|
||||
const service& ser,
|
||||
std::string_view name,
|
||||
stdx::string_view name,
|
||||
const role_config& config,
|
||||
const authentication_options& options) {
|
||||
return ser.underlying_role_manager().create(name, config).then([&ser, name, &options] {
|
||||
@@ -425,7 +439,7 @@ future<> create_role(
|
||||
|
||||
future<> alter_role(
|
||||
const service& ser,
|
||||
std::string_view name,
|
||||
stdx::string_view name,
|
||||
const role_config_update& config_update,
|
||||
const authentication_options& options) {
|
||||
return ser.underlying_role_manager().alter(name, config_update).then([&ser, name, &options] {
|
||||
@@ -442,7 +456,7 @@ future<> alter_role(
|
||||
});
|
||||
}
|
||||
|
||||
future<> drop_role(const service& ser, std::string_view name) {
|
||||
future<> drop_role(const service& ser, stdx::string_view name) {
|
||||
return do_with(make_role_resource(name), [&ser, name](const resource& r) {
|
||||
auto& a = ser.underlying_authorizer();
|
||||
|
||||
@@ -458,14 +472,14 @@ future<> drop_role(const service& ser, std::string_view name) {
|
||||
});
|
||||
}
|
||||
|
||||
future<bool> has_role(const service& ser, std::string_view grantee, std::string_view name) {
|
||||
future<bool> has_role(const service& ser, stdx::string_view grantee, stdx::string_view name) {
|
||||
return when_all_succeed(
|
||||
validate_role_exists(ser, name),
|
||||
ser.get_roles(grantee)).then([name](role_set all_roles) {
|
||||
return make_ready_future<bool>(all_roles.count(sstring(name)) != 0);
|
||||
});
|
||||
}
|
||||
future<bool> has_role(const service& ser, const authenticated_user& u, std::string_view name) {
|
||||
future<bool> has_role(const service& ser, const authenticated_user& u, stdx::string_view name) {
|
||||
if (is_anonymous(u)) {
|
||||
return make_ready_future<bool>(false);
|
||||
}
|
||||
@@ -475,7 +489,7 @@ future<bool> has_role(const service& ser, const authenticated_user& u, std::stri
|
||||
|
||||
future<> grant_permissions(
|
||||
const service& ser,
|
||||
std::string_view role_name,
|
||||
stdx::string_view role_name,
|
||||
permission_set perms,
|
||||
const resource& r) {
|
||||
return validate_role_exists(ser, role_name).then([&ser, role_name, perms, &r] {
|
||||
@@ -483,7 +497,7 @@ future<> grant_permissions(
|
||||
});
|
||||
}
|
||||
|
||||
future<> grant_applicable_permissions(const service& ser, std::string_view role_name, const resource& r) {
|
||||
future<> grant_applicable_permissions(const service& ser, stdx::string_view role_name, const resource& r) {
|
||||
return grant_permissions(ser, role_name, r.applicable_permissions(), r);
|
||||
}
|
||||
future<> grant_applicable_permissions(const service& ser, const authenticated_user& u, const resource& r) {
|
||||
@@ -496,7 +510,7 @@ future<> grant_applicable_permissions(const service& ser, const authenticated_us
|
||||
|
||||
future<> revoke_permissions(
|
||||
const service& ser,
|
||||
std::string_view role_name,
|
||||
stdx::string_view role_name,
|
||||
permission_set perms,
|
||||
const resource& r) {
|
||||
return validate_role_exists(ser, role_name).then([&ser, role_name, perms, &r] {
|
||||
@@ -507,7 +521,7 @@ future<> revoke_permissions(
|
||||
future<std::vector<permission_details>> list_filtered_permissions(
|
||||
const service& ser,
|
||||
permission_set perms,
|
||||
std::optional<std::string_view> role_name,
|
||||
std::optional<stdx::string_view> role_name,
|
||||
const std::optional<std::pair<resource, recursive_permissions>>& resource_filter) {
|
||||
return ser.underlying_authorizer().list_all().then([&ser, perms, role_name, &resource_filter](
|
||||
std::vector<permission_details> all_details) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
@@ -35,11 +35,16 @@
|
||||
#include "auth/permissions_cache.hh"
|
||||
#include "auth/role_manager.hh"
|
||||
#include "seastarx.hh"
|
||||
#include "stdx.hh"
|
||||
|
||||
namespace cql3 {
|
||||
class query_processor;
|
||||
}
|
||||
|
||||
namespace db {
|
||||
class config;
|
||||
}
|
||||
|
||||
namespace service {
|
||||
class migration_manager;
|
||||
class migration_listener;
|
||||
@@ -50,6 +55,8 @@ namespace auth {
|
||||
class role_or_anonymous;
|
||||
|
||||
struct service_config final {
|
||||
static service_config from_db_config(const db::config&);
|
||||
|
||||
sstring authorizer_java_name;
|
||||
sstring authenticator_java_name;
|
||||
sstring role_manager_java_name;
|
||||
@@ -134,13 +141,13 @@ public:
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the role does not exist.
|
||||
///
|
||||
future<bool> has_superuser(std::string_view role_name) const;
|
||||
future<bool> has_superuser(stdx::string_view role_name) const;
|
||||
|
||||
///
|
||||
/// Return the set of all roles granted to the given role, including itself and roles granted through other roles.
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistent_role if the role does not exist.
|
||||
future<role_set> get_roles(std::string_view role_name) const;
|
||||
future<role_set> get_roles(stdx::string_view role_name) const;
|
||||
|
||||
future<bool> exists(const resource&) const;
|
||||
|
||||
@@ -190,7 +197,7 @@ bool is_protected(const service&, const resource&) noexcept;
|
||||
///
|
||||
future<> create_role(
|
||||
const service&,
|
||||
std::string_view name,
|
||||
stdx::string_view name,
|
||||
const role_config&,
|
||||
const authentication_options&);
|
||||
|
||||
@@ -203,7 +210,7 @@ future<> create_role(
|
||||
///
|
||||
future<> alter_role(
|
||||
const service&,
|
||||
std::string_view name,
|
||||
stdx::string_view name,
|
||||
const role_config_update&,
|
||||
const authentication_options&);
|
||||
|
||||
@@ -212,20 +219,20 @@ future<> alter_role(
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistant_role if the named role does not exist.
|
||||
///
|
||||
future<> drop_role(const service&, std::string_view name);
|
||||
future<> drop_role(const service&, stdx::string_view name);
|
||||
|
||||
///
|
||||
/// Check if `grantee` has been granted the named role.
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistent_role if `grantee` or `name` do not exist.
|
||||
///
|
||||
future<bool> has_role(const service&, std::string_view grantee, std::string_view name);
|
||||
future<bool> has_role(const service&, stdx::string_view grantee, stdx::string_view name);
|
||||
///
|
||||
/// Check if the authenticated user has been granted the named role.
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistent_role if the user or `name` do not exist.
|
||||
///
|
||||
future<bool> has_role(const service&, const authenticated_user&, std::string_view name);
|
||||
future<bool> has_role(const service&, const authenticated_user&, stdx::string_view name);
|
||||
|
||||
///
|
||||
/// \returns an exceptional future with \ref nonexistent_role if the named role does not exist.
|
||||
@@ -235,7 +242,7 @@ future<bool> has_role(const service&, const authenticated_user&, std::string_vie
|
||||
///
|
||||
future<> grant_permissions(
|
||||
const service&,
|
||||
std::string_view role_name,
|
||||
stdx::string_view role_name,
|
||||
permission_set,
|
||||
const resource&);
|
||||
|
||||
@@ -247,7 +254,7 @@ future<> grant_permissions(
|
||||
/// \returns an exceptional future with \ref unsupported_authorization_operation if granting permissions is not
|
||||
/// supported.
|
||||
///
|
||||
future<> grant_applicable_permissions(const service&, std::string_view role_name, const resource&);
|
||||
future<> grant_applicable_permissions(const service&, stdx::string_view role_name, const resource&);
|
||||
future<> grant_applicable_permissions(const service&, const authenticated_user&, const resource&);
|
||||
|
||||
///
|
||||
@@ -258,7 +265,7 @@ future<> grant_applicable_permissions(const service&, const authenticated_user&,
|
||||
///
|
||||
future<> revoke_permissions(
|
||||
const service&,
|
||||
std::string_view role_name,
|
||||
stdx::string_view role_name,
|
||||
permission_set,
|
||||
const resource&);
|
||||
|
||||
@@ -283,7 +290,7 @@ using recursive_permissions = bool_class<struct recursive_permissions_tag>;
|
||||
future<std::vector<permission_details>> list_filtered_permissions(
|
||||
const service&,
|
||||
permission_set,
|
||||
std::optional<std::string_view> role_name,
|
||||
std::optional<stdx::string_view> role_name,
|
||||
const std::optional<std::pair<resource, recursive_permissions>>& resource_filter);
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "auth/standard_role_manager.hh"
|
||||
|
||||
#include <optional>
|
||||
#include <experimental/optional>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "log.hh"
|
||||
#include "utils/class_registrator.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace auth {
|
||||
|
||||
@@ -47,9 +46,9 @@ namespace meta {
|
||||
|
||||
namespace role_members_table {
|
||||
|
||||
constexpr std::string_view name{"role_members" , 12};
|
||||
constexpr stdx::string_view name{"role_members" , 12};
|
||||
|
||||
static std::string_view qualified_name() noexcept {
|
||||
static stdx::string_view qualified_name() noexcept {
|
||||
static const sstring instance = AUTH_KS + "." + sstring(name);
|
||||
return instance;
|
||||
}
|
||||
@@ -73,7 +72,7 @@ struct record final {
|
||||
role_set member_of;
|
||||
};
|
||||
|
||||
static db::consistency_level consistency_for_role(std::string_view role_name) noexcept {
|
||||
static db::consistency_level consistency_for_role(stdx::string_view role_name) noexcept {
|
||||
if (role_name == meta::DEFAULT_SUPERUSER_NAME) {
|
||||
return db::consistency_level::QUORUM;
|
||||
}
|
||||
@@ -81,8 +80,9 @@ static db::consistency_level consistency_for_role(std::string_view role_name) no
|
||||
return db::consistency_level::LOCAL_ONE;
|
||||
}
|
||||
|
||||
static future<std::optional<record>> find_record(cql3::query_processor& qp, std::string_view role_name) {
|
||||
static const sstring query = format("SELECT * FROM {} WHERE {} = ?",
|
||||
static future<stdx::optional<record>> find_record(cql3::query_processor& qp, stdx::string_view role_name) {
|
||||
static const sstring query = sprint(
|
||||
"SELECT * FROM %s WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
@@ -93,12 +93,12 @@ static future<std::optional<record>> find_record(cql3::query_processor& qp, std:
|
||||
{sstring(role_name)},
|
||||
true).then([](::shared_ptr<cql3::untyped_result_set> results) {
|
||||
if (results->empty()) {
|
||||
return std::optional<record>();
|
||||
return stdx::optional<record>();
|
||||
}
|
||||
|
||||
const cql3::untyped_result_set_row& row = results->one();
|
||||
|
||||
return std::make_optional(
|
||||
return stdx::make_optional(
|
||||
record{
|
||||
row.get_as<sstring>(sstring(meta::roles_table::role_col_name)),
|
||||
row.get_as<bool>("is_superuser"),
|
||||
@@ -109,8 +109,8 @@ static future<std::optional<record>> find_record(cql3::query_processor& qp, std:
|
||||
});
|
||||
}
|
||||
|
||||
static future<record> require_record(cql3::query_processor& qp, std::string_view role_name) {
|
||||
return find_record(qp, role_name).then([role_name](std::optional<record> mr) {
|
||||
static future<record> require_record(cql3::query_processor& qp, stdx::string_view role_name) {
|
||||
return find_record(qp, role_name).then([role_name](stdx::optional<record> mr) {
|
||||
if (!mr) {
|
||||
throw nonexistant_role(role_name);
|
||||
}
|
||||
@@ -123,12 +123,12 @@ static bool has_can_login(const cql3::untyped_result_set_row& row) {
|
||||
return row.has("can_login") && !(boolean_type->deserialize(row.get_blob("can_login")).is_null());
|
||||
}
|
||||
|
||||
std::string_view standard_role_manager_name() noexcept {
|
||||
stdx::string_view standard_role_manager_name() noexcept {
|
||||
static const sstring instance = meta::AUTH_PACKAGE_NAME + "CassandraRoleManager";
|
||||
return instance;
|
||||
}
|
||||
|
||||
std::string_view standard_role_manager::qualified_java_name() const noexcept {
|
||||
stdx::string_view standard_role_manager::qualified_java_name() const noexcept {
|
||||
return standard_role_manager_name();
|
||||
}
|
||||
|
||||
@@ -166,7 +166,8 @@ future<> standard_role_manager::create_metadata_tables_if_missing() const {
|
||||
future<> standard_role_manager::create_default_role_if_missing() const {
|
||||
return default_role_row_satisfies(_qp, &has_can_login).then([this](bool exists) {
|
||||
if (!exists) {
|
||||
static const sstring query = format("INSERT INTO {} ({}, is_superuser, can_login) VALUES (?, true, true)",
|
||||
static const sstring query = sprint(
|
||||
"INSERT INTO %s (%s, is_superuser, can_login) VALUES (?, true, true)",
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
@@ -190,12 +191,12 @@ future<> standard_role_manager::create_default_role_if_missing() const {
|
||||
static const sstring legacy_table_name{"users"};
|
||||
|
||||
bool standard_role_manager::legacy_metadata_exists() const {
|
||||
return _qp.db().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
return _qp.db().local().has_schema(meta::AUTH_KS, legacy_table_name);
|
||||
}
|
||||
|
||||
future<> standard_role_manager::migrate_legacy_metadata() const {
|
||||
log.info("Starting migration of legacy user metadata.");
|
||||
static const sstring query = format("SELECT * FROM {}.{}", meta::AUTH_KS, legacy_table_name);
|
||||
static const sstring query = sprint("SELECT * FROM %s.%s", meta::AUTH_KS, legacy_table_name);
|
||||
|
||||
return _qp.process(
|
||||
query,
|
||||
@@ -226,7 +227,7 @@ future<> standard_role_manager::start() {
|
||||
return this->create_metadata_tables_if_missing().then([this] {
|
||||
_stopped = auth::do_after_system_ready(_as, [this] {
|
||||
return seastar::async([this] {
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db(), _as).get0();
|
||||
wait_for_schema_agreement(_migration_manager, _qp.db().local()).get0();
|
||||
|
||||
if (any_nondefault_role_row_satisfies(_qp, &has_can_login).get0()) {
|
||||
if (this->legacy_metadata_exists()) {
|
||||
@@ -250,11 +251,12 @@ future<> standard_role_manager::start() {
|
||||
|
||||
future<> standard_role_manager::stop() {
|
||||
_as.request_abort();
|
||||
return _stopped.handle_exception_type([] (const sleep_aborted&) { }).handle_exception_type([](const abort_requested_exception&) {});;
|
||||
return _stopped.handle_exception_type([] (const sleep_aborted&) { });
|
||||
}
|
||||
|
||||
future<> standard_role_manager::create_or_replace(std::string_view role_name, const role_config& c) const {
|
||||
static const sstring query = format("INSERT INTO {} ({}, is_superuser, can_login) VALUES (?, ?, ?)",
|
||||
future<> standard_role_manager::create_or_replace(stdx::string_view role_name, const role_config& c) const {
|
||||
static const sstring query = sprint(
|
||||
"INSERT INTO %s (%s, is_superuser, can_login) VALUES (?, ?, ?)",
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
@@ -267,7 +269,7 @@ future<> standard_role_manager::create_or_replace(std::string_view role_name, co
|
||||
}
|
||||
|
||||
future<>
|
||||
standard_role_manager::create(std::string_view role_name, const role_config& c) const {
|
||||
standard_role_manager::create(stdx::string_view role_name, const role_config& c) const {
|
||||
return this->exists(role_name).then([this, role_name, &c](bool role_exists) {
|
||||
if (role_exists) {
|
||||
throw role_already_exists(role_name);
|
||||
@@ -278,7 +280,7 @@ standard_role_manager::create(std::string_view role_name, const role_config& c)
|
||||
}
|
||||
|
||||
future<>
|
||||
standard_role_manager::alter(std::string_view role_name, const role_config_update& u) const {
|
||||
standard_role_manager::alter(stdx::string_view role_name, const role_config_update& u) const {
|
||||
static const auto build_column_assignments = [](const role_config_update& u) -> sstring {
|
||||
std::vector<sstring> assignments;
|
||||
|
||||
@@ -299,7 +301,8 @@ standard_role_manager::alter(std::string_view role_name, const role_config_updat
|
||||
}
|
||||
|
||||
return _qp.process(
|
||||
format("UPDATE {} SET {} WHERE {} = ?",
|
||||
sprint(
|
||||
"UPDATE %s SET %s WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
build_column_assignments(u),
|
||||
meta::roles_table::role_col_name),
|
||||
@@ -309,7 +312,7 @@ standard_role_manager::alter(std::string_view role_name, const role_config_updat
|
||||
});
|
||||
}
|
||||
|
||||
future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
future<> standard_role_manager::drop(stdx::string_view role_name) const {
|
||||
return this->exists(role_name).then([this, role_name](bool role_exists) {
|
||||
if (!role_exists) {
|
||||
throw nonexistant_role(role_name);
|
||||
@@ -317,7 +320,8 @@ future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
|
||||
// First, revoke this role from all roles that are members of it.
|
||||
const auto revoke_from_members = [this, role_name] {
|
||||
static const sstring query = format("SELECT member FROM {} WHERE role = ?",
|
||||
static const sstring query = sprint(
|
||||
"SELECT member FROM %s WHERE role = ?",
|
||||
meta::role_members_table::qualified_name());
|
||||
|
||||
return _qp.process(
|
||||
@@ -355,7 +359,8 @@ future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
|
||||
// Finally, delete the role itself.
|
||||
auto delete_role = [this, role_name] {
|
||||
static const sstring query = format("DELETE FROM {} WHERE {} = ?",
|
||||
static const sstring query = sprint(
|
||||
"DELETE FROM %s WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
meta::roles_table::role_col_name);
|
||||
|
||||
@@ -374,14 +379,14 @@ future<> standard_role_manager::drop(std::string_view role_name) const {
|
||||
|
||||
future<>
|
||||
standard_role_manager::modify_membership(
|
||||
std::string_view grantee_name,
|
||||
std::string_view role_name,
|
||||
stdx::string_view grantee_name,
|
||||
stdx::string_view role_name,
|
||||
membership_change ch) const {
|
||||
|
||||
|
||||
const auto modify_roles = [this, role_name, grantee_name, ch] {
|
||||
const auto query = format(
|
||||
"UPDATE {} SET member_of = member_of {} ? WHERE {} = ?",
|
||||
const auto query = sprint(
|
||||
"UPDATE %s SET member_of = member_of %s ? WHERE %s = ?",
|
||||
meta::roles_table::qualified_name(),
|
||||
(ch == membership_change::add ? '+' : '-'),
|
||||
meta::roles_table::role_col_name);
|
||||
@@ -397,7 +402,8 @@ standard_role_manager::modify_membership(
|
||||
switch (ch) {
|
||||
case membership_change::add:
|
||||
return _qp.process(
|
||||
format("INSERT INTO {} (role, member) VALUES (?, ?)",
|
||||
sprint(
|
||||
"INSERT INTO %s (role, member) VALUES (?, ?)",
|
||||
meta::role_members_table::qualified_name()),
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_timeout_config(),
|
||||
@@ -405,7 +411,8 @@ standard_role_manager::modify_membership(
|
||||
|
||||
case membership_change::remove:
|
||||
return _qp.process(
|
||||
format("DELETE FROM {} WHERE role = ? AND member = ?",
|
||||
sprint(
|
||||
"DELETE FROM %s WHERE role = ? AND member = ?",
|
||||
meta::role_members_table::qualified_name()),
|
||||
consistency_for_role(role_name),
|
||||
internal_distributed_timeout_config(),
|
||||
@@ -419,7 +426,7 @@ standard_role_manager::modify_membership(
|
||||
}
|
||||
|
||||
future<>
|
||||
standard_role_manager::grant(std::string_view grantee_name, std::string_view role_name) const {
|
||||
standard_role_manager::grant(stdx::string_view grantee_name, stdx::string_view role_name) const {
|
||||
const auto check_redundant = [this, role_name, grantee_name] {
|
||||
return this->query_granted(
|
||||
grantee_name,
|
||||
@@ -450,7 +457,7 @@ standard_role_manager::grant(std::string_view grantee_name, std::string_view rol
|
||||
}
|
||||
|
||||
future<>
|
||||
standard_role_manager::revoke(std::string_view revokee_name, std::string_view role_name) const {
|
||||
standard_role_manager::revoke(stdx::string_view revokee_name, stdx::string_view role_name) const {
|
||||
return this->exists(role_name).then([this, revokee_name, role_name](bool role_exists) {
|
||||
if (!role_exists) {
|
||||
throw nonexistant_role(sstring(role_name));
|
||||
@@ -472,7 +479,7 @@ standard_role_manager::revoke(std::string_view revokee_name, std::string_view ro
|
||||
|
||||
static future<> collect_roles(
|
||||
cql3::query_processor& qp,
|
||||
std::string_view grantee_name,
|
||||
stdx::string_view grantee_name,
|
||||
bool recurse,
|
||||
role_set& roles) {
|
||||
return require_record(qp, grantee_name).then([&qp, &roles, recurse](record r) {
|
||||
@@ -490,7 +497,7 @@ static future<> collect_roles(
|
||||
});
|
||||
}
|
||||
|
||||
future<role_set> standard_role_manager::query_granted(std::string_view grantee_name, recursive_role_query m) const {
|
||||
future<role_set> standard_role_manager::query_granted(stdx::string_view grantee_name, recursive_role_query m) const {
|
||||
const bool recurse = (m == recursive_role_query::yes);
|
||||
|
||||
return do_with(
|
||||
@@ -501,7 +508,8 @@ future<role_set> standard_role_manager::query_granted(std::string_view grantee_n
|
||||
}
|
||||
|
||||
future<role_set> standard_role_manager::query_all() const {
|
||||
static const sstring query = format("SELECT {} FROM {}",
|
||||
static const sstring query = sprint(
|
||||
"SELECT %s FROM %s",
|
||||
meta::roles_table::role_col_name,
|
||||
meta::roles_table::qualified_name());
|
||||
|
||||
@@ -526,19 +534,19 @@ future<role_set> standard_role_manager::query_all() const {
|
||||
});
|
||||
}
|
||||
|
||||
future<bool> standard_role_manager::exists(std::string_view role_name) const {
|
||||
return find_record(_qp, role_name).then([](std::optional<record> mr) {
|
||||
future<bool> standard_role_manager::exists(stdx::string_view role_name) const {
|
||||
return find_record(_qp, role_name).then([](stdx::optional<record> mr) {
|
||||
return static_cast<bool>(mr);
|
||||
});
|
||||
}
|
||||
|
||||
future<bool> standard_role_manager::is_superuser(std::string_view role_name) const {
|
||||
future<bool> standard_role_manager::is_superuser(stdx::string_view role_name) const {
|
||||
return require_record(_qp, role_name).then([](record r) {
|
||||
return r.is_superuser;
|
||||
});
|
||||
}
|
||||
|
||||
future<bool> standard_role_manager::can_login(std::string_view role_name) const {
|
||||
future<bool> standard_role_manager::can_login(stdx::string_view role_name) const {
|
||||
return require_record(_qp, role_name).then([](record r) {
|
||||
return r.can_login;
|
||||
});
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
|
||||
#include "auth/role_manager.hh"
|
||||
|
||||
#include <string_view>
|
||||
#include <experimental/string_view>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <seastar/core/abort_source.hh>
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
#include "stdx.hh"
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace cql3 {
|
||||
@@ -42,7 +43,7 @@ class migration_manager;
|
||||
|
||||
namespace auth {
|
||||
|
||||
std::string_view standard_role_manager_name() noexcept;
|
||||
stdx::string_view standard_role_manager_name() noexcept;
|
||||
|
||||
class standard_role_manager final : public role_manager {
|
||||
cql3::query_processor& _qp;
|
||||
@@ -57,7 +58,7 @@ public:
|
||||
, _stopped(make_ready_future<>()) {
|
||||
}
|
||||
|
||||
virtual std::string_view qualified_java_name() const noexcept override;
|
||||
virtual stdx::string_view qualified_java_name() const noexcept override;
|
||||
|
||||
virtual const resource_set& protected_resources() const override;
|
||||
|
||||
@@ -65,25 +66,25 @@ public:
|
||||
|
||||
virtual future<> stop() override;
|
||||
|
||||
virtual future<> create(std::string_view role_name, const role_config&) const override;
|
||||
virtual future<> create(stdx::string_view role_name, const role_config&) const override;
|
||||
|
||||
virtual future<> drop(std::string_view role_name) const override;
|
||||
virtual future<> drop(stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<> alter(std::string_view role_name, const role_config_update&) const override;
|
||||
virtual future<> alter(stdx::string_view role_name, const role_config_update&) const override;
|
||||
|
||||
virtual future<> grant(std::string_view grantee_name, std::string_view role_name) const override;
|
||||
virtual future<> grant(stdx::string_view grantee_name, stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<> revoke(std::string_view revokee_name, std::string_view role_name) const override;
|
||||
virtual future<> revoke(stdx::string_view revokee_name, stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<role_set> query_granted(std::string_view grantee_name, recursive_role_query) const override;
|
||||
virtual future<role_set> query_granted(stdx::string_view grantee_name, recursive_role_query) const override;
|
||||
|
||||
virtual future<role_set> query_all() const override;
|
||||
|
||||
virtual future<bool> exists(std::string_view role_name) const override;
|
||||
virtual future<bool> exists(stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<bool> is_superuser(std::string_view role_name) const override;
|
||||
virtual future<bool> is_superuser(stdx::string_view role_name) const override;
|
||||
|
||||
virtual future<bool> can_login(std::string_view role_name) const override;
|
||||
virtual future<bool> can_login(stdx::string_view role_name) const override;
|
||||
|
||||
private:
|
||||
enum class membership_change { add, remove };
|
||||
@@ -96,9 +97,9 @@ private:
|
||||
|
||||
future<> create_default_role_if_missing() const;
|
||||
|
||||
future<> create_or_replace(std::string_view role_name, const role_config&) const;
|
||||
future<> create_or_replace(stdx::string_view role_name, const role_config&) const;
|
||||
|
||||
future<> modify_membership(std::string_view role_name, std::string_view grantee_name, membership_change) const;
|
||||
future<> modify_membership(stdx::string_view role_name, stdx::string_view grantee_name, membership_change) const;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "auth/default_authorizer.hh"
|
||||
#include "auth/password_authenticator.hh"
|
||||
#include "auth/permission.hh"
|
||||
#include "db/config.hh"
|
||||
#include "utils/class_registrator.hh"
|
||||
|
||||
namespace auth {
|
||||
@@ -117,19 +118,19 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
virtual future<> create(std::string_view role_name, const authentication_options& options) const override {
|
||||
virtual future<> create(stdx::string_view role_name, const authentication_options& options) const override {
|
||||
return _authenticator->create(role_name, options);
|
||||
}
|
||||
|
||||
virtual future<> alter(std::string_view role_name, const authentication_options& options) const override {
|
||||
virtual future<> alter(stdx::string_view role_name, const authentication_options& options) const override {
|
||||
return _authenticator->alter(role_name, options);
|
||||
}
|
||||
|
||||
virtual future<> drop(std::string_view role_name) const override {
|
||||
virtual future<> drop(stdx::string_view role_name) const override {
|
||||
return _authenticator->drop(role_name);
|
||||
}
|
||||
|
||||
virtual future<custom_options> query_custom_options(std::string_view role_name) const override {
|
||||
virtual future<custom_options> query_custom_options(stdx::string_view role_name) const override {
|
||||
return _authenticator->query_custom_options(role_name);
|
||||
}
|
||||
|
||||
@@ -217,11 +218,11 @@ public:
|
||||
return make_ready_future<permission_set>(transitional_permissions);
|
||||
}
|
||||
|
||||
virtual future<> grant(std::string_view s, permission_set ps, const resource& r) const override {
|
||||
virtual future<> grant(stdx::string_view s, permission_set ps, const resource& r) const override {
|
||||
return _authorizer->grant(s, std::move(ps), r);
|
||||
}
|
||||
|
||||
virtual future<> revoke(std::string_view s, permission_set ps, const resource& r) const override {
|
||||
virtual future<> revoke(stdx::string_view s, permission_set ps, const resource& r) const override {
|
||||
return _authorizer->revoke(s, std::move(ps), r);
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ public:
|
||||
return _authorizer->list_all();
|
||||
}
|
||||
|
||||
virtual future<> revoke_all(std::string_view s) const override {
|
||||
virtual future<> revoke_all(stdx::string_view s) const override {
|
||||
return _authorizer->revoke_all(s);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ protected:
|
||||
, _io_priority(iop)
|
||||
, _interval(interval)
|
||||
, _update_timer([this] { adjust(); })
|
||||
, _control_points()
|
||||
, _control_points({{0,0}})
|
||||
, _current_backlog(std::move(backlog))
|
||||
, _inflight_update(make_ready_future<>())
|
||||
{
|
||||
@@ -125,7 +125,7 @@ public:
|
||||
flush_controller(seastar::scheduling_group sg, const ::io_priority_class& iop, float static_shares) : backlog_controller(sg, iop, static_shares) {}
|
||||
flush_controller(seastar::scheduling_group sg, const ::io_priority_class& iop, std::chrono::milliseconds interval, float soft_limit, std::function<float()> current_dirty)
|
||||
: backlog_controller(sg, iop, std::move(interval),
|
||||
std::vector<backlog_controller::control_point>({{0.0, 0.0}, {soft_limit, 10}, {soft_limit + (hard_dirty_limit - soft_limit) / 2, 200} , {hard_dirty_limit, 1000}}),
|
||||
std::vector<backlog_controller::control_point>({{soft_limit, 10}, {soft_limit + (hard_dirty_limit - soft_limit) / 2, 200} , {hard_dirty_limit, 1000}}),
|
||||
std::move(current_dirty)
|
||||
)
|
||||
{}
|
||||
@@ -139,7 +139,7 @@ public:
|
||||
compaction_controller(seastar::scheduling_group sg, const ::io_priority_class& iop, float static_shares) : backlog_controller(sg, iop, static_shares) {}
|
||||
compaction_controller(seastar::scheduling_group sg, const ::io_priority_class& iop, std::chrono::milliseconds interval, std::function<float()> current_backlog)
|
||||
: backlog_controller(sg, iop, std::move(interval),
|
||||
std::vector<backlog_controller::control_point>({{0.0, 50}, {1.5, 100} , {normalization_factor, 1000}}),
|
||||
std::vector<backlog_controller::control_point>({{0.5, 10}, {1.5, 100} , {normalization_factor, 1000}}),
|
||||
std::move(current_backlog)
|
||||
)
|
||||
{}
|
||||
|
||||
4
bytes.cc
4
bytes.cc
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "bytes.hh"
|
||||
#include <seastar/core/print.hh>
|
||||
#include "core/print.hh"
|
||||
|
||||
static inline int8_t hex_to_int(unsigned char c) {
|
||||
switch (c) {
|
||||
@@ -55,7 +55,7 @@ bytes from_hex(sstring_view s) {
|
||||
auto half_byte1 = hex_to_int(s[i * 2]);
|
||||
auto half_byte2 = hex_to_int(s[i * 2 + 1]);
|
||||
if (half_byte1 == -1 || half_byte2 == -1) {
|
||||
throw std::invalid_argument(format("Non-hex characters in {}", s));
|
||||
throw std::invalid_argument(sprint("Non-hex characters in %s", s));
|
||||
}
|
||||
out[i] = (half_byte1 << 4) | half_byte2;
|
||||
}
|
||||
|
||||
14
bytes.hh
14
bytes.hh
@@ -22,22 +22,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/core/sstring.hh>
|
||||
#include "core/sstring.hh"
|
||||
#include "hashing.hh"
|
||||
#include <optional>
|
||||
#include <experimental/optional>
|
||||
#include <iosfwd>
|
||||
#include <functional>
|
||||
#include "utils/mutable_view.hh"
|
||||
|
||||
using bytes = basic_sstring<int8_t, uint32_t, 31, false>;
|
||||
using bytes_view = std::basic_string_view<int8_t>;
|
||||
using bytes_view = std::experimental::basic_string_view<int8_t>;
|
||||
using bytes_mutable_view = basic_mutable_view<bytes_view::value_type>;
|
||||
using bytes_opt = std::optional<bytes>;
|
||||
using sstring_view = std::string_view;
|
||||
|
||||
inline sstring_view to_sstring_view(bytes_view view) {
|
||||
return {reinterpret_cast<const char*>(view.data()), view.size()};
|
||||
}
|
||||
using bytes_opt = std::experimental::optional<bytes>;
|
||||
using sstring_view = std::experimental::string_view;
|
||||
|
||||
namespace std {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user