Compare commits
175 Commits
alternator
...
next-3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b46b9f1a8 | ||
|
|
93da2e2ff0 | ||
|
|
b764db3f1c | ||
|
|
304d339193 | ||
|
|
9f7ba4203d | ||
|
|
8b6a792f81 | ||
|
|
8a94f6b180 | ||
|
|
27209a5b2e | ||
|
|
c25f627a6e | ||
|
|
58b1bdc20c | ||
|
|
e1a7df174c | ||
|
|
6c39e17838 | ||
|
|
507d763f45 | ||
|
|
b042e27f0a | ||
|
|
375ce345a3 | ||
|
|
493b821dfa | ||
|
|
e1999c76b2 | ||
|
|
4791b726a0 | ||
|
|
d7354a5b8d | ||
|
|
6815b72b06 | ||
|
|
efc2df8ca3 | ||
|
|
dbe90f131f | ||
|
|
31d5d16c3d | ||
|
|
b0d122f9c5 | ||
|
|
9a10e4a245 | ||
|
|
871d1ebdd5 | ||
|
|
bff996959d | ||
|
|
1bdc83540b | ||
|
|
478c35e07a | ||
|
|
ba968ab9ec | ||
|
|
883b5e8395 | ||
|
|
b47033676a | ||
|
|
67e45b73f0 | ||
|
|
37eac75b6f | ||
|
|
e8431a3474 | ||
|
|
9d78d848e6 | ||
|
|
32aa6ddd7e | ||
|
|
74cc9477af | ||
|
|
95acf71680 | ||
|
|
921f8baf00 | ||
|
|
071d7d9210 | ||
|
|
769b9bbe59 | ||
|
|
d4e553c153 | ||
|
|
d983411488 | ||
|
|
27de1bb8e6 | ||
|
|
854f8ccb40 | ||
|
|
a68170c9a3 | ||
|
|
7e4bcf2c0f | ||
|
|
a74b3a182e | ||
|
|
e9bc579565 | ||
|
|
ad46bf06a7 | ||
|
|
1ff21a28b7 | ||
|
|
fb3dfaa736 | ||
|
|
5a02e6976f | ||
|
|
5202eea7a7 | ||
|
|
038733f1a5 | ||
|
|
0ed2e90925 | ||
|
|
9ee6d2bc15 | ||
|
|
23582a2ce9 | ||
|
|
5ddf0ec1df | ||
|
|
e6eb54af90 | ||
|
|
f5a869966a | ||
|
|
0c70cd626b | ||
|
|
0928aa4791 | ||
|
|
f32ec885c4 | ||
|
|
762eec2bc6 | ||
|
|
3f4d9f210f | ||
|
|
9c3cdded9e | ||
|
|
05272c53ed | ||
|
|
393b2abdc9 | ||
|
|
d9dc8f92cc | ||
|
|
c009f7b182 | ||
|
|
303a56f2bd | ||
|
|
57512d3df9 | ||
|
|
a894868298 | ||
|
|
a5d385d702 | ||
|
|
6413063b1b | ||
|
|
0d31c6da62 | ||
|
|
b62bb036ed | ||
|
|
bdabd2e7a4 | ||
|
|
d7fc7bcf9f | ||
|
|
21aec9c7ef | ||
|
|
02ce19e851 | ||
|
|
37c4be5e74 | ||
|
|
d81ac93728 | ||
|
|
024d1563ad | ||
|
|
4a1a281e84 | ||
|
|
d61dd1a933 | ||
|
|
447c1e3bcc | ||
|
|
834b92b3d7 | ||
|
|
2ec036f50c | ||
|
|
958fe2024f | ||
|
|
cd998b949a | ||
|
|
2e1e1392ea | ||
|
|
623ea5e3d9 | ||
|
|
f92a7ca2bf | ||
|
|
d70c2db09c | ||
|
|
e4a39ed319 | ||
|
|
bb70b9ed56 | ||
|
|
e06e795031 | ||
|
|
7d56e8e5bb | ||
|
|
417250607b | ||
|
|
d06bcef3b7 | ||
|
|
50c5cb6861 | ||
|
|
70f5154109 | ||
|
|
329c419c30 | ||
|
|
062d43c76e | ||
|
|
cf4c238b28 | ||
|
|
20090c1992 | ||
|
|
8ffb567474 | ||
|
|
710ec83d12 | ||
|
|
8d7c489436 | ||
|
|
6ec558e3a0 | ||
|
|
b1e2842c8c | ||
|
|
5a273737e3 | ||
|
|
b0d2312623 | ||
|
|
2f007d8e6b | ||
|
|
bebfd7b26c | ||
|
|
03b48b2caf | ||
|
|
95362624bc | ||
|
|
7865c314a5 | ||
|
|
0e6b62244c | ||
|
|
9d722a56b3 | ||
|
|
7009d5fb23 | ||
|
|
eb49fae020 | ||
|
|
92bf928170 | ||
|
|
deac0b0e94 | ||
|
|
c294000113 | ||
|
|
18bb2045aa | ||
|
|
5e3276d08f | ||
|
|
acff367ea8 | ||
|
|
e39724a343 | ||
|
|
31c4db83d8 | ||
|
|
433cb93f7a | ||
|
|
f553819919 | ||
|
|
48c34e7635 | ||
|
|
7f85b30941 | ||
|
|
7d14514b8a | ||
|
|
35f906f06f | ||
|
|
2c50a484f5 | ||
|
|
24ddb46707 | ||
|
|
f2fc3f32af | ||
|
|
c9f488ddc2 | ||
|
|
46498e77b8 | ||
|
|
440f33709e | ||
|
|
34696e1582 | ||
|
|
43bb290705 | ||
|
|
53980816de | ||
|
|
c1f4617530 | ||
|
|
efde9416ed | ||
|
|
224f9cee7e | ||
|
|
cd1d13f805 | ||
|
|
899291bc9b | ||
|
|
4130973f51 | ||
|
|
24e2c72888 | ||
|
|
69cc7d89c8 | ||
|
|
5f6c5d566a | ||
|
|
f32aea3834 | ||
|
|
933260cb53 | ||
|
|
f8ff0e1993 | ||
|
|
1fbab82553 | ||
|
|
c664615960 | ||
|
|
6a682dc5a2 | ||
|
|
c1271d08d3 | ||
|
|
0d5c2501b3 | ||
|
|
0dd84898ee | ||
|
|
d568270d7f | ||
|
|
78c57f18c4 | ||
|
|
ce27949797 | ||
|
|
6b47e23d29 | ||
|
|
1cb6cc0ac4 | ||
|
|
67435eff15 | ||
|
|
086ce13fb9 | ||
|
|
eb9a8f4442 | ||
|
|
178fb5fe5f |
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).
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
||||
[submodule "seastar"]
|
||||
path = seastar
|
||||
url = ../seastar
|
||||
url = ../scylla-seastar
|
||||
ignore = dirty
|
||||
[submodule "swagger-ui"]
|
||||
path = swagger-ui
|
||||
@@ -12,6 +12,3 @@
|
||||
[submodule "libdeflate"]
|
||||
path = libdeflate
|
||||
url = ../libdeflate
|
||||
[submodule "zstd"]
|
||||
path = zstd
|
||||
url = ../zstd
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
12
HACKING.md
12
HACKING.md
@@ -22,15 +22,6 @@ 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.
|
||||
|
||||
### Build system
|
||||
|
||||
**Note**: Compiling Scylla requires, conservatively, 2 GB of memory per native
|
||||
@@ -64,7 +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
|
||||
@@ -204,7 +194,7 @@ cqlsh and nodetool. They are available at
|
||||
https://github.com/scylladb/scylla-tools-java and can be built with
|
||||
|
||||
```bash
|
||||
$ sudo ./install-dependencies.sh
|
||||
$ ./install-dependencies.sh
|
||||
$ ant jar
|
||||
```
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -2,22 +2,21 @@
|
||||
|
||||
## 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.
|
||||
**Note**: GCC >= 8.1.1 is require to compile Scylla.
|
||||
|
||||
**Note**: See [frozen toolchain](tools/toolchain/README.md) for a way to build and run
|
||||
on an older distribution.
|
||||
|
||||
## Running Scylla
|
||||
|
||||
@@ -81,5 +80,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,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
PRODUCT=scylla
|
||||
VERSION=666.development
|
||||
VERSION=3.1.4
|
||||
|
||||
if test -f version
|
||||
then
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,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,14 +70,14 @@ 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));
|
||||
std::function<std::any (column_family&)> mapper;
|
||||
std::function<std::any (std::any, std::any)> reducer;
|
||||
future<std::any> operator()(database& db) const {
|
||||
auto res = seastar::make_lw_shared<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())));
|
||||
*res = reducer(*res.get(), mapper(*i.second.get()));
|
||||
}).then([res] {
|
||||
return std::move(*res);
|
||||
return *res;
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -85,17 +85,16 @@ struct map_reduce_column_families_locally {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
|
||||
@@ -44,14 +44,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 +88,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -47,10 +47,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 +377,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,15 +22,13 @@
|
||||
#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 <seastar/http/exception.hh>
|
||||
#include "repair/repair.hh"
|
||||
#include "locator/snitch_base.hh"
|
||||
#include "column_family.hh"
|
||||
@@ -39,7 +37,6 @@
|
||||
#include "sstables/compaction_manager.hh"
|
||||
#include "sstables/sstables.hh"
|
||||
#include "database.hh"
|
||||
#include "db/extensions.hh"
|
||||
|
||||
sstables::sstable::version_types get_highest_supported_format();
|
||||
|
||||
@@ -55,6 +52,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)) {
|
||||
@@ -902,133 +900,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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ class cache_flat_mutation_reader final : public flat_mutation_reader::impl {
|
||||
// - _last_row points at a direct predecessor of the next row which is going to be read.
|
||||
// Used for populating continuity.
|
||||
// - _population_range_starts_before_all_rows is set accordingly
|
||||
// - _underlying is engaged and fast-forwarded
|
||||
reading_from_underlying,
|
||||
|
||||
end_of_stream
|
||||
@@ -99,7 +100,13 @@ class cache_flat_mutation_reader final : public flat_mutation_reader::impl {
|
||||
// forward progress is not guaranteed in case iterators are getting constantly invalidated.
|
||||
bool _lower_bound_changed = false;
|
||||
|
||||
// Points to the underlying reader conforming to _schema,
|
||||
// either to *_underlying_holder or _read_context->underlying().underlying().
|
||||
flat_mutation_reader* _underlying = nullptr;
|
||||
std::optional<flat_mutation_reader> _underlying_holder;
|
||||
|
||||
future<> do_fill_buffer(db::timeout_clock::time_point);
|
||||
future<> ensure_underlying(db::timeout_clock::time_point);
|
||||
void copy_from_cache_to_buffer();
|
||||
future<> process_static_row(db::timeout_clock::time_point);
|
||||
void move_to_end();
|
||||
@@ -186,23 +193,22 @@ future<> cache_flat_mutation_reader::process_static_row(db::timeout_clock::time_
|
||||
return make_ready_future<>();
|
||||
} else {
|
||||
_read_context->cache().on_row_miss();
|
||||
return _read_context->get_next_fragment(timeout).then([this] (mutation_fragment_opt&& sr) {
|
||||
if (sr) {
|
||||
assert(sr->is_static_row());
|
||||
maybe_add_to_cache(sr->as_static_row());
|
||||
push_mutation_fragment(std::move(*sr));
|
||||
}
|
||||
maybe_set_static_row_continuous();
|
||||
return ensure_underlying(timeout).then([this, timeout] {
|
||||
return (*_underlying)(timeout).then([this] (mutation_fragment_opt&& sr) {
|
||||
if (sr) {
|
||||
assert(sr->is_static_row());
|
||||
maybe_add_to_cache(sr->as_static_row());
|
||||
push_mutation_fragment(std::move(*sr));
|
||||
}
|
||||
maybe_set_static_row_continuous();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
inline
|
||||
void cache_flat_mutation_reader::touch_partition() {
|
||||
if (_snp->at_latest_version()) {
|
||||
rows_entry& last_dummy = *_snp->version()->partition().clustered_rows().rbegin();
|
||||
_snp->tracker()->touch(last_dummy);
|
||||
}
|
||||
_snp->touch();
|
||||
}
|
||||
|
||||
inline
|
||||
@@ -232,14 +238,36 @@ future<> cache_flat_mutation_reader::fill_buffer(db::timeout_clock::time_point t
|
||||
});
|
||||
}
|
||||
|
||||
inline
|
||||
future<> cache_flat_mutation_reader::ensure_underlying(db::timeout_clock::time_point timeout) {
|
||||
if (_underlying) {
|
||||
return make_ready_future<>();
|
||||
}
|
||||
return _read_context->ensure_underlying(timeout).then([this, timeout] {
|
||||
flat_mutation_reader& ctx_underlying = _read_context->underlying().underlying();
|
||||
if (ctx_underlying.schema() != _schema) {
|
||||
_underlying_holder = make_delegating_reader(ctx_underlying);
|
||||
_underlying_holder->upgrade_schema(_schema);
|
||||
_underlying = &*_underlying_holder;
|
||||
} else {
|
||||
_underlying = &ctx_underlying;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inline
|
||||
future<> cache_flat_mutation_reader::do_fill_buffer(db::timeout_clock::time_point timeout) {
|
||||
if (_state == state::move_to_underlying) {
|
||||
if (!_underlying) {
|
||||
return ensure_underlying(timeout).then([this, timeout] {
|
||||
return do_fill_buffer(timeout);
|
||||
});
|
||||
}
|
||||
_state = state::reading_from_underlying;
|
||||
_population_range_starts_before_all_rows = _lower_bound.is_before_all_clustered_rows(*_schema);
|
||||
auto end = _next_row_in_range ? position_in_partition(_next_row.position())
|
||||
: position_in_partition(_upper_bound);
|
||||
return _read_context->fast_forward_to(position_range{_lower_bound, std::move(end)}, timeout).then([this, timeout] {
|
||||
return _underlying->fast_forward_to(position_range{_lower_bound, std::move(end)}, timeout).then([this, timeout] {
|
||||
return read_from_underlying(timeout);
|
||||
});
|
||||
}
|
||||
@@ -280,7 +308,7 @@ future<> cache_flat_mutation_reader::do_fill_buffer(db::timeout_clock::time_poin
|
||||
|
||||
inline
|
||||
future<> cache_flat_mutation_reader::read_from_underlying(db::timeout_clock::time_point timeout) {
|
||||
return consume_mutation_fragments_until(_read_context->underlying().underlying(),
|
||||
return consume_mutation_fragments_until(*_underlying,
|
||||
[this] { return _state != state::reading_from_underlying || is_buffer_full(); },
|
||||
[this] (mutation_fragment mf) {
|
||||
_read_context->cache().on_row_miss();
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
# Scylla Coding Style
|
||||
|
||||
Please see the [Seastar style document](https://github.com/scylladb/seastar/blob/master/coding-style.md).
|
||||
|
||||
Scylla code is written with "using namespace seastar", and should not
|
||||
explicitly add the "seastar::" prefix to Seastar symbols.
|
||||
There is usually no need to add "using namespace seastar" to Source files,
|
||||
because most Scylla header files #include "seastarx.hh", which does this.
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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 General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "bytes.hh"
|
||||
|
||||
class schema;
|
||||
class partition_key;
|
||||
class clustering_row;
|
||||
|
||||
class column_computation;
|
||||
using column_computation_ptr = std::unique_ptr<column_computation>;
|
||||
|
||||
/*
|
||||
* Column computation represents a computation performed in order to obtain a value for a computed column.
|
||||
* Computed columns description is also available at docs/system_schema_keyspace.md. They hold values
|
||||
* not provided directly by the user, but rather computed: from other column values and possibly other sources.
|
||||
* This class is able to serialize/deserialize column computations and perform the computation itself,
|
||||
* based on given schema, partition key and clustering row. Responsibility for providing enough data
|
||||
* in the clustering row in order for computation to succeed belongs to the caller. In particular,
|
||||
* generating a value might involve performing a read-before-write if the computation is performed
|
||||
* on more values than are present in the update request.
|
||||
*/
|
||||
class column_computation {
|
||||
public:
|
||||
virtual ~column_computation() = default;
|
||||
|
||||
static column_computation_ptr deserialize(bytes_view raw);
|
||||
static column_computation_ptr deserialize(const Json::Value& json);
|
||||
|
||||
virtual column_computation_ptr clone() const = 0;
|
||||
|
||||
virtual bytes serialize() const = 0;
|
||||
virtual bytes_opt compute_value(const schema& schema, const partition_key& key, const clustering_row& row) const = 0;
|
||||
};
|
||||
|
||||
class token_column_computation : public column_computation {
|
||||
public:
|
||||
virtual column_computation_ptr clone() const override {
|
||||
return std::make_unique<token_column_computation>(*this);
|
||||
}
|
||||
virtual bytes serialize() const override;
|
||||
virtual bytes_opt compute_value(const schema& schema, const partition_key& key, const clustering_row& row) const override;
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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 General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "schema.hh"
|
||||
#include "types/collection.hh"
|
||||
|
||||
class atomic_cell;
|
||||
class row_marker;
|
||||
|
||||
class compaction_garbage_collector {
|
||||
public:
|
||||
virtual ~compaction_garbage_collector() = default;
|
||||
virtual void collect(column_id id, atomic_cell) = 0;
|
||||
virtual void collect(column_id id, collection_type_impl::mutation) = 0;
|
||||
virtual void collect(row_marker) = 0;
|
||||
};
|
||||
@@ -21,19 +21,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/util/noncopyable_function.hh>
|
||||
|
||||
#include "schema_fwd.hh"
|
||||
#include "sstables/shared_sstable.hh"
|
||||
#include "exceptions/exceptions.hh"
|
||||
#include "sstables/compaction_backlog_manager.hh"
|
||||
|
||||
class table;
|
||||
using column_family = table;
|
||||
|
||||
class flat_mutation_reader;
|
||||
struct mutation_source_metadata;
|
||||
class schema;
|
||||
using schema_ptr = lw_shared_ptr<const schema>;
|
||||
|
||||
namespace sstables {
|
||||
|
||||
@@ -52,8 +47,6 @@ class sstable_set;
|
||||
struct compaction_descriptor;
|
||||
struct resharding_descriptor;
|
||||
|
||||
using reader_consumer = noncopyable_function<future<> (flat_mutation_reader)>;
|
||||
|
||||
class compaction_strategy {
|
||||
::shared_ptr<compaction_strategy_impl> _compaction_strategy_impl;
|
||||
public:
|
||||
@@ -82,8 +75,8 @@ public:
|
||||
// Return if optimization to rule out sstables based on clustering key filter should be applied.
|
||||
bool use_clustering_key_filter() const;
|
||||
|
||||
// Return true if compaction strategy doesn't care if a sstable belonging to partial sstable run is compacted.
|
||||
bool can_compact_partial_runs() const;
|
||||
// Return true if compaction strategy ignores sstables coming from partial runs.
|
||||
bool ignore_partial_runs() const;
|
||||
|
||||
// An estimation of number of compaction for strategy to be satisfied.
|
||||
int64_t estimated_pending_compactions(column_family& cf) const;
|
||||
@@ -136,10 +129,6 @@ public:
|
||||
sstable_set make_sstable_set(schema_ptr schema) const;
|
||||
|
||||
compaction_backlog_tracker& get_backlog_tracker();
|
||||
|
||||
uint64_t adjust_partition_estimate(const mutation_source_metadata& ms_meta, uint64_t partition_estimate);
|
||||
|
||||
reader_consumer make_interposer_consumer(const mutation_source_metadata& ms_meta, reader_consumer end_consumer);
|
||||
};
|
||||
|
||||
// Creates a compaction_strategy object from one of the strategies available.
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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 General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "query-request.hh"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
// Wraps ring_position_view so it is compatible with old-style C++: default
|
||||
// constructor, stateless comparators, yada yada.
|
||||
class compatible_ring_position_view {
|
||||
const ::schema* _schema = nullptr;
|
||||
// Optional to supply a default constructor, no more.
|
||||
std::optional<dht::ring_position_view> _rpv;
|
||||
public:
|
||||
constexpr compatible_ring_position_view() = default;
|
||||
compatible_ring_position_view(const schema& s, dht::ring_position_view rpv)
|
||||
: _schema(&s), _rpv(rpv) {
|
||||
}
|
||||
const dht::ring_position_view& position() const {
|
||||
return *_rpv;
|
||||
}
|
||||
const ::schema& schema() const {
|
||||
return *_schema;
|
||||
}
|
||||
friend int tri_compare(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return dht::ring_position_tri_compare(*x._schema, *x._rpv, *y._rpv);
|
||||
}
|
||||
friend bool operator<(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) < 0;
|
||||
}
|
||||
friend bool operator<=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) <= 0;
|
||||
}
|
||||
friend bool operator>(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) > 0;
|
||||
}
|
||||
friend bool operator>=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) >= 0;
|
||||
}
|
||||
friend bool operator==(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) == 0;
|
||||
}
|
||||
friend bool operator!=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Wraps ring_position so it is compatible with old-style C++: default
|
||||
// constructor, stateless comparators, yada yada.
|
||||
class compatible_ring_position {
|
||||
schema_ptr _schema;
|
||||
// Optional to supply a default constructor, no more.
|
||||
std::optional<dht::ring_position> _rp;
|
||||
public:
|
||||
constexpr compatible_ring_position() = default;
|
||||
compatible_ring_position(schema_ptr s, dht::ring_position rp)
|
||||
: _schema(std::move(s)), _rp(std::move(rp)) {
|
||||
}
|
||||
dht::ring_position_view position() const {
|
||||
return *_rp;
|
||||
}
|
||||
const ::schema& schema() const {
|
||||
return *_schema;
|
||||
}
|
||||
friend int tri_compare(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return dht::ring_position_tri_compare(*x._schema, *x._rp, *y._rp);
|
||||
}
|
||||
friend bool operator<(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) < 0;
|
||||
}
|
||||
friend bool operator<=(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) <= 0;
|
||||
}
|
||||
friend bool operator>(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) > 0;
|
||||
}
|
||||
friend bool operator>=(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) >= 0;
|
||||
}
|
||||
friend bool operator==(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) == 0;
|
||||
}
|
||||
friend bool operator!=(const compatible_ring_position& x, const compatible_ring_position& y) {
|
||||
return tri_compare(x, y) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Wraps ring_position or ring_position_view so either is compatible with old-style C++: default
|
||||
// constructor, stateless comparators, yada yada.
|
||||
// The motivations for supporting both types are to make containers self-sufficient by not relying
|
||||
// on callers to keep ring position alive, allow lookup on containers that don't support different
|
||||
// key types, and also avoiding unnecessary copies.
|
||||
class compatible_ring_position_or_view {
|
||||
// Optional to supply a default constructor, no more.
|
||||
std::optional<std::variant<compatible_ring_position, compatible_ring_position_view>> _crp_or_view;
|
||||
public:
|
||||
constexpr compatible_ring_position_or_view() = default;
|
||||
explicit compatible_ring_position_or_view(schema_ptr s, dht::ring_position rp)
|
||||
: _crp_or_view(compatible_ring_position(std::move(s), std::move(rp))) {
|
||||
}
|
||||
explicit compatible_ring_position_or_view(const schema& s, dht::ring_position_view rpv)
|
||||
: _crp_or_view(compatible_ring_position_view(s, rpv)) {
|
||||
}
|
||||
dht::ring_position_view position() const {
|
||||
struct rpv_accessor {
|
||||
dht::ring_position_view operator()(const compatible_ring_position& crp) {
|
||||
return crp.position();
|
||||
}
|
||||
dht::ring_position_view operator()(const compatible_ring_position_view& crpv) {
|
||||
return crpv.position();
|
||||
}
|
||||
};
|
||||
return std::visit(rpv_accessor{}, *_crp_or_view);
|
||||
}
|
||||
friend int tri_compare(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
struct schema_accessor {
|
||||
const ::schema& operator()(const compatible_ring_position& crp) {
|
||||
return crp.schema();
|
||||
}
|
||||
const ::schema& operator()(const compatible_ring_position_view& crpv) {
|
||||
return crpv.schema();
|
||||
}
|
||||
};
|
||||
return dht::ring_position_tri_compare(std::visit(schema_accessor{}, *x._crp_or_view), x.position(), y.position());
|
||||
}
|
||||
friend bool operator<(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) < 0;
|
||||
}
|
||||
friend bool operator<=(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) <= 0;
|
||||
}
|
||||
friend bool operator>(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) > 0;
|
||||
}
|
||||
friend bool operator>=(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) >= 0;
|
||||
}
|
||||
friend bool operator==(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) == 0;
|
||||
}
|
||||
friend bool operator!=(const compatible_ring_position_or_view& x, const compatible_ring_position_or_view& y) {
|
||||
return tri_compare(x, y) != 0;
|
||||
}
|
||||
};
|
||||
64
compatible_ring_position_view.hh
Normal file
64
compatible_ring_position_view.hh
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 "query-request.hh"
|
||||
#include <optional>
|
||||
|
||||
// Wraps ring_position_view so it is compatible with old-style C++: default
|
||||
// constructor, stateless comparators, yada yada.
|
||||
class compatible_ring_position_view {
|
||||
const schema* _schema = nullptr;
|
||||
// Optional to supply a default constructor, no more.
|
||||
std::optional<dht::ring_position_view> _rpv;
|
||||
public:
|
||||
constexpr compatible_ring_position_view() = default;
|
||||
compatible_ring_position_view(const schema& s, dht::ring_position_view rpv)
|
||||
: _schema(&s), _rpv(rpv) {
|
||||
}
|
||||
const dht::ring_position_view& position() const {
|
||||
return *_rpv;
|
||||
}
|
||||
friend int tri_compare(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return dht::ring_position_tri_compare(*x._schema, *x._rpv, *y._rpv);
|
||||
}
|
||||
friend bool operator<(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) < 0;
|
||||
}
|
||||
friend bool operator<=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) <= 0;
|
||||
}
|
||||
friend bool operator>(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) > 0;
|
||||
}
|
||||
friend bool operator>=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) >= 0;
|
||||
}
|
||||
friend bool operator==(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) == 0;
|
||||
}
|
||||
friend bool operator!=(const compatible_ring_position_view& x, const compatible_ring_position_view& y) {
|
||||
return tri_compare(x, y) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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 General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <seastar/net/inet_address.hh>
|
||||
|
||||
#include "types.hh"
|
||||
#include "types/list.hh"
|
||||
#include "types/map.hh"
|
||||
#include "types/set.hh"
|
||||
#include "types/tuple.hh"
|
||||
#include "types/user.hh"
|
||||
#include "utils/big_decimal.hh"
|
||||
|
||||
struct empty_type_impl final : public abstract_type {
|
||||
empty_type_impl();
|
||||
};
|
||||
|
||||
struct counter_type_impl final : public abstract_type {
|
||||
counter_type_impl();
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct simple_type_impl : public concrete_type<T> {
|
||||
simple_type_impl(abstract_type::kind k, sstring name, std::optional<uint32_t> value_length_if_fixed);
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct integer_type_impl : public simple_type_impl<T> {
|
||||
integer_type_impl(abstract_type::kind k, sstring name, std::optional<uint32_t> value_length_if_fixed);
|
||||
};
|
||||
|
||||
struct byte_type_impl final : public integer_type_impl<int8_t> {
|
||||
byte_type_impl();
|
||||
};
|
||||
|
||||
struct short_type_impl final : public integer_type_impl<int16_t> {
|
||||
short_type_impl();
|
||||
};
|
||||
|
||||
struct int32_type_impl final : public integer_type_impl<int32_t> {
|
||||
int32_type_impl();
|
||||
};
|
||||
|
||||
struct long_type_impl final : public integer_type_impl<int64_t> {
|
||||
long_type_impl();
|
||||
};
|
||||
|
||||
struct boolean_type_impl final : public simple_type_impl<bool> {
|
||||
boolean_type_impl();
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct floating_type_impl : public simple_type_impl<T> {
|
||||
floating_type_impl(abstract_type::kind k, sstring name, std::optional<uint32_t> value_length_if_fixed);
|
||||
};
|
||||
|
||||
struct double_type_impl final : public floating_type_impl<double> {
|
||||
double_type_impl();
|
||||
};
|
||||
|
||||
struct float_type_impl final : public floating_type_impl<float> {
|
||||
float_type_impl();
|
||||
};
|
||||
|
||||
struct decimal_type_impl final : public concrete_type<big_decimal> {
|
||||
decimal_type_impl();
|
||||
};
|
||||
|
||||
struct duration_type_impl final : public concrete_type<cql_duration> {
|
||||
duration_type_impl();
|
||||
};
|
||||
|
||||
struct timestamp_type_impl final : public simple_type_impl<db_clock::time_point> {
|
||||
timestamp_type_impl();
|
||||
};
|
||||
|
||||
struct simple_date_type_impl final : public simple_type_impl<uint32_t> {
|
||||
simple_date_type_impl();
|
||||
};
|
||||
|
||||
struct time_type_impl final : public simple_type_impl<int64_t> {
|
||||
time_type_impl();
|
||||
};
|
||||
|
||||
struct string_type_impl : public concrete_type<sstring> {
|
||||
string_type_impl(kind k, sstring name);
|
||||
};
|
||||
|
||||
struct ascii_type_impl final : public string_type_impl {
|
||||
ascii_type_impl();
|
||||
};
|
||||
|
||||
struct utf8_type_impl final : public string_type_impl {
|
||||
utf8_type_impl();
|
||||
};
|
||||
|
||||
struct bytes_type_impl final : public concrete_type<bytes> {
|
||||
bytes_type_impl();
|
||||
};
|
||||
|
||||
// This is the old version of timestamp_type_impl, but has been replaced as it
|
||||
// wasn't comparing pre-epoch timestamps correctly. This is kept for backward
|
||||
// compatibility but shouldn't be used in new code.
|
||||
struct date_type_impl final : public concrete_type<db_clock::time_point> {
|
||||
date_type_impl();
|
||||
};
|
||||
|
||||
struct timeuuid_type_impl final : public concrete_type<utils::UUID> {
|
||||
timeuuid_type_impl();
|
||||
};
|
||||
|
||||
struct varint_type_impl final : public concrete_type<boost::multiprecision::cpp_int> {
|
||||
varint_type_impl();
|
||||
};
|
||||
|
||||
struct inet_addr_type_impl final : public concrete_type<seastar::net::inet_address> {
|
||||
inet_addr_type_impl();
|
||||
};
|
||||
|
||||
struct uuid_type_impl final : public concrete_type<utils::UUID> {
|
||||
uuid_type_impl();
|
||||
};
|
||||
|
||||
template <typename Func> using visit_ret_type = std::invoke_result_t<Func, const ascii_type_impl&>;
|
||||
|
||||
GCC6_CONCEPT(
|
||||
template <typename Func> concept bool CanHandleAllTypes = requires(Func f) {
|
||||
{ f(*static_cast<const ascii_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const boolean_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const byte_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const bytes_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const counter_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const date_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const decimal_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const double_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const duration_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const empty_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const float_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const inet_addr_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const int32_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const list_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const long_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const map_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const reversed_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const set_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const short_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const simple_date_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const time_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const timestamp_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const timeuuid_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const tuple_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const user_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const utf8_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const uuid_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
{ f(*static_cast<const varint_type_impl*>(nullptr)) } -> visit_ret_type<Func>;
|
||||
};
|
||||
)
|
||||
|
||||
template<typename Func>
|
||||
GCC6_CONCEPT(requires CanHandleAllTypes<Func>)
|
||||
static inline visit_ret_type<Func> visit(const abstract_type& t, Func&& f) {
|
||||
switch (t.get_kind()) {
|
||||
case abstract_type::kind::ascii:
|
||||
return f(*static_cast<const ascii_type_impl*>(&t));
|
||||
case abstract_type::kind::boolean:
|
||||
return f(*static_cast<const boolean_type_impl*>(&t));
|
||||
case abstract_type::kind::byte:
|
||||
return f(*static_cast<const byte_type_impl*>(&t));
|
||||
case abstract_type::kind::bytes:
|
||||
return f(*static_cast<const bytes_type_impl*>(&t));
|
||||
case abstract_type::kind::counter:
|
||||
return f(*static_cast<const counter_type_impl*>(&t));
|
||||
case abstract_type::kind::date:
|
||||
return f(*static_cast<const date_type_impl*>(&t));
|
||||
case abstract_type::kind::decimal:
|
||||
return f(*static_cast<const decimal_type_impl*>(&t));
|
||||
case abstract_type::kind::double_kind:
|
||||
return f(*static_cast<const double_type_impl*>(&t));
|
||||
case abstract_type::kind::duration:
|
||||
return f(*static_cast<const duration_type_impl*>(&t));
|
||||
case abstract_type::kind::empty:
|
||||
return f(*static_cast<const empty_type_impl*>(&t));
|
||||
case abstract_type::kind::float_kind:
|
||||
return f(*static_cast<const float_type_impl*>(&t));
|
||||
case abstract_type::kind::inet:
|
||||
return f(*static_cast<const inet_addr_type_impl*>(&t));
|
||||
case abstract_type::kind::int32:
|
||||
return f(*static_cast<const int32_type_impl*>(&t));
|
||||
case abstract_type::kind::list:
|
||||
return f(*static_cast<const list_type_impl*>(&t));
|
||||
case abstract_type::kind::long_kind:
|
||||
return f(*static_cast<const long_type_impl*>(&t));
|
||||
case abstract_type::kind::map:
|
||||
return f(*static_cast<const map_type_impl*>(&t));
|
||||
case abstract_type::kind::reversed:
|
||||
return f(*static_cast<const reversed_type_impl*>(&t));
|
||||
case abstract_type::kind::set:
|
||||
return f(*static_cast<const set_type_impl*>(&t));
|
||||
case abstract_type::kind::short_kind:
|
||||
return f(*static_cast<const short_type_impl*>(&t));
|
||||
case abstract_type::kind::simple_date:
|
||||
return f(*static_cast<const simple_date_type_impl*>(&t));
|
||||
case abstract_type::kind::time:
|
||||
return f(*static_cast<const time_type_impl*>(&t));
|
||||
case abstract_type::kind::timestamp:
|
||||
return f(*static_cast<const timestamp_type_impl*>(&t));
|
||||
case abstract_type::kind::timeuuid:
|
||||
return f(*static_cast<const timeuuid_type_impl*>(&t));
|
||||
case abstract_type::kind::tuple:
|
||||
return f(*static_cast<const tuple_type_impl*>(&t));
|
||||
case abstract_type::kind::user:
|
||||
return f(*static_cast<const user_type_impl*>(&t));
|
||||
case abstract_type::kind::utf8:
|
||||
return f(*static_cast<const utf8_type_impl*>(&t));
|
||||
case abstract_type::kind::uuid:
|
||||
return f(*static_cast<const uuid_type_impl*>(&t));
|
||||
case abstract_type::kind::varint:
|
||||
return f(*static_cast<const varint_type_impl*>(&t));
|
||||
}
|
||||
__builtin_unreachable();
|
||||
}
|
||||
@@ -613,16 +613,10 @@ commitlog_total_space_in_mb: -1
|
||||
# compaction_throughput_mb_per_sec: 16
|
||||
|
||||
# Log a warning when writing partitions larger than this value
|
||||
# compaction_large_partition_warning_threshold_mb: 1000
|
||||
# compaction_large_partition_warning_threshold_mb: 100
|
||||
|
||||
# Log a warning when writing rows larger than this value
|
||||
# compaction_large_row_warning_threshold_mb: 10
|
||||
|
||||
# Log a warning when writing cells larger than this value
|
||||
# compaction_large_cell_warning_threshold_mb: 1
|
||||
|
||||
# Log a warning when row number is larger than this value
|
||||
# compaction_rows_count_warning_threshold: 100000
|
||||
# compaction_large_row_warning_threshold_mb: 50
|
||||
|
||||
# When compacting, the replacement sstable(s) can be opened before they
|
||||
# are completely written, and used in place of the prior sstables for
|
||||
|
||||
122
configure.py
122
configure.py
@@ -240,7 +240,8 @@ def find_headers(repodir, excluded_dirs):
|
||||
modes = {
|
||||
'debug': {
|
||||
'cxxflags': '-DDEBUG -DDEBUG_LSA_SANITIZER',
|
||||
'cxx_ld_flags': '',
|
||||
# Disable vptr because of https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88684
|
||||
'cxx_ld_flags': '-O0 -fsanitize=address -fsanitize=leak -fsanitize=undefined -fno-sanitize=vptr',
|
||||
},
|
||||
'release': {
|
||||
'cxxflags': '',
|
||||
@@ -250,10 +251,6 @@ modes = {
|
||||
'cxxflags': '',
|
||||
'cxx_ld_flags': '-O1',
|
||||
},
|
||||
'sanitize': {
|
||||
'cxxflags': '-DDEBUG -DDEBUG_LSA_SANITIZER',
|
||||
'cxx_ld_flags': '-Os',
|
||||
}
|
||||
}
|
||||
|
||||
scylla_tests = [
|
||||
@@ -366,7 +363,7 @@ scylla_tests = [
|
||||
'tests/imr_test',
|
||||
'tests/partition_data_test',
|
||||
'tests/reusable_buffer_test',
|
||||
'tests/mutation_writer_test',
|
||||
'tests/multishard_writer_test',
|
||||
'tests/observable_test',
|
||||
'tests/transport_test',
|
||||
'tests/fragmented_temporary_buffer_test',
|
||||
@@ -378,7 +375,6 @@ scylla_tests = [
|
||||
'tests/small_vector_test',
|
||||
'tests/data_listeners_test',
|
||||
'tests/truncation_migration_test',
|
||||
'tests/like_matcher_test',
|
||||
]
|
||||
|
||||
perf_tests = [
|
||||
@@ -484,7 +480,6 @@ scylla_core = (['database.cc',
|
||||
'utils/large_bitset.cc',
|
||||
'utils/buffer_input_stream.cc',
|
||||
'utils/limiting_data_source.cc',
|
||||
'utils/updateable_value.cc',
|
||||
'mutation_partition.cc',
|
||||
'mutation_partition_view.cc',
|
||||
'mutation_partition_serializer.cc',
|
||||
@@ -495,7 +490,6 @@ scylla_core = (['database.cc',
|
||||
'keys.cc',
|
||||
'counters.cc',
|
||||
'compress.cc',
|
||||
'zstd.cc',
|
||||
'sstables/mp_row_consumer.cc',
|
||||
'sstables/sstables.cc',
|
||||
'sstables/sstables_manager.cc',
|
||||
@@ -725,14 +719,12 @@ scylla_core = (['database.cc',
|
||||
'utils/arch/powerpc/crc32-vpmsum/crc32_wrapper.cc',
|
||||
'querier.cc',
|
||||
'data/cell.cc',
|
||||
'mutation_writer/multishard_writer.cc',
|
||||
'multishard_writer.cc',
|
||||
'multishard_mutation_query.cc',
|
||||
'reader_concurrency_semaphore.cc',
|
||||
'distributed_loader.cc',
|
||||
'utils/utf8.cc',
|
||||
'utils/ascii.cc',
|
||||
'utils/like_matcher.cc',
|
||||
'mutation_writer/timestamp_based_splitting_writer.cc',
|
||||
] + [Antlr3Grammar('cql3/Cql.g')] + [Thrift('interface/cassandra.thrift', 'Cassandra')]
|
||||
)
|
||||
|
||||
@@ -772,18 +764,6 @@ api = ['api/api.cc',
|
||||
'api/api-doc/config.json',
|
||||
]
|
||||
|
||||
alternator = [
|
||||
'alternator/server.cc',
|
||||
'alternator/executor.cc',
|
||||
'alternator/stats.cc',
|
||||
'alternator/base64.cc',
|
||||
'alternator/serialization.cc',
|
||||
'alternator/expressions.cc',
|
||||
Antlr3Grammar('alternator/expressions.g'),
|
||||
'alternator/conditions.cc',
|
||||
'alternator/rjson.cc',
|
||||
]
|
||||
|
||||
idls = ['idl/gossip_digest.idl.hh',
|
||||
'idl/uuid.idl.hh',
|
||||
'idl/range.idl.hh',
|
||||
@@ -822,12 +802,10 @@ scylla_tests_dependencies = scylla_core + idls + scylla_tests_generic_dependenci
|
||||
'tests/result_set_assertions.cc',
|
||||
'tests/mutation_source_test.cc',
|
||||
'tests/data_model.cc',
|
||||
'tests/exception_utils.cc',
|
||||
'tests/random_schema.cc',
|
||||
]
|
||||
|
||||
deps = {
|
||||
'scylla': idls + ['main.cc', 'release.cc'] + scylla_core + api + alternator,
|
||||
'scylla': idls + ['main.cc', 'release.cc'] + scylla_core + api,
|
||||
}
|
||||
|
||||
pure_boost_tests = set([
|
||||
@@ -856,12 +834,14 @@ pure_boost_tests = set([
|
||||
'tests/enum_set_test',
|
||||
'tests/cql_auth_syntax_test',
|
||||
'tests/meta_test',
|
||||
'tests/imr_test',
|
||||
'tests/partition_data_test',
|
||||
'tests/reusable_buffer_test',
|
||||
'tests/observable_test',
|
||||
'tests/json_test',
|
||||
'tests/auth_passwords_test',
|
||||
'tests/top_k_test',
|
||||
'tests/small_vector_test',
|
||||
'tests/like_matcher_test',
|
||||
])
|
||||
|
||||
tests_not_using_seastar_test_framework = set([
|
||||
@@ -873,6 +853,8 @@ tests_not_using_seastar_test_framework = set([
|
||||
'tests/perf/perf_hash',
|
||||
'tests/perf/perf_cql_parser',
|
||||
'tests/message',
|
||||
'tests/perf/perf_simple_query',
|
||||
'tests/perf/perf_fast_forward',
|
||||
'tests/perf/perf_cache_eviction',
|
||||
'tests/row_cache_stress_test',
|
||||
'tests/memory_footprint',
|
||||
@@ -920,8 +902,6 @@ deps['tests/small_vector_test'] = ['tests/small_vector_test.cc']
|
||||
deps['tests/multishard_mutation_query_test'] += ['tests/test_table.cc']
|
||||
deps['tests/vint_serialization_test'] = ['tests/vint_serialization_test.cc', 'vint-serialization.cc', 'bytes.cc']
|
||||
|
||||
deps['tests/duration_test'] += ['tests/exception_utils.cc']
|
||||
|
||||
deps['utils/gz/gen_crc_combine_table'] = ['utils/gz/gen_crc_combine_table.cc']
|
||||
|
||||
warnings = [
|
||||
@@ -1056,7 +1036,6 @@ total_memory = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
|
||||
link_pool_depth = max(int(total_memory / 7e9), 1)
|
||||
|
||||
selected_modes = args.selected_modes or modes.keys()
|
||||
default_modes = args.selected_modes or ['debug', 'release', 'dev']
|
||||
build_modes = {m: modes[m] for m in selected_modes}
|
||||
build_artifacts = all_artifacts if not args.artifacts else args.artifacts
|
||||
|
||||
@@ -1089,8 +1068,8 @@ modes['debug']['cxxflags'] += ' -gz'
|
||||
flag_dest = 'cxx_ld_flags' if args.compress_exec_debuginfo else 'cxxflags'
|
||||
modes['release'][flag_dest] += ' -gz'
|
||||
|
||||
for m in ['debug', 'release', 'sanitize']:
|
||||
modes[m]['cxxflags'] += ' ' + dbgflag
|
||||
modes['debug']['cxxflags'] += ' ' + dbgflag
|
||||
modes['release']['cxxflags'] += ' ' + dbgflag
|
||||
|
||||
seastar_cflags = args.user_cflags
|
||||
seastar_cflags += ' -Wno-error'
|
||||
@@ -1101,7 +1080,6 @@ seastar_flags += ['--compiler', args.cxx, '--c-compiler', args.cc, '--cflags=%s'
|
||||
'--c++-dialect=gnu++17', '--use-std-optional-variant-stringview=1', '--optflags=%s' % (modes['release']['cxx_ld_flags']), ]
|
||||
|
||||
libdeflate_cflags = seastar_cflags
|
||||
zstd_cflags = seastar_cflags + ' -Wno-implicit-fallthrough'
|
||||
|
||||
status = subprocess.call([args.python, './configure.py'] + seastar_flags, cwd='seastar')
|
||||
|
||||
@@ -1131,32 +1109,11 @@ for mode in build_modes:
|
||||
modes[mode]['seastar_cflags'] = seastar_cflags
|
||||
modes[mode]['seastar_libs'] = seastar_libs
|
||||
|
||||
MODE_TO_CMAKE_BUILD_TYPE = {'release' : 'RelWithDebInfo', 'debug' : 'Debug', 'dev' : 'Dev', 'sanitize' : 'Sanitize' }
|
||||
|
||||
# We need to use experimental features of the zstd library (to use our own allocators for the (de)compression context),
|
||||
# which are available only when the library is linked statically.
|
||||
def configure_zstd(build_dir, mode):
|
||||
zstd_build_dir = os.path.join(build_dir, mode, 'zstd')
|
||||
|
||||
zstd_cmake_args = [
|
||||
'-DCMAKE_BUILD_TYPE={}'.format(MODE_TO_CMAKE_BUILD_TYPE[mode]),
|
||||
'-DCMAKE_C_COMPILER={}'.format(args.cc),
|
||||
'-DCMAKE_CXX_COMPILER={}'.format(args.cxx),
|
||||
'-DCMAKE_C_FLAGS={}'.format(zstd_cflags),
|
||||
'-DZSTD_BUILD_PROGRAMS=OFF'
|
||||
]
|
||||
|
||||
zstd_cmd = ['cmake', '-G', 'Ninja', os.path.relpath('zstd/build/cmake', zstd_build_dir)] + zstd_cmake_args
|
||||
|
||||
print(zstd_cmd)
|
||||
os.makedirs(zstd_build_dir, exist_ok=True)
|
||||
subprocess.check_call(zstd_cmd, shell=False, cwd=zstd_build_dir)
|
||||
|
||||
args.user_cflags += " " + pkg_config('jsoncpp', '--cflags')
|
||||
args.user_cflags += ' -march=' + args.target
|
||||
libs = ' '.join([maybe_static(args.staticyamlcpp, '-lyaml-cpp'), '-latomic', '-llz4', '-lz', '-lsnappy', pkg_config('jsoncpp', '--libs'),
|
||||
' -lstdc++fs', ' -lcrypt', ' -lcryptopp', ' -lpthread',
|
||||
maybe_static(args.staticboost, '-lboost_date_time -lboost_regex -licuuc'), ])
|
||||
maybe_static(args.staticboost, '-lboost_date_time'), ])
|
||||
|
||||
xxhash_dir = 'xxHash'
|
||||
|
||||
@@ -1166,17 +1123,6 @@ if not os.path.exists(xxhash_dir) or not os.listdir(xxhash_dir):
|
||||
if not args.staticboost:
|
||||
args.user_cflags += ' -DBOOST_TEST_DYN_LINK'
|
||||
|
||||
# thrift version detection, see #4538
|
||||
proc_res = subprocess.run(["thrift", "-version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
proc_res_output = proc_res.stdout.decode("utf-8")
|
||||
if proc_res.returncode != 0 and not re.search(r'^Thrift version', proc_res_output):
|
||||
raise Exception("Thrift compiler must be missing: {}".format(proc_res_output))
|
||||
|
||||
thrift_version = proc_res_output.split(" ")[-1]
|
||||
thrift_boost_versions = ["0.{}.".format(n) for n in range(1, 11)]
|
||||
if any(filter(thrift_version.startswith, thrift_boost_versions)):
|
||||
args.user_cflags += ' -DTHRIFT_USES_BOOST'
|
||||
|
||||
for pkg in pkgs:
|
||||
args.user_cflags += ' ' + pkg_config(pkg, '--cflags')
|
||||
libs += ' ' + pkg_config(pkg, '--libs')
|
||||
@@ -1202,16 +1148,7 @@ if args.antlr3_exec:
|
||||
else:
|
||||
antlr3_exec = "antlr3"
|
||||
|
||||
for mode in build_modes:
|
||||
configure_zstd(outdir, mode)
|
||||
|
||||
# configure.py may run automatically from an already-existing build.ninja.
|
||||
# If the user interrupts configure.py in the middle, we need build.ninja
|
||||
# to remain in a valid state. So we write our output to a temporary
|
||||
# file, and only when done we rename it atomically to build.ninja.
|
||||
buildfile_tmp = buildfile + ".tmp"
|
||||
|
||||
with open(buildfile_tmp, 'w') as f:
|
||||
with open(buildfile, 'w') as f:
|
||||
f.write(textwrap.dedent('''\
|
||||
configure_args = {configure_args}
|
||||
builddir = {outdir}
|
||||
@@ -1221,7 +1158,7 @@ with open(buildfile_tmp, 'w') as f:
|
||||
libs = {libs}
|
||||
pool link_pool
|
||||
depth = {link_pool_depth}
|
||||
pool submodule_pool
|
||||
pool seastar_pool
|
||||
depth = 1
|
||||
rule gen
|
||||
command = echo -e $text > $out
|
||||
@@ -1321,8 +1258,7 @@ with open(buildfile_tmp, 'w') as f:
|
||||
f.write('build $builddir/{}/{}: ar.{} {}\n'.format(mode, binary, mode, str.join(' ', objs)))
|
||||
else:
|
||||
objs.extend(['$builddir/' + mode + '/' + artifact for artifact in [
|
||||
'libdeflate/libdeflate.a',
|
||||
'zstd/lib/libzstd.a',
|
||||
'libdeflate/libdeflate.a'
|
||||
]])
|
||||
objs.append('$builddir/' + mode + '/gen/utils/gz/crc_combine_table.o')
|
||||
if binary.startswith('tests/'):
|
||||
@@ -1369,7 +1305,6 @@ with open(buildfile_tmp, 'w') as f:
|
||||
'$builddir/' + mode + '/utils/gz/gen_crc_combine_table'))
|
||||
f.write('build {}: link.{} {}\n'.format('$builddir/' + mode + '/utils/gz/gen_crc_combine_table', mode,
|
||||
'$builddir/' + mode + '/utils/gz/gen_crc_combine_table.o'))
|
||||
f.write(' libs = $seastar_libs_{}\n'.format(mode))
|
||||
f.write(
|
||||
'build {mode}-objects: phony {objs}\n'.format(
|
||||
mode=mode,
|
||||
@@ -1418,30 +1353,19 @@ with open(buildfile_tmp, 'w') as f:
|
||||
f.write('build $builddir/{mode}/{hh}.o: checkhh.{mode} {hh} || {gen_headers_dep}\n'.format(
|
||||
mode=mode, hh=hh, gen_headers_dep=gen_headers_dep))
|
||||
|
||||
f.write('build seastar/build/{mode}/libseastar.a: ninja | always\n'
|
||||
f.write('build seastar/build/{mode}/libseastar.a seastar/build/{mode}/apps/iotune/iotune: ninja | always\n'
|
||||
.format(**locals()))
|
||||
f.write(' pool = submodule_pool\n')
|
||||
f.write(' pool = seastar_pool\n')
|
||||
f.write(' subdir = seastar/build/{mode}\n'.format(**locals()))
|
||||
f.write(' target = seastar seastar_testing\n'.format(**locals()))
|
||||
f.write('build seastar/build/{mode}/apps/iotune/iotune: ninja\n'
|
||||
.format(**locals()))
|
||||
f.write(' pool = submodule_pool\n')
|
||||
f.write(' subdir = seastar/build/{mode}\n'.format(**locals()))
|
||||
f.write(' target = iotune\n'.format(**locals()))
|
||||
f.write(' target = seastar seastar_testing app_iotune\n'.format(**locals()))
|
||||
f.write(textwrap.dedent('''\
|
||||
build build/{mode}/iotune: copy seastar/build/{mode}/apps/iotune/iotune
|
||||
''').format(**locals()))
|
||||
f.write('build build/{mode}/scylla-package.tar.gz: package build/{mode}/scylla build/{mode}/iotune build/SCYLLA-RELEASE-FILE build/SCYLLA-VERSION-FILE | always\n'.format(**locals()))
|
||||
f.write(' pool = submodule_pool\n')
|
||||
f.write(' mode = {mode}\n'.format(**locals()))
|
||||
f.write(' mode = {mode}\n'.format(**locals()))
|
||||
f.write('rule libdeflate.{mode}\n'.format(**locals()))
|
||||
f.write(' command = make -C libdeflate BUILD_DIR=../build/{mode}/libdeflate/ CFLAGS="{libdeflate_cflags}" CC={args.cc} ../build/{mode}/libdeflate//libdeflate.a\n'.format(**locals()))
|
||||
f.write(' command = make -C libdeflate BUILD_DIR=../build/{mode}/libdeflate/ CFLAGS="{libdeflate_cflags}" CC={args.cc} ../build/{mode}/libdeflate//libdeflate.a\n'.format(**locals()))
|
||||
f.write('build build/{mode}/libdeflate/libdeflate.a: libdeflate.{mode}\n'.format(**locals()))
|
||||
f.write(' pool = submodule_pool\n')
|
||||
f.write('build build/{mode}/zstd/lib/libzstd.a: ninja\n'.format(**locals()))
|
||||
f.write(' pool = submodule_pool\n')
|
||||
f.write(' subdir = build/{mode}/zstd\n'.format(**locals()))
|
||||
f.write(' target = libzstd.a\n'.format(**locals()))
|
||||
|
||||
mode = 'dev' if 'dev' in modes else modes[0]
|
||||
f.write('build checkheaders: phony || {}\n'.format(' '.join(['$builddir/{}/{}.o'.format(mode, hh) for hh in headers])))
|
||||
@@ -1460,12 +1384,10 @@ with open(buildfile_tmp, 'w') as f:
|
||||
description = CLEAN
|
||||
build clean: clean
|
||||
default {modes_list}
|
||||
''').format(modes_list=' '.join(default_modes), **globals()))
|
||||
''').format(modes_list=' '.join(build_modes), **globals()))
|
||||
f.write(textwrap.dedent('''\
|
||||
build always: phony
|
||||
rule scylla_version_gen
|
||||
command = ./SCYLLA-VERSION-GEN
|
||||
build build/SCYLLA-RELEASE-FILE build/SCYLLA-VERSION-FILE: scylla_version_gen
|
||||
''').format(modes_list=' '.join(build_modes), **globals()))
|
||||
|
||||
os.rename(buildfile_tmp, buildfile)
|
||||
|
||||
@@ -309,8 +309,7 @@ class basic_counter_cell_view {
|
||||
protected:
|
||||
using linearized_value_view = std::conditional_t<is_mutable == mutable_view::no,
|
||||
bytes_view, bytes_mutable_view>;
|
||||
using pointer_type = std::conditional_t<is_mutable == mutable_view::no,
|
||||
bytes_view::const_pointer, bytes_mutable_view::pointer>;
|
||||
using pointer_type = typename linearized_value_view::pointer;
|
||||
basic_atomic_cell_view<is_mutable> _cell;
|
||||
linearized_value_view _value;
|
||||
private:
|
||||
|
||||
35
cql3/Cql.g
35
cql3/Cql.g
@@ -125,7 +125,6 @@ struct uninitialized {
|
||||
uninitialized& operator=(uninitialized&&) = default;
|
||||
operator const T&() const & { return check(), *_val; }
|
||||
operator T&&() && { return check(), std::move(*_val); }
|
||||
operator std::optional<T>&&() && { return check(), std::move(_val); }
|
||||
void check() const { if (!_val) { throw std::runtime_error("not intitialized"); } }
|
||||
};
|
||||
|
||||
@@ -395,7 +394,6 @@ selectStatement returns [shared_ptr<raw::select_statement> expr]
|
||||
)
|
||||
K_FROM cf=columnFamilyName
|
||||
( K_WHERE wclause=whereClause )?
|
||||
( K_GROUP K_BY gbcolumns=listOfIdentifiers)?
|
||||
( K_ORDER K_BY orderByClause[orderings] ( ',' orderByClause[orderings] )* )?
|
||||
( K_PER K_PARTITION K_LIMIT rows=intValue { per_partition_limit = rows; } )?
|
||||
( K_LIMIT rows=intValue { limit = rows; } )?
|
||||
@@ -404,8 +402,7 @@ selectStatement returns [shared_ptr<raw::select_statement> expr]
|
||||
{
|
||||
auto params = ::make_shared<raw::select_statement::parameters>(std::move(orderings), is_distinct, allow_filtering, is_json, bypass_cache);
|
||||
$expr = ::make_shared<raw::select_statement>(std::move(cf), std::move(params),
|
||||
std::move(sclause), std::move(wclause), std::move(limit), std::move(per_partition_limit),
|
||||
std::move(gbcolumns));
|
||||
std::move(sclause), std::move(wclause), std::move(limit), std::move(per_partition_limit));
|
||||
}
|
||||
;
|
||||
|
||||
@@ -1128,10 +1125,10 @@ createUserStatement returns [::shared_ptr<create_role_statement> stmt]
|
||||
|
||||
bool ifNotExists = false;
|
||||
}
|
||||
: K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? u=username
|
||||
: K_CREATE K_USER (K_IF K_NOT K_EXISTS { ifNotExists = true; })? username
|
||||
( K_WITH K_PASSWORD v=STRING_LITERAL { opts.password = $v.text; })?
|
||||
( K_SUPERUSER { opts.is_superuser = true; } | K_NOSUPERUSER { opts.is_superuser = false; } )?
|
||||
{ $stmt = ::make_shared<create_role_statement>(cql3::role_name(u, cql3::preserve_role_case::yes), std::move(opts), ifNotExists); }
|
||||
{ $stmt = ::make_shared<create_role_statement>(cql3::role_name($username.text, cql3::preserve_role_case::yes), std::move(opts), ifNotExists); }
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -1141,10 +1138,10 @@ alterUserStatement returns [::shared_ptr<alter_role_statement> stmt]
|
||||
@init {
|
||||
cql3::role_options opts;
|
||||
}
|
||||
: K_ALTER K_USER u=username
|
||||
: K_ALTER K_USER username
|
||||
( K_WITH K_PASSWORD v=STRING_LITERAL { opts.password = $v.text; })?
|
||||
( K_SUPERUSER { opts.is_superuser = true; } | K_NOSUPERUSER { opts.is_superuser = false; } )?
|
||||
{ $stmt = ::make_shared<alter_role_statement>(cql3::role_name(u, cql3::preserve_role_case::yes), std::move(opts)); }
|
||||
{ $stmt = ::make_shared<alter_role_statement>(cql3::role_name($username.text, cql3::preserve_role_case::yes), std::move(opts)); }
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -1152,8 +1149,8 @@ alterUserStatement returns [::shared_ptr<alter_role_statement> stmt]
|
||||
*/
|
||||
dropUserStatement returns [::shared_ptr<drop_role_statement> stmt]
|
||||
@init { bool ifExists = false; }
|
||||
: K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? u=username
|
||||
{ $stmt = ::make_shared<drop_role_statement>(cql3::role_name(u, cql3::preserve_role_case::yes), ifExists); }
|
||||
: K_DROP K_USER (K_IF K_EXISTS { ifExists = true; })? username
|
||||
{ $stmt = ::make_shared<drop_role_statement>(cql3::role_name($username.text, cql3::preserve_role_case::yes), ifExists); }
|
||||
;
|
||||
|
||||
/**
|
||||
@@ -1491,7 +1488,6 @@ relationType returns [const cql3::operator_type* op = nullptr]
|
||||
| '>' { $op = &cql3::operator_type::GT; }
|
||||
| '>=' { $op = &cql3::operator_type::GTE; }
|
||||
| '!=' { $op = &cql3::operator_type::NEQ; }
|
||||
| K_LIKE { $op = &cql3::operator_type::LIKE; }
|
||||
;
|
||||
|
||||
relation[std::vector<cql3::relation_ptr>& clauses]
|
||||
@@ -1541,10 +1537,6 @@ tupleOfIdentifiers returns [std::vector<::shared_ptr<cql3::column_identifier::ra
|
||||
: '(' n1=cident { $ids.push_back(n1); } (',' ni=cident { $ids.push_back(ni); })* ')'
|
||||
;
|
||||
|
||||
listOfIdentifiers returns [std::vector<::shared_ptr<cql3::column_identifier::raw>> ids]
|
||||
: n1=cident { $ids.push_back(n1); } (',' ni=cident { $ids.push_back(ni); })*
|
||||
;
|
||||
|
||||
singleColumnInValues returns [std::vector<::shared_ptr<cql3::term::raw>> terms]
|
||||
: '(' ( t1 = term { $terms.push_back(t1); } (',' ti=term { $terms.push_back(ti); })* )? ')'
|
||||
;
|
||||
@@ -1665,10 +1657,9 @@ tuple_type [bool internal] returns [shared_ptr<cql3::cql3_type::raw> t]
|
||||
'>' { $t = cql3::cql3_type::raw::tuple(std::move(types)); }
|
||||
;
|
||||
|
||||
username returns [sstring str]
|
||||
: t=IDENT { $str = $t.text; }
|
||||
| t=STRING_LITERAL { $str = $t.text; }
|
||||
| s=unreserved_keyword { $str = s; }
|
||||
username
|
||||
: IDENT
|
||||
| STRING_LITERAL
|
||||
| QUOTED_NAME { add_recognition_error("Quoted strings are not supported for user names"); }
|
||||
;
|
||||
|
||||
@@ -1737,10 +1728,8 @@ basic_unreserved_keyword returns [sstring str]
|
||||
| K_JSON
|
||||
| K_CACHE
|
||||
| K_BYPASS
|
||||
| K_LIKE
|
||||
| K_PER
|
||||
| K_PARTITION
|
||||
| K_GROUP
|
||||
) { $str = $k.text; }
|
||||
;
|
||||
|
||||
@@ -1892,10 +1881,6 @@ K_PARTITION: P A R T I T I O N;
|
||||
K_SCYLLA_TIMEUUID_LIST_INDEX: S C Y L L A '_' T I M E U U I D '_' L I S T '_' I N D E X;
|
||||
K_SCYLLA_COUNTER_SHARD_LIST: S C Y L L A '_' C O U N T E R '_' S H A R D '_' L I S T;
|
||||
|
||||
K_GROUP: G R O U P;
|
||||
|
||||
K_LIKE: L I K E;
|
||||
|
||||
// Case-insensitive alpha characters
|
||||
fragment A: ('a'|'A');
|
||||
fragment B: ('b'|'B');
|
||||
|
||||
@@ -72,11 +72,11 @@ abstract_marker::raw::raw(int32_t bind_index)
|
||||
if (receiver_type == nullptr) {
|
||||
return ::make_shared<constants::marker>(_bind_index, receiver);
|
||||
}
|
||||
if (receiver_type->get_kind() == abstract_type::kind::list) {
|
||||
if (&receiver_type->_kind == &collection_type_impl::kind::list) {
|
||||
return ::make_shared<lists::marker>(_bind_index, receiver);
|
||||
} else if (receiver_type->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&receiver_type->_kind == &collection_type_impl::kind::set) {
|
||||
return ::make_shared<sets::marker>(_bind_index, receiver);
|
||||
} else if (receiver_type->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&receiver_type->_kind == &collection_type_impl::kind::map) {
|
||||
return ::make_shared<maps::marker>(_bind_index, receiver);
|
||||
}
|
||||
assert(0);
|
||||
|
||||
@@ -124,13 +124,13 @@ column_condition::raw::prepare(database& db, const sstring& keyspace, const colu
|
||||
|
||||
shared_ptr<column_specification> element_spec, value_spec;
|
||||
auto ctype = static_cast<const collection_type_impl*>(receiver.type.get());
|
||||
if (ctype->get_kind() == abstract_type::kind::list) {
|
||||
if (&ctype->_kind == &collection_type_impl::kind::list) {
|
||||
element_spec = lists::index_spec_of(receiver.column_specification);
|
||||
value_spec = lists::value_spec_of(receiver.column_specification);
|
||||
} else if (ctype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::map) {
|
||||
element_spec = maps::key_spec_of(*receiver.column_specification);
|
||||
value_spec = maps::value_spec_of(*receiver.column_specification);
|
||||
} else if (ctype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::set) {
|
||||
throw exceptions::invalid_request_exception(format("Invalid element access syntax for set column {}", receiver.name()));
|
||||
} else {
|
||||
abort();
|
||||
|
||||
@@ -79,16 +79,16 @@ public:
|
||||
}
|
||||
|
||||
virtual bool is_duration() const override {
|
||||
return _type.get_type() == duration_type;
|
||||
return _type.get_type()->equals(duration_type);
|
||||
}
|
||||
};
|
||||
|
||||
class cql3_type::raw_collection : public raw {
|
||||
const abstract_type::kind _kind;
|
||||
const collection_type_impl::kind* _kind;
|
||||
shared_ptr<raw> _keys;
|
||||
shared_ptr<raw> _values;
|
||||
public:
|
||||
raw_collection(const abstract_type::kind kind, shared_ptr<raw> keys, shared_ptr<raw> values)
|
||||
raw_collection(const collection_type_impl::kind* kind, shared_ptr<raw> keys, shared_ptr<raw> values)
|
||||
: _kind(kind), _keys(std::move(keys)), _values(std::move(values)) {
|
||||
}
|
||||
|
||||
@@ -126,14 +126,14 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if (_kind == abstract_type::kind::list) {
|
||||
if (_kind == &collection_type_impl::kind::list) {
|
||||
return cql3_type(list_type_impl::get_instance(_values->prepare_internal(keyspace, user_types).get_type(), !_frozen));
|
||||
} else if (_kind == abstract_type::kind::set) {
|
||||
} else if (_kind == &collection_type_impl::kind::set) {
|
||||
if (_values->is_duration()) {
|
||||
throw exceptions::invalid_request_exception(format("Durations are not allowed inside sets: {}", *this));
|
||||
}
|
||||
return cql3_type(set_type_impl::get_instance(_values->prepare_internal(keyspace, user_types).get_type(), !_frozen));
|
||||
} else if (_kind == abstract_type::kind::map) {
|
||||
} else if (_kind == &collection_type_impl::kind::map) {
|
||||
assert(_keys); // "Got null keys type for a collection";
|
||||
if (_keys->is_duration()) {
|
||||
throw exceptions::invalid_request_exception(format("Durations are not allowed as map keys: {}", *this));
|
||||
@@ -154,11 +154,11 @@ public:
|
||||
virtual sstring to_string() const override {
|
||||
sstring start = _frozen ? "frozen<" : "";
|
||||
sstring end = _frozen ? ">" : "";
|
||||
if (_kind == abstract_type::kind::list) {
|
||||
if (_kind == &collection_type_impl::kind::list) {
|
||||
return format("{}list<{}>{}", start, _values, end);
|
||||
} else if (_kind == abstract_type::kind::set) {
|
||||
} else if (_kind == &collection_type_impl::kind::set) {
|
||||
return format("{}set<{}>{}", start, _values, end);
|
||||
} else if (_kind == abstract_type::kind::map) {
|
||||
} else if (_kind == &collection_type_impl::kind::map) {
|
||||
return format("{}map<{}, {}>{}", start, _keys, _values, end);
|
||||
}
|
||||
abort();
|
||||
@@ -297,17 +297,17 @@ cql3_type::raw::user_type(ut_name name) {
|
||||
|
||||
shared_ptr<cql3_type::raw>
|
||||
cql3_type::raw::map(shared_ptr<raw> t1, shared_ptr<raw> t2) {
|
||||
return make_shared(raw_collection(abstract_type::kind::map, std::move(t1), std::move(t2)));
|
||||
return make_shared(raw_collection(&collection_type_impl::kind::map, std::move(t1), std::move(t2)));
|
||||
}
|
||||
|
||||
shared_ptr<cql3_type::raw>
|
||||
cql3_type::raw::list(shared_ptr<raw> t) {
|
||||
return make_shared(raw_collection(abstract_type::kind::list, {}, std::move(t)));
|
||||
return make_shared(raw_collection(&collection_type_impl::kind::list, {}, std::move(t)));
|
||||
}
|
||||
|
||||
shared_ptr<cql3_type::raw>
|
||||
cql3_type::raw::set(shared_ptr<raw> t) {
|
||||
return make_shared(raw_collection(abstract_type::kind::set, {}, std::move(t)));
|
||||
return make_shared(raw_collection(&collection_type_impl::kind::set, {}, std::move(t)));
|
||||
}
|
||||
|
||||
shared_ptr<cql3_type::raw>
|
||||
|
||||
@@ -131,10 +131,6 @@ functions::init() {
|
||||
declare(aggregate_fcts::make_max_function<bytes>());
|
||||
declare(aggregate_fcts::make_min_function<bytes>());
|
||||
|
||||
declare(aggregate_fcts::make_count_function<bool>());
|
||||
declare(aggregate_fcts::make_max_function<bool>());
|
||||
declare(aggregate_fcts::make_min_function<bool>());
|
||||
|
||||
// FIXME: more count/min/max
|
||||
|
||||
declare(make_varchar_as_blob_fct());
|
||||
@@ -198,7 +194,7 @@ make_from_json_function(database& db, const sstring& keyspace, data_type t) {
|
||||
if (!json_value.isNull()) {
|
||||
parsed_json_value.emplace(t->from_json_object(json_value, sf));
|
||||
}
|
||||
return parsed_json_value;
|
||||
return std::move(parsed_json_value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -472,11 +468,11 @@ function_call::make_terminal(shared_ptr<function> fun, cql3::raw_value result, c
|
||||
if (result) {
|
||||
res = fragmented_temporary_buffer::view(bytes_view(*result));
|
||||
}
|
||||
if (ctype->get_kind() == abstract_type::kind::list) {
|
||||
if (&ctype->_kind == &collection_type_impl::kind::list) {
|
||||
return make_shared(lists::value::from_serialized(std::move(res), static_pointer_cast<const list_type_impl>(ctype), sf));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::set) {
|
||||
return make_shared(sets::value::from_serialized(std::move(res), static_pointer_cast<const set_type_impl>(ctype), sf));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::map) {
|
||||
return make_shared(maps::value::from_serialized(std::move(res), static_pointer_cast<const map_type_impl>(ctype), sf));
|
||||
}
|
||||
abort();
|
||||
@@ -555,7 +551,7 @@ function_call::raw::test_assignment(database& db, const sstring& keyspace, share
|
||||
// later with a more helpful error message that if we were to return false here.
|
||||
try {
|
||||
auto&& fun = functions::get(db, keyspace, _name, _terms, receiver->ks_name, receiver->cf_name, receiver);
|
||||
if (fun && receiver->type == fun->return_type()) {
|
||||
if (fun && receiver->type->equals(fun->return_type())) {
|
||||
return assignment_testable::test_result::EXACT_MATCH;
|
||||
} else if (!fun || receiver->type->is_value_compatible_with(*fun->return_type())) {
|
||||
return assignment_testable::test_result::WEAKLY_ASSIGNABLE;
|
||||
|
||||
@@ -61,6 +61,16 @@ make_now_fct() {
|
||||
});
|
||||
}
|
||||
|
||||
static int64_t get_valid_timestamp(const data_value& ts_obj) {
|
||||
auto ts = value_cast<db_clock::time_point>(ts_obj);
|
||||
int64_t ms = ts.time_since_epoch().count();
|
||||
auto nanos_since = utils::UUID_gen::make_nanos_since(ms);
|
||||
if (!utils::UUID_gen::is_valid_nanos_since(nanos_since)) {
|
||||
throw exceptions::server_exception(format("{}: timestamp is out of range. Must be in milliseconds since epoch", ms));
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
inline
|
||||
shared_ptr<function>
|
||||
make_min_timeuuid_fct() {
|
||||
@@ -74,8 +84,7 @@ make_min_timeuuid_fct() {
|
||||
if (ts_obj.is_null()) {
|
||||
return {};
|
||||
}
|
||||
auto ts = value_cast<db_clock::time_point>(ts_obj);
|
||||
auto uuid = utils::UUID_gen::min_time_UUID(ts.time_since_epoch().count());
|
||||
auto uuid = utils::UUID_gen::min_time_UUID(get_valid_timestamp(ts_obj));
|
||||
return {timeuuid_type->decompose(uuid)};
|
||||
});
|
||||
}
|
||||
@@ -85,7 +94,6 @@ shared_ptr<function>
|
||||
make_max_timeuuid_fct() {
|
||||
return make_native_scalar_function<true>("maxtimeuuid", timeuuid_type, { timestamp_type },
|
||||
[] (cql_serialization_format sf, const std::vector<bytes_opt>& values) -> bytes_opt {
|
||||
// FIXME: should values be a vector<optional<bytes>>?
|
||||
auto& bb = values[0];
|
||||
if (!bb) {
|
||||
return {};
|
||||
@@ -94,12 +102,22 @@ make_max_timeuuid_fct() {
|
||||
if (ts_obj.is_null()) {
|
||||
return {};
|
||||
}
|
||||
auto ts = value_cast<db_clock::time_point>(ts_obj);
|
||||
auto uuid = utils::UUID_gen::max_time_UUID(ts.time_since_epoch().count());
|
||||
auto uuid = utils::UUID_gen::max_time_UUID(get_valid_timestamp(ts_obj));
|
||||
return {timeuuid_type->decompose(uuid)};
|
||||
});
|
||||
}
|
||||
|
||||
inline utils::UUID get_valid_timeuuid(bytes raw) {
|
||||
if (!utils::UUID_gen::is_valid_UUID(raw)) {
|
||||
throw exceptions::server_exception(format("invalid timeuuid: size={}", raw.size()));
|
||||
}
|
||||
auto uuid = utils::UUID_gen::get_UUID(raw);
|
||||
if (!uuid.is_timestamp()) {
|
||||
throw exceptions::server_exception(format("{}: Not a timeuuid: version={}", uuid, uuid.version()));
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
inline
|
||||
shared_ptr<function>
|
||||
make_date_of_fct() {
|
||||
@@ -110,7 +128,7 @@ make_date_of_fct() {
|
||||
if (!bb) {
|
||||
return {};
|
||||
}
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(UUID_gen::get_UUID(*bb))));
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(get_valid_timeuuid(*bb))));
|
||||
return {timestamp_type->decompose(ts)};
|
||||
});
|
||||
}
|
||||
@@ -125,7 +143,7 @@ make_unix_timestamp_of_fct() {
|
||||
if (!bb) {
|
||||
return {};
|
||||
}
|
||||
return {long_type->decompose(UUID_gen::unix_timestamp(UUID_gen::get_UUID(*bb)))};
|
||||
return {long_type->decompose(UUID_gen::unix_timestamp(get_valid_timeuuid(*bb)))};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,7 +194,7 @@ make_timeuuidtodate_fct() {
|
||||
if (!bb) {
|
||||
return {};
|
||||
}
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(UUID_gen::get_UUID(*bb))));
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(get_valid_timeuuid(*bb))));
|
||||
auto to_simple_date = get_castas_fctn(simple_date_type, timestamp_type);
|
||||
return {simple_date_type->decompose(to_simple_date(ts))};
|
||||
});
|
||||
@@ -211,7 +229,7 @@ make_timeuuidtotimestamp_fct() {
|
||||
if (!bb) {
|
||||
return {};
|
||||
}
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(UUID_gen::get_UUID(*bb))));
|
||||
auto ts = db_clock::time_point(db_clock::duration(UUID_gen::unix_timestamp(get_valid_timeuuid(*bb))));
|
||||
return {timestamp_type->decompose(ts)};
|
||||
});
|
||||
}
|
||||
@@ -245,10 +263,14 @@ make_timeuuidtounixtimestamp_fct() {
|
||||
if (!bb) {
|
||||
return {};
|
||||
}
|
||||
return {long_type->decompose(UUID_gen::unix_timestamp(UUID_gen::get_UUID(*bb)))};
|
||||
return {long_type->decompose(UUID_gen::unix_timestamp(get_valid_timeuuid(*bb)))};
|
||||
});
|
||||
}
|
||||
|
||||
inline bytes time_point_to_long(const data_value& v) {
|
||||
return data_value(get_valid_timestamp(v)).serialize();
|
||||
}
|
||||
|
||||
inline
|
||||
shared_ptr<function>
|
||||
make_timestamptounixtimestamp_fct() {
|
||||
@@ -263,7 +285,7 @@ make_timestamptounixtimestamp_fct() {
|
||||
if (ts_obj.is_null()) {
|
||||
return {};
|
||||
}
|
||||
return {long_type->decompose(ts_obj)};
|
||||
return time_point_to_long(ts_obj);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,7 +304,7 @@ make_datetounixtimestamp_fct() {
|
||||
return {};
|
||||
}
|
||||
auto from_simple_date = get_castas_fctn(timestamp_type, simple_date_type);
|
||||
return {long_type->decompose(from_simple_date(simple_date_obj))};
|
||||
return time_point_to_long(from_simple_date(simple_date_obj));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -188,11 +188,6 @@ protected:
|
||||
throw exceptions::invalid_request_exception(format("{} cannot be used for Multi-column relations", get_operator()));
|
||||
}
|
||||
|
||||
virtual ::shared_ptr<restrictions::restriction> new_LIKE_restriction(
|
||||
database& db, schema_ptr schema, ::shared_ptr<variable_specifications> bound_names) override {
|
||||
throw exceptions::invalid_request_exception("LIKE cannot be used for Multi-column relations");
|
||||
}
|
||||
|
||||
virtual ::shared_ptr<relation> maybe_rename_identifier(const column_identifier::raw& from, column_identifier::raw to) override {
|
||||
auto new_entities = boost::copy_range<decltype(_entities)>(_entities | boost::adaptors::transformed([&] (auto&& entity) {
|
||||
return *entity == from ? ::make_shared<column_identifier::raw>(to) : entity;
|
||||
|
||||
@@ -65,7 +65,7 @@ operation::set_element::prepare(database& db, const sstring& keyspace, const col
|
||||
throw invalid_request_exception(format("Invalid operation ({}) for frozen collection column {}", to_string(receiver), receiver.name()));
|
||||
}
|
||||
|
||||
if (rtype->get_kind() == abstract_type::kind::list) {
|
||||
if (&rtype->_kind == &collection_type_impl::kind::list) {
|
||||
auto&& lval = _value->prepare(db, keyspace, lists::value_spec_of(receiver.column_specification));
|
||||
if (_by_uuid) {
|
||||
auto&& idx = _selector->prepare(db, keyspace, lists::uuid_index_spec_of(receiver.column_specification));
|
||||
@@ -74,9 +74,9 @@ operation::set_element::prepare(database& db, const sstring& keyspace, const col
|
||||
auto&& idx = _selector->prepare(db, keyspace, lists::index_spec_of(receiver.column_specification));
|
||||
return make_shared<lists::setter_by_index>(receiver, idx, lval);
|
||||
}
|
||||
} else if (rtype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&rtype->_kind == &collection_type_impl::kind::set) {
|
||||
throw invalid_request_exception(format("Invalid operation ({}) for set column {}", to_string(receiver), receiver.name()));
|
||||
} else if (rtype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&rtype->_kind == &collection_type_impl::kind::map) {
|
||||
auto key = _selector->prepare(db, keyspace, maps::key_spec_of(*receiver.column_specification));
|
||||
auto mval = _value->prepare(db, keyspace, maps::value_spec_of(*receiver.column_specification));
|
||||
return make_shared<maps::setter_by_key>(receiver, key, mval);
|
||||
@@ -110,11 +110,11 @@ operation::addition::prepare(database& db, const sstring& keyspace, const column
|
||||
throw exceptions::invalid_request_exception(format("Invalid operation ({}) for frozen collection column {}", to_string(receiver), receiver.name()));
|
||||
}
|
||||
|
||||
if (ctype->get_kind() == abstract_type::kind::list) {
|
||||
if (&ctype->_kind == &collection_type_impl::kind::list) {
|
||||
return make_shared<lists::appender>(receiver, v);
|
||||
} else if (ctype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::set) {
|
||||
return make_shared<sets::adder>(receiver, v);
|
||||
} else if (ctype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::map) {
|
||||
return make_shared<maps::putter>(receiver, v);
|
||||
} else {
|
||||
abort();
|
||||
@@ -146,11 +146,11 @@ operation::subtraction::prepare(database& db, const sstring& keyspace, const col
|
||||
format("Invalid operation ({}) for frozen collection column {}", to_string(receiver), receiver.name()));
|
||||
}
|
||||
|
||||
if (ctype->get_kind() == abstract_type::kind::list) {
|
||||
if (&ctype->_kind == &collection_type_impl::kind::list) {
|
||||
return make_shared<lists::discarder>(receiver, _value->prepare(db, keyspace, receiver.column_specification));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::set) {
|
||||
return make_shared<sets::discarder>(receiver, _value->prepare(db, keyspace, receiver.column_specification));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::map) {
|
||||
auto&& mtype = dynamic_pointer_cast<const map_type_impl>(ctype);
|
||||
// The value for a map subtraction is actually a set
|
||||
auto&& vr = make_shared<column_specification>(
|
||||
@@ -204,12 +204,12 @@ operation::set_value::prepare(database& db, const sstring& keyspace, const colum
|
||||
return ::make_shared<constants::setter>(receiver, v);
|
||||
}
|
||||
|
||||
auto k = static_pointer_cast<const collection_type_impl>(receiver.type)->get_kind();
|
||||
if (k == abstract_type::kind::list) {
|
||||
auto& k = static_pointer_cast<const collection_type_impl>(receiver.type)->_kind;
|
||||
if (&k == &collection_type_impl::kind::list) {
|
||||
return make_shared<lists::setter>(receiver, v);
|
||||
} else if (k == abstract_type::kind::set) {
|
||||
} else if (&k == &collection_type_impl::kind::set) {
|
||||
return make_shared<sets::setter>(receiver, v);
|
||||
} else if (k == abstract_type::kind::map) {
|
||||
} else if (&k == &collection_type_impl::kind::map) {
|
||||
return make_shared<maps::setter>(receiver, v);
|
||||
} else {
|
||||
abort();
|
||||
@@ -308,13 +308,13 @@ operation::element_deletion::prepare(database& db, const sstring& keyspace, cons
|
||||
throw exceptions::invalid_request_exception(format("Invalid deletion operation for frozen collection column {}", receiver.name()));
|
||||
}
|
||||
auto ctype = static_pointer_cast<const collection_type_impl>(receiver.type);
|
||||
if (ctype->get_kind() == abstract_type::kind::list) {
|
||||
if (&ctype->_kind == &collection_type_impl::kind::list) {
|
||||
auto&& idx = _element->prepare(db, keyspace, lists::index_spec_of(receiver.column_specification));
|
||||
return make_shared<lists::discarder_by_index>(receiver, std::move(idx));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::set) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::set) {
|
||||
auto&& elt = _element->prepare(db, keyspace, sets::value_spec_of(receiver.column_specification));
|
||||
return make_shared<sets::element_discarder>(receiver, std::move(elt));
|
||||
} else if (ctype->get_kind() == abstract_type::kind::map) {
|
||||
} else if (&ctype->_kind == &collection_type_impl::kind::map) {
|
||||
auto&& key = _element->prepare(db, keyspace, maps::key_spec_of(*receiver.column_specification));
|
||||
return make_shared<maps::discarder_by_key>(receiver, std::move(key));
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@ const operator_type operator_type::CONTAINS(5, operator_type::CONTAINS, "CONTAIN
|
||||
const operator_type operator_type::CONTAINS_KEY(6, operator_type::CONTAINS_KEY, "CONTAINS_KEY");
|
||||
const operator_type operator_type::NEQ(8, operator_type::NEQ, "!=");
|
||||
const operator_type operator_type::IS_NOT(9, operator_type::IS_NOT, "IS NOT");
|
||||
const operator_type operator_type::LIKE(10, operator_type::LIKE, "LIKE");
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ public:
|
||||
static const operator_type CONTAINS_KEY;
|
||||
static const operator_type NEQ;
|
||||
static const operator_type IS_NOT;
|
||||
static const operator_type LIKE;
|
||||
private:
|
||||
int32_t _b;
|
||||
const operator_type& _reverse;
|
||||
|
||||
@@ -69,7 +69,7 @@ const std::chrono::minutes prepared_statements_cache::entry_expiry = std::chrono
|
||||
class query_processor::internal_state {
|
||||
service::query_state _qs;
|
||||
public:
|
||||
internal_state() : _qs(service::client_state{service::client_state::internal_tag()}, empty_service_permit()) {
|
||||
internal_state() : _qs(service::client_state{service::client_state::internal_tag()}) {
|
||||
}
|
||||
operator service::query_state&() {
|
||||
return _qs;
|
||||
|
||||
@@ -160,8 +160,6 @@ public:
|
||||
// This case is not supposed to happen: statement_restrictions
|
||||
// constructor does not call this function for views' IS_NOT.
|
||||
throw exceptions::invalid_request_exception(format("Unsupported \"IS NOT\" relation: {}", to_string()));
|
||||
} else if (_relation_type == operator_type::LIKE) {
|
||||
return new_LIKE_restriction(db, schema, bound_names);
|
||||
} else {
|
||||
throw exceptions::invalid_request_exception(format("Unsupported \"!=\" relation: {}", to_string()));
|
||||
}
|
||||
@@ -222,12 +220,6 @@ public:
|
||||
virtual ::shared_ptr<restrictions::restriction> new_contains_restriction(database& db, schema_ptr schema,
|
||||
::shared_ptr<variable_specifications> bound_names, bool isKey) = 0;
|
||||
|
||||
/**
|
||||
* Creates a new LIKE restriction instance.
|
||||
*/
|
||||
virtual ::shared_ptr<restrictions::restriction> new_LIKE_restriction(database& db, schema_ptr schema,
|
||||
::shared_ptr<variable_specifications> bound_names) = 0;
|
||||
|
||||
/**
|
||||
* Renames an identifier in this Relation, if applicable.
|
||||
* @param from the old identifier
|
||||
|
||||
160
cql3/restrictions/abstract_restriction.hh
Normal file
160
cql3/restrictions/abstract_restriction.hh
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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) 2015 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 <vector>
|
||||
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
#include <seastar/core/sstring.hh>
|
||||
#include "cql3/restrictions/restriction.hh"
|
||||
#include "cql3/term.hh"
|
||||
#include "types.hh"
|
||||
|
||||
namespace cql3 {
|
||||
|
||||
namespace restrictions {
|
||||
|
||||
/**
|
||||
* Base class for <code>Restriction</code>s
|
||||
*/
|
||||
class abstract_restriction : public restriction {
|
||||
public:
|
||||
virtual bool is_on_token() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_multi_column() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_slice() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_EQ() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_IN() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_contains() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool has_bound(statements::bound b) const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> bounds(statements::bound b, const query_options& options) const override {
|
||||
return values(options);
|
||||
}
|
||||
|
||||
virtual bool is_inclusive(statements::bound b) const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the specified row satisfied this restriction.
|
||||
* Assumes the row is live, but not all cells. If a cell
|
||||
* isn't live and there's a restriction on its column,
|
||||
* then the function returns false.
|
||||
*
|
||||
* @param schema the schema the row belongs to
|
||||
* @param key the partition key
|
||||
* @param ckey the clustering key
|
||||
* @param cells the remaining row columns
|
||||
* @return the restriction resulting of the merge
|
||||
* @throws InvalidRequestException if the restrictions cannot be merged
|
||||
*/
|
||||
virtual bool is_satisfied_by(const schema& schema,
|
||||
const partition_key& key,
|
||||
const clustering_key_prefix& ckey,
|
||||
const row& cells,
|
||||
const query_options& options,
|
||||
gc_clock::time_point now) const = 0;
|
||||
|
||||
protected:
|
||||
#if 0
|
||||
protected static ByteBuffer validateIndexedValue(ColumnSpecification columnSpec,
|
||||
ByteBuffer value)
|
||||
throws InvalidRequestException
|
||||
{
|
||||
checkNotNull(value, "Unsupported null value for indexed column %s", columnSpec.name);
|
||||
checkFalse(value.remaining() > 0xFFFF, "Index expression values may not be larger than 64K");
|
||||
return value;
|
||||
}
|
||||
#endif
|
||||
/**
|
||||
* Checks if the specified term is using the specified function.
|
||||
*
|
||||
* @param term the term to check
|
||||
* @param ks_name the function keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if the specified term is using the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
static bool term_uses_function(::shared_ptr<term> term, const sstring& ks_name, const sstring& function_name) {
|
||||
return bool(term) && term->uses_function(ks_name, function_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one of the specified term is using the specified function.
|
||||
*
|
||||
* @param terms the terms to check
|
||||
* @param ks_name the function keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if one of the specified term is using the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
static bool term_uses_function(const std::vector<::shared_ptr<term>>& terms, const sstring& ks_name, const sstring& function_name) {
|
||||
for (auto&& value : terms) {
|
||||
if (term_uses_function(value, ks_name, function_name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,14 +61,17 @@ protected:
|
||||
schema_ptr _schema;
|
||||
std::vector<const column_definition*> _column_defs;
|
||||
public:
|
||||
multi_column_restriction(op op, schema_ptr schema, std::vector<const column_definition*>&& defs)
|
||||
: clustering_key_restrictions(op, target::MULTIPLE_COLUMNS)
|
||||
, _schema(schema)
|
||||
multi_column_restriction(schema_ptr schema, std::vector<const column_definition*>&& defs)
|
||||
: _schema(schema)
|
||||
, _column_defs(std::move(defs))
|
||||
{
|
||||
update_asc_desc_existence();
|
||||
}
|
||||
|
||||
virtual bool is_multi_column() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<const column_definition*> get_column_defs() const override {
|
||||
return _column_defs;
|
||||
}
|
||||
@@ -208,12 +211,12 @@ private:
|
||||
::shared_ptr<term> _value;
|
||||
public:
|
||||
EQ(schema_ptr schema, std::vector<const column_definition*> defs, ::shared_ptr<term> value)
|
||||
: multi_column_restriction(op::EQ, schema, std::move(defs))
|
||||
: multi_column_restriction(schema, std::move(defs))
|
||||
, _value(std::move(value))
|
||||
{ }
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_value, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_value, ks_name, function_name);
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const override {
|
||||
@@ -282,9 +285,7 @@ public:
|
||||
|
||||
class multi_column_restriction::IN : public multi_column_restriction {
|
||||
public:
|
||||
IN(schema_ptr schema, std::vector<const column_definition*> defs)
|
||||
: multi_column_restriction(op::IN, schema, std::move(defs))
|
||||
{ }
|
||||
using multi_column_restriction::multi_column_restriction;
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const override {
|
||||
for (auto* cdef : _column_defs) {
|
||||
@@ -295,6 +296,10 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_IN() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<clustering_key_prefix> values_as_keys(const query_options& options) const override {
|
||||
auto split_in_values = split_values(options);
|
||||
std::vector<clustering_key_prefix> keys;
|
||||
@@ -377,7 +382,7 @@ public:
|
||||
{ }
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_values, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_values, ks_name, function_name);
|
||||
}
|
||||
|
||||
virtual sstring to_string() const override {
|
||||
@@ -431,7 +436,7 @@ private:
|
||||
term_slice _slice;
|
||||
|
||||
slice(schema_ptr schema, std::vector<const column_definition*> defs, term_slice slice)
|
||||
: multi_column_restriction(op::SLICE, schema, std::move(defs))
|
||||
: multi_column_restriction(schema, std::move(defs))
|
||||
, _slice(slice)
|
||||
{ }
|
||||
public:
|
||||
@@ -448,6 +453,10 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_slice() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<clustering_key_prefix> values_as_keys(const query_options&) const override {
|
||||
throw exceptions::unsupported_operation_exception();
|
||||
}
|
||||
@@ -494,8 +503,8 @@ public:
|
||||
}
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return (_slice.has_bound(statements::bound::START) && restriction::term_uses_function(_slice.bound(statements::bound::START), ks_name, function_name))
|
||||
|| (_slice.has_bound(statements::bound::END) && restriction::term_uses_function(_slice.bound(statements::bound::END), ks_name, function_name));
|
||||
return (_slice.has_bound(statements::bound::START) && abstract_restriction::term_uses_function(_slice.bound(statements::bound::START), ks_name, function_name))
|
||||
|| (_slice.has_bound(statements::bound::END) && abstract_restriction::term_uses_function(_slice.bound(statements::bound::END), ks_name, function_name));
|
||||
}
|
||||
|
||||
virtual bool is_inclusive(statements::bound b) const override {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
#include "cql3/statements/bound.hh"
|
||||
#include "cql3/restrictions/restrictions.hh"
|
||||
#include "cql3/restrictions/restriction.hh"
|
||||
#include "cql3/restrictions/restriction.hh"
|
||||
#include "cql3/restrictions/abstract_restriction.hh"
|
||||
#include "types.hh"
|
||||
#include "query-request.hh"
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
@@ -63,13 +63,10 @@ namespace restrictions {
|
||||
* implementations of methods).
|
||||
*/
|
||||
|
||||
class partition_key_restrictions: public restriction, public restrictions, public enable_shared_from_this<partition_key_restrictions> {
|
||||
class partition_key_restrictions: public abstract_restriction, public restrictions, public enable_shared_from_this<partition_key_restrictions> {
|
||||
public:
|
||||
using bounds_range_type = dht::partition_range;
|
||||
|
||||
partition_key_restrictions() = default;
|
||||
partition_key_restrictions(op op, target target) : restriction(op, target) {}
|
||||
|
||||
virtual ::shared_ptr<partition_key_restrictions> merge_to(schema_ptr, ::shared_ptr<restriction> restriction) {
|
||||
merge_with(restriction);
|
||||
return this->shared_from_this();
|
||||
@@ -94,8 +91,7 @@ public:
|
||||
}
|
||||
|
||||
virtual bool needs_filtering(const schema& schema) const {
|
||||
return !empty() && !is_on_token() &&
|
||||
(has_unrestricted_components(schema) || is_contains() || is_slice() || is_LIKE());
|
||||
return !empty() && !is_on_token() && (has_unrestricted_components(schema) || is_contains() || is_slice());
|
||||
}
|
||||
|
||||
// NOTICE(sarna): This function is useless for partition key restrictions,
|
||||
@@ -117,13 +113,10 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class clustering_key_restrictions : public restriction, public restrictions, public enable_shared_from_this<clustering_key_restrictions> {
|
||||
class clustering_key_restrictions : public abstract_restriction, public restrictions, public enable_shared_from_this<clustering_key_restrictions> {
|
||||
public:
|
||||
using bounds_range_type = query::clustering_range;
|
||||
|
||||
clustering_key_restrictions() = default;
|
||||
clustering_key_restrictions(op op, target target) : restriction(op, target) {}
|
||||
|
||||
virtual ::shared_ptr<clustering_key_restrictions> merge_to(schema_ptr, ::shared_ptr<restriction> restriction) {
|
||||
merge_with(restriction);
|
||||
return this->shared_from_this();
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*
|
||||
* Modified by ScyllaDB
|
||||
*/
|
||||
@@ -43,12 +43,9 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
#include <seastar/core/sstring.hh>
|
||||
#include "cql3/query_options.hh"
|
||||
#include "cql3/term.hh"
|
||||
#include "cql3/statements/bound.hh"
|
||||
#include "index/secondary_index_manager.hh"
|
||||
#include "cql3/query_options.hh"
|
||||
#include "cql3/statements/bound.hh"
|
||||
#include "types.hh"
|
||||
|
||||
namespace cql3 {
|
||||
@@ -59,80 +56,51 @@ struct allow_local_index_tag {};
|
||||
using allow_local_index = bool_class<allow_local_index_tag>;
|
||||
|
||||
/**
|
||||
* Base class for <code>Restriction</code>s
|
||||
* A restriction/clause on a column.
|
||||
* The goal of this class being to group all conditions for a column in a SELECT.
|
||||
*/
|
||||
class restriction {
|
||||
public:
|
||||
enum class op {
|
||||
EQ, SLICE, IN, CONTAINS, LIKE
|
||||
};
|
||||
enum class target {
|
||||
SINGLE_COLUMN, MULTIPLE_COLUMNS, TOKEN
|
||||
};
|
||||
protected:
|
||||
using op_enum = super_enum<restriction::op, restriction::op::EQ, restriction::op::SLICE, restriction::op::IN, restriction::op::CONTAINS, restriction::op::LIKE>;
|
||||
enum_set<op_enum> _ops;
|
||||
target _target = target::SINGLE_COLUMN;
|
||||
public:
|
||||
virtual ~restriction() {}
|
||||
virtual bool is_on_token() const = 0;
|
||||
virtual bool is_slice() const = 0;
|
||||
virtual bool is_EQ() const = 0;
|
||||
virtual bool is_IN() const = 0;
|
||||
virtual bool is_contains() const = 0;
|
||||
virtual bool is_multi_column() const = 0;
|
||||
|
||||
restriction() = default;
|
||||
explicit restriction(op op) : _target(target::SINGLE_COLUMN) {
|
||||
_ops.set(op);
|
||||
virtual std::vector<bytes_opt> values(const query_options& options) const = 0;
|
||||
|
||||
virtual bytes_opt value(const query_options& options) const {
|
||||
auto vec = values(options);
|
||||
assert(vec.size() == 1);
|
||||
return std::move(vec[0]);
|
||||
}
|
||||
|
||||
restriction(op op, target target) : _target(target) {
|
||||
_ops.set(op);
|
||||
}
|
||||
|
||||
bool is_on_token() const {
|
||||
return _target == target::TOKEN;
|
||||
}
|
||||
|
||||
bool is_multi_column() const {
|
||||
return _target == target::MULTIPLE_COLUMNS;
|
||||
}
|
||||
|
||||
bool is_slice() const {
|
||||
return _ops.contains(op::SLICE);
|
||||
}
|
||||
|
||||
bool is_EQ() const {
|
||||
return _ops.contains(op::EQ);
|
||||
}
|
||||
|
||||
bool is_IN() const {
|
||||
return _ops.contains(op::IN);
|
||||
}
|
||||
|
||||
bool is_contains() const {
|
||||
return _ops.contains(op::CONTAINS);
|
||||
}
|
||||
|
||||
bool is_LIKE() const {
|
||||
return _ops.contains(op::LIKE);
|
||||
}
|
||||
|
||||
const enum_set<op_enum>& get_ops() const {
|
||||
return _ops;
|
||||
}
|
||||
/**
|
||||
* Returns <code>true</code> if one of the restrictions use the specified function.
|
||||
*
|
||||
* @param ks_name the keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if one of the restrictions use the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const = 0;
|
||||
|
||||
/**
|
||||
* Checks if the specified bound is set or not.
|
||||
* @param b the bound type
|
||||
* @return <code>true</code> if the specified bound is set, <code>false</code> otherwise
|
||||
*/
|
||||
virtual bool has_bound(statements::bound b) const {
|
||||
return true;
|
||||
}
|
||||
virtual bool has_bound(statements::bound b) const = 0;
|
||||
|
||||
virtual std::vector<bytes_opt> bounds(statements::bound b, const query_options& options) const {
|
||||
return values(options);
|
||||
}
|
||||
virtual std::vector<bytes_opt> bounds(statements::bound b, const query_options& options) const = 0;
|
||||
|
||||
virtual bool is_inclusive(statements::bound b) const {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Checks if the specified bound is inclusive or not.
|
||||
* @param b the bound type
|
||||
* @return <code>true</code> if the specified bound is inclusive, <code>false</code> otherwise
|
||||
*/
|
||||
virtual bool is_inclusive(statements::bound b) const = 0;
|
||||
|
||||
/**
|
||||
* Merges this restriction with the specified one.
|
||||
@@ -151,74 +119,21 @@ public:
|
||||
*/
|
||||
virtual bool has_supporting_index(const secondary_index::secondary_index_manager& index_manager, allow_local_index allow_local) const = 0;
|
||||
|
||||
#if 0
|
||||
/**
|
||||
* Adds to the specified list the <code>IndexExpression</code>s corresponding to this <code>Restriction</code>.
|
||||
*
|
||||
* @param expressions the list to add the <code>IndexExpression</code>s to
|
||||
* @param options the query options
|
||||
* @throws InvalidRequestException if this <code>Restriction</code> cannot be converted into
|
||||
* <code>IndexExpression</code>s
|
||||
*/
|
||||
public void addIndexExpressionTo(List<IndexExpression> expressions,
|
||||
QueryOptions options)
|
||||
throws InvalidRequestException;
|
||||
#endif
|
||||
|
||||
virtual sstring to_string() const = 0;
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if one of the restrictions use the specified function.
|
||||
*
|
||||
* @param ks_name the keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if one of the restrictions use the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const = 0;
|
||||
|
||||
virtual std::vector<bytes_opt> values(const query_options& options) const = 0;
|
||||
|
||||
virtual bytes_opt value(const query_options& options) const {
|
||||
auto vec = values(options);
|
||||
assert(vec.size() == 1);
|
||||
return std::move(vec[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the specified row satisfied this restriction.
|
||||
* Assumes the row is live, but not all cells. If a cell
|
||||
* isn't live and there's a restriction on its column,
|
||||
* then the function returns false.
|
||||
*
|
||||
* @param schema the schema the row belongs to
|
||||
* @param key the partition key
|
||||
* @param ckey the clustering key
|
||||
* @param cells the remaining row columns
|
||||
* @return the restriction resulting of the merge
|
||||
* @throws InvalidRequestException if the restrictions cannot be merged
|
||||
*/
|
||||
virtual bool is_satisfied_by(const schema& schema,
|
||||
const partition_key& key,
|
||||
const clustering_key_prefix& ckey,
|
||||
const row& cells,
|
||||
const query_options& options,
|
||||
gc_clock::time_point now) const = 0;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Checks if the specified term is using the specified function.
|
||||
*
|
||||
* @param term the term to check
|
||||
* @param ks_name the function keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if the specified term is using the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
static bool term_uses_function(::shared_ptr<term> term, const sstring& ks_name, const sstring& function_name) {
|
||||
return bool(term) && term->uses_function(ks_name, function_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if one of the specified term is using the specified function.
|
||||
*
|
||||
* @param terms the terms to check
|
||||
* @param ks_name the function keyspace name
|
||||
* @param function_name the function name
|
||||
* @return <code>true</code> if one of the specified term is using the specified function, <code>false</code> otherwise.
|
||||
*/
|
||||
static bool term_uses_function(const std::vector<::shared_ptr<term>>& terms, const sstring& ks_name, const sstring& function_name) {
|
||||
for (auto&& value : terms) {
|
||||
if (term_uses_function(value, ks_name, function_name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -58,9 +58,6 @@ namespace restrictions {
|
||||
* Sets of restrictions
|
||||
*/
|
||||
class restrictions {
|
||||
protected:
|
||||
using op_enum = super_enum<restriction::op, restriction::op::EQ, restriction::op::SLICE, restriction::op::IN, restriction::op::CONTAINS, restriction::op::LIKE>;
|
||||
enum_set<op_enum> _ops;
|
||||
public:
|
||||
virtual ~restrictions() {}
|
||||
|
||||
|
||||
@@ -69,11 +69,17 @@ private:
|
||||
schema_ptr _schema;
|
||||
bool _allow_filtering;
|
||||
::shared_ptr<single_column_restrictions> _restrictions;
|
||||
bool _slice;
|
||||
bool _contains;
|
||||
bool _in;
|
||||
public:
|
||||
single_column_primary_key_restrictions(schema_ptr schema, bool allow_filtering)
|
||||
: _schema(schema)
|
||||
, _allow_filtering(allow_filtering)
|
||||
, _restrictions(::make_shared<single_column_restrictions>(schema))
|
||||
, _slice(false)
|
||||
, _contains(false)
|
||||
, _in(false)
|
||||
{ }
|
||||
|
||||
// Convert another primary key restrictions type into this type, possibly using different schema
|
||||
@@ -82,6 +88,9 @@ public:
|
||||
: _schema(schema)
|
||||
, _allow_filtering(other._allow_filtering)
|
||||
, _restrictions(::make_shared<single_column_restrictions>(schema))
|
||||
, _slice(other._slice)
|
||||
, _contains(other._contains)
|
||||
, _in(other._in)
|
||||
{
|
||||
for (const auto& entry : other._restrictions->restrictions()) {
|
||||
const column_definition* other_cdef = entry.first;
|
||||
@@ -94,6 +103,26 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool is_on_token() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_multi_column() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_slice() const override {
|
||||
return _slice;
|
||||
}
|
||||
|
||||
virtual bool is_contains() const override {
|
||||
return _contains;
|
||||
}
|
||||
|
||||
virtual bool is_IN() const override {
|
||||
return _in;
|
||||
}
|
||||
|
||||
virtual bool is_all_eq() const override {
|
||||
return _restrictions->is_all_eq();
|
||||
}
|
||||
@@ -115,7 +144,7 @@ public:
|
||||
auto last_column = *_restrictions->last_column();
|
||||
auto new_column = restriction->get_column_def();
|
||||
|
||||
if (this->is_slice() && _schema->position(new_column) > _schema->position(last_column)) {
|
||||
if (_slice && _schema->position(new_column) > _schema->position(last_column)) {
|
||||
throw exceptions::invalid_request_exception(format("Clustering column \"{}\" cannot be restricted (preceding column \"{}\" is restricted by a non-EQ relation)",
|
||||
new_column.name_as_text(), last_column.name_as_text()));
|
||||
}
|
||||
@@ -127,7 +156,10 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
restriction::_ops.add(restriction->get_ops());
|
||||
|
||||
_slice |= restriction->is_slice();
|
||||
_in |= restriction->is_IN();
|
||||
_contains |= restriction->is_contains();
|
||||
_restrictions->add_restriction(restriction);
|
||||
}
|
||||
|
||||
@@ -232,7 +264,7 @@ private:
|
||||
const column_definition* def = e.first;
|
||||
auto&& r = e.second;
|
||||
|
||||
if (vec_of_values.size() != _schema->position(*def) || r->is_contains() || r->is_LIKE()) {
|
||||
if (vec_of_values.size() != _schema->position(*def) || r->is_contains()) {
|
||||
// The prefixes built so far are the longest we can build,
|
||||
// the rest of the constraints will have to be applied using filtering.
|
||||
break;
|
||||
@@ -256,7 +288,7 @@ private:
|
||||
if (def->type->is_reversed()) {
|
||||
ranges.back().reverse();
|
||||
}
|
||||
return ranges;
|
||||
return std::move(ranges);
|
||||
}
|
||||
|
||||
ranges.reserve(cartesian_product_size(vec_of_values));
|
||||
@@ -285,7 +317,7 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
return std::move(ranges);
|
||||
}
|
||||
|
||||
auto values = r->values(options);
|
||||
@@ -305,7 +337,7 @@ private:
|
||||
ranges.emplace_back(range_type::make_singular(ValueType::from_optional_exploded(*_schema, std::move(prefix))));
|
||||
}
|
||||
|
||||
return ranges;
|
||||
return std::move(ranges);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cql3/restrictions/restriction.hh"
|
||||
#include "cql3/restrictions/abstract_restriction.hh"
|
||||
#include "cql3/restrictions/term_slice.hh"
|
||||
#include "cql3/term.hh"
|
||||
#include "cql3/abstract_marker.hh"
|
||||
@@ -56,16 +56,15 @@ namespace cql3 {
|
||||
|
||||
namespace restrictions {
|
||||
|
||||
class single_column_restriction : public restriction {
|
||||
class single_column_restriction : public abstract_restriction {
|
||||
protected:
|
||||
/**
|
||||
* The definition of the column to which apply the restriction.
|
||||
*/
|
||||
const column_definition& _column_def;
|
||||
public:
|
||||
single_column_restriction(op op, const column_definition& column_def)
|
||||
: restriction(op)
|
||||
, _column_def(column_def)
|
||||
single_column_restriction(const column_definition& column_def)
|
||||
: _column_def(column_def)
|
||||
{ }
|
||||
|
||||
const column_definition& get_column_def() const {
|
||||
@@ -97,7 +96,7 @@ public:
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const = 0;
|
||||
using restriction::is_satisfied_by;
|
||||
using abstract_restriction::is_satisfied_by;
|
||||
virtual bool is_satisfied_by(bytes_view data, const query_options& options) const = 0;
|
||||
virtual ::shared_ptr<single_column_restriction> apply_to(const column_definition& cdef) = 0;
|
||||
#if 0
|
||||
@@ -115,7 +114,7 @@ public:
|
||||
class IN;
|
||||
class IN_with_values;
|
||||
class IN_with_marker;
|
||||
class LIKE;
|
||||
|
||||
class slice;
|
||||
class contains;
|
||||
|
||||
@@ -132,18 +131,22 @@ private:
|
||||
::shared_ptr<term> _value;
|
||||
public:
|
||||
EQ(const column_definition& column_def, ::shared_ptr<term> value)
|
||||
: single_column_restriction(op::EQ, column_def)
|
||||
: single_column_restriction(column_def)
|
||||
, _value(std::move(value))
|
||||
{ }
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_value, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_value, ks_name, function_name);
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const override {
|
||||
return index.supports_expression(_column_def, cql3::operator_type::EQ);
|
||||
}
|
||||
|
||||
virtual bool is_EQ() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> values(const query_options& options) const override {
|
||||
std::vector<bytes_opt> v;
|
||||
v.push_back(to_bytes_opt(_value->bind_and_get(options)));
|
||||
@@ -185,9 +188,13 @@ public:
|
||||
class single_column_restriction::IN : public single_column_restriction {
|
||||
public:
|
||||
IN(const column_definition& column_def)
|
||||
: single_column_restriction(op::IN, column_def)
|
||||
: single_column_restriction(column_def)
|
||||
{ }
|
||||
|
||||
virtual bool is_IN() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const override {
|
||||
return index.supports_expression(_column_def, cql3::operator_type::IN);
|
||||
}
|
||||
@@ -234,7 +241,7 @@ public:
|
||||
{ }
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_values, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_values, ks_name, function_name);
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> values_raw(const query_options& options) const override {
|
||||
@@ -288,24 +295,28 @@ private:
|
||||
term_slice _slice;
|
||||
public:
|
||||
slice(const column_definition& column_def, statements::bound bound, bool inclusive, ::shared_ptr<term> term)
|
||||
: single_column_restriction(op::SLICE, column_def)
|
||||
: single_column_restriction(column_def)
|
||||
, _slice(term_slice::new_instance(bound, inclusive, std::move(term)))
|
||||
{ }
|
||||
|
||||
slice(const column_definition& column_def, term_slice slice)
|
||||
: single_column_restriction(op::SLICE, column_def)
|
||||
: single_column_restriction(column_def)
|
||||
, _slice(slice)
|
||||
{ }
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return (_slice.has_bound(statements::bound::START) && restriction::term_uses_function(_slice.bound(statements::bound::START), ks_name, function_name))
|
||||
|| (_slice.has_bound(statements::bound::END) && restriction::term_uses_function(_slice.bound(statements::bound::END), ks_name, function_name));
|
||||
return (_slice.has_bound(statements::bound::START) && abstract_restriction::term_uses_function(_slice.bound(statements::bound::START), ks_name, function_name))
|
||||
|| (_slice.has_bound(statements::bound::END) && abstract_restriction::term_uses_function(_slice.bound(statements::bound::END), ks_name, function_name));
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const override {
|
||||
return _slice.is_supported_by(_column_def, index);
|
||||
}
|
||||
|
||||
virtual bool is_slice() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> values(const query_options& options) const override {
|
||||
throw exceptions::unsupported_operation_exception();
|
||||
}
|
||||
@@ -381,51 +392,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class single_column_restriction::LIKE final : public single_column_restriction {
|
||||
private:
|
||||
::shared_ptr<term> _value;
|
||||
public:
|
||||
LIKE(const column_definition& column_def, ::shared_ptr<term> value)
|
||||
: single_column_restriction(op::LIKE, column_def)
|
||||
, _value(value)
|
||||
{ }
|
||||
|
||||
virtual std::vector<bytes_opt> values(const query_options& options) const override {
|
||||
std::vector<bytes_opt> v;
|
||||
v.push_back(to_bytes_opt(_value->bind_and_get(options)));
|
||||
return v;
|
||||
}
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool is_supported_by(const secondary_index::index& index) const {
|
||||
return index.supports_expression(_column_def, cql3::operator_type::LIKE);
|
||||
}
|
||||
|
||||
virtual sstring to_string() const override {
|
||||
return format("LIKE({})", _value->to_string());
|
||||
}
|
||||
|
||||
virtual void merge_with(::shared_ptr<restriction> other) {
|
||||
throw exceptions::invalid_request_exception(
|
||||
format("{} cannot be restricted by more than one relation if it includes a LIKE",
|
||||
_column_def.name_as_text()));
|
||||
}
|
||||
|
||||
virtual bool is_satisfied_by(const schema& schema,
|
||||
const partition_key& key,
|
||||
const clustering_key_prefix& ckey,
|
||||
const row& cells,
|
||||
const query_options& options,
|
||||
gc_clock::time_point now) const override;
|
||||
virtual bool is_satisfied_by(bytes_view data, const query_options& options) const override;
|
||||
virtual ::shared_ptr<single_column_restriction> apply_to(const column_definition& cdef) override {
|
||||
return ::make_shared<LIKE>(cdef, _value);
|
||||
}
|
||||
};
|
||||
|
||||
// This holds CONTAINS, CONTAINS_KEY, and map[key] = value restrictions because we might want to have any combination of them.
|
||||
class single_column_restriction::contains final : public single_column_restriction {
|
||||
private:
|
||||
@@ -435,7 +401,7 @@ private:
|
||||
std::vector<::shared_ptr<term>> _entry_values;
|
||||
public:
|
||||
contains(const column_definition& column_def, ::shared_ptr<term> t, bool is_key)
|
||||
: single_column_restriction(op::CONTAINS, column_def) {
|
||||
: single_column_restriction(column_def) {
|
||||
if (is_key) {
|
||||
_keys.emplace_back(std::move(t));
|
||||
} else {
|
||||
@@ -444,8 +410,7 @@ public:
|
||||
}
|
||||
|
||||
contains(const column_definition& column_def, ::shared_ptr<term> map_key, ::shared_ptr<term> map_value)
|
||||
: single_column_restriction(op::CONTAINS, column_def)
|
||||
{
|
||||
: single_column_restriction(column_def) {
|
||||
_entry_keys.emplace_back(std::move(map_key));
|
||||
_entry_values.emplace_back(std::move(map_value));
|
||||
}
|
||||
@@ -454,6 +419,10 @@ public:
|
||||
return bind_and_get(_values, options);
|
||||
}
|
||||
|
||||
virtual bool is_contains() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void merge_with(::shared_ptr<restriction> other_restriction) override {
|
||||
if (!other_restriction->is_contains()) {
|
||||
throw exceptions::invalid_request_exception(format("Collection column {} can only be restricted by CONTAINS, CONTAINS KEY, or map-entry equality",
|
||||
@@ -512,10 +481,10 @@ public:
|
||||
}
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_values, ks_name, function_name)
|
||||
|| restriction::term_uses_function(_keys, ks_name, function_name)
|
||||
|| restriction::term_uses_function(_entry_keys, ks_name, function_name)
|
||||
|| restriction::term_uses_function(_entry_values, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_values, ks_name, function_name)
|
||||
|| abstract_restriction::term_uses_function(_keys, ks_name, function_name)
|
||||
|| abstract_restriction::term_uses_function(_entry_keys, ks_name, function_name)
|
||||
|| abstract_restriction::term_uses_function(_entry_values, ks_name, function_name);
|
||||
}
|
||||
|
||||
virtual sstring to_string() const override {
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
#include "types/map.hh"
|
||||
#include "types/list.hh"
|
||||
#include "types/set.hh"
|
||||
#include "utils/like_matcher.hh"
|
||||
|
||||
namespace cql3 {
|
||||
namespace restrictions {
|
||||
@@ -381,28 +380,45 @@ std::vector<const column_definition*> statement_restrictions::get_column_defs_fo
|
||||
if (need_filtering()) {
|
||||
auto& sim = db.find_column_family(_schema).get_index_manager();
|
||||
auto [opt_idx, _] = find_idx(sim);
|
||||
auto column_uses_indexing = [&opt_idx] (const column_definition* cdef) {
|
||||
return opt_idx && opt_idx->depends_on(*cdef);
|
||||
auto column_uses_indexing = [&opt_idx] (const column_definition* cdef, ::shared_ptr<single_column_restriction> restr) {
|
||||
return opt_idx && restr && restr->is_supported_by(*opt_idx);
|
||||
};
|
||||
auto single_pk_restrs = dynamic_pointer_cast<single_column_partition_key_restrictions>(_partition_key_restrictions);
|
||||
if (_partition_key_restrictions->needs_filtering(*_schema)) {
|
||||
for (auto&& cdef : _partition_key_restrictions->get_column_defs()) {
|
||||
if (!column_uses_indexing(cdef)) {
|
||||
::shared_ptr<single_column_restriction> restr;
|
||||
if (single_pk_restrs) {
|
||||
auto it = single_pk_restrs->restrictions().find(cdef);
|
||||
if (it != single_pk_restrs->restrictions().end()) {
|
||||
restr = dynamic_pointer_cast<single_column_restriction>(it->second);
|
||||
}
|
||||
}
|
||||
if (!column_uses_indexing(cdef, restr)) {
|
||||
column_defs_for_filtering.emplace_back(cdef);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto single_ck_restrs = dynamic_pointer_cast<single_column_clustering_key_restrictions>(_clustering_columns_restrictions);
|
||||
const bool pk_has_unrestricted_components = _partition_key_restrictions->has_unrestricted_components(*_schema);
|
||||
if (pk_has_unrestricted_components || _clustering_columns_restrictions->needs_filtering(*_schema)) {
|
||||
column_id first_filtering_id = pk_has_unrestricted_components ? 0 : _schema->clustering_key_columns().begin()->id +
|
||||
_clustering_columns_restrictions->num_prefix_columns_that_need_not_be_filtered();
|
||||
for (auto&& cdef : _clustering_columns_restrictions->get_column_defs()) {
|
||||
if (cdef->id >= first_filtering_id && !column_uses_indexing(cdef)) {
|
||||
::shared_ptr<single_column_restriction> restr;
|
||||
if (single_pk_restrs) {
|
||||
auto it = single_ck_restrs->restrictions().find(cdef);
|
||||
if (it != single_ck_restrs->restrictions().end()) {
|
||||
restr = dynamic_pointer_cast<single_column_restriction>(it->second);
|
||||
}
|
||||
}
|
||||
if (cdef->id >= first_filtering_id && !column_uses_indexing(cdef, restr)) {
|
||||
column_defs_for_filtering.emplace_back(cdef);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto&& cdef : _nonprimary_key_restrictions->get_column_defs()) {
|
||||
if (!column_uses_indexing(cdef)) {
|
||||
auto restr = dynamic_pointer_cast<single_column_restriction>(_nonprimary_key_restrictions->get_restriction(*cdef));
|
||||
if (!column_uses_indexing(cdef, restr)) {
|
||||
column_defs_for_filtering.emplace_back(cdef);
|
||||
}
|
||||
}
|
||||
@@ -916,30 +932,5 @@ bool token_restriction::slice::is_satisfied_by(const schema& schema,
|
||||
return satisfied;
|
||||
}
|
||||
|
||||
bool single_column_restriction::LIKE::is_satisfied_by(const schema& schema,
|
||||
const partition_key& key,
|
||||
const clustering_key_prefix& ckey,
|
||||
const row& cells,
|
||||
const query_options& options,
|
||||
gc_clock::time_point now) const {
|
||||
if (!_column_def.type->is_string()) {
|
||||
throw exceptions::invalid_request_exception("LIKE is allowed only on string types");
|
||||
}
|
||||
auto cell_value = get_value(schema, key, ckey, cells, now);
|
||||
return !cell_value ? false :
|
||||
cell_value->with_linearized([&] (bytes_view data) {
|
||||
auto pattern = to_bytes_opt(_value->bind_and_get(options));
|
||||
return pattern ? like_matcher(*pattern)(data) : false;
|
||||
});
|
||||
}
|
||||
|
||||
bool single_column_restriction::LIKE::is_satisfied_by(bytes_view data, const query_options& options) const {
|
||||
if (!_column_def.type->is_string()) {
|
||||
throw exceptions::invalid_request_exception("LIKE is allowed only on string types");
|
||||
}
|
||||
auto pattern = to_bytes_opt(_value->bind_and_get(options));
|
||||
return pattern ? like_matcher(*pattern)(data) : false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public:
|
||||
bool select_a_collection,
|
||||
bool for_view = false,
|
||||
bool allow_filtering = false);
|
||||
|
||||
private:
|
||||
void add_restriction(::shared_ptr<restriction> restriction, bool for_view, bool allow_filtering);
|
||||
void add_single_column_restriction(::shared_ptr<single_column_restriction> restriction, bool for_view, bool allow_filtering);
|
||||
public:
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cql3/restrictions/restriction.hh"
|
||||
#include "cql3/restrictions/abstract_restriction.hh"
|
||||
#include "cql3/term.hh"
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
#include "to_string.hh"
|
||||
|
||||
@@ -62,10 +62,13 @@ private:
|
||||
*/
|
||||
std::vector<const column_definition *> _column_definitions;
|
||||
public:
|
||||
token_restriction(op op, std::vector<const column_definition *> c)
|
||||
: partition_key_restrictions(op, target::TOKEN), _column_definitions(std::move(c)) {
|
||||
token_restriction(std::vector<const column_definition *> c)
|
||||
: _column_definitions(std::move(c)) {
|
||||
}
|
||||
|
||||
bool is_on_token() const override {
|
||||
return true;
|
||||
}
|
||||
std::vector<const column_definition*> get_column_defs() const override {
|
||||
return _column_definitions;
|
||||
}
|
||||
@@ -145,12 +148,16 @@ private:
|
||||
::shared_ptr<term> _value;
|
||||
public:
|
||||
EQ(std::vector<const column_definition*> column_defs, ::shared_ptr<term> value)
|
||||
: token_restriction(op::EQ, column_defs)
|
||||
: token_restriction(column_defs)
|
||||
, _value(std::move(value))
|
||||
{}
|
||||
|
||||
bool is_EQ() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return restriction::term_uses_function(_value, ks_name, function_name);
|
||||
return abstract_restriction::term_uses_function(_value, ks_name, function_name);
|
||||
}
|
||||
|
||||
void merge_with(::shared_ptr<restriction>) override {
|
||||
@@ -180,9 +187,14 @@ private:
|
||||
term_slice _slice;
|
||||
public:
|
||||
slice(std::vector<const column_definition*> column_defs, statements::bound bound, bool inclusive, ::shared_ptr<term> term)
|
||||
: token_restriction(op::SLICE, column_defs)
|
||||
: token_restriction(column_defs)
|
||||
, _slice(term_slice::new_instance(bound, inclusive, std::move(term)))
|
||||
{}
|
||||
|
||||
bool is_slice() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool has_bound(statements::bound b) const override {
|
||||
return _slice.has_bound(b);
|
||||
}
|
||||
@@ -198,11 +210,11 @@ public:
|
||||
bool uses_function(const sstring& ks_name,
|
||||
const sstring& function_name) const override {
|
||||
return (_slice.has_bound(statements::bound::START)
|
||||
&& restriction::term_uses_function(
|
||||
&& abstract_restriction::term_uses_function(
|
||||
_slice.bound(statements::bound::START), ks_name,
|
||||
function_name))
|
||||
|| (_slice.has_bound(statements::bound::END)
|
||||
&& restriction::term_uses_function(
|
||||
&& abstract_restriction::term_uses_function(
|
||||
_slice.bound(statements::bound::END),
|
||||
ks_name, function_name));
|
||||
}
|
||||
|
||||
@@ -92,6 +92,14 @@ public:
|
||||
: abstract_function_selector(fun, std::move(arg_selectors))
|
||||
, _tfun(dynamic_pointer_cast<T>(fun)) {
|
||||
}
|
||||
|
||||
const functions::function_name& name() const {
|
||||
return _tfun->name();
|
||||
}
|
||||
|
||||
virtual sstring assignment_testable_source_context() const override {
|
||||
return format("{}", this->name());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -79,11 +79,6 @@ public:
|
||||
dynamic_pointer_cast<functions::aggregate_function>(func), std::move(arg_selectors))
|
||||
, _aggregate(fun()->new_aggregate()) {
|
||||
}
|
||||
|
||||
virtual sstring assignment_testable_source_context() const override {
|
||||
// FIXME:
|
||||
return "FIXME";
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -82,12 +82,6 @@ public:
|
||||
: abstract_function_selector_for<functions::scalar_function>(
|
||||
dynamic_pointer_cast<functions::scalar_function>(std::move(fun)), std::move(arg_selectors)) {
|
||||
}
|
||||
|
||||
virtual sstring assignment_testable_source_context() const override {
|
||||
// FIXME:
|
||||
return "FIXME";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -39,9 +39,8 @@
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <boost/range/adaptors.hpp>
|
||||
#include <boost/range/algorithm/equal.hpp>
|
||||
#include <boost/range/algorithm/transform.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/range/adaptor/filtered.hpp>
|
||||
|
||||
#include "cql3/selection/selection.hh"
|
||||
#include "cql3/selection/selector_factories.hh"
|
||||
@@ -115,11 +114,9 @@ protected:
|
||||
class simple_selectors : public selectors {
|
||||
private:
|
||||
std::vector<bytes_opt> _current;
|
||||
bool _first = true; ///< Whether the next row we receive is the first in its group.
|
||||
public:
|
||||
virtual void reset() override {
|
||||
_current.clear();
|
||||
_first = true;
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> get_output_row(cql_serialization_format sf) override {
|
||||
@@ -127,13 +124,7 @@ protected:
|
||||
}
|
||||
|
||||
virtual void add_input_row(cql_serialization_format sf, result_set_builder& rs) override {
|
||||
// GROUP BY calls add_input_row() repeatedly without reset() in between, and it expects
|
||||
// the output to be the first value encountered:
|
||||
// https://cassandra.apache.org/doc/latest/cql/dml.html#grouping-results
|
||||
if (_first) {
|
||||
_current = std::move(*rs.current);
|
||||
_first = false;
|
||||
}
|
||||
_current = std::move(*rs.current);
|
||||
}
|
||||
|
||||
virtual bool is_aggregate() {
|
||||
@@ -156,7 +147,11 @@ public:
|
||||
factories->contains_write_time_selector_factory(),
|
||||
factories->contains_ttl_selector_factory())
|
||||
, _factories(std::move(factories))
|
||||
{ }
|
||||
{
|
||||
if (_factories->does_aggregation() && !_factories->contains_only_aggregate_functions()) {
|
||||
throw exceptions::invalid_request_exception("the select clause must either contains only aggregates or none");
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool uses_function(const sstring& ks_name, const sstring& function_name) const override {
|
||||
return _factories->uses_function(ks_name, function_name);
|
||||
@@ -169,7 +164,7 @@ public:
|
||||
}
|
||||
|
||||
virtual bool is_aggregate() const override {
|
||||
return _factories->does_aggregation();
|
||||
return _factories->contains_only_aggregate_functions();
|
||||
}
|
||||
protected:
|
||||
class selectors_with_processing : public selectors {
|
||||
@@ -189,7 +184,7 @@ protected:
|
||||
}
|
||||
|
||||
virtual bool is_aggregate() override {
|
||||
return _factories->does_aggregation();
|
||||
return _factories->contains_only_aggregate_functions();
|
||||
}
|
||||
|
||||
virtual std::vector<bytes_opt> get_output_row(cql_serialization_format sf) override {
|
||||
@@ -268,13 +263,9 @@ selection::collect_metadata(schema_ptr schema, const std::vector<::shared_ptr<ra
|
||||
return r;
|
||||
}
|
||||
|
||||
result_set_builder::result_set_builder(const selection& s, gc_clock::time_point now, cql_serialization_format sf,
|
||||
std::vector<size_t> group_by_cell_indices)
|
||||
result_set_builder::result_set_builder(const selection& s, gc_clock::time_point now, cql_serialization_format sf)
|
||||
: _result_set(std::make_unique<result_set>(::make_shared<metadata>(*(s.get_result_metadata()))))
|
||||
, _selectors(s.new_selectors())
|
||||
, _group_by_cell_indices(std::move(group_by_cell_indices))
|
||||
, _last_group(_group_by_cell_indices.size())
|
||||
, _group_began(false)
|
||||
, _now(now)
|
||||
, _cql_serialization_format(sf)
|
||||
{
|
||||
@@ -320,56 +311,29 @@ void result_set_builder::add_collection(const column_definition& def, bytes_view
|
||||
// timestamps, ttls meaningless for collections
|
||||
}
|
||||
|
||||
void result_set_builder::update_last_group() {
|
||||
_group_began = true;
|
||||
boost::transform(_group_by_cell_indices, _last_group.begin(), [this](size_t i) { return (*current)[i]; });
|
||||
}
|
||||
|
||||
bool result_set_builder::last_group_ended() const {
|
||||
if (!_group_began) {
|
||||
return false;
|
||||
}
|
||||
if (_last_group.empty()) {
|
||||
return !_selectors->is_aggregate();
|
||||
}
|
||||
using boost::adaptors::reversed;
|
||||
using boost::adaptors::transformed;
|
||||
return !boost::equal(
|
||||
_last_group | reversed,
|
||||
_group_by_cell_indices | reversed | transformed([this](size_t i) { return (*current)[i]; }));
|
||||
}
|
||||
|
||||
void result_set_builder::flush_selectors() {
|
||||
_result_set->add_row(_selectors->get_output_row(_cql_serialization_format));
|
||||
_selectors->reset();
|
||||
}
|
||||
|
||||
void result_set_builder::process_current_row(bool more_rows_coming) {
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
if (last_group_ended()) {
|
||||
flush_selectors();
|
||||
}
|
||||
update_last_group();
|
||||
_selectors->add_input_row(_cql_serialization_format, *this);
|
||||
if (more_rows_coming) {
|
||||
void result_set_builder::new_row() {
|
||||
if (current) {
|
||||
_selectors->add_input_row(_cql_serialization_format, *this);
|
||||
if (!_selectors->is_aggregate()) {
|
||||
_result_set->add_row(_selectors->get_output_row(_cql_serialization_format));
|
||||
_selectors->reset();
|
||||
}
|
||||
current->clear();
|
||||
} else {
|
||||
flush_selectors();
|
||||
// FIXME: we use optional<> here because we don't have an end_row() signal
|
||||
// instead, !current means that new_row has never been called, so this
|
||||
// call to new_row() does not end a previous row.
|
||||
current.emplace();
|
||||
}
|
||||
}
|
||||
|
||||
void result_set_builder::new_row() {
|
||||
process_current_row(/*more_rows_coming=*/true);
|
||||
// FIXME: we use optional<> here because we don't have an end_row() signal
|
||||
// instead, !current means that new_row has never been called, so this
|
||||
// call to new_row() does not end a previous row.
|
||||
current.emplace();
|
||||
}
|
||||
|
||||
std::unique_ptr<result_set> result_set_builder::build() {
|
||||
process_current_row(/*more_rows_coming=*/false);
|
||||
if (current) {
|
||||
_selectors->add_input_row(_cql_serialization_format, *this);
|
||||
_result_set->add_row(_selectors->get_output_row(_cql_serialization_format));
|
||||
_selectors->reset();
|
||||
current = std::nullopt;
|
||||
}
|
||||
if (_result_set->empty() && _selectors->is_aggregate()) {
|
||||
_result_set->add_row(_selectors->get_output_row(_cql_serialization_format));
|
||||
}
|
||||
|
||||
@@ -244,9 +244,6 @@ class result_set_builder {
|
||||
private:
|
||||
std::unique_ptr<result_set> _result_set;
|
||||
std::unique_ptr<selectors> _selectors;
|
||||
const std::vector<size_t> _group_by_cell_indices; ///< Indices in \c current of cells holding GROUP BY values.
|
||||
std::vector<bytes_opt> _last_group; ///< Previous row's group: all of GROUP BY column values.
|
||||
bool _group_began; ///< Whether a group began being formed.
|
||||
public:
|
||||
std::optional<std::vector<bytes_opt>> current;
|
||||
private:
|
||||
@@ -309,8 +306,7 @@ public:
|
||||
bool do_filter(const selection& selection, const std::vector<bytes>& pk, const std::vector<bytes>& ck, const query::result_row_view& static_row, const query::result_row_view& row) const;
|
||||
};
|
||||
|
||||
result_set_builder(const selection& s, gc_clock::time_point now, cql_serialization_format sf,
|
||||
std::vector<size_t> group_by_cell_indices = {});
|
||||
result_set_builder(const selection& s, gc_clock::time_point now, cql_serialization_format sf);
|
||||
void add_empty();
|
||||
void add(bytes_opt value);
|
||||
void add(const column_definition& def, const query::result_atomic_cell_view& c);
|
||||
@@ -427,20 +423,6 @@ public:
|
||||
|
||||
private:
|
||||
bytes_opt get_value(data_type t, query::result_atomic_cell_view c);
|
||||
|
||||
/// True iff the \c current row ends a previously started group, either according to
|
||||
/// _group_by_cell_indices or aggregation.
|
||||
bool last_group_ended() const;
|
||||
|
||||
/// If there is a valid row in this->current, process it; if \p more_rows_coming, get ready to
|
||||
/// receive another.
|
||||
void process_current_row(bool more_rows_coming);
|
||||
|
||||
/// Gets output row from _selectors and resets them.
|
||||
void flush_selectors();
|
||||
|
||||
/// Updates _last_group from the \c current row.
|
||||
void update_last_group();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ private:
|
||||
const uint32_t _idx;
|
||||
data_type _type;
|
||||
bytes_opt _current;
|
||||
bool _first; ///< Whether the next row we receive is the first in its group.
|
||||
public:
|
||||
static ::shared_ptr<factory> new_factory(const sstring& column_name, uint32_t idx, data_type type) {
|
||||
return ::make_shared<simple_selector_factory>(column_name, idx, type);
|
||||
@@ -87,18 +86,11 @@ public:
|
||||
: _column_name(std::move(column_name))
|
||||
, _idx(idx)
|
||||
, _type(type)
|
||||
, _first(true)
|
||||
{ }
|
||||
|
||||
virtual void add_input(cql_serialization_format sf, result_set_builder& rs) override {
|
||||
// GROUP BY calls add_input() repeatedly without reset() in between, and it expects the
|
||||
// output to be the first value encountered:
|
||||
// https://cassandra.apache.org/doc/latest/cql/dml.html#grouping-results
|
||||
if (_first) {
|
||||
// TODO: can we steal it?
|
||||
_current = (*rs.current)[_idx];
|
||||
_first = false;
|
||||
}
|
||||
// TODO: can we steal it?
|
||||
_current = (*rs.current)[_idx];
|
||||
}
|
||||
|
||||
virtual bytes_opt get_output(cql_serialization_format sf) override {
|
||||
@@ -107,7 +99,6 @@ public:
|
||||
|
||||
virtual void reset() override {
|
||||
_current = {};
|
||||
_first = true;
|
||||
}
|
||||
|
||||
virtual data_type get_type() override {
|
||||
|
||||
@@ -95,18 +95,6 @@ single_column_relation::new_IN_restriction(database& db, schema_ptr schema, ::sh
|
||||
return ::make_shared<single_column_restriction::IN_with_values>(column_def, std::move(terms));
|
||||
}
|
||||
|
||||
::shared_ptr<restrictions::restriction>
|
||||
single_column_relation::new_LIKE_restriction(
|
||||
database& db, schema_ptr schema, ::shared_ptr<variable_specifications> bound_names) {
|
||||
const column_definition& column_def = to_column_definition(schema, _entity);
|
||||
if (!column_def.type->is_string()) {
|
||||
throw exceptions::invalid_request_exception(
|
||||
format("LIKE is allowed only on string types, which {} is not", column_def.name_as_text()));
|
||||
}
|
||||
auto term = to_term(to_receivers(schema, column_def), _value, db, schema->ks_name(), bound_names);
|
||||
return ::make_shared<single_column_restriction::LIKE>(column_def, std::move(term));
|
||||
}
|
||||
|
||||
std::vector<::shared_ptr<column_specification>>
|
||||
single_column_relation::to_receivers(schema_ptr schema, const column_definition& column_def)
|
||||
{
|
||||
|
||||
@@ -183,9 +183,6 @@ protected:
|
||||
return ::make_shared<restrictions::single_column_restriction::contains>(column_def, std::move(term), is_key);
|
||||
}
|
||||
|
||||
virtual ::shared_ptr<restrictions::restriction> new_LIKE_restriction(
|
||||
database& db, schema_ptr schema, ::shared_ptr<variable_specifications> bound_names) override;
|
||||
|
||||
virtual ::shared_ptr<relation> maybe_rename_identifier(const column_identifier::raw& from, column_identifier::raw to) override {
|
||||
return *_entity == from
|
||||
? ::make_shared(single_column_relation(
|
||||
|
||||
@@ -221,7 +221,7 @@ void alter_table_statement::add_column(schema_ptr schema, const table& cf, schem
|
||||
schema_builder builder(view);
|
||||
if (view->view_info()->include_all_columns()) {
|
||||
builder.with_column(column_name->name(), type);
|
||||
} else if (view->view_info()->base_non_pk_columns_in_view_pk().empty()) {
|
||||
} else if (!view->view_info()->base_non_pk_column_in_view_pk()) {
|
||||
db::view::create_virtual_column(builder, column_name->name(), type);
|
||||
}
|
||||
view_updates.push_back(view_ptr(builder.build()));
|
||||
|
||||
@@ -192,21 +192,20 @@ const std::vector<batch_statement::single_statement>& batch_statement::get_state
|
||||
return _statements;
|
||||
}
|
||||
|
||||
future<std::vector<mutation>> batch_statement::get_mutations(service::storage_proxy& storage, const query_options& options, db::timeout_clock::time_point timeout, bool local, api::timestamp_type now, tracing::trace_state_ptr trace_state,
|
||||
service_permit permit) {
|
||||
future<std::vector<mutation>> batch_statement::get_mutations(service::storage_proxy& storage, const query_options& options, db::timeout_clock::time_point timeout, bool local, api::timestamp_type now, tracing::trace_state_ptr trace_state) {
|
||||
// Do not process in parallel because operations like list append/prepend depend on execution order.
|
||||
using mutation_set_type = std::unordered_set<mutation, mutation_hash_by_key, mutation_equals_by_key>;
|
||||
return do_with(mutation_set_type(), [this, &storage, &options, timeout, now, local, trace_state, permit = std::move(permit)] (auto& result) mutable {
|
||||
return do_with(mutation_set_type(), [this, &storage, &options, timeout, now, local, trace_state] (auto& result) {
|
||||
result.reserve(_statements.size());
|
||||
_stats.statements_in_batches += _statements.size();
|
||||
return do_for_each(boost::make_counting_iterator<size_t>(0),
|
||||
boost::make_counting_iterator<size_t>(_statements.size()),
|
||||
[this, &storage, &options, now, local, &result, timeout, trace_state, permit = std::move(permit)] (size_t i) {
|
||||
[this, &storage, &options, now, local, &result, timeout, trace_state] (size_t i) {
|
||||
auto&& statement = _statements[i].statement;
|
||||
statement->inc_cql_stats();
|
||||
auto&& statement_options = options.for_statement(i);
|
||||
auto timestamp = _attrs->get_timestamp(now, statement_options);
|
||||
return statement->get_mutations(storage, statement_options, timeout, local, timestamp, trace_state, permit).then([&result] (auto&& more) {
|
||||
return statement->get_mutations(storage, statement_options, timeout, local, timestamp, trace_state).then([&result] (auto&& more) {
|
||||
for (auto&& m : more) {
|
||||
// We want unordered_set::try_emplace(), but we don't have it
|
||||
auto pos = result.find(m);
|
||||
@@ -296,9 +295,8 @@ future<shared_ptr<cql_transport::messages::result_message>> batch_statement::do_
|
||||
}
|
||||
|
||||
auto timeout = db::timeout_clock::now() + options.get_timeout_config().*get_timeout_config_selector();
|
||||
return get_mutations(storage, options, timeout, local, now, query_state.get_trace_state(), query_state.get_permit()).then([this, &storage, &options, timeout, tr_state = query_state.get_trace_state(),
|
||||
permit = query_state.get_permit()] (std::vector<mutation> ms) mutable {
|
||||
return execute_without_conditions(storage, std::move(ms), options.get_consistency(), timeout, std::move(tr_state), std::move(permit));
|
||||
return get_mutations(storage, options, timeout, local, now, query_state.get_trace_state()).then([this, &storage, &options, timeout, tr_state = query_state.get_trace_state()] (std::vector<mutation> ms) mutable {
|
||||
return execute_without_conditions(storage, std::move(ms), options.get_consistency(), timeout, std::move(tr_state));
|
||||
}).then([] {
|
||||
return make_ready_future<shared_ptr<cql_transport::messages::result_message>>(
|
||||
make_shared<cql_transport::messages::result_message::void_message>());
|
||||
@@ -310,8 +308,7 @@ future<> batch_statement::execute_without_conditions(
|
||||
std::vector<mutation> mutations,
|
||||
db::consistency_level cl,
|
||||
db::timeout_clock::time_point timeout,
|
||||
tracing::trace_state_ptr tr_state,
|
||||
service_permit permit)
|
||||
tracing::trace_state_ptr tr_state)
|
||||
{
|
||||
// FIXME: do we need to do this?
|
||||
#if 0
|
||||
@@ -338,7 +335,7 @@ future<> batch_statement::execute_without_conditions(
|
||||
mutate_atomic = false;
|
||||
}
|
||||
}
|
||||
return storage.mutate_with_triggers(std::move(mutations), cl, timeout, mutate_atomic, std::move(tr_state), std::move(permit));
|
||||
return storage.mutate_with_triggers(std::move(mutations), cl, timeout, mutate_atomic, std::move(tr_state));
|
||||
}
|
||||
|
||||
future<shared_ptr<cql_transport::messages::result_message>> batch_statement::execute_with_conditions(
|
||||
|
||||
@@ -125,8 +125,7 @@ public:
|
||||
|
||||
const std::vector<single_statement>& get_statements();
|
||||
private:
|
||||
future<std::vector<mutation>> get_mutations(service::storage_proxy& storage, const query_options& options, db::timeout_clock::time_point timeout, bool local, api::timestamp_type now, tracing::trace_state_ptr trace_state,
|
||||
service_permit permit);
|
||||
future<std::vector<mutation>> get_mutations(service::storage_proxy& storage, const query_options& options, db::timeout_clock::time_point timeout, bool local, api::timestamp_type now, tracing::trace_state_ptr trace_state);
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -149,8 +148,7 @@ private:
|
||||
std::vector<mutation> mutations,
|
||||
db::consistency_level cl,
|
||||
db::timeout_clock::time_point timeout,
|
||||
tracing::trace_state_ptr tr_state,
|
||||
service_permit permit);
|
||||
tracing::trace_state_ptr tr_state);
|
||||
|
||||
future<shared_ptr<cql_transport::messages::result_message>> execute_with_conditions(
|
||||
service::storage_proxy& storage,
|
||||
|
||||
@@ -43,8 +43,6 @@
|
||||
#include "prepared_statement.hh"
|
||||
#include "database.hh"
|
||||
#include "service/migration_manager.hh"
|
||||
#include "service/storage_service.hh"
|
||||
#include "transport/messages/result_message.hh"
|
||||
|
||||
#include <regex>
|
||||
|
||||
@@ -54,8 +52,6 @@ namespace cql3 {
|
||||
|
||||
namespace statements {
|
||||
|
||||
logging::logger create_keyspace_statement::_logger("create_keyspace");
|
||||
|
||||
create_keyspace_statement::create_keyspace_statement(const sstring& name, shared_ptr<ks_prop_defs> attrs, bool if_not_exists)
|
||||
: _name{name}
|
||||
, _attrs{attrs}
|
||||
@@ -143,24 +139,6 @@ future<> cql3::statements::create_keyspace_statement::grant_permissions_to_creat
|
||||
});
|
||||
}
|
||||
|
||||
future<::shared_ptr<messages::result_message>>
|
||||
create_keyspace_statement::execute(service::storage_proxy& proxy, service::query_state& state, const query_options& options) {
|
||||
return schema_altering_statement::execute(proxy, state, options).then([this] (::shared_ptr<messages::result_message> msg) {
|
||||
bool multidc = service::get_local_storage_service().get_token_metadata().
|
||||
get_topology().get_datacenter_endpoints().size() > 1;
|
||||
bool simple = _attrs->get_replication_strategy_class() == "SimpleStrategy";
|
||||
|
||||
if (multidc && simple) {
|
||||
static const auto warning = "Using SimpleStrategy in a multi-datacenter environment is not recommended.";
|
||||
|
||||
msg->add_warning(warning);
|
||||
_logger.warn(warning);
|
||||
}
|
||||
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#include "cql3/statements/schema_altering_statement.hh"
|
||||
#include "cql3/statements/ks_prop_defs.hh"
|
||||
#include "transport/event.hh"
|
||||
#include "log.hh"
|
||||
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
|
||||
@@ -55,8 +54,6 @@ namespace statements {
|
||||
/** A <code>CREATE KEYSPACE</code> statement parsed from a CQL query. */
|
||||
class create_keyspace_statement : public schema_altering_statement {
|
||||
private:
|
||||
static logging::logger _logger;
|
||||
|
||||
sstring _name;
|
||||
shared_ptr<ks_prop_defs> _attrs;
|
||||
bool _if_not_exists;
|
||||
@@ -89,9 +86,6 @@ public:
|
||||
virtual std::unique_ptr<prepared> prepare(database& db, cql_stats& stats) override;
|
||||
|
||||
virtual future<> grant_permissions_to_creator(const service::client_state&) override;
|
||||
|
||||
virtual future<::shared_ptr<messages::result_message>>
|
||||
execute(service::storage_proxy& proxy, service::query_state& state, const query_options& options) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ future<shared_ptr<cql_transport::event::schema_change>> create_view_statement::a
|
||||
}
|
||||
|
||||
auto parameters = ::make_shared<raw::select_statement::parameters>(raw::select_statement::parameters::orderings_type(), false, true);
|
||||
raw::select_statement raw_select(_base_name, std::move(parameters), _select_clause, _where_clause, nullptr, nullptr, {});
|
||||
raw::select_statement raw_select(_base_name, std::move(parameters), _select_clause, _where_clause, nullptr, nullptr);
|
||||
raw_select.prepare_keyspace(keyspace());
|
||||
raw_select.set_bound_variables({});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
|
||||
#include "delete_statement.hh"
|
||||
#include "raw/delete_statement.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace cql3 {
|
||||
|
||||
@@ -103,11 +102,9 @@ delete_statement::prepare_internal(database& db, schema_ptr schema, shared_ptr<v
|
||||
}
|
||||
|
||||
stmt->process_where_clause(db, _where_clause, std::move(bound_names));
|
||||
if (!db.supports_infinite_bound_range_deletions()) {
|
||||
if (!stmt->restrictions()->get_clustering_columns_restrictions()->has_bound(bound::START)
|
||||
|| !stmt->restrictions()->get_clustering_columns_restrictions()->has_bound(bound::END)) {
|
||||
throw exceptions::invalid_request_exception("A range deletion operation needs to specify both bounds for clusters without sstable mc format support");
|
||||
}
|
||||
if (!stmt->restrictions()->get_clustering_columns_restrictions()->has_bound(bound::START)
|
||||
|| !stmt->restrictions()->get_clustering_columns_restrictions()->has_bound(bound::END)) {
|
||||
throw exceptions::invalid_request_exception("A range deletion operation needs to specify both bounds");
|
||||
}
|
||||
if (!schema->is_compound() && stmt->restrictions()->get_clustering_columns_restrictions()->is_slice()) {
|
||||
throw exceptions::invalid_request_exception("Range deletions on \"compact storage\" schemas are not supported");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user