Compare commits
282 Commits
next-4.6
...
alternator
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aac47f494b | ||
|
|
d93df8c184 | ||
|
|
48e22c2910 | ||
|
|
bbb6689938 | ||
|
|
6c252feb0c | ||
|
|
33d5015edf | ||
|
|
0be2f17952 | ||
|
|
a74d5fcce8 | ||
|
|
8e9e7cc952 | ||
|
|
ce5db951be | ||
|
|
5a9b9bdf4d | ||
|
|
518f01099c | ||
|
|
db617b459c | ||
|
|
72ac19623a | ||
|
|
eb1cc6610c | ||
|
|
24789b549a | ||
|
|
e11ba56dbc | ||
|
|
a2d68eac4c | ||
|
|
c14fc8d854 | ||
|
|
81dcee8961 | ||
|
|
6aa840543c | ||
|
|
84598b39b9 | ||
|
|
287ba6ddde | ||
|
|
a700da216d | ||
|
|
a6d2117a77 | ||
|
|
c46458f61d | ||
|
|
dc09d96b22 | ||
|
|
350180125d | ||
|
|
78a153518a | ||
|
|
70d4afc637 | ||
|
|
fea29fdfd1 | ||
|
|
93f1072cee | ||
|
|
edd03312ed | ||
|
|
faad21f72b | ||
|
|
298f5caa38 | ||
|
|
b6662bac35 | ||
|
|
707a70235e | ||
|
|
c223fafba3 | ||
|
|
7a749a4772 | ||
|
|
5fc1e70751 | ||
|
|
93fbbbc202 | ||
|
|
cfcf841c0d | ||
|
|
c0c4d90df9 | ||
|
|
86e80ea320 | ||
|
|
6d62ed213a | ||
|
|
05920f7c3b | ||
|
|
9390dd41d0 | ||
|
|
a795c290f3 | ||
|
|
d4b19f55b2 | ||
|
|
5e8644413e | ||
|
|
40e3b7a0b6 | ||
|
|
76c3e4e0d6 | ||
|
|
9567691551 | ||
|
|
2220c8c681 | ||
|
|
b2fa0c9c1c | ||
|
|
ace1ffc8da | ||
|
|
6b8224ef85 | ||
|
|
2e68eafb22 | ||
|
|
3044f71438 | ||
|
|
0af6ab6fc6 | ||
|
|
70ae5cc235 | ||
|
|
c9345d8a0e | ||
|
|
587b38cd69 | ||
|
|
7d68d5030d | ||
|
|
c49e009e3e | ||
|
|
eebb2f0a0f | ||
|
|
fd10eee1ae | ||
|
|
28b3819c23 | ||
|
|
656f62722b | ||
|
|
ecb571d7e3 | ||
|
|
dd4638d499 | ||
|
|
aaf559c4f9 | ||
|
|
f7d0ca3c92 | ||
|
|
8394225741 | ||
|
|
091b1b40c2 | ||
|
|
b914ba11fa | ||
|
|
1b2b2c7009 | ||
|
|
188c6a552a | ||
|
|
b8964ab0ba | ||
|
|
c4fd846dbb | ||
|
|
338b7e9e67 | ||
|
|
154d1649c6 | ||
|
|
5620a46024 | ||
|
|
fc9744791c | ||
|
|
e0b01a0233 | ||
|
|
b3bf2fab2e | ||
|
|
05b895ca84 | ||
|
|
6b145b59d3 | ||
|
|
76bc30a82d | ||
|
|
972474a215 | ||
|
|
3342ebff22 | ||
|
|
cd2c581c7c | ||
|
|
e19a7f908e | ||
|
|
e9f1540de1 | ||
|
|
eb678ed63a | ||
|
|
ebdd4022cf | ||
|
|
d6a8626e90 | ||
|
|
62858c8466 | ||
|
|
3716d23ce4 | ||
|
|
1611b5dd4f | ||
|
|
105533c046 | ||
|
|
7c23e23e7d | ||
|
|
a5057a3b6e | ||
|
|
8d8baccdc4 | ||
|
|
f8c7a2e0b8 | ||
|
|
99fd032b1f | ||
|
|
7984050054 | ||
|
|
d7f75b405b | ||
|
|
1d19934bc6 | ||
|
|
493890c6f6 | ||
|
|
e550a666e3 | ||
|
|
ea62ce67f4 | ||
|
|
7ce0a30766 | ||
|
|
d141c3b5bd | ||
|
|
a6ca5e19e4 | ||
|
|
482ac08a45 | ||
|
|
7efa1aa48a | ||
|
|
196efbba13 | ||
|
|
19111136c3 | ||
|
|
87626beaae | ||
|
|
b5bbdc18e4 | ||
|
|
34833707be | ||
|
|
cb9cb0e58e | ||
|
|
a69455df2b | ||
|
|
6435895e2f | ||
|
|
3c7b07c20c | ||
|
|
f9063208e9 | ||
|
|
99d7a34c00 | ||
|
|
3212167ac4 | ||
|
|
33300fd30c | ||
|
|
0372ce0649 | ||
|
|
43049bbec0 | ||
|
|
80163a67a2 | ||
|
|
f0448c67b0 | ||
|
|
94097b98d4 | ||
|
|
7062b19461 | ||
|
|
7af4304592 | ||
|
|
f2fac82bf0 | ||
|
|
30aea6d942 | ||
|
|
e3b1e2860c | ||
|
|
bb169ff51c | ||
|
|
76fa348dd9 | ||
|
|
2933648013 | ||
|
|
2a2e2a5b3b | ||
|
|
3135b9b5a5 | ||
|
|
8d117c0f25 | ||
|
|
9cb2bf0820 | ||
|
|
ddbcdd9736 | ||
|
|
c66f32a4ff | ||
|
|
de284b1111 | ||
|
|
a0825043f4 | ||
|
|
7107923e1e | ||
|
|
a6b007b753 | ||
|
|
3909370146 | ||
|
|
0230c04702 | ||
|
|
d5a92727d2 | ||
|
|
042e087066 | ||
|
|
dcd271393e | ||
|
|
f2767d973b | ||
|
|
51a1a01605 | ||
|
|
067f00050e | ||
|
|
9fc44d2f0e | ||
|
|
67c86461cb | ||
|
|
d018539b07 | ||
|
|
1e488458b2 | ||
|
|
b4b6a377e4 | ||
|
|
90b8870a16 | ||
|
|
00ed12e56c | ||
|
|
04610f5299 | ||
|
|
ef58e3fc1e | ||
|
|
21c7e53c1c | ||
|
|
66cad2975f | ||
|
|
bd251e81df | ||
|
|
f807e2792d | ||
|
|
d1942091d7 | ||
|
|
01ff0d1ba8 | ||
|
|
19c855287c | ||
|
|
4c2c920250 | ||
|
|
e6f1841756 | ||
|
|
d4ad58f353 | ||
|
|
102034a742 | ||
|
|
d914f37a08 | ||
|
|
c63fefd37b | ||
|
|
c2cf2ab99b | ||
|
|
41526884f1 | ||
|
|
2e703e6b73 | ||
|
|
ecd585ef59 | ||
|
|
cd040d6674 | ||
|
|
a1dbde66fd | ||
|
|
67c35cde40 | ||
|
|
18b2f656f2 | ||
|
|
90d7e6673e | ||
|
|
182450623a | ||
|
|
f14c7f8200 | ||
|
|
b81fbabe37 | ||
|
|
9c934b64f8 | ||
|
|
4ad39f714f | ||
|
|
f56a0fbcd9 | ||
|
|
296c2566c5 | ||
|
|
d760097dad | ||
|
|
523c5ee159 | ||
|
|
8146eb6027 | ||
|
|
725c18bf6c | ||
|
|
c8270831ec | ||
|
|
0987916542 | ||
|
|
ec23a14f82 | ||
|
|
7e4f7a20dc | ||
|
|
930234e48e | ||
|
|
9055096021 | ||
|
|
65934ada59 | ||
|
|
b28d3cdb5d | ||
|
|
cf58acc23f | ||
|
|
75c3f33a8c | ||
|
|
be516a080a | ||
|
|
176b7dfd17 | ||
|
|
30d4b4e689 | ||
|
|
3b73c49ac8 | ||
|
|
b556356a7d | ||
|
|
90c12b4ea3 | ||
|
|
287a986715 | ||
|
|
9b46c6ac2d | ||
|
|
f0436aeecc | ||
|
|
e3ba65003d | ||
|
|
d25a07a6c0 | ||
|
|
426f53bc89 | ||
|
|
1d85558d47 | ||
|
|
1c7b1ac165 | ||
|
|
c4c71989bf | ||
|
|
4dad76a6a7 | ||
|
|
323268e9ab | ||
|
|
a441ad9360 | ||
|
|
d04a5b01c3 | ||
|
|
4da8171b42 | ||
|
|
fbc6f222b8 | ||
|
|
31cce0323e | ||
|
|
f3d1cefe3e | ||
|
|
8bf6e963f2 | ||
|
|
ac65f91b5d | ||
|
|
c7d7c1e50d | ||
|
|
829f5fe359 | ||
|
|
5f9409af68 | ||
|
|
f28680ec5c | ||
|
|
a7175ddd44 | ||
|
|
90f56c32b0 | ||
|
|
2808f7ae3f | ||
|
|
ac278d67ca | ||
|
|
53a9567804 | ||
|
|
5a751ddcff | ||
|
|
dae70c892f | ||
|
|
4cbb40d5d8 | ||
|
|
ae779ade37 | ||
|
|
8839b22277 | ||
|
|
35bc488f5b | ||
|
|
c85dcfb71d | ||
|
|
a37b334e16 | ||
|
|
d52e7de7be | ||
|
|
dd884b5552 | ||
|
|
6c6c5a37a1 | ||
|
|
4ff599b21f | ||
|
|
6178694b0e | ||
|
|
700a3bb7be | ||
|
|
eb13166fb5 | ||
|
|
325773d65b | ||
|
|
4e12a4f212 | ||
|
|
3600a096be | ||
|
|
9fbf601f44 | ||
|
|
7b400913fc | ||
|
|
623fd5d10d | ||
|
|
8c9e88f77d | ||
|
|
4f3b0af6d0 | ||
|
|
830805e608 | ||
|
|
b87057eb08 | ||
|
|
53ac8dfb0a | ||
|
|
8a19ae8e39 | ||
|
|
41775a6e5d | ||
|
|
7adde41f55 | ||
|
|
c926f1bb53 | ||
|
|
e6ff060944 | ||
|
|
b5fa4ce3f3 | ||
|
|
6e697e4cca | ||
|
|
b2b7ae4e41 | ||
|
|
fca1e655b4 |
@@ -1,4 +1,3 @@
|
||||
.git
|
||||
build
|
||||
seastar/build
|
||||
testlog
|
||||
|
||||
97
.github/CODEOWNERS
vendored
97
.github/CODEOWNERS
vendored
@@ -1,97 +0,0 @@
|
||||
# AUTH
|
||||
auth/* @elcallio @vladzcloudius
|
||||
|
||||
# CACHE
|
||||
row_cache* @tgrabiec @haaawk
|
||||
*mutation* @tgrabiec @haaawk
|
||||
test/boost/mvcc* @tgrabiec @haaawk
|
||||
|
||||
# CDC
|
||||
cdc/* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
test/cql/cdc_* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
test/boost/cdc_* @haaawk @kbr- @elcallio @piodul @jul-stas
|
||||
|
||||
# COMMITLOG / BATCHLOG
|
||||
db/commitlog/* @elcallio
|
||||
db/batch* @elcallio
|
||||
|
||||
# COORDINATOR
|
||||
service/storage_proxy* @gleb-cloudius
|
||||
|
||||
# COMPACTION
|
||||
compaction/* @raphaelsc @nyh
|
||||
|
||||
# CQL TRANSPORT LAYER
|
||||
transport/*
|
||||
|
||||
# CQL QUERY LANGUAGE
|
||||
cql3/* @tgrabiec @psarna @cvybhu
|
||||
|
||||
# COUNTERS
|
||||
counters* @haaawk @jul-stas
|
||||
tests/counter_test* @haaawk @jul-stas
|
||||
|
||||
# GOSSIP
|
||||
gms/* @tgrabiec @asias
|
||||
|
||||
# DOCKER
|
||||
dist/docker/*
|
||||
|
||||
# LSA
|
||||
utils/logalloc* @tgrabiec
|
||||
|
||||
# MATERIALIZED VIEWS
|
||||
db/view/* @nyh @psarna
|
||||
cql3/statements/*view* @nyh @psarna
|
||||
test/boost/view_* @nyh @psarna
|
||||
|
||||
# PACKAGING
|
||||
dist/* @syuu1228
|
||||
|
||||
# REPAIR
|
||||
repair/* @tgrabiec @asias @nyh
|
||||
|
||||
# SCHEMA MANAGEMENT
|
||||
db/schema_tables* @tgrabiec @nyh
|
||||
db/legacy_schema_migrator* @tgrabiec @nyh
|
||||
service/migration* @tgrabiec @nyh
|
||||
schema* @tgrabiec @nyh
|
||||
|
||||
# SECONDARY INDEXES
|
||||
db/index/* @nyh @psarna
|
||||
cql3/statements/*index* @nyh @psarna
|
||||
test/boost/*index* @nyh @psarna
|
||||
|
||||
# SSTABLES
|
||||
sstables/* @tgrabiec @raphaelsc @nyh
|
||||
|
||||
# STREAMING
|
||||
streaming/* @tgrabiec @asias
|
||||
service/storage_service.* @tgrabiec @asias
|
||||
|
||||
# ALTERNATOR
|
||||
alternator/* @nyh @psarna
|
||||
test/alternator/* @nyh @psarna
|
||||
|
||||
# HINTED HANDOFF
|
||||
db/hints/* @haaawk @piodul @vladzcloudius
|
||||
|
||||
# REDIS
|
||||
redis/* @nyh @syuu1228
|
||||
test/redis/* @nyh @syuu1228
|
||||
|
||||
# READERS
|
||||
reader_* @denesb
|
||||
querier* @denesb
|
||||
test/boost/mutation_reader_test.cc @denesb
|
||||
test/boost/querier_cache_test.cc @denesb
|
||||
|
||||
# PYTEST-BASED CQL TESTS
|
||||
test/cql-pytest/* @nyh
|
||||
|
||||
# RAFT
|
||||
raft/* @kbr- @gleb-cloudius @kostja
|
||||
test/raft/* @kbr- @gleb-cloudius @kostja
|
||||
|
||||
# HEAT-WEIGHTED LOAD BALANCING
|
||||
db/heat_load_balance.* @nyh @gleb-cloudius
|
||||
29
.github/workflows/docs-pages@v2.yaml
vendored
29
.github/workflows/docs-pages@v2.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: "Docs / Publish"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Build docs
|
||||
run: make -C docs multiversion
|
||||
- name: Deploy
|
||||
run: ./docs/_utils/deploy.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
25
.github/workflows/docs-pr@v1.yaml
vendored
25
.github/workflows/docs-pr@v1.yaml
vendored
@@ -1,25 +0,0 @@
|
||||
name: "Docs / Build PR"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Build docs
|
||||
run: make -C docs test
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -19,13 +19,3 @@ CMakeLists.txt.user
|
||||
__pycache__CMakeLists.txt.user
|
||||
.gdbinit
|
||||
resources
|
||||
.pytest_cache
|
||||
/expressions.tokens
|
||||
tags
|
||||
testlog
|
||||
test/*/*.reject
|
||||
.vscode
|
||||
docs/_build
|
||||
docs/poetry.lock
|
||||
compile_commands.json
|
||||
.ccls-cache/
|
||||
|
||||
20
.gitmodules
vendored
20
.gitmodules
vendored
@@ -1,23 +1,17 @@
|
||||
[submodule "seastar"]
|
||||
path = seastar
|
||||
url = ../scylla-seastar
|
||||
url = ../seastar
|
||||
ignore = dirty
|
||||
[submodule "swagger-ui"]
|
||||
path = swagger-ui
|
||||
url = ../scylla-swagger-ui
|
||||
ignore = dirty
|
||||
[submodule "xxHash"]
|
||||
path = xxHash
|
||||
url = ../xxHash
|
||||
[submodule "libdeflate"]
|
||||
path = libdeflate
|
||||
url = ../libdeflate
|
||||
[submodule "abseil"]
|
||||
path = abseil
|
||||
url = ../abseil-cpp
|
||||
[submodule "scylla-jmx"]
|
||||
path = tools/jmx
|
||||
url = ../scylla-jmx
|
||||
[submodule "scylla-tools"]
|
||||
path = tools/java
|
||||
url = ../scylla-tools-java
|
||||
[submodule "scylla-python3"]
|
||||
path = tools/python3
|
||||
url = ../scylla-python3
|
||||
[submodule "zstd"]
|
||||
path = zstd
|
||||
url = ../zstd
|
||||
|
||||
891
CMakeLists.txt
891
CMakeLists.txt
@@ -1,795 +1,142 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
##
|
||||
## For best results, first compile the project using the Ninja build-system.
|
||||
##
|
||||
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
project(scylla)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
message(STATUS "Setting build type to 'Release' as none was specified.")
|
||||
set(CMAKE_BUILD_TYPE "Release" CACHE
|
||||
STRING "Choose the type of build." FORCE)
|
||||
# Set the possible values of build type for cmake-gui
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Release" "Dev" "Sanitize")
|
||||
if (NOT DEFINED FOR_IDE AND NOT DEFINED ENV{FOR_IDE} AND NOT DEFINED ENV{CLION_IDE})
|
||||
message(FATAL_ERROR "This CMakeLists.txt file is only valid for use in IDEs, please define FOR_IDE to acknowledge this.")
|
||||
endif()
|
||||
|
||||
if(CMAKE_BUILD_TYPE)
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE)
|
||||
else()
|
||||
set(BUILD_TYPE "release")
|
||||
endif()
|
||||
# Default value. A more accurate list is populated through `pkg-config` below if `seastar.pc` is available.
|
||||
set(SEASTAR_INCLUDE_DIRS "seastar")
|
||||
|
||||
function(default_target_arch arch)
|
||||
set(x86_instruction_sets i386 i686 x86_64)
|
||||
if(CMAKE_SYSTEM_PROCESSOR IN_LIST x86_instruction_sets)
|
||||
set(${arch} "westmere" PARENT_SCOPE)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR EQUAL "aarch64")
|
||||
set(${arch} "armv8-a+crc+crypto" PARENT_SCOPE)
|
||||
# These paths are always available, since they're included in the repository. Additional DPDK headers are placed while
|
||||
# Seastar is built, and are captured in `SEASTAR_INCLUDE_DIRS` through parsing the Seastar pkg-config file (below).
|
||||
set(SEASTAR_DPDK_INCLUDE_DIRS
|
||||
seastar/dpdk/lib/librte_eal/common/include
|
||||
seastar/dpdk/lib/librte_eal/common/include/generic
|
||||
seastar/dpdk/lib/librte_eal/common/include/x86
|
||||
seastar/dpdk/lib/librte_ether)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
set(ENV{PKG_CONFIG_PATH} "${CMAKE_SOURCE_DIR}/seastar/build/release:$ENV{PKG_CONFIG_PATH}")
|
||||
pkg_check_modules(SEASTAR seastar)
|
||||
|
||||
find_package(Boost COMPONENTS filesystem program_options system thread)
|
||||
|
||||
##
|
||||
## Populate the names of all source and header files in the indicated paths in a designated variable.
|
||||
##
|
||||
## When RECURSIVE is specified, directories are traversed recursively.
|
||||
##
|
||||
## Use: scan_scylla_source_directories(VAR my_result_var [RECURSIVE] PATHS [path1 path2 ...])
|
||||
##
|
||||
function (scan_scylla_source_directories)
|
||||
set(options RECURSIVE)
|
||||
set(oneValueArgs VAR)
|
||||
set(multiValueArgs PATHS)
|
||||
cmake_parse_arguments(args "${options}" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
|
||||
|
||||
set(globs "")
|
||||
|
||||
foreach (dir ${args_PATHS})
|
||||
list(APPEND globs "${dir}/*.cc" "${dir}/*.hh")
|
||||
endforeach()
|
||||
|
||||
if (args_RECURSIVE)
|
||||
set(glob_kind GLOB_RECURSE)
|
||||
else()
|
||||
set(${arch} "" PARENT_SCOPE)
|
||||
set(glob_kind GLOB)
|
||||
endif()
|
||||
endfunction()
|
||||
default_target_arch(target_arch)
|
||||
if(target_arch)
|
||||
set(target_arch_flag "-march=${target_arch}")
|
||||
endif()
|
||||
|
||||
set(cxx_coro_flag)
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES GNU)
|
||||
set(cxx_coro_flag -fcoroutines)
|
||||
endif()
|
||||
file(${glob_kind} var
|
||||
${globs})
|
||||
|
||||
# Configure Seastar compile options to align with Scylla
|
||||
set(Seastar_CXX_FLAGS ${cxx_coro_flag} ${target_arch_flag} CACHE INTERNAL "" FORCE)
|
||||
set(Seastar_CXX_DIALECT gnu++20 CACHE INTERNAL "" FORCE)
|
||||
|
||||
add_subdirectory(seastar)
|
||||
add_subdirectory(abseil)
|
||||
# Exclude absl::strerror from the default "all" target since it's not
|
||||
# used in Scylla build and, moreover, makes use of deprecated glibc APIs,
|
||||
# such as sys_nerr, which are not exposed from "stdio.h" since glibc 2.32,
|
||||
# which happens to be the case for recent Fedora distribution versions.
|
||||
#
|
||||
# Need to use the internal "absl_strerror" target name instead of namespaced
|
||||
# variant because `set_target_properties` does not understand the latter form,
|
||||
# unfortunately.
|
||||
set_target_properties(absl_strerror PROPERTIES EXCLUDE_FROM_ALL TRUE)
|
||||
|
||||
# System libraries dependencies
|
||||
find_package(Boost COMPONENTS filesystem program_options system thread regex REQUIRED)
|
||||
find_package(Lua REQUIRED)
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_package(ICU COMPONENTS uc REQUIRED)
|
||||
|
||||
set(scylla_build_dir "${CMAKE_BINARY_DIR}/build/${BUILD_TYPE}")
|
||||
set(scylla_gen_build_dir "${scylla_build_dir}/gen")
|
||||
file(MAKE_DIRECTORY "${scylla_build_dir}" "${scylla_gen_build_dir}")
|
||||
|
||||
# Place libraries, executables and archives in ${buildroot}/build/${mode}/
|
||||
foreach(mode RUNTIME LIBRARY ARCHIVE)
|
||||
set(CMAKE_${mode}_OUTPUT_DIRECTORY "${scylla_build_dir}")
|
||||
endforeach()
|
||||
|
||||
# Generate C++ source files from thrift definitions
|
||||
function(scylla_generate_thrift)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_DIR SERVICE)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
|
||||
get_filename_component(in_file_name ${args_IN_FILE} NAME_WE)
|
||||
|
||||
set(aux_out_file_name ${args_OUT_DIR}/${in_file_name})
|
||||
set(outputs
|
||||
${aux_out_file_name}_types.cpp
|
||||
${aux_out_file_name}_types.h
|
||||
${aux_out_file_name}_constants.cpp
|
||||
${aux_out_file_name}_constants.h
|
||||
${args_OUT_DIR}/${args_SERVICE}.cpp
|
||||
${args_OUT_DIR}/${args_SERVICE}.h)
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
thrift
|
||||
OUTPUT ${outputs}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${args_OUT_DIR}
|
||||
COMMAND thrift -gen cpp:cob_style,no_skeleton -out "${args_OUT_DIR}" "${args_IN_FILE}")
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${outputs})
|
||||
|
||||
set(${args_VAR} ${outputs} PARENT_SCOPE)
|
||||
set(${args_VAR} ${var} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
scylla_generate_thrift(
|
||||
TARGET scylla_thrift_gen_cassandra
|
||||
VAR scylla_thrift_gen_cassandra_files
|
||||
IN_FILE "${CMAKE_SOURCE_DIR}/interface/cassandra.thrift"
|
||||
OUT_DIR ${scylla_gen_build_dir}
|
||||
SERVICE Cassandra)
|
||||
## Although Seastar is an external project, it is common enough to explore the sources while doing
|
||||
## Scylla development that we'll treat the Seastar sources as part of this project for easier navigation.
|
||||
scan_scylla_source_directories(
|
||||
VAR SEASTAR_SOURCE_FILES
|
||||
RECURSIVE
|
||||
|
||||
# Parse antlr3 grammar files and generate C++ sources
|
||||
function(scylla_generate_antlr3)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_DIR)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
PATHS
|
||||
seastar/core
|
||||
seastar/http
|
||||
seastar/json
|
||||
seastar/net
|
||||
seastar/rpc
|
||||
seastar/tests
|
||||
seastar/util)
|
||||
|
||||
get_filename_component(in_file_pure_name ${args_IN_FILE} NAME)
|
||||
get_filename_component(stem ${in_file_pure_name} NAME_WE)
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_ROOT_SOURCE_FILES
|
||||
PATHS .)
|
||||
|
||||
set(outputs
|
||||
"${args_OUT_DIR}/${stem}Lexer.hpp"
|
||||
"${args_OUT_DIR}/${stem}Lexer.cpp"
|
||||
"${args_OUT_DIR}/${stem}Parser.hpp"
|
||||
"${args_OUT_DIR}/${stem}Parser.cpp")
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_SUB_SOURCE_FILES
|
||||
RECURSIVE
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
OUTPUT ${outputs}
|
||||
# Remove #ifdef'ed code from the grammar source code
|
||||
COMMAND sed -e "/^#if 0/,/^#endif/d" "${args_IN_FILE}" > "${args_OUT_DIR}/${in_file_pure_name}"
|
||||
COMMAND antlr3 "${args_OUT_DIR}/${in_file_pure_name}"
|
||||
# We replace many local `ExceptionBaseType* ex` variables with a single function-scope one.
|
||||
# Because we add such a variable to every function, and because `ExceptionBaseType` is not a global
|
||||
# name, we also add a global typedef to avoid compilation errors.
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Lexer.hpp"
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Lexer.cpp"
|
||||
COMMAND sed -i -e "/^.*On :.*$/d" "${args_OUT_DIR}/${stem}Parser.hpp"
|
||||
COMMAND sed -i
|
||||
-e "s/^\\( *\\)\\(ImplTraits::CommonTokenType\\* [a-zA-Z0-9_]* = NULL;\\)$/\\1const \\2/"
|
||||
-e "/^.*On :.*$/d"
|
||||
-e "1i using ExceptionBaseType = int;"
|
||||
-e "s/^{/{ ExceptionBaseType\\* ex = nullptr;/; s/ExceptionBaseType\\* ex = new/ex = new/; s/exceptions::syntax_exception e/exceptions::syntax_exception\\& e/"
|
||||
"${args_OUT_DIR}/${stem}Parser.cpp"
|
||||
VERBATIM)
|
||||
PATHS
|
||||
api
|
||||
auth
|
||||
cql3
|
||||
db
|
||||
dht
|
||||
exceptions
|
||||
gms
|
||||
index
|
||||
io
|
||||
locator
|
||||
message
|
||||
repair
|
||||
service
|
||||
sstables
|
||||
streaming
|
||||
tests
|
||||
thrift
|
||||
tracing
|
||||
transport
|
||||
utils)
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${outputs})
|
||||
scan_scylla_source_directories(
|
||||
VAR SCYLLA_GEN_SOURCE_FILES
|
||||
RECURSIVE
|
||||
PATHS build/release/gen)
|
||||
|
||||
set(${args_VAR} ${outputs} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(antlr3_grammar_files
|
||||
cql3/Cql.g
|
||||
alternator/expressions.g)
|
||||
|
||||
set(antlr3_gen_files)
|
||||
|
||||
foreach(f ${antlr3_grammar_files})
|
||||
get_filename_component(grammar_file_name "${f}" NAME_WE)
|
||||
get_filename_component(f_dir "${f}" DIRECTORY)
|
||||
scylla_generate_antlr3(
|
||||
TARGET scylla_antlr3_gen_${grammar_file_name}
|
||||
VAR scylla_antlr3_gen_${grammar_file_name}_files
|
||||
IN_FILE "${CMAKE_SOURCE_DIR}/${f}"
|
||||
OUT_DIR ${scylla_gen_build_dir}/${f_dir})
|
||||
list(APPEND antlr3_gen_files "${scylla_antlr3_gen_${grammar_file_name}_files}")
|
||||
endforeach()
|
||||
|
||||
# Generate C++ sources from ragel grammar files
|
||||
seastar_generate_ragel(
|
||||
TARGET scylla_ragel_gen_protocol_parser
|
||||
VAR scylla_ragel_gen_protocol_parser_file
|
||||
IN_FILE "${CMAKE_SOURCE_DIR}/redis/protocol_parser.rl"
|
||||
OUT_FILE ${scylla_gen_build_dir}/redis/protocol_parser.hh)
|
||||
|
||||
# Generate C++ sources from Swagger definitions
|
||||
set(swagger_files
|
||||
api/api-doc/cache_service.json
|
||||
api/api-doc/collectd.json
|
||||
api/api-doc/column_family.json
|
||||
api/api-doc/commitlog.json
|
||||
api/api-doc/compaction_manager.json
|
||||
api/api-doc/config.json
|
||||
api/api-doc/endpoint_snitch_info.json
|
||||
api/api-doc/error_injection.json
|
||||
api/api-doc/failure_detector.json
|
||||
api/api-doc/gossiper.json
|
||||
api/api-doc/hinted_handoff.json
|
||||
api/api-doc/lsa.json
|
||||
api/api-doc/messaging_service.json
|
||||
api/api-doc/storage_proxy.json
|
||||
api/api-doc/storage_service.json
|
||||
api/api-doc/stream_manager.json
|
||||
api/api-doc/system.json
|
||||
api/api-doc/utils.json)
|
||||
|
||||
set(swagger_gen_files)
|
||||
|
||||
foreach(f ${swagger_files})
|
||||
get_filename_component(fname "${f}" NAME_WE)
|
||||
get_filename_component(dir "${f}" DIRECTORY)
|
||||
seastar_generate_swagger(
|
||||
TARGET scylla_swagger_gen_${fname}
|
||||
VAR scylla_swagger_gen_${fname}_files
|
||||
IN_FILE "${CMAKE_SOURCE_DIR}/${f}"
|
||||
OUT_DIR "${scylla_gen_build_dir}/${dir}")
|
||||
list(APPEND swagger_gen_files "${scylla_swagger_gen_${fname}_files}")
|
||||
endforeach()
|
||||
|
||||
# Create C++ bindings for IDL serializers
|
||||
function(scylla_generate_idl_serializer)
|
||||
set(one_value_args TARGET VAR IN_FILE OUT_FILE)
|
||||
cmake_parse_arguments(args "" "${one_value_args}" "" ${ARGN})
|
||||
get_filename_component(out_dir ${args_OUT_FILE} DIRECTORY)
|
||||
set(idl_compiler "${CMAKE_SOURCE_DIR}/idl-compiler.py")
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
|
||||
add_custom_command(
|
||||
DEPENDS
|
||||
${args_IN_FILE}
|
||||
${idl_compiler}
|
||||
OUTPUT ${args_OUT_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${out_dir}
|
||||
COMMAND Python3::Interpreter ${idl_compiler} --ns ser -f ${args_IN_FILE} -o ${args_OUT_FILE})
|
||||
|
||||
add_custom_target(${args_TARGET}
|
||||
DEPENDS ${args_OUT_FILE})
|
||||
|
||||
set(${args_VAR} ${args_OUT_FILE} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(idl_serializers
|
||||
idl/cache_temperature.idl.hh
|
||||
idl/commitlog.idl.hh
|
||||
idl/consistency_level.idl.hh
|
||||
idl/frozen_mutation.idl.hh
|
||||
idl/frozen_schema.idl.hh
|
||||
idl/gossip_digest.idl.hh
|
||||
idl/hinted_handoff.idl.hh
|
||||
idl/idl_test.idl.hh
|
||||
idl/keys.idl.hh
|
||||
idl/messaging_service.idl.hh
|
||||
idl/mutation.idl.hh
|
||||
idl/paging_state.idl.hh
|
||||
idl/partition_checksum.idl.hh
|
||||
idl/paxos.idl.hh
|
||||
idl/query.idl.hh
|
||||
idl/raft.idl.hh
|
||||
idl/range.idl.hh
|
||||
idl/read_command.idl.hh
|
||||
idl/reconcilable_result.idl.hh
|
||||
idl/replay_position.idl.hh
|
||||
idl/result.idl.hh
|
||||
idl/ring_position.idl.hh
|
||||
idl/streaming.idl.hh
|
||||
idl/token.idl.hh
|
||||
idl/tracing.idl.hh
|
||||
idl/truncation_record.idl.hh
|
||||
idl/uuid.idl.hh
|
||||
idl/view.idl.hh)
|
||||
|
||||
set(idl_gen_files)
|
||||
|
||||
foreach(f ${idl_serializers})
|
||||
get_filename_component(idl_name "${f}" NAME)
|
||||
get_filename_component(idl_target "${idl_name}" NAME_WE)
|
||||
get_filename_component(idl_dir "${f}" DIRECTORY)
|
||||
string(REPLACE ".idl.hh" ".dist.hh" idl_out_hdr_name "${idl_name}")
|
||||
scylla_generate_idl_serializer(
|
||||
TARGET scylla_idl_gen_${idl_target}
|
||||
VAR scylla_idl_gen_${idl_target}_files
|
||||
IN_FILE "${CMAKE_SOURCE_DIR}/${f}"
|
||||
OUT_FILE ${scylla_gen_build_dir}/${idl_dir}/${idl_out_hdr_name})
|
||||
list(APPEND idl_gen_files "${scylla_idl_gen_${idl_target}_files}")
|
||||
endforeach()
|
||||
|
||||
set(scylla_sources
|
||||
absl-flat_hash_map.cc
|
||||
alternator/auth.cc
|
||||
alternator/conditions.cc
|
||||
alternator/controller.cc
|
||||
alternator/executor.cc
|
||||
alternator/expressions.cc
|
||||
alternator/serialization.cc
|
||||
alternator/server.cc
|
||||
alternator/stats.cc
|
||||
alternator/streams.cc
|
||||
api/api.cc
|
||||
api/cache_service.cc
|
||||
api/collectd.cc
|
||||
api/column_family.cc
|
||||
api/commitlog.cc
|
||||
api/compaction_manager.cc
|
||||
api/config.cc
|
||||
api/endpoint_snitch.cc
|
||||
api/error_injection.cc
|
||||
api/failure_detector.cc
|
||||
api/gossiper.cc
|
||||
api/hinted_handoff.cc
|
||||
api/lsa.cc
|
||||
api/messaging_service.cc
|
||||
api/storage_proxy.cc
|
||||
api/storage_service.cc
|
||||
api/stream_manager.cc
|
||||
api/system.cc
|
||||
atomic_cell.cc
|
||||
auth/allow_all_authenticator.cc
|
||||
auth/allow_all_authorizer.cc
|
||||
auth/authenticated_user.cc
|
||||
auth/authentication_options.cc
|
||||
auth/authenticator.cc
|
||||
auth/common.cc
|
||||
auth/default_authorizer.cc
|
||||
auth/password_authenticator.cc
|
||||
auth/passwords.cc
|
||||
auth/permission.cc
|
||||
auth/permissions_cache.cc
|
||||
auth/resource.cc
|
||||
auth/role_or_anonymous.cc
|
||||
auth/roles-metadata.cc
|
||||
auth/sasl_challenge.cc
|
||||
auth/service.cc
|
||||
auth/standard_role_manager.cc
|
||||
auth/transitional.cc
|
||||
bytes.cc
|
||||
caching_options.cc
|
||||
canonical_mutation.cc
|
||||
cdc/cdc_partitioner.cc
|
||||
cdc/generation.cc
|
||||
cdc/log.cc
|
||||
cdc/metadata.cc
|
||||
cdc/split.cc
|
||||
clocks-impl.cc
|
||||
collection_mutation.cc
|
||||
compaction/compaction.cc
|
||||
compaction/compaction_manager.cc
|
||||
compaction/compaction_strategy.cc
|
||||
compaction/leveled_compaction_strategy.cc
|
||||
compaction/size_tiered_compaction_strategy.cc
|
||||
compaction/time_window_compaction_strategy.cc
|
||||
compress.cc
|
||||
connection_notifier.cc
|
||||
converting_mutation_partition_applier.cc
|
||||
counters.cc
|
||||
cql3/abstract_marker.cc
|
||||
cql3/attributes.cc
|
||||
cql3/cf_name.cc
|
||||
cql3/column_condition.cc
|
||||
cql3/column_identifier.cc
|
||||
cql3/column_specification.cc
|
||||
cql3/constants.cc
|
||||
cql3/cql3_type.cc
|
||||
cql3/expr/expression.cc
|
||||
cql3/expr/term_expr.cc
|
||||
cql3/functions/aggregate_fcts.cc
|
||||
cql3/functions/castas_fcts.cc
|
||||
cql3/functions/error_injection_fcts.cc
|
||||
cql3/functions/functions.cc
|
||||
cql3/functions/user_function.cc
|
||||
cql3/index_name.cc
|
||||
cql3/keyspace_element_name.cc
|
||||
cql3/lists.cc
|
||||
cql3/maps.cc
|
||||
cql3/operation.cc
|
||||
cql3/prepare_context.cc
|
||||
cql3/query_options.cc
|
||||
cql3/query_processor.cc
|
||||
cql3/relation.cc
|
||||
cql3/restrictions/statement_restrictions.cc
|
||||
cql3/result_set.cc
|
||||
cql3/role_name.cc
|
||||
cql3/selection/abstract_function_selector.cc
|
||||
cql3/selection/selectable.cc
|
||||
cql3/selection/selection.cc
|
||||
cql3/selection/selector.cc
|
||||
cql3/selection/selector_factories.cc
|
||||
cql3/selection/simple_selector.cc
|
||||
cql3/sets.cc
|
||||
cql3/single_column_relation.cc
|
||||
cql3/statements/alter_keyspace_statement.cc
|
||||
cql3/statements/alter_service_level_statement.cc
|
||||
cql3/statements/alter_table_statement.cc
|
||||
cql3/statements/alter_type_statement.cc
|
||||
cql3/statements/alter_view_statement.cc
|
||||
cql3/statements/attach_service_level_statement.cc
|
||||
cql3/statements/authentication_statement.cc
|
||||
cql3/statements/authorization_statement.cc
|
||||
cql3/statements/batch_statement.cc
|
||||
cql3/statements/cas_request.cc
|
||||
cql3/statements/cf_prop_defs.cc
|
||||
cql3/statements/cf_statement.cc
|
||||
cql3/statements/create_aggregate_statement.cc
|
||||
cql3/statements/create_function_statement.cc
|
||||
cql3/statements/create_index_statement.cc
|
||||
cql3/statements/create_keyspace_statement.cc
|
||||
cql3/statements/create_service_level_statement.cc
|
||||
cql3/statements/create_table_statement.cc
|
||||
cql3/statements/create_type_statement.cc
|
||||
cql3/statements/create_view_statement.cc
|
||||
cql3/statements/delete_statement.cc
|
||||
cql3/statements/detach_service_level_statement.cc
|
||||
cql3/statements/drop_aggregate_statement.cc
|
||||
cql3/statements/drop_function_statement.cc
|
||||
cql3/statements/drop_index_statement.cc
|
||||
cql3/statements/drop_keyspace_statement.cc
|
||||
cql3/statements/drop_service_level_statement.cc
|
||||
cql3/statements/drop_table_statement.cc
|
||||
cql3/statements/drop_type_statement.cc
|
||||
cql3/statements/drop_view_statement.cc
|
||||
cql3/statements/function_statement.cc
|
||||
cql3/statements/grant_statement.cc
|
||||
cql3/statements/index_prop_defs.cc
|
||||
cql3/statements/index_target.cc
|
||||
cql3/statements/ks_prop_defs.cc
|
||||
cql3/statements/list_permissions_statement.cc
|
||||
cql3/statements/list_service_level_attachments_statement.cc
|
||||
cql3/statements/list_service_level_statement.cc
|
||||
cql3/statements/list_users_statement.cc
|
||||
cql3/statements/modification_statement.cc
|
||||
cql3/statements/permission_altering_statement.cc
|
||||
cql3/statements/property_definitions.cc
|
||||
cql3/statements/raw/parsed_statement.cc
|
||||
cql3/statements/revoke_statement.cc
|
||||
cql3/statements/role-management-statements.cc
|
||||
cql3/statements/schema_altering_statement.cc
|
||||
cql3/statements/select_statement.cc
|
||||
cql3/statements/service_level_statement.cc
|
||||
cql3/statements/sl_prop_defs.cc
|
||||
cql3/statements/truncate_statement.cc
|
||||
cql3/statements/update_statement.cc
|
||||
cql3/statements/use_statement.cc
|
||||
cql3/token_relation.cc
|
||||
cql3/tuples.cc
|
||||
cql3/type_json.cc
|
||||
cql3/untyped_result_set.cc
|
||||
cql3/update_parameters.cc
|
||||
cql3/user_types.cc
|
||||
cql3/util.cc
|
||||
cql3/ut_name.cc
|
||||
cql3/values.cc
|
||||
database.cc
|
||||
db/batchlog_manager.cc
|
||||
db/commitlog/commitlog.cc
|
||||
db/commitlog/commitlog_entry.cc
|
||||
db/commitlog/commitlog_replayer.cc
|
||||
db/config.cc
|
||||
db/consistency_level.cc
|
||||
db/cql_type_parser.cc
|
||||
db/data_listeners.cc
|
||||
db/extensions.cc
|
||||
db/heat_load_balance.cc
|
||||
db/hints/host_filter.cc
|
||||
db/hints/manager.cc
|
||||
db/hints/resource_manager.cc
|
||||
db/hints/sync_point.cc
|
||||
db/large_data_handler.cc
|
||||
db/legacy_schema_migrator.cc
|
||||
db/marshal/type_parser.cc
|
||||
db/schema_tables.cc
|
||||
db/size_estimates_virtual_reader.cc
|
||||
db/snapshot-ctl.cc
|
||||
db/sstables-format-selector.cc
|
||||
db/system_distributed_keyspace.cc
|
||||
db/system_keyspace.cc
|
||||
db/view/row_locking.cc
|
||||
db/view/view.cc
|
||||
db/view/view_update_generator.cc
|
||||
db/virtual_table.cc
|
||||
dht/boot_strapper.cc
|
||||
dht/i_partitioner.cc
|
||||
dht/murmur3_partitioner.cc
|
||||
dht/range_streamer.cc
|
||||
dht/token.cc
|
||||
distributed_loader.cc
|
||||
duration.cc
|
||||
exceptions/exceptions.cc
|
||||
flat_mutation_reader.cc
|
||||
frozen_mutation.cc
|
||||
frozen_schema.cc
|
||||
generic_server.cc
|
||||
gms/application_state.cc
|
||||
gms/endpoint_state.cc
|
||||
gms/failure_detector.cc
|
||||
gms/feature_service.cc
|
||||
gms/gossip_digest_ack2.cc
|
||||
gms/gossip_digest_ack.cc
|
||||
gms/gossip_digest_syn.cc
|
||||
gms/gossiper.cc
|
||||
gms/inet_address.cc
|
||||
gms/versioned_value.cc
|
||||
gms/version_generator.cc
|
||||
hashers.cc
|
||||
index/secondary_index.cc
|
||||
index/secondary_index_manager.cc
|
||||
init.cc
|
||||
keys.cc
|
||||
lister.cc
|
||||
locator/abstract_replication_strategy.cc
|
||||
locator/azure_snitch.cc
|
||||
locator/ec2_multi_region_snitch.cc
|
||||
locator/ec2_snitch.cc
|
||||
locator/everywhere_replication_strategy.cc
|
||||
locator/gce_snitch.cc
|
||||
locator/gossiping_property_file_snitch.cc
|
||||
locator/local_strategy.cc
|
||||
locator/network_topology_strategy.cc
|
||||
locator/production_snitch_base.cc
|
||||
locator/rack_inferring_snitch.cc
|
||||
locator/simple_snitch.cc
|
||||
locator/simple_strategy.cc
|
||||
locator/snitch_base.cc
|
||||
locator/token_metadata.cc
|
||||
lang/lua.cc
|
||||
main.cc
|
||||
memtable.cc
|
||||
message/messaging_service.cc
|
||||
multishard_mutation_query.cc
|
||||
mutation.cc
|
||||
mutation_fragment.cc
|
||||
mutation_partition.cc
|
||||
mutation_partition_serializer.cc
|
||||
mutation_partition_view.cc
|
||||
mutation_query.cc
|
||||
mutation_reader.cc
|
||||
mutation_writer/feed_writers.cc
|
||||
mutation_writer/multishard_writer.cc
|
||||
mutation_writer/partition_based_splitting_writer.cc
|
||||
mutation_writer/shard_based_splitting_writer.cc
|
||||
mutation_writer/timestamp_based_splitting_writer.cc
|
||||
partition_slice_builder.cc
|
||||
partition_version.cc
|
||||
querier.cc
|
||||
query.cc
|
||||
query-result-set.cc
|
||||
raft/fsm.cc
|
||||
raft/log.cc
|
||||
raft/raft.cc
|
||||
raft/server.cc
|
||||
raft/tracker.cc
|
||||
range_tombstone.cc
|
||||
range_tombstone_list.cc
|
||||
reader_concurrency_semaphore.cc
|
||||
redis/abstract_command.cc
|
||||
redis/command_factory.cc
|
||||
redis/commands.cc
|
||||
redis/keyspace_utils.cc
|
||||
redis/lolwut.cc
|
||||
redis/mutation_utils.cc
|
||||
redis/options.cc
|
||||
redis/query_processor.cc
|
||||
redis/query_utils.cc
|
||||
redis/server.cc
|
||||
redis/service.cc
|
||||
redis/stats.cc
|
||||
release.cc
|
||||
repair/repair.cc
|
||||
repair/row_level.cc
|
||||
row_cache.cc
|
||||
schema.cc
|
||||
schema_mutations.cc
|
||||
schema_registry.cc
|
||||
serializer.cc
|
||||
service/client_state.cc
|
||||
service/migration_manager.cc
|
||||
service/misc_services.cc
|
||||
service/pager/paging_state.cc
|
||||
service/pager/query_pagers.cc
|
||||
service/paxos/paxos_state.cc
|
||||
service/paxos/prepare_response.cc
|
||||
service/paxos/prepare_summary.cc
|
||||
service/paxos/proposal.cc
|
||||
service/priority_manager.cc
|
||||
service/qos/qos_common.cc
|
||||
service/qos/service_level_controller.cc
|
||||
service/qos/standard_service_level_distributed_data_accessor.cc
|
||||
service/raft/raft_gossip_failure_detector.cc
|
||||
service/raft/raft_group_registry.cc
|
||||
service/raft/raft_rpc.cc
|
||||
service/raft/raft_sys_table_storage.cc
|
||||
service/raft/schema_raft_state_machine.cc
|
||||
service/storage_proxy.cc
|
||||
service/storage_service.cc
|
||||
sstables/compress.cc
|
||||
sstables/integrity_checked_file_impl.cc
|
||||
sstables/kl/reader.cc
|
||||
sstables/metadata_collector.cc
|
||||
sstables/m_format_read_helpers.cc
|
||||
sstables/mx/reader.cc
|
||||
sstables/mx/writer.cc
|
||||
sstables/prepended_input_stream.cc
|
||||
sstables/random_access_reader.cc
|
||||
sstables/sstable_directory.cc
|
||||
sstables/sstable_mutation_reader.cc
|
||||
sstables/sstables.cc
|
||||
sstables/sstable_set.cc
|
||||
sstables/sstables_manager.cc
|
||||
sstables/sstable_version.cc
|
||||
sstables/writer.cc
|
||||
streaming/consumer.cc
|
||||
streaming/progress_info.cc
|
||||
streaming/session_info.cc
|
||||
streaming/stream_coordinator.cc
|
||||
streaming/stream_manager.cc
|
||||
streaming/stream_plan.cc
|
||||
streaming/stream_reason.cc
|
||||
streaming/stream_receive_task.cc
|
||||
streaming/stream_request.cc
|
||||
streaming/stream_result_future.cc
|
||||
streaming/stream_session.cc
|
||||
streaming/stream_session_state.cc
|
||||
streaming/stream_summary.cc
|
||||
streaming/stream_task.cc
|
||||
streaming/stream_transfer_task.cc
|
||||
table.cc
|
||||
table_helper.cc
|
||||
thrift/controller.cc
|
||||
thrift/handler.cc
|
||||
thrift/server.cc
|
||||
thrift/thrift_validation.cc
|
||||
timeout_config.cc
|
||||
tools/scylla-sstable-index.cc
|
||||
tools/scylla-types.cc
|
||||
tracing/traced_file.cc
|
||||
tracing/trace_keyspace_helper.cc
|
||||
tracing/trace_state.cc
|
||||
tracing/tracing_backend_registry.cc
|
||||
tracing/tracing.cc
|
||||
transport/controller.cc
|
||||
transport/cql_protocol_extension.cc
|
||||
transport/event.cc
|
||||
transport/event_notifier.cc
|
||||
transport/messages/result_message.cc
|
||||
transport/server.cc
|
||||
types.cc
|
||||
unimplemented.cc
|
||||
utils/arch/powerpc/crc32-vpmsum/crc32_wrapper.cc
|
||||
utils/array-search.cc
|
||||
utils/ascii.cc
|
||||
utils/base64.cc
|
||||
utils/big_decimal.cc
|
||||
utils/bloom_calculations.cc
|
||||
utils/bloom_filter.cc
|
||||
utils/buffer_input_stream.cc
|
||||
utils/build_id.cc
|
||||
utils/config_file.cc
|
||||
utils/directories.cc
|
||||
utils/disk-error-handler.cc
|
||||
utils/dynamic_bitset.cc
|
||||
utils/error_injection.cc
|
||||
utils/exceptions.cc
|
||||
utils/file_lock.cc
|
||||
utils/generation-number.cc
|
||||
utils/gz/crc_combine.cc
|
||||
utils/gz/gen_crc_combine_table.cc
|
||||
utils/human_readable.cc
|
||||
utils/i_filter.cc
|
||||
utils/large_bitset.cc
|
||||
utils/like_matcher.cc
|
||||
utils/limiting_data_source.cc
|
||||
utils/logalloc.cc
|
||||
utils/managed_bytes.cc
|
||||
utils/multiprecision_int.cc
|
||||
utils/murmur_hash.cc
|
||||
utils/rate_limiter.cc
|
||||
utils/rjson.cc
|
||||
utils/runtime.cc
|
||||
utils/updateable_value.cc
|
||||
utils/utf8.cc
|
||||
utils/uuid.cc
|
||||
utils/UUID_gen.cc
|
||||
validation.cc
|
||||
vint-serialization.cc
|
||||
zstd.cc)
|
||||
|
||||
set(scylla_gen_sources
|
||||
"${scylla_thrift_gen_cassandra_files}"
|
||||
"${scylla_ragel_gen_protocol_parser_file}"
|
||||
"${swagger_gen_files}"
|
||||
"${idl_gen_files}"
|
||||
"${antlr3_gen_files}")
|
||||
set(SCYLLA_SOURCE_FILES
|
||||
${SCYLLA_ROOT_SOURCE_FILES}
|
||||
${SCYLLA_GEN_SOURCE_FILES}
|
||||
${SCYLLA_SUB_SOURCE_FILES})
|
||||
|
||||
add_executable(scylla
|
||||
${scylla_sources}
|
||||
${scylla_gen_sources})
|
||||
${SEASTAR_SOURCE_FILES}
|
||||
${SCYLLA_SOURCE_FILES})
|
||||
|
||||
target_link_libraries(scylla PRIVATE
|
||||
seastar
|
||||
# Boost dependencies
|
||||
Boost::filesystem
|
||||
Boost::program_options
|
||||
Boost::system
|
||||
Boost::thread
|
||||
Boost::regex
|
||||
Boost::headers
|
||||
# Abseil libs
|
||||
absl::hashtablez_sampler
|
||||
absl::raw_hash_set
|
||||
absl::synchronization
|
||||
absl::graphcycles_internal
|
||||
absl::stacktrace
|
||||
absl::symbolize
|
||||
absl::debugging_internal
|
||||
absl::demangle_internal
|
||||
absl::time
|
||||
absl::time_zone
|
||||
absl::int128
|
||||
absl::city
|
||||
absl::hash
|
||||
absl::malloc_internal
|
||||
absl::spinlock_wait
|
||||
absl::base
|
||||
absl::dynamic_annotations
|
||||
absl::raw_logging_internal
|
||||
absl::exponential_biased
|
||||
absl::throw_delegate
|
||||
# System libs
|
||||
ZLIB::ZLIB
|
||||
ICU::uc
|
||||
systemd
|
||||
zstd
|
||||
snappy
|
||||
${LUA_LIBRARIES}
|
||||
thrift
|
||||
crypt)
|
||||
# Note that since CLion does not undestand GCC6 concepts, we always disable them (even if users configure otherwise).
|
||||
# CLion seems to have trouble with `-U` (macro undefinition), so we do it this way instead.
|
||||
list(REMOVE_ITEM SEASTAR_CFLAGS "-DHAVE_GCC6_CONCEPTS")
|
||||
|
||||
target_link_libraries(scylla PRIVATE
|
||||
-Wl,--build-id=sha1 # Force SHA1 build-id generation
|
||||
# TODO: Use lld linker if it's available, otherwise gold, else bfd
|
||||
-fuse-ld=lld)
|
||||
# TODO: patch dynamic linker to match configure.py behavior
|
||||
# If the Seastar pkg-config information is available, append to the default flags.
|
||||
#
|
||||
# For ease of browsing the source code, we always pretend that DPDK is enabled.
|
||||
target_compile_options(scylla PUBLIC
|
||||
-std=gnu++1z
|
||||
-DHAVE_DPDK
|
||||
-DHAVE_HWLOC
|
||||
"${SEASTAR_CFLAGS}")
|
||||
|
||||
target_compile_options(scylla PRIVATE
|
||||
-std=gnu++20
|
||||
${cxx_coro_flag}
|
||||
${target_arch_flag})
|
||||
# Hacks needed to expose internal APIs for xxhash dependencies
|
||||
target_compile_definitions(scylla PRIVATE XXH_PRIVATE_API HAVE_LZ4_COMPRESS_DEFAULT)
|
||||
|
||||
target_include_directories(scylla PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
libdeflate
|
||||
abseil
|
||||
"${scylla_gen_build_dir}")
|
||||
|
||||
###
|
||||
### Create crc_combine_table helper executable.
|
||||
### Use it to generate crc_combine_table.cc to be used in scylla at build time.
|
||||
###
|
||||
add_executable(crc_combine_table utils/gz/gen_crc_combine_table.cc)
|
||||
target_link_libraries(crc_combine_table PRIVATE seastar)
|
||||
target_include_directories(crc_combine_table PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_compile_options(crc_combine_table PRIVATE
|
||||
-std=gnu++20
|
||||
${cxx_coro_flag}
|
||||
${target_arch_flag})
|
||||
add_dependencies(scylla crc_combine_table)
|
||||
|
||||
# Generate an additional source file at build time that is needed for Scylla compilation
|
||||
add_custom_command(OUTPUT "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc"
|
||||
COMMAND $<TARGET_FILE:crc_combine_table> > "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc"
|
||||
DEPENDS crc_combine_table)
|
||||
target_sources(scylla PRIVATE "${scylla_gen_build_dir}/utils/gz/crc_combine_table.cc")
|
||||
|
||||
###
|
||||
### Generate version file and supply appropriate compile definitions for release.cc
|
||||
###
|
||||
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/SCYLLA-VERSION-GEN --output-dir "${CMAKE_BINARY_DIR}/gen" RESULT_VARIABLE scylla_version_gen_res)
|
||||
if(scylla_version_gen_res)
|
||||
message(SEND_ERROR "Version file generation failed. Return code: ${scylla_version_gen_res}")
|
||||
endif()
|
||||
|
||||
file(READ "${CMAKE_BINARY_DIR}/gen/SCYLLA-VERSION-FILE" scylla_version)
|
||||
string(STRIP "${scylla_version}" scylla_version)
|
||||
|
||||
file(READ "${CMAKE_BINARY_DIR}/gen/SCYLLA-RELEASE-FILE" scylla_release)
|
||||
string(STRIP "${scylla_release}" scylla_release)
|
||||
|
||||
get_property(release_cdefs SOURCE "${CMAKE_SOURCE_DIR}/release.cc" PROPERTY COMPILE_DEFINITIONS)
|
||||
list(APPEND release_cdefs "SCYLLA_VERSION=\"${scylla_version}\"" "SCYLLA_RELEASE=\"${scylla_release}\"")
|
||||
set_source_files_properties("${CMAKE_SOURCE_DIR}/release.cc" PROPERTIES COMPILE_DEFINITIONS "${release_cdefs}")
|
||||
|
||||
###
|
||||
### Custom command for building libdeflate. Link the library to scylla.
|
||||
###
|
||||
set(libdeflate_lib "${scylla_build_dir}/libdeflate/libdeflate.a")
|
||||
add_custom_command(OUTPUT "${libdeflate_lib}"
|
||||
COMMAND make -C "${CMAKE_SOURCE_DIR}/libdeflate"
|
||||
BUILD_DIR=../build/${BUILD_TYPE}/libdeflate/
|
||||
CC=${CMAKE_C_COMPILER}
|
||||
"CFLAGS=${target_arch_flag}"
|
||||
../build/${BUILD_TYPE}/libdeflate//libdeflate.a) # Two backslashes are important!
|
||||
# Hack to force generating custom command to produce libdeflate.a
|
||||
add_custom_target(libdeflate DEPENDS "${libdeflate_lib}")
|
||||
target_link_libraries(scylla PRIVATE "${libdeflate_lib}")
|
||||
|
||||
# TODO: create cmake/ directory and move utilities (generate functions etc) there
|
||||
# TODO: Build tests if BUILD_TESTING=on (using CTest module)
|
||||
# The order matters here: prefer the "static" DPDK directories to any dynamic paths from pkg-config. Some files are only
|
||||
# available dynamically, though.
|
||||
target_include_directories(scylla PUBLIC
|
||||
.
|
||||
${SEASTAR_DPDK_INCLUDE_DIRS}
|
||||
${SEASTAR_INCLUDE_DIRS}
|
||||
${Boost_INCLUDE_DIRS}
|
||||
xxhash
|
||||
libdeflate
|
||||
build/release/gen)
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
# Contributing to Scylla
|
||||
# Asking questions or requesting help
|
||||
|
||||
## 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 [Scylla Users mailing list](https://groups.google.com/g/scylladb-users) or the [Slack workspace](http://slack.scylladb.com) for general questions and help.
|
||||
# Reporting an issue
|
||||
|
||||
Join the [Scylla Developers mailing list](https://groups.google.com/g/scylladb-dev) for deeper technical discussions and to discuss your ideas for contributions.
|
||||
Please use the [Issue Tracker](https://github.com/scylladb/scylla/issues/) to report issues. Fill in as much information as you can in the issue template, especially for performance problems.
|
||||
|
||||
## Reporting an issue
|
||||
# Contributing Code to Scylla
|
||||
|
||||
Please use the [issue tracker](https://github.com/scylladb/scylla/issues/) to report issues or to suggest features. Fill in as much information as you can in the issue template, especially for performance problems.
|
||||
|
||||
## Contributing code to Scylla
|
||||
|
||||
Before you can contribute code to Scylla for the first time, you should sign the [Contributor License Agreement](https://www.scylladb.com/open-source/contributor-agreement/) and send the signed form cla@scylladb.com. You can then submit your changes as patches to the to the [scylladb-dev mailing list](https://groups.google.com/forum/#!forum/scylladb-dev) or as a pull request to the [Scylla project on github](https://github.com/scylladb/scylla).
|
||||
If you need help formatting or sending patches, [check out these instructions](https://github.com/scylladb/scylla/wiki/Formatting-and-sending-patches).
|
||||
|
||||
The Scylla C++ source code uses the [Seastar coding style](https://github.com/scylladb/seastar/blob/master/coding-style.md) so please adhere to that in your patches. Note that Scylla code is written with `using namespace seastar`, so should not explicitly add the `seastar::` prefix to Seastar symbols. You will usually not need to add `using namespace seastar` to new source files, because most Scylla header files have `#include "seastarx.hh"`, which does this.
|
||||
|
||||
Header files in Scylla must be self-contained, i.e., each can be included without having to include specific other headers first. To verify that your change did not break this property, run `ninja dev-headers`. If you added or removed header files, you must `touch configure.py` first - this will cause `configure.py` to be automatically re-run to generate a fresh list of header files.
|
||||
To contribute code to Scylla, you need to sign the [Contributor License Agreement](http://www.scylladb.com/opensource/cla/) and send your changes as [patches](https://github.com/scylladb/scylla/wiki/Formatting-and-sending-patches) to the [mailing list](https://groups.google.com/forum/#!forum/scylladb-dev). We don't accept pull requests on GitHub.
|
||||
|
||||
62
HACKING.md
62
HACKING.md
@@ -18,35 +18,23 @@ $ git submodule update --init --recursive
|
||||
|
||||
### Dependencies
|
||||
|
||||
Scylla is fairly fussy about its build environment, requiring a very recent
|
||||
version of the C++20 compiler and numerous tools and libraries to build.
|
||||
Scylla depends on the system package manager for its development dependencies.
|
||||
|
||||
Run `./install-dependencies.sh` (as root) to use your Linux distributions's
|
||||
package manager to install the appropriate packages on your build machine.
|
||||
However, this will only work on very recent distributions. For example,
|
||||
currently Fedora users must upgrade to Fedora 32 otherwise the C++ compiler
|
||||
will be too old, and not support the new C++20 standard that Scylla uses.
|
||||
Running `./install-dependencies.sh` (as root) installs the appropriate packages based on your Linux distribution.
|
||||
|
||||
Alternatively, to avoid having to upgrade your build machine or install
|
||||
various packages on it, we provide another option - the **frozen toolchain**.
|
||||
This is a script, `./tools/toolchain/dbuild`, that can execute build or run
|
||||
commands inside a Docker image that contains exactly the right build tools and
|
||||
libraries. The `dbuild` technique is useful for beginners, but is also the way
|
||||
in which ScyllaDB produces official releases, so it is highly recommended.
|
||||
On Ubuntu and Debian based Linux distributions, some packages
|
||||
required to build Scylla are missing in the official upstream:
|
||||
|
||||
To use `dbuild`, you simply prefix any build or run command with it. Building
|
||||
and running Scylla becomes as easy as:
|
||||
- libthrift-dev and libthrift
|
||||
- antlr3-c++-dev
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
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
|
||||
thread, and up to 3 GB per native thread while linking. GCC >= 10 is
|
||||
thread, and up to 3 GB per native thread while linking. GCC >= 8.1.1. is
|
||||
required.
|
||||
|
||||
Scylla is built with [Ninja](https://ninja-build.org/), a low-level rule-based system. A Python script, `configure.py`, generates a Ninja file (`build.ninja`) based on configuration options.
|
||||
@@ -68,7 +56,7 @@ $ ./configure.py --help
|
||||
|
||||
The most important option is:
|
||||
|
||||
- `--enable-dpdk`: [DPDK](http://dpdk.org/) is a set of libraries and drivers for fast packet processing. During development, it's not necessary to enable support even if it is supported by your platform.
|
||||
- `--{enable,disable}-dpdk`: [DPDK](http://dpdk.org/) is a set of libraries and drivers for fast packet processing. During development, it's not necessary to enable support even if it is supported by your platform.
|
||||
|
||||
Source files and build targets are tracked manually in `configure.py`, so the script needs to be updated when new files or targets are added or removed.
|
||||
|
||||
@@ -153,7 +141,7 @@ In v3:
|
||||
"Tests: unit ({mode}), dtest ({smp})"
|
||||
```
|
||||
|
||||
The usual is "Tests: unit (dev)", although running debug tests is encouraged.
|
||||
The usual is "Tests: unit (release)", although running debug tests is encouraged.
|
||||
|
||||
5. When answering review comments, prefer inline quotes as they make it easier to track the conversation across multiple e-mails.
|
||||
|
||||
@@ -172,8 +160,12 @@ and you will get output like this:
|
||||
```
|
||||
CQL QUERY LANGUAGE
|
||||
Tomasz Grabiec <tgrabiec@scylladb.com> [maintainer]
|
||||
Pekka Enberg <penberg@scylladb.com> [maintainer]
|
||||
MATERIALIZED VIEWS
|
||||
Pekka Enberg <penberg@scylladb.com> [maintainer]
|
||||
Duarte Nunes <duarte@scylladb.com> [maintainer]
|
||||
Nadav Har'El <nyh@scylladb.com> [reviewer]
|
||||
Duarte Nunes <duarte@scylladb.com> [reviewer]
|
||||
```
|
||||
|
||||
### Running Scylla
|
||||
@@ -362,27 +354,7 @@ $ git remote update
|
||||
$ git checkout -t local/my_local_seastar_branch
|
||||
```
|
||||
|
||||
### Generating code coverage report
|
||||
|
||||
Install dependencies:
|
||||
|
||||
$ dnf install llvm # for llvm-profdata and llvm-cov
|
||||
$ dnf install lcov # for genhtml
|
||||
|
||||
Instruct `configure.py` to generate build files for `coverage` mode:
|
||||
|
||||
$ ./configure.py --mode=coverage
|
||||
|
||||
Build the tests you want to run, then run them via `test.py` (important!):
|
||||
|
||||
$ ./test.py --mode=coverage [...]
|
||||
|
||||
Alternatively, you can run individual tests via `./scripts/coverage.py --run`.
|
||||
|
||||
Open the link printed at the end. Be horrified. Go and write more tests.
|
||||
|
||||
For more details see `./scripts/coverage.py --help`.
|
||||
|
||||
### Core dump debugging
|
||||
|
||||
See [debugging.md](debugging.md).
|
||||
Slides:
|
||||
2018.11.20: https://www.slideshare.net/tomekgrabiec/scylla-core-dump-debugging-tools
|
||||
|
||||
103
IDL.md
Normal file
103
IDL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
#IDL definition
|
||||
The schema we use similar to c++ schema.
|
||||
Use class or struct similar to the object you need the serializer for.
|
||||
Use namespace when applicable.
|
||||
|
||||
##keywords
|
||||
* class/struct - a class or a struct like C++
|
||||
class/struct can have final or stub marker
|
||||
* namespace - has the same C++ meaning
|
||||
* enum class - has the same C++ meaning
|
||||
* final modifier for class - when a class mark as final it will not contain a size parameter. Note that final class cannot be extended by future version, so use with care
|
||||
* stub class - when a class is mark as stub, it means that no code will be generated for this class and it is only there as a documentation.
|
||||
* version attributes - mark with [[version id ]] mark that a field is available from a specific version
|
||||
* template - A template class definition like C++
|
||||
##Syntax
|
||||
|
||||
###Namespace
|
||||
```
|
||||
namespace ns_name { namespace-body }
|
||||
```
|
||||
* ns_name: either a previously unused identifier, in which case this is original-namespace-definition or the name of a namespace, in which case this is extension-namespace-definition
|
||||
* namespace-body: possibly empty sequence of declarations of any kind (including class and struct definitions as well as nested namespaces)
|
||||
|
||||
###class/struct
|
||||
`
|
||||
class-key class-name final(optional) stub(optional) { member-specification } ;(optional)
|
||||
`
|
||||
* class-key: one of class or struct.
|
||||
* class-name: the name of the class that's being defined. optionally followed by keyword final, optionally followed by keyword stub
|
||||
* final: when a class mark as final, it means it can not be extended and there is no need to serialize its size, use with care.
|
||||
* stub: when a class is mark as stub, it means no code will generate for it and it is added for documentation only.
|
||||
* member-specification: list of access specifiers, and public member accessor see class member below.
|
||||
* to be compatible with C++ a class definition can be followed by a semicolon.
|
||||
###enum
|
||||
`enum-key identifier enum-base { enumerator-list(optional) }`
|
||||
* enum-key: only enum class is supported
|
||||
* identifier: the name of the enumeration that's being declared.
|
||||
* enum-base: colon (:), followed by a type-specifier-seq that names an integral type (see the C++ standard for the full list of all possible integral types).
|
||||
* enumerator-list: comma-separated list of enumerator definitions, each of which is either simply an identifier, which becomes the name of the enumerator, or an identifier with an initializer: identifier = integral value.
|
||||
Note that though C++ allows constexpr as an initialize value, it makes the documentation less readable, hence is not permitted.
|
||||
|
||||
###class member
|
||||
`type member-access attributes(optional) default-value(optional);`
|
||||
* type: Any valid C++ type, following the C++ notation. note that there should be a serializer for the type, but deceleration order is not mandatory
|
||||
* member-access: is the way the member can be access. If the member is public it can be the name itself. if not it could be a getter function that should be followed by braces. Note that getter can (and probably should) be const methods.
|
||||
* attributes: Attributes define by square brackets. Currently are use to mark a version in which a specific member was added [ [ version version-number] ] would mark that the specific member was added in the given version number.
|
||||
|
||||
###template
|
||||
`template < parameter-list > class-declaration`
|
||||
* parameter-list - a non-empty comma-separated list of the template parameters.
|
||||
* class-decleration - (See class section) The class name declared become a template name.
|
||||
|
||||
##IDL example
|
||||
Forward slashes comments are ignored until the end of the line.
|
||||
```
|
||||
namespace utils {
|
||||
// An example of a stub class
|
||||
class UUID stub {
|
||||
int64_t most_sig_bits;
|
||||
int64_t least_sig_bits;
|
||||
}
|
||||
}
|
||||
|
||||
namespace gms {
|
||||
//an enum example
|
||||
enum class application_state:int {STATUS = 0,
|
||||
LOAD,
|
||||
SCHEMA,
|
||||
DC};
|
||||
|
||||
// example of final class
|
||||
class versioned_value final {
|
||||
// getter and setter as public member
|
||||
int version;
|
||||
sstring value;
|
||||
}
|
||||
|
||||
class heart_beat_state {
|
||||
//getter as function
|
||||
int32_t get_generation();
|
||||
//default value example
|
||||
int32_t get_heart_beat_version() = 1;
|
||||
}
|
||||
|
||||
class endpoint_state {
|
||||
heart_beat_state get_heart_beat_state();
|
||||
std::map<application_state, versioned_value> get_application_state_map();
|
||||
}
|
||||
|
||||
class gossip_digest {
|
||||
inet_address get_endpoint();
|
||||
int32_t get_generation();
|
||||
//mark that a field was added on a specific version
|
||||
int32_t get_max_version() [ [version 0.14.2] ];
|
||||
}
|
||||
|
||||
class gossip_digest_ack {
|
||||
std::vector<gossip_digest> digests();
|
||||
std::map<inet_address, gms::endpoint_state> get_endpoint_state_map();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
131
MAINTAINERS
Normal file
131
MAINTAINERS
Normal file
@@ -0,0 +1,131 @@
|
||||
M: Maintainer with commit access
|
||||
R: Reviewer with subsystem expertise
|
||||
F: Filename, directory, or pattern for the subsystem
|
||||
|
||||
---
|
||||
|
||||
AUTH
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Calle Wilund <calle@scylladb.com>
|
||||
R: Vlad Zolotarov <vladz@scylladb.com>
|
||||
R: Jesse Haber-Kucharsky <jhaberku@scylladb.com>
|
||||
F: auth/*
|
||||
|
||||
CACHE
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
R: Piotr Jastrzebski <piotr@scylladb.com>
|
||||
F: row_cache*
|
||||
F: *mutation*
|
||||
F: tests/mvcc*
|
||||
|
||||
COMMITLOG / BATCHLOGa
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Calle Wilund <calle@scylladb.com>
|
||||
F: db/commitlog/*
|
||||
F: db/batch*
|
||||
|
||||
COORDINATOR
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Gleb Natapov <gleb@scylladb.com>
|
||||
F: service/storage_proxy*
|
||||
|
||||
COMPACTION
|
||||
R: Raphael S. Carvalho <raphaelsc@scylladb.com>
|
||||
R: Glauber Costa <glauber@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: sstables/compaction*
|
||||
|
||||
CQL TRANSPORT LAYER
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: transport/*
|
||||
|
||||
CQL QUERY LANGUAGE
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: cql3/*
|
||||
|
||||
COUNTERS
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
F: counters*
|
||||
F: tests/counter_test*
|
||||
|
||||
GOSSIP
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
F: gms/*
|
||||
|
||||
DOCKER
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: dist/docker/*
|
||||
|
||||
LSA
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
F: utils/logalloc*
|
||||
|
||||
MATERIALIZED VIEWS
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
R: Duarte Nunes <duarte@scylladb.com>
|
||||
F: db/view/*
|
||||
F: cql3/statements/*view*
|
||||
|
||||
PACKAGING
|
||||
R: Takuya ASADA <syuu@scylladb.com>
|
||||
F: dist/*
|
||||
|
||||
REPAIR
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: repair/*
|
||||
|
||||
SCHEMA MANAGEMENT
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
F: db/schema_tables*
|
||||
F: db/legacy_schema_migrator*
|
||||
F: service/migration*
|
||||
F: schema*
|
||||
|
||||
SECONDARY INDEXES
|
||||
M: Pekka Enberg <penberg@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
R: Pekka Enberg <penberg@scylladb.com>
|
||||
F: db/index/*
|
||||
F: cql3/statements/*index*
|
||||
|
||||
SSTABLES
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Raphael S. Carvalho <raphaelsc@scylladb.com>
|
||||
R: Glauber Costa <glauber@scylladb.com>
|
||||
R: Nadav Har'El <nyh@scylladb.com>
|
||||
F: sstables/*
|
||||
|
||||
STREAMING
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
R: Asias He <asias@scylladb.com>
|
||||
F: streaming/*
|
||||
F: service/storage_service.*
|
||||
|
||||
THRIFT TRANSPORT LAYER
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
F: thrift/*
|
||||
|
||||
THE REST
|
||||
M: Avi Kivity <avi@scylladb.com>
|
||||
M: Paweł Dziepak <pdziepak@scylladb.com>
|
||||
M: Duarte Nunes <duarte@scylladb.com>
|
||||
M: Tomasz Grabiec <tgrabiec@scylladb.com>
|
||||
F: *
|
||||
@@ -1,11 +1,5 @@
|
||||
This project includes code developed by the Apache Software Foundation (http://www.apache.org/),
|
||||
especially Apache Cassandra.
|
||||
|
||||
It includes files from https://github.com/antonblanchard/crc32-vpmsum (author Anton Blanchard <anton@au.ibm.com>, IBM).
|
||||
It also includes files from https://github.com/antonblanchard/crc32-vpmsum (author Anton Blanchard <anton@au.ibm.com>, IBM).
|
||||
These files are located in utils/arch/powerpc/crc32-vpmsum. Their license may be found in licenses/LICENSE-crc32-vpmsum.TXT.
|
||||
|
||||
It includes modified code from https://gitbox.apache.org/repos/asf?p=cassandra-dtest.git (owned by The Apache Software Foundation)
|
||||
|
||||
It includes modified tests from https://github.com/etcd-io/etcd.git (owned by The etcd Authors)
|
||||
|
||||
It includes files from https://github.com/bytecodealliance/wasmtime-cpp (owned by Bytecode Alliance), licensed with Apache License 2.0.
|
||||
|
||||
29
README-DPDK.md
Normal file
29
README-DPDK.md
Normal file
@@ -0,0 +1,29 @@
|
||||
Seastar and DPDK
|
||||
================
|
||||
|
||||
Seastar uses the Data Plane Development Kit to drive NIC hardware directly. This
|
||||
provides an enormous performance boost.
|
||||
|
||||
To enable DPDK, specify `--enable-dpdk` to `./configure.py`, and `--dpdk-pmd` as a
|
||||
run-time parameter. This will use the DPDK package provided as a git submodule with the
|
||||
seastar sources.
|
||||
|
||||
To use your own self-compiled DPDK package, follow this procedure:
|
||||
|
||||
1. Setup host to compile DPDK:
|
||||
- Ubuntu
|
||||
`sudo apt-get install -y build-essential linux-image-extra-$(uname -r)`
|
||||
2. Prepare a DPDK SDK:
|
||||
- Download the latest DPDK release: `wget http://dpdk.org/browse/dpdk/snapshot/dpdk-1.8.0.tar.gz`
|
||||
- Untar it.
|
||||
- Edit config/common_linuxapp: set CONFIG_RTE_MBUF_REFCNT and CONFIG_RTE_LIBRTE_KNI to 'n'.
|
||||
- For DPDK 1.7.x: edit config/common_linuxapp:
|
||||
- Set CONFIG_RTE_LIBRTE_PMD_BOND to 'n'.
|
||||
- Set CONFIG_RTE_MBUF_SCATTER_GATHER to 'n'.
|
||||
- Set CONFIG_RTE_LIBRTE_IP_FRAG to 'n'.
|
||||
- Start the tools/setup.sh script as root.
|
||||
- Compile a linuxapp target (option 9).
|
||||
- Install IGB_UIO module (option 11).
|
||||
- Bind some physical port to IGB_UIO (option 17).
|
||||
- Configure hugepage mappings (option 14/15).
|
||||
3. Run a configure.py: `./configure.py --dpdk-target <Path to untared dpdk-1.8.0 above>/x86_64-native-linuxapp-gcc`.
|
||||
145
README.md
145
README.md
@@ -1,110 +1,85 @@
|
||||
# Scylla
|
||||
|
||||
[](http://slack.scylladb.com)
|
||||
[](https://twitter.com/intent/follow?screen_name=ScyllaDB)
|
||||
## Quick-start
|
||||
|
||||
## What is Scylla?
|
||||
|
||||
Scylla is the real-time big data database that is API-compatible with Apache Cassandra and Amazon DynamoDB.
|
||||
Scylla embraces a shared-nothing approach that increases throughput and storage capacity to realize order-of-magnitude performance improvements and reduce hardware costs.
|
||||
|
||||
For more information, please see the [ScyllaDB web site].
|
||||
|
||||
[ScyllaDB web site]: https://www.scylladb.com
|
||||
|
||||
## Build Prerequisites
|
||||
|
||||
Scylla is fairly fussy about its build environment, requiring very recent
|
||||
versions of the C++20 compiler and of many libraries to build. The document
|
||||
[HACKING.md](HACKING.md) includes detailed information on building and
|
||||
developing Scylla, but to get Scylla building quickly on (almost) any build
|
||||
machine, Scylla offers a [frozen toolchain](tools/toolchain/README.md),
|
||||
This is a pre-configured Docker image which includes recent versions of all
|
||||
the required compilers, libraries and build tools. Using the frozen toolchain
|
||||
allows you to avoid changing anything in your build machine to meet Scylla's
|
||||
requirements - you just need to meet the frozen toolchain's prerequisites
|
||||
(mostly, Docker or Podman being available).
|
||||
|
||||
## Building Scylla
|
||||
|
||||
Building Scylla with the frozen toolchain `dbuild` is as easy as:
|
||||
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
|
||||
$ git submodule update --init --force --recursive
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
```
|
||||
$ ./tools/toolchain/dbuild ./configure.py
|
||||
$ ./tools/toolchain/dbuild ninja build/release/scylla
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --developer-mode 1
|
||||
```
|
||||
|
||||
For further information, please see:
|
||||
Please see [HACKING.md](HACKING.md) for detailed information on building and developing Scylla.
|
||||
|
||||
* [Developer documentation] for more information on building Scylla.
|
||||
* [Build documentation] on how to build Scylla binaries, tests, and packages.
|
||||
* [Docker image build documentation] for information on how to build Docker images.
|
||||
|
||||
[developer documentation]: HACKING.md
|
||||
[build documentation]: docs/guides/building.md
|
||||
[docker image build documentation]: dist/docker/debian/README.md
|
||||
**Note**: GCC >= 8.1.1 is required to compile Scylla.
|
||||
|
||||
## Running Scylla
|
||||
|
||||
To start Scylla server, run:
|
||||
* Run Scylla
|
||||
```
|
||||
./build/release/scylla
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --workdir tmp --smp 1 --developer-mode 1
|
||||
```
|
||||
|
||||
This will start a Scylla node with one CPU core allocated to it and data files stored in the `tmp` directory.
|
||||
The `--developer-mode` is needed to disable the various checks Scylla performs at startup to ensure the machine is configured for maximum performance (not relevant on development workstations).
|
||||
Please note that you need to run Scylla with `dbuild` if you built it with the frozen toolchain.
|
||||
* run Scylla with one CPU and ./tmp as data directory
|
||||
|
||||
For more run options, run:
|
||||
|
||||
```bash
|
||||
$ ./tools/toolchain/dbuild ./build/release/scylla --help
|
||||
```
|
||||
./build/release/scylla --datadir tmp --commitlog-directory tmp --smp 1
|
||||
```
|
||||
|
||||
## Testing
|
||||
* For more run options:
|
||||
```
|
||||
./build/release/scylla --help
|
||||
```
|
||||
|
||||
See [test.py manual](docs/guides/testing.md).
|
||||
## Building Fedora RPM
|
||||
|
||||
## Scylla APIs and compatibility
|
||||
By default, Scylla is compatible with Apache Cassandra and its APIs - CQL and
|
||||
Thrift. There is also support for the API of Amazon DynamoDB™,
|
||||
which needs to be enabled and configured in order to be used. For more
|
||||
information on how to enable the DynamoDB™ API in Scylla,
|
||||
and the current compatibility of this feature as well as Scylla-specific extensions, see
|
||||
[Alternator](docs/alternator/alternator.md) and
|
||||
[Getting started with Alternator](docs/alternator/getting-started.md).
|
||||
As a pre-requisite, you need to install [Mock](https://fedoraproject.org/wiki/Mock) on your machine:
|
||||
|
||||
## Documentation
|
||||
```
|
||||
# Install mock:
|
||||
sudo yum install mock
|
||||
|
||||
Documentation can be found [here](https://scylla.docs.scylladb.com).
|
||||
Seastar documentation can be found [here](http://docs.seastar.io/master/index.html).
|
||||
User documentation can be found [here](https://docs.scylladb.com/).
|
||||
# Add user to the "mock" group:
|
||||
usermod -a -G mock $USER && newgrp mock
|
||||
```
|
||||
|
||||
## Training
|
||||
Then, to build an RPM, run:
|
||||
|
||||
Training material and online courses can be found at [Scylla University](https://university.scylladb.com/).
|
||||
The courses are free, self-paced and include hands-on examples. They cover a variety of topics including Scylla data modeling,
|
||||
administration, architecture, basic NoSQL concepts, using drivers for application development, Scylla setup, failover, compactions,
|
||||
multi-datacenters and how Scylla integrates with third-party applications.
|
||||
```
|
||||
./dist/redhat/build_rpm.sh
|
||||
```
|
||||
|
||||
The built RPM is stored in ``/var/lib/mock/<configuration>/result`` directory.
|
||||
For example, on Fedora 21 mock reports the following:
|
||||
|
||||
```
|
||||
INFO: Done(scylla-server-0.00-1.fc21.src.rpm) Config(default) 20 minutes 7 seconds
|
||||
INFO: Results and/or logs in: /var/lib/mock/fedora-21-x86_64/result
|
||||
```
|
||||
|
||||
## Building Fedora-based Docker image
|
||||
|
||||
Build a Docker image with:
|
||||
|
||||
```
|
||||
cd dist/docker
|
||||
docker build -t <image-name> .
|
||||
```
|
||||
|
||||
Run the image with:
|
||||
|
||||
```
|
||||
docker run -p $(hostname -i):9042:9042 -i -t <image name>
|
||||
```
|
||||
|
||||
## Contributing to Scylla
|
||||
|
||||
If you want to report a bug or submit a pull request or a patch, please read the [contribution guidelines].
|
||||
|
||||
If you are a developer working on Scylla, please read the [developer guidelines].
|
||||
|
||||
[contribution guidelines]: CONTRIBUTING.md
|
||||
[developer guidelines]: HACKING.md
|
||||
|
||||
## Contact
|
||||
|
||||
* The [users mailing list] and [Slack channel] are for users to discuss configuration, management, and operations of the ScyllaDB open source.
|
||||
* The [developers mailing list] is for developers and people interested in following the development of ScyllaDB to discuss technical topics.
|
||||
|
||||
[Users mailing list]: https://groups.google.com/forum/#!forum/scylladb-users
|
||||
|
||||
[Slack channel]: http://slack.scylladb.com/
|
||||
|
||||
[Developers mailing list]: https://groups.google.com/forum/#!forum/scylladb-dev
|
||||
[Hacking howto](HACKING.md)
|
||||
[Guidelines for contributing](CONTRIBUTING.md)
|
||||
|
||||
@@ -1,66 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
USAGE=$(cat <<-END
|
||||
Usage: $(basename "$0") [-h|--help] [-o|--output-dir PATH] -- generate Scylla version and build information files.
|
||||
|
||||
Options:
|
||||
-h|--help show this help message.
|
||||
-o|--output-dir PATH specify destination path at which the version files are to be created.
|
||||
|
||||
By default, the script will attempt to parse 'version' file
|
||||
in the current directory, which should contain a string of
|
||||
'\$version-\$release' form.
|
||||
|
||||
Otherwise, it will call 'git log' on the source tree (the
|
||||
directory, which contains the script) to obtain current
|
||||
commit hash and use it for building the version and release
|
||||
strings.
|
||||
|
||||
The script assumes that it's called from the Scylla source
|
||||
tree.
|
||||
|
||||
The files created are:
|
||||
SCYLLA-VERSION-FILE
|
||||
SCYLLA-RELEASE-FILE
|
||||
SCYLLA-PRODUCT-FILE
|
||||
|
||||
By default, these files are created in the 'build'
|
||||
subdirectory under the directory containing the script.
|
||||
The destination directory can be overriden by
|
||||
using '-o PATH' option.
|
||||
END
|
||||
)
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
opt="$1"
|
||||
case $opt in
|
||||
-h|--help)
|
||||
echo "$USAGE"
|
||||
exit 0
|
||||
;;
|
||||
-o|--output-dir)
|
||||
OUTPUT_DIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected argument found: $1"
|
||||
echo
|
||||
echo "$USAGE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
if [ -z "$OUTPUT_DIR" ]; then
|
||||
OUTPUT_DIR="$SCRIPT_DIR/build"
|
||||
fi
|
||||
|
||||
# Default scylla product/version tags
|
||||
PRODUCT=scylla
|
||||
VERSION=4.6.11
|
||||
VERSION=666.development
|
||||
|
||||
if test -f version
|
||||
then
|
||||
@@ -68,7 +9,7 @@ then
|
||||
SCYLLA_RELEASE=$(cat version | awk -F'-' '{print $2}')
|
||||
else
|
||||
DATE=$(date +%Y%m%d)
|
||||
GIT_COMMIT=$(git -C "$SCRIPT_DIR" log --pretty=format:'%h' -n 1)
|
||||
GIT_COMMIT=$(git log --pretty=format:'%h' -n 1)
|
||||
SCYLLA_VERSION=$VERSION
|
||||
# For custom package builds, replace "0" with "counter.your_name",
|
||||
# where counter starts at 1 and increments for successive versions.
|
||||
@@ -78,15 +19,8 @@ else
|
||||
SCYLLA_RELEASE=$SCYLLA_BUILD.$DATE.$GIT_COMMIT
|
||||
fi
|
||||
|
||||
if [ -f "$OUTPUT_DIR/SCYLLA-RELEASE-FILE" ]; then
|
||||
GIT_COMMIT_FILE=$(cat "$OUTPUT_DIR/SCYLLA-RELEASE-FILE" |cut -d . -f 3)
|
||||
if [ "$GIT_COMMIT" = "$GIT_COMMIT_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$SCYLLA_VERSION-$SCYLLA_RELEASE"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
echo "$SCYLLA_VERSION" > "$OUTPUT_DIR/SCYLLA-VERSION-FILE"
|
||||
echo "$SCYLLA_RELEASE" > "$OUTPUT_DIR/SCYLLA-RELEASE-FILE"
|
||||
echo "$PRODUCT" > "$OUTPUT_DIR/SCYLLA-PRODUCT-FILE"
|
||||
mkdir -p build
|
||||
echo "$SCYLLA_VERSION" > build/SCYLLA-VERSION-FILE
|
||||
echo "$SCYLLA_RELEASE" > build/SCYLLA-RELEASE-FILE
|
||||
echo "$PRODUCT" > build/SCYLLA-PRODUCT-FILE
|
||||
|
||||
1
abseil
1
abseil
Submodule abseil deleted from f70eadadd7
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "absl-flat_hash_map.hh"
|
||||
|
||||
size_t sstring_hash::operator()(std::string_view v) const noexcept {
|
||||
return absl::Hash<std::string_view>{}(v);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020-present 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 <absl/container/flat_hash_map.h>
|
||||
#include <seastar/core/sstring.hh>
|
||||
|
||||
using namespace seastar;
|
||||
|
||||
struct sstring_hash {
|
||||
using is_transparent = void;
|
||||
size_t operator()(std::string_view v) const noexcept;
|
||||
};
|
||||
|
||||
struct sstring_eq {
|
||||
using is_transparent = void;
|
||||
bool operator()(std::string_view a, std::string_view b) const noexcept {
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename K, typename V, typename... Ts>
|
||||
struct flat_hash_map : public absl::flat_hash_map<K, V, Ts...> {
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
struct flat_hash_map<sstring, V>
|
||||
: public absl::flat_hash_map<sstring, V, sstring_hash, sstring_eq> {};
|
||||
33
alternator-test/README.md
Normal file
33
alternator-test/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -24,17 +24,8 @@
|
||||
|
||||
import pytest
|
||||
import boto3
|
||||
import requests
|
||||
from util import create_test_table
|
||||
|
||||
# When tests are run with HTTPS, the server often won't have its SSL
|
||||
# certificate signed by a known authority. So we will disable certificate
|
||||
# verification with the "verify=False" request option. However, once we do
|
||||
# that, we start getting scary-looking warning messages, saying that this
|
||||
# makes HTTPS insecure. The following silences those warnings:
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# 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
|
||||
@@ -52,25 +43,6 @@ if (LooseVersion(botocore.__version__) < LooseVersion('1.12.54')):
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--aws", action="store_true",
|
||||
help="run against AWS instead of a local Scylla installation")
|
||||
parser.addoption("--https", action="store_true",
|
||||
help="communicate via HTTPS protocol on port 8043 instead of HTTP when"
|
||||
" running against a local Scylla installation")
|
||||
parser.addoption("--url", action="store",
|
||||
help="communicate with given URL instead of defaults")
|
||||
parser.addoption("--runveryslow", action="store_true",
|
||||
help="run tests marked veryslow instead of skipping them")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "veryslow: mark test as very slow to run")
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
if config.getoption("--runveryslow"):
|
||||
# --runveryslow given in cli: do not skip veryslow tests
|
||||
return
|
||||
skip_veryslow = pytest.mark.skip(reason="need --runveryslow option to run")
|
||||
for item in items:
|
||||
if "veryslow" in item.keywords:
|
||||
item.add_marker(skip_veryslow)
|
||||
|
||||
# "dynamodb" fixture: set up client object for communicating with the DynamoDB
|
||||
# API. Currently this chooses either Amazon's DynamoDB in the default region
|
||||
@@ -80,61 +52,15 @@ def pytest_collection_modifyitems(config, items):
|
||||
# We use scope="session" so that all tests will reuse the same client object.
|
||||
@pytest.fixture(scope="session")
|
||||
def dynamodb(request):
|
||||
# Disable boto3's client-side validation of parameters. This validation
|
||||
# only makes it impossible for us to test various error conditions,
|
||||
# because boto3 checks them before we can get the server to check them.
|
||||
boto_config = botocore.client.Config(parameter_validation=False)
|
||||
if request.config.getoption('aws'):
|
||||
return boto3.resource('dynamodb', config=boto_config)
|
||||
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.
|
||||
if request.config.getoption('url') != None:
|
||||
local_url = request.config.getoption('url')
|
||||
else:
|
||||
local_url = 'https://localhost:8043' if request.config.getoption('https') else 'http://localhost:8000'
|
||||
# Disable verifying in order to be able to use self-signed TLS certificates
|
||||
verify = not request.config.getoption('https')
|
||||
return boto3.resource('dynamodb', endpoint_url=local_url, verify=verify,
|
||||
region_name='us-east-1', aws_access_key_id='alternator', aws_secret_access_key='secret_pass',
|
||||
config=boto_config.merge(botocore.client.Config(retries={"max_attempts": 0}, read_timeout=300)))
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dynamodbstreams(request):
|
||||
if request.config.getoption('aws'):
|
||||
return boto3.client('dynamodbstreams')
|
||||
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.
|
||||
if request.config.getoption('url') != None:
|
||||
local_url = request.config.getoption('url')
|
||||
else:
|
||||
local_url = 'https://localhost:8043' if request.config.getoption('https') else 'http://localhost:8000'
|
||||
# Disable verifying in order to be able to use self-signed TLS certificates
|
||||
verify = not request.config.getoption('https')
|
||||
return boto3.client('dynamodbstreams', endpoint_url=local_url, verify=verify,
|
||||
region_name='us-east-1', aws_access_key_id='alternator', aws_secret_access_key='secret_pass',
|
||||
config=botocore.client.Config(retries={"max_attempts": 3}))
|
||||
|
||||
# A function-scoped autouse=True fixture allows us to test after every test
|
||||
# that the server is still alive - and if not report the test which crashed
|
||||
# it and stop running any more tests.
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def dynamodb_test_connection(dynamodb, request):
|
||||
yield
|
||||
try:
|
||||
# We want to run a do-nothing DynamoDB command. The health-check
|
||||
# URL is the fastest one.
|
||||
url = dynamodb.meta.client._endpoint.host
|
||||
response = requests.get(url, verify=False)
|
||||
assert response.ok
|
||||
except:
|
||||
pytest.exit(f"Scylla appears to have crashed in test {request.node.parent.name}::{request.node.name}")
|
||||
|
||||
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.
|
||||
@@ -179,15 +105,6 @@ def test_table_s(dynamodb):
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
# test_table_s_2 has exactly the same schema as test_table_s, and is useful
|
||||
# for tests which need two different tables with the same schema.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_s_2(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,
|
||||
@@ -209,13 +126,6 @@ def test_table_sn(dynamodb):
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' }, { 'AttributeName': 'c', 'AttributeType': 'N' } ])
|
||||
yield table
|
||||
table.delete()
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_ss(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
|
||||
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
|
||||
@@ -257,27 +167,3 @@ def filled_test_table(dynamodb):
|
||||
|
||||
yield table, items
|
||||
table.delete()
|
||||
|
||||
# The "scylla_only" fixture can be used by tests for Scylla-only features,
|
||||
# which do not exist on AWS DynamoDB. A test using this fixture will be
|
||||
# skipped if running with "--aws".
|
||||
@pytest.fixture(scope="session")
|
||||
def scylla_only(dynamodb):
|
||||
if dynamodb.meta.client._endpoint.host.endswith('.amazonaws.com'):
|
||||
pytest.skip('Scylla-only feature not supported by AWS')
|
||||
|
||||
# The "test_table_s_forbid_rmw" fixture is the same as test_table_s, except
|
||||
# with the "forbid_rmw" write isolation mode. This is useful for verifying
|
||||
# that writes that we think should not need a read-before-write in fact do
|
||||
# not need it.
|
||||
# Because forbid_rmw is a Scylla-only feature, this test is skipped when not
|
||||
# running against Scylla.
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_s_forbid_rmw(dynamodb, scylla_only):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ])
|
||||
arn = table.meta.client.describe_table(TableName=table.name)['Table']['TableArn']
|
||||
table.meta.client.tag_resource(ResourceArn=arn, Tags=[{'Key': 'system:write_isolation', 'Value': 'forbid_rmw'}])
|
||||
yield table
|
||||
table.delete()
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -20,7 +20,6 @@
|
||||
# so they are actually tested by other tests as well.
|
||||
|
||||
import pytest
|
||||
import random
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, full_scan, full_query, multiset
|
||||
|
||||
@@ -45,19 +44,6 @@ def test_basic_batch_write_item(test_table):
|
||||
assert item['attribute'] == str(i)
|
||||
assert item['another'] == 'xyz'
|
||||
|
||||
# Try a batch which includes both multiple writes to the same partition
|
||||
# and several partitions. The LWT code collects multiple mutations to the
|
||||
# same partition together, and we want to test that this worked correctly.
|
||||
def test_batch_write_item_mixed(test_table):
|
||||
partitions = [random_string() for i in range(4)]
|
||||
items = [{'p': p, 'c': str(i)} for p in partitions for i in range(4)]
|
||||
with test_table.batch_writer() as batch:
|
||||
# Reorder items randomly, just for the heck of it
|
||||
for item in random.sample(items, len(items)):
|
||||
batch.put_item(item)
|
||||
for item in items:
|
||||
assert test_table.get_item(Key={'p': item['p'], 'c': item['c']}, ConsistentRead=True)['Item'] == item
|
||||
|
||||
# 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)]
|
||||
@@ -152,20 +138,6 @@ def test_batch_write_duplicate_write_and_delete(test_table_s, test_table):
|
||||
batch.put_item({'p': p, 'c': other})
|
||||
batch.put_item({'p': other, 'c': c})
|
||||
|
||||
# The BatchWriteIem API allows writing to more than one table in the same
|
||||
# batch. This test verifies that the duplicate-key checking doesn't mistake
|
||||
# updates to the same key in different tables to be duplicates.
|
||||
def test_batch_write_nonduplicate_multiple_tables(test_table_s, test_table_s_2):
|
||||
p = random_string()
|
||||
# The batch_writer() function used in previous tests can't write to more
|
||||
# than one table. So we use the lower level interface boto3 gives us.
|
||||
reply = test_table_s.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p, 'a': 'hi'}}}],
|
||||
test_table_s_2.name: [{'PutRequest': {'Item': {'p': p, 'b': 'hello'}}}]
|
||||
})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 'hi'}
|
||||
assert test_table_s_2.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hello'}
|
||||
|
||||
# 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().
|
||||
@@ -210,32 +182,6 @@ def test_batch_write_invalid_operation(test_table_s):
|
||||
for p in [p1, p2]:
|
||||
assert not 'item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
|
||||
# In test_item.py we have a bunch of test_empty_* tests on different ways to
|
||||
# create an empty item (which in Scylla requires the special CQL row marker
|
||||
# to be supported correctly). BatchWriteItems provides yet another way of
|
||||
# creating items, so check the empty case here too:
|
||||
def test_empty_batch_write(test_table):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
with test_table.batch_writer() as batch:
|
||||
batch.put_item({'p': p, 'c': c})
|
||||
assert test_table.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item'] == {'p': p, 'c': c}
|
||||
|
||||
# Test that BatchWriteItems allows writing to multiple tables in one operation
|
||||
def test_batch_write_multiple_tables(test_table_s, test_table):
|
||||
p1 = random_string()
|
||||
c1 = random_string()
|
||||
p2 = random_string()
|
||||
# We use the low-level batch_write_item API for lack of a more convenient
|
||||
# API (the batch_writer() API can only write to one table). At least it
|
||||
# spares us the need to encode the key's types...
|
||||
reply = test_table.meta.client.batch_write_item(RequestItems = {
|
||||
test_table.name: [{'PutRequest': {'Item': {'p': p1, 'c': c1, 'a': 'hi'}}}],
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p2, 'b': 'hello'}}}]
|
||||
})
|
||||
assert test_table.get_item(Key={'p': p1, 'c': c1}, ConsistentRead=True)['Item'] == {'p': p1, 'c': c1, 'a': 'hi'}
|
||||
assert test_table_s.get_item(Key={'p': p2}, ConsistentRead=True)['Item'] == {'p': p2, 'b': 'hello'}
|
||||
|
||||
# Basic test for BatchGetItem, reading several entire items.
|
||||
# Schema has both hash and sort keys.
|
||||
def test_batch_get_item(test_table):
|
||||
@@ -305,84 +251,3 @@ def test_batch_get_item_projection_expression(test_table):
|
||||
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 that we return the required UnprocessedKeys/UnprocessedItems parameters
|
||||
def test_batch_unprocessed(test_table_s):
|
||||
p = random_string()
|
||||
write_reply = test_table_s.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_s.name: [{'PutRequest': {'Item': {'p': p, 'a': 'hi'}}}],
|
||||
})
|
||||
assert 'UnprocessedItems' in write_reply and write_reply['UnprocessedItems'] == dict()
|
||||
|
||||
read_reply = test_table_s.meta.client.batch_get_item(RequestItems = {
|
||||
test_table_s.name: {'Keys': [{'p': p}], 'ProjectionExpression': 'p, a', 'ConsistentRead': True}
|
||||
})
|
||||
assert 'UnprocessedKeys' in read_reply and read_reply['UnprocessedKeys'] == dict()
|
||||
|
||||
# According to the DynamoDB document, a single BatchWriteItem operation is
|
||||
# limited to 25 update requests, up to 400 KB each, or 16 MB total (25*400
|
||||
# is only 10 MB, but the JSON format has additional overheads). If we write
|
||||
# less than those limits in a single BatchWriteItem operation, it should
|
||||
# work. Testing a large request exercises our code which calculates the
|
||||
# request signature, and parses a long request (issue #7213) as well as the
|
||||
# resulting long write (issue #8183).
|
||||
def test_batch_write_item_large(test_table_sn):
|
||||
p = random_string()
|
||||
long_content = random_string(100)*500
|
||||
write_reply = test_table_sn.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_sn.name: [{'PutRequest': {'Item': {'p': p, 'c': i, 'content': long_content}}} for i in range(25)],
|
||||
})
|
||||
assert 'UnprocessedItems' in write_reply and write_reply['UnprocessedItems'] == dict()
|
||||
assert full_query(test_table_sn, KeyConditionExpression='p=:p', ExpressionAttributeValues={':p': p}
|
||||
) == [{'p': p, 'c': i, 'content': long_content} for i in range(25)]
|
||||
|
||||
# DynamoDB limits the number of items written by a BatchWriteItem operation
|
||||
# to 25, even if they are small. Exceeding this limit results in a
|
||||
# ValidationException error - and none of the items in the batch are written.
|
||||
# Reproduces #5057
|
||||
@pytest.mark.xfail(reason="Issue #5057")
|
||||
def test_batch_write_item_too_many(test_table_sn):
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*length'):
|
||||
test_table_sn.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_sn.name: [{'PutRequest': {'Item': {'p': p, 'c': i}}} for i in range(30)]
|
||||
})
|
||||
with pytest.raises(ClientError, match='ValidationException.*length'):
|
||||
test_table_sn.meta.client.batch_write_item(RequestItems = {
|
||||
test_table_sn.name: [{'DeleteRequest': {'Key': {'p': p, 'c': i}}} for i in range(30)]
|
||||
})
|
||||
|
||||
# According to the DynamoDB documention, a single BatchGetItem operation is
|
||||
# limited to retrieving up to 100 items or a total of 16 MB of data,
|
||||
# whichever is smaller. If we read less than those limits in a single
|
||||
# BatchGetItem operation, it should work - though may still return only
|
||||
# partial results (filling UnprocessedKeys instead). Testing a BatchGetItem
|
||||
# with a large response exercises our code which builds such large responses
|
||||
# and how it may cause warnings about excessively large allocations or long
|
||||
# unpreemptable computation (see issues #8661, #7926).
|
||||
def test_batch_get_item_large(test_table_sn):
|
||||
p = random_string()
|
||||
long_content = random_string(100)*500
|
||||
count = 30
|
||||
with test_table_sn.batch_writer() as batch:
|
||||
for i in range(count):
|
||||
batch.put_item(Item={
|
||||
'p': p, 'c': i, 'content': long_content})
|
||||
# long_content is 49 KB, 30 such items is about 1.5 MB, well below the
|
||||
# BatchGetItem response limit, so the following BatchGetItem call should
|
||||
# be able to return all items we just wrote in one response - and in fact
|
||||
# does so in Alternator and thus exercises its handling of large
|
||||
# responses. Strangely, in DynamoDB, even though we're below the limit,
|
||||
# it often returns only partial results (with the unread keys in
|
||||
# UnprocessedKeys), so for reliable success of this test we need a loop:
|
||||
responses = []
|
||||
to_read = { test_table_sn.name: {'Keys': [{'p': p, 'c': c} for c in range(count)], 'ConsistentRead': True } }
|
||||
while to_read:
|
||||
reply = test_table_sn.meta.client.batch_get_item(RequestItems = to_read)
|
||||
assert 'UnprocessedKeys' in reply
|
||||
to_read = reply['UnprocessedKeys']
|
||||
assert 'Responses' in reply
|
||||
assert test_table_sn.name in reply['Responses']
|
||||
responses.extend(reply['Responses'][test_table_sn.name])
|
||||
assert multiset(responses) == multiset(
|
||||
[{'p': p, 'c': i, 'content': long_content} for i in range(count)])
|
||||
40
alternator-test/test_condition_expression.py
Normal file
40
alternator-test/test_condition_expression.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -22,7 +22,7 @@ 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(request, dynamodb):
|
||||
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).
|
||||
@@ -34,16 +34,14 @@ def test_describe_endpoints(request, dynamodb):
|
||||
# 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.
|
||||
prefix = "https://" if request.config.getoption('https') else "http://"
|
||||
verify = not request.config.getoption('https')
|
||||
url = prefix + address
|
||||
url = "http://" + address
|
||||
if address.endswith('.amazonaws.com'):
|
||||
boto3.client('dynamodb',endpoint_url=url, verify=verify).describe_endpoints()
|
||||
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='alternator', aws_secret_access_key='secret_pass', verify=verify).describe_endpoints()
|
||||
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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -41,6 +41,7 @@ def test_describe_table_basic(test_table):
|
||||
|
||||
# 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
|
||||
@@ -107,9 +108,9 @@ def test_describe_table_size(test_table):
|
||||
# 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. But DynamoDB documents that zeros be returned for WriteCapacityUnits
|
||||
# and ReadCapacityUnits, and does this in practice as well - and some
|
||||
# applications assume these numbers are always there (even if 0).
|
||||
# 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
|
||||
@@ -141,16 +142,19 @@ def test_describe_table_stream_specification(test_table):
|
||||
# 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.
|
||||
# DynamoDB documentation states that this id must look like a UUID.
|
||||
# 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
|
||||
assert re.fullmatch('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', got['TableId'])
|
||||
|
||||
# DescribeTable error path: trying to describe a non-existent table should
|
||||
# result in a ResourceNotFoundException.
|
||||
369
alternator-test/test_expected.py
Normal file
369
alternator-test/test_expected.py
Normal file
@@ -0,0 +1,369 @@
|
||||
# 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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from botocore.exceptions import ClientError
|
||||
from util import create_test_table, random_string, full_scan, full_query, multiset, list_tables, new_test_table
|
||||
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
|
||||
@@ -35,42 +35,24 @@ from util import create_test_table, random_string, full_scan, full_query, multis
|
||||
# secondary indexes within a fraction of a second, under normal conditions"
|
||||
# and indeed, in practice, the tests here almost always succeed without a
|
||||
# retry.
|
||||
# However, it is worthwhile to differenciate between the case where the
|
||||
# result set is not *yet* complete (which is ok, and requires retry), and
|
||||
# the case that the result set has wrong data. In the latter case, the
|
||||
# test will surely fail and no amount of retry will help, so we should
|
||||
# fail quickly, to avoid xfailing tests being very slow.
|
||||
def assert_index_query(table, index_name, expected_items, **kwargs):
|
||||
expected = multiset(expected_items)
|
||||
for i in range(5):
|
||||
got = multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
if expected == got:
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
elif got - expected:
|
||||
# If we got any items that weren't expected, there's no point to retry.
|
||||
pytest.fail("assert_index_query() found unexpected items: " + str(got - expected))
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
expected = multiset(expected_items)
|
||||
for i in range(5):
|
||||
got = multiset(full_scan(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
if expected == got:
|
||||
for i in range(3):
|
||||
if multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
elif got - expected:
|
||||
# If we got any items that weren't expected, there's no point to retry.
|
||||
pytest.fail("assert_index_scan() found unexpected items: " + str(got - expected))
|
||||
print('assert_index_scan retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
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.
|
||||
# The following test does not work for KA/LA tables due to #6157,
|
||||
# so it's hereby skipped.
|
||||
@pytest.mark.skip
|
||||
def test_gsi_identical(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
@@ -89,15 +71,15 @@ def test_gsi_identical(dynamodb):
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existent index
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, ConsistentRead=False, IndexName='wrong')
|
||||
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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_1(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
@@ -168,18 +150,13 @@ def test_gsi_missing_table(dynamodb):
|
||||
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)
|
||||
|
||||
# Test that setting an indexed string column to an empty string is illegal,
|
||||
# since keys cannot contain empty strings
|
||||
def test_gsi_empty_value(test_table_gsi_2):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_gsi_2.put_item(Item={'p': random_string(), 'x': ''})
|
||||
|
||||
# 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):
|
||||
@@ -218,7 +195,7 @@ def test_gsi_missing_attribute_definition(dynamodb):
|
||||
# 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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_1_hash_only(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
@@ -265,7 +242,7 @@ def test_gsi_key_not_in_index(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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_2(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -310,8 +287,8 @@ def test_gsi_missing_attribute(test_table_gsi_2):
|
||||
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}, ConsistentRead=True)['Item'] == {'p': p1, 'x': x1}
|
||||
assert test_table_gsi_2.get_item(Key={'p': p2}, ConsistentRead=True)['Item'] == {'p': p2}
|
||||
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
|
||||
@@ -323,7 +300,7 @@ def test_gsi_missing_attribute(test_table_gsi_2):
|
||||
# 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, ConsistentRead=False, IndexName='hello')])
|
||||
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
|
||||
@@ -349,33 +326,6 @@ def test_gsi_wrong_type_attribute_update(test_table_gsi_2):
|
||||
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}
|
||||
|
||||
# Since a GSI key x cannot be a map or an array, in particular updates to
|
||||
# nested attributes like x.y or x[1] are not legal. The error that DynamoDB
|
||||
# reports is "Key attributes must be scalars; list random access '[]' and map
|
||||
# lookup '.' are not allowed: IndexKey: x".
|
||||
def test_gsi_wrong_type_attribute_update_nested(test_table_gsi_2):
|
||||
p = random_string()
|
||||
x = random_string()
|
||||
test_table_gsi_2.put_item(Item={'p': p, 'x': x})
|
||||
# We can't write a map into a GSI key column, which in this case can only
|
||||
# be a string and in any case can never be a map. DynamoDB and Alternator
|
||||
# report different errors here: DynamoDB reports a type mismatch (exactly
|
||||
# like in test test_gsi_wrong_type_attribute_update), but Alternator
|
||||
# reports the obscure message "Malformed value object for key column x".
|
||||
# Alternator's error message should probably be improved here, but let's
|
||||
# not test it in this test.
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_gsi_2.update_item(Key={'p': p}, UpdateExpression='SET x = :val1',
|
||||
ExpressionAttributeValues={':val1': {'a': 3, 'b': 4}})
|
||||
# Here we try to set x.y for the GSI key column x. Again DynamoDB and
|
||||
# Alternator produce different error messages - but both make sense.
|
||||
# DynamoDB says "Key attributes must be scalars; list random access '[]'
|
||||
# and map # lookup '.' are not allowed: IndexKey: x", while Alternator
|
||||
# complains that "document paths not valid for this item: x.y".
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_gsi_2.update_item(Key={'p': p}, UpdateExpression='SET x.y = :val1',
|
||||
ExpressionAttributeValues={':val1': 3})
|
||||
|
||||
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.
|
||||
@@ -397,7 +347,7 @@ def test_gsi_wrong_type_attribute_batch(test_table_gsi_2):
|
||||
# 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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_3(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -427,6 +377,7 @@ def test_gsi_3(test_table_gsi_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:
|
||||
@@ -438,157 +389,9 @@ def test_gsi_update_second_regular_base_column(test_table_gsi_3):
|
||||
KeyConditions={'a': {'AttributeValueList': [items[3]['a']], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [items[3]['b']], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Test reproducing issue #11801: In issue #5006 we noticed that in the special
|
||||
# case of a GSI with with two non-key attributes as keys (test_table_gsi_3),
|
||||
# an update of the second attribute forgot to delete the old row. We fixed
|
||||
# that bug, but a bug remained for updates which update the value to the *same*
|
||||
# value - in that case the old row shouldn't be deleted, but we did - as
|
||||
# noticed in issue #11801.
|
||||
def test_11801(test_table_gsi_3):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
b = random_string()
|
||||
item = {'p': p, 'a': a, 'b': b, 'd': random_string()}
|
||||
test_table_gsi_3.put_item(Item=item)
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
# Update the attribute 'b' to the same value b that it already had.
|
||||
# This shouldn't change anything in the base table or in the GSI
|
||||
test_table_gsi_3.update_item(Key={'p': p}, AttributeUpdates={'b': {'Value': b, 'Action': 'PUT'}})
|
||||
assert item == test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
||||
# In issue #11801, the following assertion failed (the view row was
|
||||
# deleted and nothing matched the query).
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
# Above we checked that setting 'b' to the same value didn't remove
|
||||
# the old GSI row. But the same update may actually modify the GSI row
|
||||
# (e.g., an unrelated attribute d) - check this modification took place:
|
||||
item['d'] = random_string()
|
||||
test_table_gsi_3.update_item(Key={'p': p},
|
||||
AttributeUpdates={'b': {'Value': b, 'Action': 'PUT'},
|
||||
'd': {'Value': item['d'], 'Action': 'PUT'}})
|
||||
assert item == test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# This test is the same as test_11801, but updating the first attribute (a)
|
||||
# instead of the second (b). This test didn't fail, showing that issue #11801
|
||||
# is - like #5006 - specific to the case of updating the second attribute.
|
||||
def test_11801_variant1(test_table_gsi_3):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
b = random_string()
|
||||
d = random_string()
|
||||
item = {'p': p, 'a': a, 'b': b, 'd': d}
|
||||
test_table_gsi_3.put_item(Item=item)
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
test_table_gsi_3.update_item(Key={'p': p}, AttributeUpdates={'a': {'Value': a, 'Action': 'PUT'}})
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# This test is the same as test_11801, but updates b to a different value
|
||||
# (newb) instead of to the same one. This test didn't fail, showing that
|
||||
# issue #11801 is specific to updates to the same value. This test basically
|
||||
# reproduces the already-fixed #5006 (we also have another test above which
|
||||
# reproduces that issue - test_gsi_update_second_regular_base_column())
|
||||
def test_11801_variant2(test_table_gsi_3):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
b = random_string()
|
||||
item = {'p': p, 'a': a, 'b': b, 'd': random_string()}
|
||||
test_table_gsi_3.put_item(Item=item)
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
newb = random_string()
|
||||
item['b'] = newb
|
||||
test_table_gsi_3.update_item(Key={'p': p}, AttributeUpdates={'b': {'Value': newb, 'Action': 'PUT'}})
|
||||
assert_index_query(test_table_gsi_3, 'hello', [],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [newb], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# This test is the same as test_11801, but uses a different table schema
|
||||
# (test_table_gsi_5) where there is only one new key column in the view (x).
|
||||
# This test passed, showing that issue #11801 was specific to the special
|
||||
# case of a view with two new key columns (test_table_gsi_3).
|
||||
def test_11801_variant3(test_table_gsi_5):
|
||||
p = random_string()
|
||||
c = random_string()
|
||||
x = random_string()
|
||||
item = {'p': p, 'c': c, 'x': x, 'd': random_string()}
|
||||
test_table_gsi_5.put_item(Item=item)
|
||||
assert_index_query(test_table_gsi_5, 'hello', [item],
|
||||
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'},
|
||||
'x': {'AttributeValueList': [x], 'ComparisonOperator': 'EQ'}})
|
||||
test_table_gsi_5.update_item(Key={'p': p, 'c': c}, AttributeUpdates={'x': {'Value': x, 'Action': 'PUT'}})
|
||||
assert item == test_table_gsi_5.get_item(Key={'p': p, 'c': c}, ConsistentRead=True)['Item']
|
||||
assert_index_query(test_table_gsi_5, 'hello', [item],
|
||||
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'},
|
||||
'x': {'AttributeValueList': [x], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Another test similar to test_11801, but instead of updating a view key
|
||||
# column to the same value it already has, simply don't update it at all
|
||||
# (and just modify some other regular column). This test passed, showing
|
||||
# that issue #11801 is specific to the case of updating a view key column
|
||||
# to the same value it already had.
|
||||
def test_11801_variant4(test_table_gsi_3):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
b = random_string()
|
||||
item = {'p': p, 'a': a, 'b': b, 'd': random_string()}
|
||||
test_table_gsi_3.put_item(Item=item)
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], 'ComparisonOperator': 'EQ'}})
|
||||
# An update that doesn't change the GSI keys (a or b), just a regular
|
||||
# column d.
|
||||
item['d'] = random_string()
|
||||
test_table_gsi_3.update_item(Key={'p': p}, AttributeUpdates={'d': {'Value': item['d'], 'Action': 'PUT'}})
|
||||
assert item == test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
||||
assert_index_query(test_table_gsi_3, 'hello', [item],
|
||||
KeyConditions={'a': {'AttributeValueList': [a], 'ComparisonOperator': 'EQ'},
|
||||
'b': {'AttributeValueList': [b], '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.
|
||||
# This is the same feature we already tested in test_gsi_missing_attribute()
|
||||
# above, but on a different table: In that test we used test_table_gsi_2,
|
||||
# with one indexed attribute, and in this test we use test_table_gsi_3 which
|
||||
# has two base regular attributes in the view key, and more possibilities
|
||||
# of which value might be missing. Reproduces issue #6008.
|
||||
def test_gsi_missing_attribute_3(test_table_gsi_3):
|
||||
p = random_string()
|
||||
a = random_string()
|
||||
b = random_string()
|
||||
# First, add an item with a missing "a" value. It should appear in the
|
||||
# base table, but not in the index:
|
||||
test_table_gsi_3.put_item(Item={'p': p, 'b': b})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': b}
|
||||
# Note: with eventually consistent read, we can't really be sure that
|
||||
# an item will "never" appear in the index. We hope that if a bug exists
|
||||
# and such an item did appear, sometimes the delay here will be enough
|
||||
# for the unexpected item to become visible.
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
# Same thing for an item with a missing "b" value:
|
||||
test_table_gsi_3.put_item(Item={'p': p, 'a': a})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': a}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
# And for an item missing both:
|
||||
test_table_gsi_3.put_item(Item={'p': p})
|
||||
assert test_table_gsi_3.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p}
|
||||
assert not any([i['p'] == p for i in full_scan(test_table_gsi_3, ConsistentRead=False, IndexName='hello')])
|
||||
|
||||
# A fourth scenario of GSI. Two GSIs on a single base table.
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_4(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -635,7 +438,7 @@ def test_gsi_4_describe(test_table_gsi_4):
|
||||
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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_5(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
@@ -674,57 +477,11 @@ def test_gsi_5(test_table_gsi_5):
|
||||
KeyConditions={'p': {'AttributeValueList': [p2], 'ComparisonOperator': 'EQ'},
|
||||
'x': {'AttributeValueList': [x2], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Verify that DescribeTable correctly returns the schema of both base-table
|
||||
# and secondary indexes. KeySchema is given for each of the base table and
|
||||
# indexes, and AttributeDefinitions is merged for all of them together.
|
||||
def test_gsi_5_describe_table_schema(test_table_gsi_5):
|
||||
got = test_table_gsi_5.meta.client.describe_table(TableName=test_table_gsi_5.name)['Table']
|
||||
# Copied from test_table_gsi_5 fixture
|
||||
expected_base_keyschema = [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' } ]
|
||||
expected_gsi_keyschema = [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'x', 'KeyType': 'RANGE' } ]
|
||||
expected_all_attribute_definitions = [
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' } ]
|
||||
assert got['KeySchema'] == expected_base_keyschema
|
||||
gsis = got['GlobalSecondaryIndexes']
|
||||
assert len(gsis) == 1
|
||||
assert gsis[0]['KeySchema'] == expected_gsi_keyschema
|
||||
# The list of attribute definitions may be arbitrarily reordered
|
||||
assert multiset(got['AttributeDefinitions']) == multiset(expected_all_attribute_definitions)
|
||||
|
||||
# Similar DescribeTable schema test for test_table_gsi_2. The peculiarity
|
||||
# in that table is that the base table has only a hash key p, and index
|
||||
# only hash hash key x; Now, while internally Scylla needs to add "p" as a
|
||||
# clustering key in the materialized view (in Scylla the view key always
|
||||
# contains the base key), when describing the table, "p" shouldn't be
|
||||
# returned as a range key, because the user didn't ask for it.
|
||||
# This test reproduces issue #5320.
|
||||
@pytest.mark.xfail(reason="GSI DescribeTable spurious range key (#5320)")
|
||||
def test_gsi_2_describe_table_schema(test_table_gsi_2):
|
||||
got = test_table_gsi_2.meta.client.describe_table(TableName=test_table_gsi_2.name)['Table']
|
||||
# Copied from test_table_gsi_2 fixture
|
||||
expected_base_keyschema = [ { 'AttributeName': 'p', 'KeyType': 'HASH' } ]
|
||||
expected_gsi_keyschema = [ { 'AttributeName': 'x', 'KeyType': 'HASH' } ]
|
||||
expected_all_attribute_definitions = [
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'x', 'AttributeType': 'S' } ]
|
||||
assert got['KeySchema'] == expected_base_keyschema
|
||||
gsis = got['GlobalSecondaryIndexes']
|
||||
assert len(gsis) == 1
|
||||
assert gsis[0]['KeySchema'] == expected_gsi_keyschema
|
||||
# The list of attribute definitions may be arbitrarily reordered
|
||||
assert multiset(got['AttributeDefinitions']) == multiset(expected_all_attribute_definitions)
|
||||
|
||||
# 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 projection not supported - issue #5036")
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_projection_keys_only(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -752,7 +509,7 @@ def test_gsi_projection_keys_only(dynamodb):
|
||||
# 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 projection not supported - issue #5036")
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_projection_include(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -787,7 +544,7 @@ def test_gsi_projection_include(dynamodb):
|
||||
# "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 projection not supported - issue #5036")
|
||||
@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,
|
||||
@@ -814,18 +571,18 @@ 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(600):
|
||||
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(f'{i} Table {table.name} status still {table_status}')
|
||||
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(f'{i} Index {gsi_name} status still {index_status}')
|
||||
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]
|
||||
@@ -837,18 +594,18 @@ def wait_for_gsi(table, gsi_name):
|
||||
# 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(600):
|
||||
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(f'{i} Table {table.name} status still {table_status}')
|
||||
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(f'{i} Index {gsi_name} status still {index_status}')
|
||||
print('%d Index status still %s' % (i, index_status))
|
||||
continue
|
||||
print('wait_for_gsi_gone took %d seconds' % (time.time() - start_time))
|
||||
return
|
||||
@@ -862,8 +619,7 @@ def wait_for_gsi_gone(table, gsi_name):
|
||||
# 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).
|
||||
# Reproduces issue #5022.
|
||||
@pytest.mark.xfail(reason="issue #5022")
|
||||
@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
|
||||
@@ -898,10 +654,10 @@ def test_gsi_backfill(dynamodb):
|
||||
# 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, ConsistentRead=False, IndexName='hello'))
|
||||
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,
|
||||
ConsistentRead=False, IndexName='hello',
|
||||
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
|
||||
@@ -916,8 +672,7 @@ def test_gsi_backfill(dynamodb):
|
||||
table.delete()
|
||||
|
||||
# Test deleting an existing GSI using UpdateTable
|
||||
# Reproduces issue #5022.
|
||||
@pytest.mark.xfail(reason="issue #5022")
|
||||
@pytest.mark.xfail(reason="GSI not supported")
|
||||
def test_gsi_delete(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
@@ -949,7 +704,7 @@ def test_gsi_delete(dynamodb):
|
||||
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, ConsistentRead=False, IndexName='hello',
|
||||
full_query(table, IndexName='hello',
|
||||
KeyConditions={'x': {'AttributeValueList': [items[3]['x']], 'ComparisonOperator': 'EQ'}})
|
||||
table.delete()
|
||||
|
||||
@@ -977,9 +732,14 @@ def create_gsi(dynamodb, index_name):
|
||||
# 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):
|
||||
with pytest.raises(ClientError, match='ValidationException.*3'):
|
||||
# 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(ClientError, match='ValidationException.*3'):
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_gsi(dynamodb, 'nn')
|
||||
with pytest.raises(ClientError, match='ValidationException.*nnnnn'):
|
||||
create_gsi(dynamodb, 'n' * 256)
|
||||
@@ -1007,7 +767,7 @@ def test_gsi_very_long_name(dynamodb):
|
||||
# name. This assumes that materialized-view names are composed using the
|
||||
# index's name (which is currently what we do).
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_gsi_random_name(dynamodb):
|
||||
index_name = random_string()
|
||||
table = create_test_table(dynamodb,
|
||||
@@ -1039,62 +799,3 @@ def test_gsi_list_tables(dynamodb, test_table_gsi_random_name):
|
||||
assert not index_name in name
|
||||
# But of course, the table's name should be in the list:
|
||||
assert table.name in tables
|
||||
|
||||
# As noted above in test_gsi_empty_value(), setting an indexed string column
|
||||
# to an empty string is rejected, since keys (including GSI keys) are not
|
||||
# allowed to be empty strings or binary blobs.
|
||||
# However, empty strings *are* legal for ordinary non-indexed attributes, so
|
||||
# if the user adds a GSI to an existing table with pre-existing data, it might
|
||||
# contain empty string values for the indexed keys. Such values should be
|
||||
# skipped while filling the GSI - even if Scylla actually capable of
|
||||
# representing such empty view keys (see issue #9375).
|
||||
# Reproduces issue #5022 and #9424.
|
||||
@pytest.mark.xfail(reason="issue #5022, #9424")
|
||||
def test_gsi_backfill_empty_string(dynamodb):
|
||||
# First create, and fill, a table without GSI:
|
||||
with new_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' } ]) as table:
|
||||
p1 = random_string()
|
||||
p2 = random_string()
|
||||
c = random_string()
|
||||
# Create two items, one has an empty "x" attribute, the other is
|
||||
# non-empty.
|
||||
table.put_item(Item={'p': p1, 'c': c, 'x': 'hello'})
|
||||
table.put_item(Item={'p': p2, 'c': c, 'x': ''})
|
||||
# Now use UpdateTable to create two GSIs. In one of them "x" will be
|
||||
# the partition key, and in the other "x" will be a sort key.
|
||||
# DynamoDB limits the number of indexes that can be added in one
|
||||
# UpdateTable command to just one, so we need to do it in two separate
|
||||
# commands and wait for each to complete.
|
||||
dynamodb.meta.client.update_table(TableName=table.name,
|
||||
AttributeDefinitions=[{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexUpdates=[
|
||||
{ 'Create': { 'IndexName': 'index1',
|
||||
'KeySchema': [{ 'AttributeName': 'x', 'KeyType': 'HASH' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }}
|
||||
}
|
||||
])
|
||||
wait_for_gsi(table, 'index1')
|
||||
dynamodb.meta.client.update_table(TableName=table.name,
|
||||
AttributeDefinitions=[{ 'AttributeName': 'x', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' }],
|
||||
GlobalSecondaryIndexUpdates=[
|
||||
{ 'Create': { 'IndexName': 'index2',
|
||||
'KeySchema': [{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'x', 'KeyType': 'RANGE' }],
|
||||
'Projection': { 'ProjectionType': 'ALL' }}
|
||||
}
|
||||
])
|
||||
wait_for_gsi(table, 'index2')
|
||||
# Verify that the items with the empty-string x are missing from both
|
||||
# GSIs, so only the one item with x != '' should appear in both.
|
||||
# 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 [{'p': p1, 'c': c, 'x': 'hello'}] == full_scan(table, ConsistentRead=False, IndexName='index1')
|
||||
assert [{'p': p1, 'c': c, 'x': 'hello'}] == full_scan(table, ConsistentRead=False, IndexName='index2')
|
||||
402
alternator-test/test_item.py
Normal file
402
alternator-test/test_item.py
Normal file
@@ -0,0 +1,402 @@
|
||||
# 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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -23,25 +23,28 @@
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from botocore.exceptions import ClientError
|
||||
from botocore.exceptions import ClientError, ParamValidationError
|
||||
from util import create_test_table, random_string, full_scan, full_query, multiset, list_tables
|
||||
|
||||
# LSIs support strongly-consistent reads, so the following functions do not
|
||||
# need to retry like we did in test_gsi.py for GSIs:
|
||||
# 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):
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs))
|
||||
def assert_index_scan(table, index_name, expected_items, **kwargs):
|
||||
assert multiset(expected_items) == multiset(full_scan(table, IndexName=index_name, **kwargs))
|
||||
|
||||
# A version doing retries instead of ConsistentRead, to be used just for the
|
||||
# one test below which has both GSI and LSI:
|
||||
def retrying_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, ConsistentRead=False, **kwargs)):
|
||||
if multiset(expected_items) == multiset(full_query(table, IndexName=index_name, **kwargs)):
|
||||
return
|
||||
print('retrying_assert_index_query retrying')
|
||||
print('assert_index_query retrying')
|
||||
time.sleep(1)
|
||||
assert multiset(expected_items) == multiset(full_query(table, IndexName=index_name, ConsistentRead=False, **kwargs))
|
||||
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.
|
||||
@@ -63,21 +66,20 @@ def test_lsi_identical(dynamodb):
|
||||
# results (in different order).
|
||||
assert multiset(items) == multiset(full_scan(table))
|
||||
assert_index_scan(table, 'hello', items)
|
||||
# We can't scan a non-existent index
|
||||
# We can't scan a non-existant index
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(table, IndexName='wrong')
|
||||
table.delete()
|
||||
|
||||
# Check that providing a hash key different than the base table is not
|
||||
# allowed:
|
||||
def test_lsi_wrong_different_hash(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*hash key'):
|
||||
# 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' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }],
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
@@ -90,17 +92,13 @@ def test_lsi_wrong_different_hash(dynamodb):
|
||||
}
|
||||
])
|
||||
table.delete()
|
||||
|
||||
# Check that it's not allowed to create an LSI without specifying a range
|
||||
# key cannot be missing, or (obviously) making it the same as the hash key:
|
||||
def test_lsi_wrong_bad_range(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*same'):
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' }
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
@@ -114,11 +112,11 @@ def test_lsi_wrong_bad_range(dynamodb):
|
||||
table.delete()
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'b', 'AttributeType': 'S' }
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
@@ -130,33 +128,9 @@ def test_lsi_wrong_bad_range(dynamodb):
|
||||
])
|
||||
table.delete()
|
||||
|
||||
# The purpose of an LSI is to allow an alternative sort key for the
|
||||
# existing partitions - the partitions do not change. So it doesn't make
|
||||
# sense to create an LSI on a table that did not originally have a sort key
|
||||
# (so has only single-item partitions) - and this case is not allowed.
|
||||
def test_lsi_wrong_no_sort_key(dynamodb):
|
||||
with pytest.raises(ClientError, match='ValidationException.*'):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
||||
AttributeDefinitions=[
|
||||
{ 'AttributeName': 'p', 'AttributeType': 'S' },
|
||||
{ 'AttributeName': 'c', 'AttributeType': 'S' },
|
||||
],
|
||||
LocalSecondaryIndexes=[
|
||||
{ 'IndexName': 'hello',
|
||||
'KeySchema': [
|
||||
{ 'AttributeName': 'p', 'KeyType': 'HASH' },
|
||||
{ 'AttributeName': 'c', 'KeyType': 'RANGE' }
|
||||
],
|
||||
'Projection': { 'ProjectionType': 'ALL' }
|
||||
}
|
||||
])
|
||||
table.delete()
|
||||
|
||||
# A simple scenario for LSI. Base table has a partition key and a sort key,
|
||||
# index has the same partition key key but a different sort key - one of
|
||||
# the non-key attributes from the base table.
|
||||
@pytest.fixture(scope="module")
|
||||
# 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' } ],
|
||||
@@ -197,7 +171,7 @@ def test_lsi_1(test_table_lsi_1):
|
||||
|
||||
# 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="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_4(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
@@ -241,12 +215,6 @@ def test_lsi_4(test_table_lsi_4):
|
||||
KeyConditions={'p': {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'},
|
||||
column: {'AttributeValueList': [i5], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# Test that setting an indexed string column to an empty string is illegal,
|
||||
# since keys cannot contain empty strings
|
||||
def test_lsi_empty_value(test_table_lsi_1):
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_lsi_1.put_item(Item={'p': random_string(), 'c': random_string(), 'b': ''})
|
||||
|
||||
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
|
||||
@@ -257,7 +225,7 @@ def test_lsi_describe(test_table_lsi_4):
|
||||
# TODO: check also ProvisionedThroughput, IndexArn
|
||||
|
||||
# A table with selective projection - only keys are projected into the index
|
||||
@pytest.fixture(scope="module")
|
||||
@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' } ],
|
||||
@@ -334,14 +302,16 @@ def test_lsi_consistent_read(test_table_lsi_1):
|
||||
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'}})
|
||||
'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'}})
|
||||
'b': {'AttributeValueList': [b2], 'ComparisonOperator': 'EQ'}},
|
||||
ConsistentRead=True)
|
||||
|
||||
# A table with both gsi and lsi present
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_lsi_gsi(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' }, { 'AttributeName': 'c', 'KeyType': 'RANGE' } ],
|
||||
@@ -390,36 +360,6 @@ def test_lsi_and_gsi(test_table_lsi_gsi):
|
||||
|
||||
for index in ['hello_g1', 'hello_l1']:
|
||||
expected_items = [i for i in items if i['p'] == p1 and i['x1'] == x1]
|
||||
retrying_assert_index_query(test_table_lsi_gsi, index, expected_items,
|
||||
assert_index_query(test_table_lsi_gsi, index, expected_items,
|
||||
KeyConditions={'p': {'AttributeValueList': [p1], 'ComparisonOperator': 'EQ'},
|
||||
'x1': {'AttributeValueList': [x1], 'ComparisonOperator': 'EQ'}})
|
||||
|
||||
# This test is a version of test_filter_expression_and_projection_expression
|
||||
# from test_filter_expression, which involves a Query which projects only
|
||||
# one column but filters on another one, and the point is to verify that
|
||||
# the implementation got also the filtered column (for the filtering to work)
|
||||
# but did not return it with the results. This version does the same, except
|
||||
# that either the filtered column, or the projected column, is an LSI key.
|
||||
# In our implementation, LSI keys are implemented differently from ordinary
|
||||
# attributes - they are real Scylla columns and not just items in the
|
||||
# ":attrs" map - so this test checks that our implementation of the filtering
|
||||
# and projection (and their combination) did not mess up this special case.
|
||||
# This test reproduces issue #6951.
|
||||
def test_lsi_filter_expression_and_projection_expression(test_table_lsi_1):
|
||||
p = random_string()
|
||||
test_table_lsi_1.put_item(Item={'p': p, 'c': 'hi', 'b': 'dog', 'y': 'cat'})
|
||||
test_table_lsi_1.put_item(Item={'p': p, 'c': 'yo', 'b': 'mouse', 'y': 'horse'})
|
||||
# Case 1: b (the LSI key) is in filter but not in projection:
|
||||
got_items = full_query(test_table_lsi_1,
|
||||
KeyConditionExpression='p=:p',
|
||||
FilterExpression='b=:b',
|
||||
ProjectionExpression='y',
|
||||
ExpressionAttributeValues={':p': p, ':b': 'mouse'})
|
||||
assert(got_items == [{'y': 'horse'}])
|
||||
# Case 2: b (the LSI key) is in the projection, but not the filter:
|
||||
got_items = full_query(test_table_lsi_1,
|
||||
KeyConditionExpression='p=:p',
|
||||
FilterExpression='y=:y',
|
||||
ProjectionExpression='b',
|
||||
ExpressionAttributeValues={':p': p, ':y': 'cat'})
|
||||
assert(got_items == [{'b': 'dog'}])
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -116,6 +116,7 @@ def test_projection_expression_query(test_table):
|
||||
# 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={
|
||||
@@ -133,30 +134,19 @@ def test_projection_expression_path(test_table_s):
|
||||
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-existent attribute returns an empty item.
|
||||
# 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-existent member of a dictionary, as
|
||||
# well as paths which begin with a non-existent prefix.
|
||||
# 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'] == {}
|
||||
# Similarly, indexing a dictionary as an array, or array as dictionary, or
|
||||
# integer as either, yields an empty item.
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b.x')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[0]')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0].x')['Item'] == {}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b[0][0]')['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}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.c,b')['Item'] == {'a': {'c': 5}, 'b': 'hello'}
|
||||
# If some of the paths are not available, they are silently ignored (just
|
||||
# like they returned an empty item when used alone earlier)
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.x, a.b[0], x, a.b[3].x')['Item'] == {'a': {'b': [2]}}
|
||||
|
||||
# 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;
|
||||
@@ -169,109 +159,7 @@ def test_projection_expression_path(test_table_s):
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a,a.b[0]')['Item']
|
||||
|
||||
# Above we noted that asking for to project a non-existent attribute in an
|
||||
# existing item yields an empty Item object. However, if the item does not
|
||||
# exist at all, the Item object will be missing entirely:
|
||||
p = random_string()
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='x')
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.x')
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[0]')
|
||||
|
||||
# Above in test_projection_expression_toplevel_syntax() we tested how
|
||||
# name references (#name) work in top-level attributes. In the following
|
||||
# two tests we test how they work in more elaborate paths:
|
||||
# 1. Multiple path components can make multiple references, e.g., "#a.#b"
|
||||
# 2. Conversely, a single reference, e.g., "#a", is always a single path
|
||||
# component. Even if "#a" is "a.b", this refers to the literal attribute
|
||||
# "a.b" - with a dot in its name - and not to the b element in a.
|
||||
def test_projection_expression_path_references(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 1, 'c': 2}, 'b': 'hi'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b')['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#n1.b', ExpressionAttributeNames={'#n1': 'a'})['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.#n2', ExpressionAttributeNames={'#n2': 'b'})['Item'] == {'a': {'b': 1}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#n1.#n2', ExpressionAttributeNames={'#n1': 'a', '#n2': 'b'})['Item'] == {'a': {'b': 1}}
|
||||
# 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='a.#n2', ExpressionAttributeNames={'#wrong': 'b'})
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.#n2', ExpressionAttributeNames={'#n2': 'b', '#unused': 'x'})
|
||||
|
||||
def test_projection_expression_path_dot(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a.b': 'hi', 'a': {'b': 'yo', 'c': 'jo'}})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a.b')['Item'] == {'a': {'b': 'yo'}}
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='#name', ExpressionAttributeNames={'#name': 'a.b'})['Item'] == {'a.b': 'hi'}
|
||||
|
||||
# DynamoDB does not allow "overlapping" paths to be listed in
|
||||
# ProjectionExpression. This includes both identical paths, and paths where
|
||||
# one is a sub-path of the other - e.g. "a.b" and "a.b.c". As we already saw
|
||||
# above, paths with just a common *prefix* - e.g., "a.b, a.c" - are fine.
|
||||
def test_projection_expression_path_overlap(test_table_s):
|
||||
# The overlap is tested symbolically, on the given paths, without any
|
||||
# relation to what the item contains, or whether it even exists. So we
|
||||
# don't even need to create an item for this test. We still need a
|
||||
# key for the GetItem call :-)
|
||||
p = random_string()
|
||||
for expr in ['a, a',
|
||||
'a.b, a.b',
|
||||
'a[1], a[1]',
|
||||
'a, a.b',
|
||||
'a.b, a',
|
||||
'a.b, a.b[2]',
|
||||
'a.b, a.b.c',
|
||||
'a, a.b[2].c',
|
||||
'a.b.d, a.b',
|
||||
'a.b.d.e, a.b',
|
||||
'a.b, a.b.d',
|
||||
'a.b, a.b.d.e',
|
||||
]:
|
||||
with pytest.raises(ClientError, match='ValidationException.* overlap'):
|
||||
print(expr)
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expr)
|
||||
# The checks above can be easily passed by an over-zealos "overlap" check
|
||||
# which declares everything an overlap :-) Let's also check some non-
|
||||
# overlap cases - which shouldn't be declared an overlap.
|
||||
for expr in ['a, b',
|
||||
'a.b, a.c',
|
||||
'a.b.d, a.b.e',
|
||||
'a[1], a[2]',
|
||||
'a.b, a.c[2]',
|
||||
]:
|
||||
print(expr)
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expr)
|
||||
|
||||
# In addition to not allowing "overlapping" paths, DynamoDB also does not
|
||||
# allow "conflicting" paths: It does not allow giving both a.b and a[1] in a
|
||||
# single ProjectionExpression. It gives the error:
|
||||
# "Invalid ProjectionExpression: Two document paths conflict with each other;
|
||||
# must remove or rewrite one of these paths; path one: [a, b], path two:
|
||||
# [a, [1]]".
|
||||
# The reasoning is that asking for both in one request makes no sense because
|
||||
# no item will ever be able to fulfill both.
|
||||
def test_projection_expression_path_conflict(test_table_s):
|
||||
# The conflict is tested symbolically, on the given paths, without any
|
||||
# relation to what the item contains, or whether it even exists. So we
|
||||
# don't even need to create an item for this test. We still need a
|
||||
# key for the GetItem call :-)
|
||||
p = random_string()
|
||||
for expr in ['a.b, a[1]',
|
||||
'a[1], a.b',
|
||||
'a.b[1], a.b.c',
|
||||
'a.b.c, a.b[1]',
|
||||
]:
|
||||
with pytest.raises(ClientError, match='ValidationException.* conflict'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expr)
|
||||
# The checks above can be easily passed by an over-zealos "conflict" check
|
||||
# which declares everything a conflict :-) Let's also check some non-
|
||||
# conflict cases - which shouldn't be declared a conflict.
|
||||
for expr in ['a.b, a.c',
|
||||
'a.b, a.c[1]',
|
||||
]:
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression=expr)
|
||||
|
||||
# Above we nested paths in ProjectionExpression, but just for the GetItem
|
||||
# request. Let's verify they also work in Query and Scan requests:
|
||||
@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)]
|
||||
@@ -282,6 +170,7 @@ def test_query_projection_expression_path(test_table):
|
||||
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
|
||||
@@ -296,21 +185,6 @@ def test_scan_projection_expression_path(test_table):
|
||||
expected_items = [{'p': p, 'a': {'x': x['a']['x']}} for x in items]
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
|
||||
# BatchGetItem also supports ProjectionExpression, let's test that it
|
||||
# applies to all items, and that it correctly suports document paths as well.
|
||||
def test_batch_get_item_projection_expression_path(test_table_s):
|
||||
items = [{'p': random_string(), 'a': {'b': random_string(), 'x': 'hi'}, 'c': random_string()} for i in range(3)]
|
||||
with test_table_s.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
got_items = test_table_s.meta.client.batch_get_item(
|
||||
RequestItems = {test_table_s.name: {
|
||||
'Keys': [{'p': item['p']} for item in items],
|
||||
'ProjectionExpression': 'a.b',
|
||||
'ConsistentRead': True}})['Responses'][test_table_s.name]
|
||||
expected_items = [{'a': {'b': item['a']['b']}} for item in items]
|
||||
assert multiset(got_items) == multiset(expected_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
|
||||
@@ -325,36 +199,3 @@ def test_projection_expression_and_attributes_to_get(test_table_s):
|
||||
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'])
|
||||
|
||||
# above in test_projection_expression_toplevel_syntax among other things
|
||||
# we noted how spurious entries in ExpressionAttributeNames, not needed
|
||||
# the the ProjectionExpression, cause an error. Sometimes we have two
|
||||
# expressions in the same request, for example, both a ProjectionExpression
|
||||
# and a KeyConditionExpression. It's only an error if a name is not
|
||||
# needed by both of these expressions
|
||||
def test_projection_expression_and_key_condition_expression(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello', 'b': 'hi'})
|
||||
got_items = full_query(test_table_s,
|
||||
KeyConditionExpression='#name1 = :val1',
|
||||
ProjectionExpression='#name2',
|
||||
ExpressionAttributeNames={'#name1': 'p', '#name2': 'a'},
|
||||
ExpressionAttributeValues={':val1': p});
|
||||
assert got_items == [{'a': 'hello'}]
|
||||
|
||||
# Test whether the nesting depth of an a path in a projection expression
|
||||
# is limited. If the implementation is done using recursion, it is goood
|
||||
# practice to limit it and not crash the server. According to the DynamoDB
|
||||
# documentation, DynamoDB supports nested attributes up to 32 levels deep:
|
||||
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-attributes-nested-depth
|
||||
# There is no reason why Alternator should not use exactly the same limit
|
||||
# as is officially documented by DynamoDB.
|
||||
def test_projection_expression_path_nesting_levels(test_table_s):
|
||||
p = random_string()
|
||||
# 32 nesting levels (including the top-level attribute) work
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a'+('.b'*31))
|
||||
# 33 nesting levels do not. DynamoDB gives an error: "Invalid
|
||||
# ProjectionExpression: The document path has too many nesting levels;
|
||||
# nesting levels: 33".
|
||||
with pytest.raises(ClientError, match='ValidationException.*nesting levels'):
|
||||
test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a'+('.b'*32))
|
||||
358
alternator-test/test_query.py
Normal file
358
alternator-test/test_query.py
Normal file
@@ -0,0 +1,358 @@
|
||||
# -*- 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,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import random_string, random_bytes, full_scan, full_scan_and_count, multiset
|
||||
from util import random_string, full_scan, multiset
|
||||
from boto3.dynamodb.conditions import Attr
|
||||
|
||||
# Test that scanning works fine with/without pagination
|
||||
@@ -30,10 +30,10 @@ def test_scan_basic(filled_test_table):
|
||||
got_items = []
|
||||
while True:
|
||||
if limit:
|
||||
response = test_table.scan(Limit=limit, ConsistentRead=True, ExclusiveStartKey=pos) if pos else test_table.scan(Limit=limit, ConsistentRead=True)
|
||||
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, ConsistentRead=True) if pos else test_table.scan(ConsistentRead=True)
|
||||
response = test_table.scan(ExclusiveStartKey=pos) if pos else test_table.scan()
|
||||
pos = response.get('LastEvaluatedKey', None)
|
||||
got_items += response['Items']
|
||||
if not pos:
|
||||
@@ -42,11 +42,6 @@ def test_scan_basic(filled_test_table):
|
||||
assert len(items) == len(got_items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
def test_scan_nonexistent_table(dynamodb):
|
||||
client = dynamodb.meta.client
|
||||
with pytest.raises(ClientError, match="ResourceNotFoundException"):
|
||||
client.scan(TableName="i_do_not_exist")
|
||||
|
||||
def test_scan_with_paginator(dynamodb, filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
paginator = dynamodb.meta.client.get_paginator('scan')
|
||||
@@ -128,6 +123,7 @@ def test_scan_with_attribute_equality_filtering(dynamodb, filled_test_table):
|
||||
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
|
||||
|
||||
@@ -193,97 +189,3 @@ def test_scan_with_key_equality_filtering(dynamodb, filled_test_table):
|
||||
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)
|
||||
|
||||
# Test the "Select" parameter of Scan. The default Select mode,
|
||||
# ALL_ATTRIBUTES, returns items with all their attributes. Other modes
|
||||
# allow returning just specific attributes or just counting the results
|
||||
# without returning items at all.
|
||||
@pytest.mark.xfail(reason="Select not supported yet")
|
||||
def test_scan_select(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
got_items = full_scan(test_table)
|
||||
# By default, a scan returns all the items, with all their attributes:
|
||||
# query returns all attributes:
|
||||
got_items = full_scan(test_table)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
# Select=ALL_ATTRIBUTES does exactly the same as the default - return
|
||||
# all attributes:
|
||||
got_items = full_scan(test_table, Select='ALL_ATTRIBUTES')
|
||||
assert multiset(items) == multiset(got_items)
|
||||
# Select=ALL_PROJECTED_ATTRIBUTES is not allowed on a base table (it
|
||||
# is just for indexes, when IndexName is specified)
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(test_table, Select='ALL_PROJECTED_ATTRIBUTES')
|
||||
# Select=SPECIFIC_ATTRIBUTES requires that either a AttributesToGet
|
||||
# or ProjectionExpression appears, but then really does nothing beyond
|
||||
# what AttributesToGet and ProjectionExpression already do:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(test_table, Select='SPECIFIC_ATTRIBUTES')
|
||||
wanted = ['c', 'another']
|
||||
got_items = full_scan(test_table, Select='SPECIFIC_ATTRIBUTES', 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)
|
||||
got_items = full_scan(test_table, Select='SPECIFIC_ATTRIBUTES', ProjectionExpression=','.join(wanted))
|
||||
assert multiset(expected_items) == multiset(got_items)
|
||||
# Select=COUNT just returns a count - not any items
|
||||
(got_count, got_items) = full_scan_and_count(test_table, Select='COUNT')
|
||||
assert got_count == len(items)
|
||||
assert got_items == []
|
||||
# Check that we also get a count in regular scans - not just with
|
||||
# Select=COUNT, but without Select=COUNT we both items and count:
|
||||
(got_count, got_items) = full_scan_and_count(test_table)
|
||||
assert got_count == len(items)
|
||||
assert multiset(items) == multiset(got_items)
|
||||
# Select with some unknown string generates a validation exception:
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
full_scan(test_table, Select='UNKNOWN')
|
||||
|
||||
# Test parallel scan, i.e., the Segments and TotalSegments options.
|
||||
# In the following test we check that these parameters allow splitting
|
||||
# a scan into multiple parts, and that these parts are in fact disjoint,
|
||||
# and their union is the entire contents of the table. We do not actually
|
||||
# try to run these queries in *parallel* in this test.
|
||||
def test_scan_parallel(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
for nsegments in [1, 2, 17]:
|
||||
print('Testing TotalSegments={}'.format(nsegments))
|
||||
got_items = []
|
||||
for segment in range(nsegments):
|
||||
got_items.extend(full_scan(test_table, TotalSegments=nsegments, Segment=segment))
|
||||
# The following comparison verifies that each of the expected item
|
||||
# in items was returned in one - and just one - of the segments.
|
||||
assert multiset(items) == multiset(got_items)
|
||||
|
||||
# Test correct handling of incorrect parallel scan parameters.
|
||||
# Most of the corner cases (like TotalSegments=0) are validated
|
||||
# by boto3 itself, but some checks can still be performed.
|
||||
def test_scan_parallel_incorrect(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
with pytest.raises(ClientError, match='ValidationException.*Segment'):
|
||||
full_scan(test_table, TotalSegments=1000001, Segment=0)
|
||||
for segment in [7, 9]:
|
||||
with pytest.raises(ClientError, match='ValidationException.*Segment'):
|
||||
full_scan(test_table, TotalSegments=5, Segment=segment)
|
||||
|
||||
# ExclusiveStartKey must lie within the segment when using Segment/TotalSegment.
|
||||
def test_scan_parallel_with_exclusive_start_key(filled_test_table):
|
||||
test_table, items = filled_test_table
|
||||
with pytest.raises(ClientError, match='ValidationException.*Exclusive'):
|
||||
full_scan(test_table, TotalSegments=1000000, Segment=0, ExclusiveStartKey={'p': '0', 'c': '0'})
|
||||
|
||||
# We used to have a bug with formatting of LastEvaluatedKey in the response
|
||||
# of Query and Scan with bytes keys (issue #7768). In test_query_paging_byte()
|
||||
# (test_query.py) we tested the case of bytes *sort* keys. In the following
|
||||
# test we check bytes *partition* keys.
|
||||
def test_scan_paging_bytes(test_table_b):
|
||||
# We will not Scan the entire table - we have no idea what it contains.
|
||||
# But we don't need to scan the entire table - we just need the table
|
||||
# to contain at least two items, and then Scan it with Limit=1 and stop
|
||||
# after one page. Before #7768 was fixed, the test failed when the
|
||||
# LastEvaluatedKey in the response could not be parsed.
|
||||
items = [{'p': random_bytes()}, {'p': random_bytes()}]
|
||||
with test_table_b.batch_writer() as batch:
|
||||
for item in items:
|
||||
batch.put_item(item)
|
||||
response = test_table_b.scan(ConsistentRead=True, Limit=1)
|
||||
assert 'LastEvaluatedKey' in response
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -16,13 +16,10 @@
|
||||
# along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Tests for basic table operations: CreateTable, DeleteTable, ListTables.
|
||||
# Also some basic tests for UpdateTable - although UpdateTable usually
|
||||
# enables more elaborate features (such as GSI or Streams) and those are
|
||||
# tested elsewhere.
|
||||
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
from util import list_tables, unique_table_name, create_test_table, random_string
|
||||
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
|
||||
@@ -77,18 +74,19 @@ def create_and_delete_table(dynamodb, name, **kwargs):
|
||||
def test_create_and_delete_table(dynamodb):
|
||||
create_and_delete_table(dynamodb, 'alternator_test')
|
||||
|
||||
# Test that recreating a table right after deleting it works without issues
|
||||
def test_recreate_table(dynamodb):
|
||||
create_and_delete_table(dynamodb, 'alternator_recr_test')
|
||||
create_and_delete_table(dynamodb, 'alternator_recr_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):
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
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(ClientError, match='ValidationException'):
|
||||
with pytest.raises(ParamValidationError):
|
||||
create_table(dynamodb, 'nn')
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, 'n' * 256)
|
||||
@@ -197,7 +195,7 @@ def test_create_table_invalid_schema(dynamodb):
|
||||
# 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.*Table.*already exists'):
|
||||
with pytest.raises(ClientError, match='ResourceInUseException'):
|
||||
create_table(dynamodb, test_table.name)
|
||||
|
||||
# Test that BillingMode error path works as expected - only the values
|
||||
@@ -207,13 +205,13 @@ def test_create_table_already_exists(dynamodb, test_table):
|
||||
# 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, unique_table_name(), BillingMode='unknown')
|
||||
create_table(dynamodb, test_table_name(), BillingMode='unknown')
|
||||
# billing mode is case-sensitive
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
create_table(dynamodb, unique_table_name(), BillingMode='pay_per_request')
|
||||
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, unique_table_name(),
|
||||
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
|
||||
@@ -221,38 +219,14 @@ def test_create_table_billing_mode_errors(dynamodb, test_table):
|
||||
# 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, unique_table_name(), BillingMode='PROVISIONED')
|
||||
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=unique_table_name(),
|
||||
dynamodb.create_table(TableName=test_table_name(),
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }])
|
||||
|
||||
# Even before Alternator gains full support for the DynamoDB stream API
|
||||
# and CreateTable's StreamSpecification option, we should support the
|
||||
# options which mean it is turned *off*.
|
||||
def test_table_streams_off(dynamodb):
|
||||
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
||||
# if StreamSpecification was missing. StreamViewType isn't needed.
|
||||
table = create_test_table(dynamodb, StreamSpecification={'StreamEnabled': False},
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
table.delete();
|
||||
# DynamoDB doesn't allow StreamSpecification to be empty map - if it
|
||||
# exists, it must have a StreamEnabled
|
||||
# Unfortunately, new versions of boto3 doesn't let us pass this...
|
||||
#with pytest.raises(ClientError, match='ValidationException'):
|
||||
# table = create_test_table(dynamodb, StreamSpecification={},
|
||||
# KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
# AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
# table.delete();
|
||||
# Unfortunately, boto3 doesn't allow us to pass StreamSpecification=None.
|
||||
# This is what we had in issue #5796.
|
||||
|
||||
# For tests with 'StreamEnabled': True, and different 'StreamViewType', see
|
||||
# test_streams.py.
|
||||
|
||||
# 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
|
||||
@@ -260,7 +234,7 @@ def test_table_streams_off(dynamodb):
|
||||
# named columns.
|
||||
special_column_name1 = 'attrs'
|
||||
special_column_name2 = ':attrs'
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture(scope="session")
|
||||
def test_table_special_column_name(dynamodb):
|
||||
table = create_test_table(dynamodb,
|
||||
KeySchema=[
|
||||
@@ -300,31 +274,3 @@ 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)
|
||||
|
||||
# Even before Alternator gains support for configuring server-side encryption
|
||||
# ("encryption at rest") with CreateTable's SSESpecification option, we should
|
||||
# support the option "Enabled=false" which is the default, and means the server
|
||||
# takes care of whatever server-side encryption is done, on its own.
|
||||
# Reproduces issue #7031.
|
||||
def test_table_sse_off(dynamodb):
|
||||
# If StreamSpecification is given, but has StreamEnabled=false, it's as
|
||||
# if StreamSpecification was missing, and fine. No other attribues are
|
||||
# necessary.
|
||||
table = create_test_table(dynamodb, SSESpecification = {'Enabled': False},
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]);
|
||||
table.delete();
|
||||
|
||||
# Test that trying to delete a table that doesn't exist fails in the
|
||||
# appropriate way (ResourceNotFoundException)
|
||||
def test_delete_table_non_existent(dynamodb, test_table):
|
||||
client = dynamodb.meta.client
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
client.delete_table(TableName=random_string(20))
|
||||
|
||||
# Test that trying to update a table that doesn't exist fails in the
|
||||
# appropriate way (ResourceNotFoundException)
|
||||
def test_update_table_non_existent(dynamodb, test_table):
|
||||
client = dynamodb.meta.client
|
||||
with pytest.raises(ClientError, match='ResourceNotFoundException'):
|
||||
client.update_table(TableName=random_string(20), BillingMode='PAY_PER_REQUEST')
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-present ScyllaDB
|
||||
# Copyright 2019 ScyllaDB
|
||||
#
|
||||
# This file is part of Scylla.
|
||||
#
|
||||
@@ -21,6 +21,7 @@ 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,
|
||||
@@ -265,64 +266,16 @@ def test_update_expression_multi_overlap(test_table_s):
|
||||
|
||||
# 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()
|
||||
# Note that the overlap checks happen before checking the actual content
|
||||
# of the item, so it doesn't matter that the item we want to modify
|
||||
# doesn't even exist or have the structure referenced by the paths below.
|
||||
for expr in ['SET a = :val1, a.b = :val2',
|
||||
'SET a.b = :val1, a = :val2',
|
||||
'SET a.b = :val1, a.b = :val2',
|
||||
'SET a.b = :val1, a.b.c = :val2',
|
||||
'SET a.b.c = :val1, a.b = :val2',
|
||||
'SET a.b.c = :val1, a.b.c = :val2',
|
||||
'SET a.b = :val1, a.b.c.d.e = :val2',
|
||||
'SET a.b.c.d.e = :val1, a.b = :val2',
|
||||
'SET a = :val1, a[1] = :val2',
|
||||
'SET a[1] = :val1, a = :val2',
|
||||
'SET a[1] = :val1, a[1] = :val2',
|
||||
'SET a[1][1] = :val1, a[1] = :val2',
|
||||
'SET a[1] = :val1, a[1][1] = :val2',
|
||||
'SET a[1][1] = :val1, a[1][1] = :val2',
|
||||
'SET a[1][1][1][1] = :val1, a[1][1] = :val2',
|
||||
'SET a[1][1] = :val1, a[1][1][1][1] = :val2',
|
||||
'SET a[1][1][1][1] = :val1, a[1][1][1][1] = :val2',
|
||||
]:
|
||||
print(expr)
|
||||
with pytest.raises(ClientError, match='ValidationException.*overlap'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression=expr,
|
||||
ExpressionAttributeValues={':val1': 2, ':val2': 'there'})
|
||||
# Obviously this test can trivially pass if overlap checks wrongly labels
|
||||
# everything as an overlap. So the test test_update_expression_multi_nested
|
||||
# below is important - it confirms that we can do multiple modifications
|
||||
# to the same item when they do not overlap.
|
||||
|
||||
# Besides the concept of "overlapping" paths tested above, DynamoDB also has
|
||||
# the concept of "conflicting" paths - e.g., attempting to set both a.b and
|
||||
# a[1] together doesn't make sense.
|
||||
def test_update_expression_multi_conflict_nested(test_table_s):
|
||||
p = random_string()
|
||||
for expr in ['SET a.b = :val1, a[1] = :val2',
|
||||
'SET a.b.c = :val1, a.b[2] = :val2',
|
||||
]:
|
||||
print(expr)
|
||||
with pytest.raises(ClientError, match='ValidationException.*conflict'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression=expr,
|
||||
ExpressionAttributeValues={':val1': 2, ':val2': 'there'})
|
||||
|
||||
# We can do several non-overlapping modifications to the same top-level
|
||||
# attribute and to different top-level attributes in the same update
|
||||
# expression.
|
||||
def test_update_expression_multi_nested(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'x': 3, 'y': 4, 'c': {'y': 3}}, 'b': {'x': 1, 'y': 2}})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a.b = :val1, a.c.d = :val2, b.x = :val3 REMOVE a.x, b.y',
|
||||
ExpressionAttributeValues={':val1': 10, ':val2': 'dog', ':val3': 17})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {
|
||||
'p': p,
|
||||
'a': {'y': 4, 'b': 10, 'c': {'y': 3, 'd': 'dog'}},
|
||||
'b': {'x': 17}}
|
||||
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
|
||||
@@ -417,7 +370,7 @@ def test_update_expression_cannot_modify_key(test_table):
|
||||
|
||||
# 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_existent_clause(test_table_s):
|
||||
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},
|
||||
@@ -454,6 +407,21 @@ def test_update_expression_plus_basic(test_table_s):
|
||||
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):
|
||||
@@ -616,7 +584,7 @@ def test_update_expression_if_not_exists(test_table_s):
|
||||
# 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="for unknown reason, DynamoDB does not allow nesting list_append")
|
||||
@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},
|
||||
@@ -707,24 +675,6 @@ def test_update_expression_add_numbers(test_table_s):
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': 1})
|
||||
|
||||
# In test_update_expression_add_numbers() above we tested ADDing a number to
|
||||
# an existing number. The following test check that ADD can be used to
|
||||
# create a *new* number, as if it was added to zero.
|
||||
def test_update_expression_add_numbers_new(test_table_s):
|
||||
# Test that "ADD" can create a new number attribute:
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 7
|
||||
# Test that "ADD" can create an entirely new item:
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': 8})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == 8
|
||||
|
||||
# Test "ADD" operation for sets
|
||||
def test_update_expression_add_sets(test_table_s):
|
||||
p = random_string()
|
||||
@@ -753,53 +703,6 @@ def test_update_expression_add_sets(test_table_s):
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
|
||||
# In test_update_expression_add_sets() above we tested ADDing elements to an
|
||||
# existing set. The following test checks that ADD can be used to create a
|
||||
# *new* set, by adding its first item.
|
||||
def test_update_expression_add_sets_new(test_table_s):
|
||||
# Test that "ADD" can create a new set attribute:
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': 'hello'})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': set(['dog'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == set(['dog'])
|
||||
# Test that "ADD" can create an entirely new item:
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD b :val1',
|
||||
ExpressionAttributeValues={':val1': set(['cat'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['b'] == set(['cat'])
|
||||
|
||||
# Although AttributeUpdate's ADD operation works on lists (despite being
|
||||
# documented only for sets; see test_item.py::test_update_item_add), the
|
||||
# ADD operation of UpdateExpression does *not* support lists. Let's verify
|
||||
# this here.
|
||||
# Passing this test will require the two ADD implementations to be slightly
|
||||
# different.
|
||||
def test_update_expression_add_lists(test_table_s):
|
||||
p = random_string()
|
||||
# When the operand given in ExpressionAttributesValues is a list,
|
||||
# we get an error about the operand, no matter what the item contains -
|
||||
# a list too, or a set.
|
||||
test_table_s.put_item(Item={'p': p, 'a': [1, 2]})
|
||||
with pytest.raises(ClientError, match='ValidationException.*operand'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': [3, 4]})
|
||||
test_table_s.put_item(Item={'p': p, 'a': set([1, 2])})
|
||||
with pytest.raises(ClientError, match='ValidationException.*operand'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': [3, 4]})
|
||||
# If the operand is a set, the item value still cannot be a list because
|
||||
# those different types can't be added.
|
||||
with pytest.raises(ClientError, match='ValidationException.*operand'):
|
||||
test_table_s.put_item(Item={'p': p, 'a': [1, 2]})
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='ADD a :val1',
|
||||
ExpressionAttributeValues={':val1': set([3, 4])})
|
||||
|
||||
# Test "DELETE" operation for sets
|
||||
def test_update_expression_delete_sets(test_table_s):
|
||||
p = random_string()
|
||||
@@ -814,28 +717,10 @@ def test_update_expression_delete_sets(test_table_s):
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['pig'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['dog'])
|
||||
# Deleting all the elements cannot leave an empty set (which isn't
|
||||
# supported). Rather, it deletes the attribute altogether:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['dog'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hi'}
|
||||
# Deleting elements from a non-existent attribute is allowed, and
|
||||
# simply does nothing:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set(['dog'])})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'b': 'hi'}
|
||||
# An empty set parameter is not allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
ExpressionAttributeValues={':val1': set([])})
|
||||
# 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".
|
||||
test_table_s.put_item(Item={'p': p, 'a': set(['dog', 'cat', 'mouse']), 'b': 'hi'})
|
||||
with pytest.raises(ClientError, match='ValidationException.*type'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='DELETE a :val1',
|
||||
@@ -853,19 +738,10 @@ def test_update_expression_dot_in_name(test_table_s):
|
||||
ExpressionAttributeNames={'#a': 'a.b'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a.b': 3}
|
||||
|
||||
|
||||
# Below we have several tests of what happens when a nested attribute is
|
||||
# on the left-hand side of an assignment, but an every simpler case of
|
||||
# nested attributes is having one on the right hand side of an assignment:
|
||||
def test_update_expression_nested_attribute_rhs(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 3, 'c': {'x': 7, 'y': 8}}, 'd': 5})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET z = a.c.x')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
||||
|
||||
# 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})
|
||||
@@ -881,6 +757,7 @@ def test_update_expression_nested_attribute_dot(test_table_s):
|
||||
|
||||
# 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']})
|
||||
@@ -888,31 +765,10 @@ def test_update_expression_nested_attribute_index(test_table_s):
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['one', 'hello', 'three']}
|
||||
|
||||
# An index into a list must be an actual integer - DynamoDB does not support
|
||||
# a value reference (:xyz) to be used as a index. This is the same test as the
|
||||
# above test_update_expression_nested_attribute_index() - we just try to use
|
||||
# the a reference :xyz for the index 1. And it's considered a syntax error
|
||||
def test_update_expression_nested_attribute_index_reference(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': ['one', 'two', 'three']})
|
||||
with pytest.raises(ClientError, match='ValidationException.*yntax'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[:xyz] = :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello', ':xyz': 1})
|
||||
|
||||
# In the previous test we saw that a value reference (:xyz) can't be used
|
||||
# as an index. Here we see that a name reference (#xyz) also can't be used
|
||||
# as an index. It too is a syntax error - it doesn't even matter if we
|
||||
# define this #xyz or not.
|
||||
def test_update_expression_nested_attribute_index_reference_name(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': ['one', 'two', 'three']})
|
||||
with pytest.raises(ClientError, match='ValidationException.*yntax'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[#xyz] = :val1',
|
||||
ExpressionAttributeValues={':val1': 'hello'})
|
||||
|
||||
# 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}}})
|
||||
@@ -922,6 +778,7 @@ def test_update_expression_nested_different_type(test_table_s):
|
||||
|
||||
# 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]}}]}})
|
||||
@@ -935,55 +792,20 @@ def test_update_expression_nested_deep(test_table_s):
|
||||
|
||||
# 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]}}]}
|
||||
|
||||
# Removing a list item beyond the end of the list (e.g., REMOVE a[17] when
|
||||
# the list only has three items) is silently ignored.
|
||||
def test_update_expression_nested_remove_list_item_after_end(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': [4, 5, 6]})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a[17]')
|
||||
|
||||
# If we remove a[1] and then change a[3], the index "3" refers to the position
|
||||
# *before* the first removal.
|
||||
def test_update_expression_nested_remove_list_item_original_number(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': [2, 3, 4, 5, 6, 7]})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a[1] SET a[3] = :val',
|
||||
ExpressionAttributeValues={':val': 17})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == [2, 4, 17, 6, 7]
|
||||
# The order of the operations doesn't matter
|
||||
test_table_s.put_item(Item={'p': p, 'a': [2, 3, 4, 5, 6, 7]})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a[3] = :val REMOVE a[1]',
|
||||
ExpressionAttributeValues={':val': 17})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == [2, 4, 17, 6, 7]
|
||||
|
||||
# DynamoDB allows an empty map. So removing the only member from a map leaves
|
||||
# behind an empty map.
|
||||
def test_update_expression_nested_remove_singleton_map(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': {'b': 1}})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a.b')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == {}
|
||||
|
||||
# DynamoDB allows an empty list. So removing the only member from a list leaves
|
||||
# behind an empty list.
|
||||
def test_update_expression_nested_remove_singleton_list(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.put_item(Item={'p': p, 'a': [1]})
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a[0]')
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == []
|
||||
|
||||
# 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']})
|
||||
@@ -993,9 +815,9 @@ def test_nested_attribute_update_array_out_of_bounds(test_table_s):
|
||||
# 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, a[17] = :val3, a[50] = :val4',
|
||||
ExpressionAttributeValues={':val1': 'a1', ':val2': 'a2', ':val3': 'a3', ':val4': 'a4'})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': ['one', 'two', 'three', 'hello', 'a3', 'a2', 'a4', 'a1']}
|
||||
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
|
||||
@@ -1004,6 +826,9 @@ def test_nested_attribute_update_array_out_of_bounds(test_table_s):
|
||||
# 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']})
|
||||
@@ -1016,111 +841,14 @@ def test_nested_attribute_update_bad_path_dot(test_table_s):
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
# Same errors for "remove" operation.
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a.c')
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE b.c')
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE c.c')
|
||||
# Same error when the item doesn't exist at all:
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET c.c = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
# HOWEVER, although it *is* allowed to remove a random path from a non-
|
||||
# existent item. I don't know why. See the next test -
|
||||
# test_nested_attribute_remove_from_missing_item
|
||||
|
||||
# Though in the above test (test_nested_attribute_update_bad_path_dot) we
|
||||
# showed that DynamoDB does not allow REMOVE x.y if attribute x doesn't
|
||||
# exist - and generates a ValidationException, it turns out that if the
|
||||
# entire item doesn't exist, then a REMOVE x.y is silently ignored.
|
||||
# I don't understand why they did this.
|
||||
@pytest.mark.xfail(reason="for unknown reason, DynamoDB allows REMOVE x.y when item doesn't exist")
|
||||
def test_nested_attribute_remove_from_missing_item(test_table_s):
|
||||
p = random_string()
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE x.y')
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE x[0]')
|
||||
|
||||
# Though in an above test (test_nested_attribute_update_bad_path_dot) we
|
||||
# showed that DynamoDB does not allow REMOVE x.y if attribute x doesn't
|
||||
# exist - and generates a ValidationException, if x *does* exist but y
|
||||
# doesn't, it's fine and the removal should just be silently ignored.
|
||||
def test_nested_attribute_remove_missing_leaf(test_table_s):
|
||||
p = random_string()
|
||||
item = {'p': p, 'a': {'x': 3}, 'b': ['hi']}
|
||||
test_table_s.put_item(Item=item)
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a.y')
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE b[7]')
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE c')
|
||||
# The above UpdateItem calls didn't change anything...
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == item
|
||||
|
||||
# Similarly for other types of bad paths - using [0] on something which
|
||||
# doesn't exist or isn't an array.
|
||||
# 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})
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET b[0] = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
# Same errors for "remove" operation.
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE a[0]')
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='REMOVE b[0]')
|
||||
# Same error when the item doesn't exist at all:
|
||||
p = random_string()
|
||||
with pytest.raises(ClientError, match='ValidationException.*path'):
|
||||
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET b[0] = :val1',
|
||||
ExpressionAttributeValues={':val1': 7})
|
||||
# HOWEVER, although it *is* allowed to remove a random path from a non-
|
||||
# existent item. I don't know why... See test_nested_attribute_remove_from_missing_item
|
||||
|
||||
# DynamoDB Does not allow empty sets.
|
||||
# Trying to ask UpdateItem to put one of these in an attribute should be
|
||||
# forbidden. Empty lists and maps *are* allowed.
|
||||
# Note that in test_item.py::test_update_item_empty_attribute we checked
|
||||
# this with the AttributeUpdates syntax. Here we check the same with the
|
||||
# UpdateExpression syntax.
|
||||
def test_update_expression_empty_attribute(test_table_s):
|
||||
p = random_string()
|
||||
# Empty sets are *not* allowed
|
||||
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :v',
|
||||
ExpressionAttributeValues={':v': set()})
|
||||
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
||||
# But empty lists, maps, strings and binary blobs *are* allowed:
|
||||
test_table_s.update_item(Key={'p': p},
|
||||
UpdateExpression='SET d = :v1, e = :v2, f = :v3, g = :v4',
|
||||
ExpressionAttributeValues={':v1': [], ':v2': {}, ':v3': '', ':v4': b''})
|
||||
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'd': [], 'e': {}, 'f': '', 'g': b''}
|
||||
|
||||
# Verify which kind of update operations require a read-before-write (a.k.a
|
||||
# read-modify-write, or RMW). We test this by using a table configured with
|
||||
# "forbid_rmw" isolation mode and checking which writes succeed or pass.
|
||||
# This is a Scylla-only test (the test_table_s_forbid_rmw implies scylla_only).
|
||||
def test_update_expression_when_rmw(test_table_s_forbid_rmw):
|
||||
table = test_table_s_forbid_rmw
|
||||
p = random_string()
|
||||
# A write with a RHS (right-hand side) being a constant from the query
|
||||
# doesn't need RMW:
|
||||
table.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a = :val',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
# But if the LHS (left-hand side) of the assignment is a document path,
|
||||
# it *does* need RMW:
|
||||
with pytest.raises(ClientError, match='ValidationException.*write isolation policy'):
|
||||
table.update_item(Key={'p': p},
|
||||
UpdateExpression='SET a.b = :val',
|
||||
ExpressionAttributeValues={':val': 3})
|
||||
assert table.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 3
|
||||
# A write with a path in the RHS of a SET also needs RMW
|
||||
with pytest.raises(ClientError, match='ValidationException.*write isolation policy'):
|
||||
table.update_item(Key={'p': p},
|
||||
UpdateExpression='SET b = a')
|
||||
121
alternator-test/util.py
Normal file
121
alternator-test/util.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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,171 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-present 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/error.hh"
|
||||
#include "log.hh"
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <gnutls/crypto.h>
|
||||
#include "hashers.hh"
|
||||
#include "bytes.hh"
|
||||
#include "alternator/auth.hh"
|
||||
#include <fmt/format.h>
|
||||
#include "auth/common.hh"
|
||||
#include "auth/password_authenticator.hh"
|
||||
#include "auth/roles-metadata.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "alternator/executor.hh"
|
||||
#include "cql3/selection/selection.hh"
|
||||
#include "database.hh"
|
||||
#include "query-result-set.hh"
|
||||
#include "cql3/result_set.hh"
|
||||
#include <seastar/core/coroutine.hh>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
static logging::logger alogger("alternator-auth");
|
||||
|
||||
static hmac_sha256_digest hmac_sha256(std::string_view key, std::string_view msg) {
|
||||
hmac_sha256_digest digest;
|
||||
int ret = gnutls_hmac_fast(GNUTLS_MAC_SHA256, key.data(), key.size(), msg.data(), msg.size(), digest.data());
|
||||
if (ret) {
|
||||
throw std::runtime_error(fmt::format("Computing HMAC failed ({}): {}", ret, gnutls_strerror(ret)));
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
static hmac_sha256_digest get_signature_key(std::string_view key, std::string_view date_stamp, std::string_view region_name, std::string_view service_name) {
|
||||
auto date = hmac_sha256("AWS4" + std::string(key), date_stamp);
|
||||
auto region = hmac_sha256(std::string_view(date.data(), date.size()), region_name);
|
||||
auto service = hmac_sha256(std::string_view(region.data(), region.size()), service_name);
|
||||
auto signing = hmac_sha256(std::string_view(service.data(), service.size()), "aws4_request");
|
||||
return signing;
|
||||
}
|
||||
|
||||
static std::string apply_sha256(std::string_view msg) {
|
||||
sha256_hasher hasher;
|
||||
hasher.update(msg.data(), msg.size());
|
||||
return to_hex(hasher.finalize());
|
||||
}
|
||||
|
||||
static std::string apply_sha256(const std::vector<temporary_buffer<char>>& msg) {
|
||||
sha256_hasher hasher;
|
||||
for (const temporary_buffer<char>& buf : msg) {
|
||||
hasher.update(buf.get(), buf.size());
|
||||
}
|
||||
return to_hex(hasher.finalize());
|
||||
}
|
||||
|
||||
static std::string format_time_point(db_clock::time_point tp) {
|
||||
time_t time_point_repr = db_clock::to_time_t(tp);
|
||||
std::string time_point_str;
|
||||
time_point_str.resize(17);
|
||||
::tm time_buf;
|
||||
// strftime prints the terminating null character as well
|
||||
std::strftime(time_point_str.data(), time_point_str.size(), "%Y%m%dT%H%M%SZ", ::gmtime_r(&time_point_repr, &time_buf));
|
||||
time_point_str.resize(16);
|
||||
return time_point_str;
|
||||
}
|
||||
|
||||
void check_expiry(std::string_view signature_date) {
|
||||
//FIXME: The default 15min can be changed with X-Amz-Expires header - we should honor it
|
||||
std::string expiration_str = format_time_point(db_clock::now() - 15min);
|
||||
std::string validity_str = format_time_point(db_clock::now() + 15min);
|
||||
if (signature_date < expiration_str) {
|
||||
throw api_error::invalid_signature(
|
||||
fmt::format("Signature expired: {} is now earlier than {} (current time - 15 min.)",
|
||||
signature_date, expiration_str));
|
||||
}
|
||||
if (signature_date > validity_str) {
|
||||
throw api_error::invalid_signature(
|
||||
fmt::format("Signature not yet current: {} is still later than {} (current time + 15 min.)",
|
||||
signature_date, validity_str));
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method,
|
||||
std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map<std::string_view, std::string_view>& signed_headers_map,
|
||||
const std::vector<temporary_buffer<char>>& body_content, std::string_view region, std::string_view service, std::string_view query_string) {
|
||||
auto amz_date_it = signed_headers_map.find("x-amz-date");
|
||||
if (amz_date_it == signed_headers_map.end()) {
|
||||
throw api_error::invalid_signature("X-Amz-Date header is mandatory for signature verification");
|
||||
}
|
||||
std::string_view amz_date = amz_date_it->second;
|
||||
check_expiry(amz_date);
|
||||
std::string_view datestamp = amz_date.substr(0, 8);
|
||||
if (datestamp != orig_datestamp) {
|
||||
throw api_error::invalid_signature(
|
||||
format("X-Amz-Date date does not match the provided datestamp. Expected {}, got {}",
|
||||
orig_datestamp, datestamp));
|
||||
}
|
||||
std::string_view canonical_uri = "/";
|
||||
|
||||
std::stringstream canonical_headers;
|
||||
for (const auto& header : signed_headers_map) {
|
||||
canonical_headers << fmt::format("{}:{}", header.first, header.second) << '\n';
|
||||
}
|
||||
|
||||
std::string payload_hash = apply_sha256(body_content);
|
||||
std::string canonical_request = fmt::format("{}\n{}\n{}\n{}\n{}\n{}", method, canonical_uri, query_string, canonical_headers.str(), signed_headers_str, payload_hash);
|
||||
|
||||
std::string_view algorithm = "AWS4-HMAC-SHA256";
|
||||
std::string credential_scope = fmt::format("{}/{}/{}/aws4_request", datestamp, region, service);
|
||||
std::string string_to_sign = fmt::format("{}\n{}\n{}\n{}", algorithm, amz_date, credential_scope, apply_sha256(canonical_request));
|
||||
|
||||
hmac_sha256_digest signing_key = get_signature_key(secret_access_key, datestamp, region, service);
|
||||
hmac_sha256_digest signature = hmac_sha256(std::string_view(signing_key.data(), signing_key.size()), string_to_sign);
|
||||
|
||||
return to_hex(bytes_view(reinterpret_cast<const int8_t*>(signature.data()), signature.size()));
|
||||
}
|
||||
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, std::string username) {
|
||||
schema_ptr schema = proxy.get_db().local().find_schema("system_auth", "roles");
|
||||
partition_key pk = partition_key::from_single_value(*schema, utf8_type->decompose(username));
|
||||
dht::partition_range_vector partition_ranges{dht::partition_range(dht::decorate_key(*schema, pk))};
|
||||
std::vector<query::clustering_range> bounds{query::clustering_range::make_open_ended_both_sides()};
|
||||
const column_definition* salted_hash_col = schema->get_column_definition(bytes("salted_hash"));
|
||||
if (!salted_hash_col) {
|
||||
co_return coroutine::make_exception(api_error::unrecognized_client(format("Credentials cannot be fetched for: {}", username)));
|
||||
}
|
||||
auto selection = cql3::selection::selection::for_columns(schema, {salted_hash_col});
|
||||
auto partition_slice = query::partition_slice(std::move(bounds), {}, query::column_id_vector{salted_hash_col->id}, selection->get_query_options());
|
||||
auto command = ::make_lw_shared<query::read_command>(schema->id(), schema->version(), partition_slice, proxy.get_max_result_size(partition_slice));
|
||||
auto cl = auth::password_authenticator::consistency_for_user(username);
|
||||
|
||||
service::client_state client_state{service::client_state::internal_tag()};
|
||||
service::storage_proxy::coordinator_query_result qr = co_await proxy.query(schema, std::move(command), std::move(partition_ranges), cl,
|
||||
service::storage_proxy::coordinator_query_options(executor::default_timeout(), empty_service_permit(), client_state));
|
||||
|
||||
cql3::selection::result_set_builder builder(*selection, gc_clock::now(), cql_serialization_format::latest());
|
||||
query::result_view::consume(*qr.query_result, partition_slice, cql3::selection::result_set_builder::visitor(builder, *schema, *selection));
|
||||
|
||||
auto result_set = builder.build();
|
||||
if (result_set->empty()) {
|
||||
co_return coroutine::make_exception(api_error::unrecognized_client(format("User not found: {}", username)));
|
||||
}
|
||||
const bytes_opt& salted_hash = result_set->rows().front().front(); // We only asked for 1 row and 1 column
|
||||
if (!salted_hash) {
|
||||
co_return coroutine::make_exception(api_error::unrecognized_client(format("No password found for user: {}", username)));
|
||||
}
|
||||
co_return value_cast<sstring>(utf8_type->deserialize(*salted_hash));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-present 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 <string_view>
|
||||
#include <array>
|
||||
#include "gc_clock.hh"
|
||||
#include "utils/loading_cache.hh"
|
||||
|
||||
namespace service {
|
||||
class storage_proxy;
|
||||
}
|
||||
|
||||
namespace alternator {
|
||||
|
||||
using hmac_sha256_digest = std::array<char, 32>;
|
||||
|
||||
using key_cache = utils::loading_cache<std::string, std::string>;
|
||||
|
||||
std::string get_signature(std::string_view access_key_id, std::string_view secret_access_key, std::string_view host, std::string_view method,
|
||||
std::string_view orig_datestamp, std::string_view signed_headers_str, const std::map<std::string_view, std::string_view>& signed_headers_map,
|
||||
const std::vector<temporary_buffer<char>>& body_content, std::string_view region, std::string_view service, std::string_view query_string);
|
||||
|
||||
future<std::string> get_key_from_roles(service::storage_proxy& proxy, std::string username);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -19,6 +19,10 @@
|
||||
* 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>
|
||||
@@ -28,13 +32,13 @@
|
||||
// and the character used in base64 encoding to represent it.
|
||||
static class base64_chars {
|
||||
public:
|
||||
static constexpr const char to[] =
|
||||
static constexpr const char* to =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int8_t from[255];
|
||||
base64_chars() {
|
||||
static_assert(sizeof(to) == 64 + 1);
|
||||
static_assert(strlen(to) == 64);
|
||||
for (int i = 0; i < 255; i++) {
|
||||
from[i] = -1; // signal invalid character
|
||||
from[i] = 255; // signal invalid character
|
||||
}
|
||||
for (int i = 0; i < 64; i++) {
|
||||
from[(unsigned) to[i]] = i;
|
||||
@@ -73,7 +77,7 @@ std::string base64_encode(bytes_view in) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::string base64_decode_string(std::string_view in) {
|
||||
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;
|
||||
@@ -100,42 +104,8 @@ static std::string base64_decode_string(std::string_view in) {
|
||||
if (i==3)
|
||||
ret += ((chunk4[1] & 0xf) << 4) + ((chunk4[2] & 0x3c) >> 2);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bytes base64_decode(std::string_view in) {
|
||||
// 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.
|
||||
std::string ret = base64_decode_string(in);
|
||||
return bytes(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
static size_t base64_padding_len(std::string_view str) {
|
||||
size_t padding = 0;
|
||||
padding += (!str.empty() && str.back() == '=');
|
||||
padding += (str.size() > 1 && *(str.end() - 2) == '=');
|
||||
return padding;
|
||||
}
|
||||
|
||||
size_t base64_decoded_len(std::string_view str) {
|
||||
return str.size() / 4 * 3 - base64_padding_len(str);
|
||||
}
|
||||
|
||||
bool base64_begins_with(std::string_view base, std::string_view operand) {
|
||||
if (base.size() < operand.size() || base.size() % 4 != 0 || operand.size() % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
if (base64_padding_len(operand) == 0) {
|
||||
return base.starts_with(operand);
|
||||
}
|
||||
const std::string_view unpadded_base_prefix = base.substr(0, operand.size() - 4);
|
||||
const std::string_view unpadded_operand = operand.substr(0, operand.size() - 4);
|
||||
if (unpadded_base_prefix != unpadded_operand) {
|
||||
return false;
|
||||
}
|
||||
// Decode and compare last 4 bytes of base64-encoded strings
|
||||
const std::string base_remainder = base64_decode_string(base.substr(operand.size() - 4, operand.size()));
|
||||
const std::string operand_remainder = base64_decode_string(operand.substr(operand.size() - 4));
|
||||
return base_remainder.starts_with(operand_remainder);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -21,13 +21,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include "bytes.hh"
|
||||
|
||||
std::string base64_encode(bytes_view);
|
||||
|
||||
bytes base64_decode(std::string_view);
|
||||
|
||||
size_t base64_decoded_len(std::string_view str);
|
||||
|
||||
bool base64_begins_with(std::string_view base, std::string_view operand);
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -26,16 +26,7 @@
|
||||
#include "alternator/error.hh"
|
||||
#include "cql3/constants.hh"
|
||||
#include <unordered_map>
|
||||
#include "utils/rjson.hh"
|
||||
#include "serialization.hh"
|
||||
#include "utils/base64.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include <stdexcept>
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
#include <boost/algorithm/cxx11/any_of.hpp>
|
||||
#include "utils/overloaded_functor.hh"
|
||||
|
||||
#include "expressions.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -44,448 +35,91 @@ 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},
|
||||
{"NE", comparison_operator_type::NE},
|
||||
{"LE", comparison_operator_type::LE},
|
||||
{"LT", comparison_operator_type::LT},
|
||||
{"GE", comparison_operator_type::GE},
|
||||
{"GT", comparison_operator_type::GT},
|
||||
{"IN", comparison_operator_type::IN},
|
||||
{"NULL", comparison_operator_type::IS_NULL},
|
||||
{"NOT_NULL", comparison_operator_type::NOT_NULL},
|
||||
{"BETWEEN", comparison_operator_type::BETWEEN},
|
||||
{"BEGINS_WITH", comparison_operator_type::BEGINS_WITH},
|
||||
{"CONTAINS", comparison_operator_type::CONTAINS},
|
||||
{"NOT_CONTAINS", comparison_operator_type::NOT_CONTAINS},
|
||||
};
|
||||
}; //TODO(sarna): NE, IN, CONTAINS, NULL, NOT_NULL
|
||||
if (!comparison_operator.IsString()) {
|
||||
throw api_error::validation(format("Invalid comparison operator definition {}", rjson::print(comparison_operator)));
|
||||
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::validation(format("Unsupported comparison operator {}", op));
|
||||
throw api_error("ValidationException", format("Unsupported comparison operator {}", op));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct size_check {
|
||||
// True iff size passes this check.
|
||||
virtual bool operator()(rapidjson::SizeType size) const = 0;
|
||||
// Check description, such that format("expected array {}", check.what()) is human-readable.
|
||||
virtual sstring what() const = 0;
|
||||
};
|
||||
|
||||
class exact_size : public size_check {
|
||||
rapidjson::SizeType _expected;
|
||||
public:
|
||||
explicit exact_size(rapidjson::SizeType expected) : _expected(expected) {}
|
||||
bool operator()(rapidjson::SizeType size) const override { return size == _expected; }
|
||||
sstring what() const override { return format("of size {}", _expected); }
|
||||
};
|
||||
|
||||
struct empty : public size_check {
|
||||
bool operator()(rapidjson::SizeType size) const override { return size < 1; }
|
||||
sstring what() const override { return "to be empty"; }
|
||||
};
|
||||
|
||||
struct nonempty : public size_check {
|
||||
bool operator()(rapidjson::SizeType size) const override { return size > 0; }
|
||||
sstring what() const override { return "to be non-empty"; }
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Check that array has the expected number of elements
|
||||
static void verify_operand_count(const rjson::value* array, const size_check& expected, const rjson::value& op) {
|
||||
if (!array && expected(0)) {
|
||||
// If expected() allows an empty AttributeValueList, it is also fine
|
||||
// that it is missing.
|
||||
return;
|
||||
}
|
||||
if (!array || !array->IsArray()) {
|
||||
throw api_error::validation("With ComparisonOperator, AttributeValueList must be given and an array");
|
||||
}
|
||||
if (!expected(array->Size())) {
|
||||
throw api_error::validation(
|
||||
format("{} operator requires AttributeValueList {}, instead found list size {}",
|
||||
op, expected.what(), array->Size()));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
struct rjson_engaged_ptr_comp {
|
||||
bool operator()(const rjson::value* p1, const rjson::value* p2) const {
|
||||
return rjson::single_value_comp()(*p1, *p2);
|
||||
}
|
||||
};
|
||||
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));
|
||||
}
|
||||
|
||||
// It's not enough to compare underlying JSON objects when comparing sets,
|
||||
// as internally they're stored in an array, and the order of elements is
|
||||
// not important in set equality. See issue #5021
|
||||
static bool check_EQ_for_sets(const rjson::value& set1, const rjson::value& set2) {
|
||||
if (!set1.IsArray() || !set2.IsArray() || set1.Size() != set2.Size()) {
|
||||
return false;
|
||||
}
|
||||
std::set<const rjson::value*, rjson_engaged_ptr_comp> set1_raw;
|
||||
for (auto it = set1.Begin(); it != set1.End(); ++it) {
|
||||
set1_raw.insert(&*it);
|
||||
}
|
||||
for (const auto& a : set2.GetArray()) {
|
||||
if (!set1_raw.contains(&a)) {
|
||||
return false;
|
||||
::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");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Moreover, the JSON being compared can be a nested document with outer
|
||||
// layers of lists and maps and some inner set - and we need to get to that
|
||||
// inner set to compare it correctly with check_EQ_for_sets() (issue #8514).
|
||||
static bool check_EQ(const rjson::value* v1, const rjson::value& v2);
|
||||
static bool check_EQ_for_lists(const rjson::value& list1, const rjson::value& list2) {
|
||||
if (!list1.IsArray() || !list2.IsArray() || list1.Size() != list2.Size()) {
|
||||
return false;
|
||||
}
|
||||
auto it1 = list1.Begin();
|
||||
auto it2 = list2.Begin();
|
||||
while (it1 != list1.End()) {
|
||||
// Note: Alternator limits an item's depth (rjson::parse() limits
|
||||
// it to around 37 levels), so this recursion is safe.
|
||||
if (!check_EQ(&*it1, *it2)) {
|
||||
return false;
|
||||
if (attr_list.Size() != 1) {
|
||||
throw api_error("ValidationException", format("EQ restriction needs exactly 1 attribute value: {}", rjson::print(attr_list)));
|
||||
}
|
||||
++it1;
|
||||
++it2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool check_EQ_for_maps(const rjson::value& list1, const rjson::value& list2) {
|
||||
if (!list1.IsObject() || !list2.IsObject() || list1.MemberCount() != list2.MemberCount()) {
|
||||
return false;
|
||||
}
|
||||
for (auto it1 = list1.MemberBegin(); it1 != list1.MemberEnd(); ++it1) {
|
||||
auto it2 = list2.FindMember(it1->name);
|
||||
if (it2 == list2.MemberEnd() || !check_EQ(&it1->value, it2->value)) {
|
||||
return false;
|
||||
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 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) {
|
||||
if (v1 && v1->IsObject() && v1->MemberCount() == 1 && v2.IsObject() && v2.MemberCount() == 1) {
|
||||
auto it1 = v1->MemberBegin();
|
||||
auto it2 = v2.MemberBegin();
|
||||
if (it1->name != it2->name) {
|
||||
return false;
|
||||
}
|
||||
if (it1->name == "SS" || it1->name == "NS" || it1->name == "BS") {
|
||||
return check_EQ_for_sets(it1->value, it2->value);
|
||||
} else if(it1->name == "L") {
|
||||
return check_EQ_for_lists(it1->value, it2->value);
|
||||
} else if(it1->name == "M") {
|
||||
return check_EQ_for_maps(it1->value, it2->value);
|
||||
} else {
|
||||
// Other, non-nested types (number, string, etc.) can be compared
|
||||
// literally, comparing their JSON representation.
|
||||
return it1->value == it2->value;
|
||||
}
|
||||
} else {
|
||||
// If v1 and/or v2 are missing (IsNull()) the result should be false.
|
||||
// In the unlikely case that the object is malformed (issue #8070),
|
||||
// let's also return false.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the NE relation
|
||||
static bool check_NE(const rjson::value* v1, const rjson::value& v2) {
|
||||
return !check_EQ(v1, v2);
|
||||
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
|
||||
bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2,
|
||||
bool v1_from_query, bool v2_from_query) {
|
||||
bool bad = false;
|
||||
if (!v1 || !v1->IsObject() || v1->MemberCount() != 1) {
|
||||
if (v1_from_query) {
|
||||
throw api_error::validation("begins_with() encountered malformed argument");
|
||||
} else {
|
||||
bad = true;
|
||||
}
|
||||
} else if (v1->MemberBegin()->name != "S" && v1->MemberBegin()->name != "B") {
|
||||
if (v1_from_query) {
|
||||
throw api_error::validation(format("begins_with supports only string or binary type, got: {}", *v1));
|
||||
} else {
|
||||
bad = true;
|
||||
}
|
||||
}
|
||||
if (!v2.IsObject() || v2.MemberCount() != 1) {
|
||||
if (v2_from_query) {
|
||||
throw api_error::validation("begins_with() encountered malformed argument");
|
||||
} else {
|
||||
bad = true;
|
||||
}
|
||||
} else if (v2.MemberBegin()->name != "S" && v2.MemberBegin()->name != "B") {
|
||||
if (v2_from_query) {
|
||||
throw api_error::validation(format("begins_with() supports only string or binary type, got: {}", v2));
|
||||
} else {
|
||||
bad = true;
|
||||
}
|
||||
}
|
||||
if (bad) {
|
||||
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 it1 = v1.MemberBegin();
|
||||
auto it2 = v2.MemberBegin();
|
||||
if (it1->name != it2->name) {
|
||||
return false;
|
||||
}
|
||||
if (it2->name == "S") {
|
||||
return rjson::to_string_view(it1->value).starts_with(rjson::to_string_view(it2->value));
|
||||
} else /* it2->name == "B" */ {
|
||||
return base64_begins_with(rjson::to_string_view(it1->value), rjson::to_string_view(it2->value));
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_set_of(const rjson::value& type1, const rjson::value& type2) {
|
||||
return (type2 == "S" && type1 == "SS") || (type2 == "N" && type1 == "NS") || (type2 == "B" && type1 == "BS");
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the CONTAINS relation
|
||||
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2) {
|
||||
if (!v1) {
|
||||
return false;
|
||||
}
|
||||
const auto& kv1 = *v1->MemberBegin();
|
||||
const auto& kv2 = *v2.MemberBegin();
|
||||
if (kv1.name == "S" && kv2.name == "S") {
|
||||
return rjson::to_string_view(kv1.value).find(rjson::to_string_view(kv2.value)) != std::string_view::npos;
|
||||
} else if (kv1.name == "B" && kv2.name == "B") {
|
||||
return rjson::base64_decode(kv1.value).find(rjson::base64_decode(kv2.value)) != bytes::npos;
|
||||
} else if (is_set_of(kv1.name, kv2.name)) {
|
||||
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
||||
if (*i == kv2.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (kv1.name == "L") {
|
||||
for (auto i = kv1.value.Begin(); i != kv1.value.End(); ++i) {
|
||||
if (!i->IsObject() || i->MemberCount() != 1) {
|
||||
clogger.error("check_CONTAINS received a list whose element is malformed");
|
||||
return false;
|
||||
}
|
||||
const auto& el = *i->MemberBegin();
|
||||
if (el.name == kv2.name && el.value == kv2.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with the NOT_CONTAINS relation
|
||||
static bool check_NOT_CONTAINS(const rjson::value* v1, const rjson::value& v2) {
|
||||
if (!v1) {
|
||||
return false;
|
||||
}
|
||||
return !check_CONTAINS(v1, v2);
|
||||
}
|
||||
|
||||
// Check if a JSON-encoded value equals any element of an array, which must have at least one element.
|
||||
static bool check_IN(const rjson::value* val, const rjson::value& array) {
|
||||
if (!array[0].IsObject() || array[0].MemberCount() != 1) {
|
||||
throw api_error::validation(
|
||||
format("IN operator encountered malformed AttributeValue: {}", array[0]));
|
||||
}
|
||||
const auto& type = array[0].MemberBegin()->name;
|
||||
if (type != "S" && type != "N" && type != "B") {
|
||||
throw api_error::validation(
|
||||
"IN operator requires AttributeValueList elements to be of type String, Number, or Binary ");
|
||||
}
|
||||
if (!val) {
|
||||
return false;
|
||||
}
|
||||
bool have_match = false;
|
||||
for (const auto& elem : array.GetArray()) {
|
||||
if (!elem.IsObject() || elem.MemberCount() != 1 || elem.MemberBegin()->name != type) {
|
||||
throw api_error::validation(
|
||||
"IN operator requires all AttributeValueList elements to have the same type ");
|
||||
}
|
||||
if (!have_match && *val == elem) {
|
||||
// Can't return yet, must check types of all array elements. <sigh>
|
||||
have_match = true;
|
||||
}
|
||||
}
|
||||
return have_match;
|
||||
}
|
||||
|
||||
// Another variant of check_IN, this one for ConditionExpression. It needs to
|
||||
// check whether the first element in the given vector is equal to any of the
|
||||
// others.
|
||||
static bool check_IN(const std::vector<rjson::value>& array) {
|
||||
const rjson::value* first = &array[0];
|
||||
for (unsigned i = 1; i < array.size(); i++) {
|
||||
if (check_EQ(first, array[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool check_NULL(const rjson::value* val) {
|
||||
return val == nullptr;
|
||||
}
|
||||
|
||||
static bool check_NOT_NULL(const rjson::value* val) {
|
||||
return val != nullptr;
|
||||
}
|
||||
|
||||
// Only types S, N or B (string, number or bytes) may be compared by the
|
||||
// various comparion operators - lt, le, gt, ge, and between.
|
||||
// Note that in particular, if the value is missing (v->IsNull()), this
|
||||
// check returns false.
|
||||
static bool check_comparable_type(const rjson::value& v) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
return false;
|
||||
}
|
||||
const rjson::value& type = v.MemberBegin()->name;
|
||||
return type == "S" || type == "N" || type == "B";
|
||||
}
|
||||
|
||||
// Check if two JSON-encoded values match with cmp.
|
||||
template <typename Comparator>
|
||||
bool check_compare(const rjson::value* v1, const rjson::value& v2, const Comparator& cmp,
|
||||
bool v1_from_query, bool v2_from_query) {
|
||||
bool bad = false;
|
||||
if (!v1 || !check_comparable_type(*v1)) {
|
||||
if (v1_from_query) {
|
||||
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
||||
}
|
||||
bad = true;
|
||||
}
|
||||
if (!check_comparable_type(v2)) {
|
||||
if (v2_from_query) {
|
||||
throw api_error::validation(format("{} allow only the types String, Number, or Binary", cmp.diagnostic));
|
||||
}
|
||||
bad = true;
|
||||
}
|
||||
if (bad) {
|
||||
return false;
|
||||
}
|
||||
const auto& kv1 = *v1->MemberBegin();
|
||||
const auto& kv2 = *v2.MemberBegin();
|
||||
if (kv1.name != kv2.name) {
|
||||
return false;
|
||||
}
|
||||
if (kv1.name == "N") {
|
||||
return cmp(unwrap_number(*v1, cmp.diagnostic), unwrap_number(v2, cmp.diagnostic));
|
||||
}
|
||||
if (kv1.name == "S") {
|
||||
return cmp(std::string_view(kv1.value.GetString(), kv1.value.GetStringLength()),
|
||||
std::string_view(kv2.value.GetString(), kv2.value.GetStringLength()));
|
||||
}
|
||||
if (kv1.name == "B") {
|
||||
return cmp(rjson::base64_decode(kv1.value), rjson::base64_decode(kv2.value));
|
||||
}
|
||||
// cannot reach here, as check_comparable_type() verifies the type is one
|
||||
// of the above options.
|
||||
return false;
|
||||
}
|
||||
|
||||
struct cmp_lt {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; }
|
||||
// We cannot use the normal comparison operators like "<" on the bytes
|
||||
// type, because they treat individual bytes as signed but we need to
|
||||
// compare them as *unsigned*. So we need a specialization for bytes.
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) < 0; }
|
||||
static constexpr const char* diagnostic = "LT operator";
|
||||
};
|
||||
|
||||
struct cmp_le {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs <= rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) <= 0; }
|
||||
static constexpr const char* diagnostic = "LE operator";
|
||||
};
|
||||
|
||||
struct cmp_ge {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs >= rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) >= 0; }
|
||||
static constexpr const char* diagnostic = "GE operator";
|
||||
};
|
||||
|
||||
struct cmp_gt {
|
||||
template <typename T> bool operator()(const T& lhs, const T& rhs) const { return lhs > rhs; }
|
||||
bool operator()(const bytes& lhs, const bytes& rhs) const { return compare_unsigned(lhs, rhs) > 0; }
|
||||
static constexpr const char* diagnostic = "GT operator";
|
||||
};
|
||||
|
||||
// True if v is between lb and ub, inclusive. Throws or returns false
|
||||
// (depending on bounds_from_query parameter) if lb > ub.
|
||||
template <typename T>
|
||||
static bool check_BETWEEN(const T& v, const T& lb, const T& ub, bool bounds_from_query) {
|
||||
if (cmp_lt()(ub, lb)) {
|
||||
if (bounds_from_query) {
|
||||
throw api_error::validation(
|
||||
format("BETWEEN operator requires lower_bound <= upper_bound, but {} > {}", lb, ub));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return cmp_ge()(v, lb) && cmp_le()(v, ub);
|
||||
}
|
||||
|
||||
static bool check_BETWEEN(const rjson::value* v, const rjson::value& lb, const rjson::value& ub,
|
||||
bool v_from_query, bool lb_from_query, bool ub_from_query) {
|
||||
if ((v && v_from_query && !check_comparable_type(*v)) ||
|
||||
(lb_from_query && !check_comparable_type(lb)) ||
|
||||
(ub_from_query && !check_comparable_type(ub))) {
|
||||
throw api_error::validation("between allow only the types String, Number, or Binary");
|
||||
|
||||
}
|
||||
if (!v || !v->IsObject() || v->MemberCount() != 1 ||
|
||||
!lb.IsObject() || lb.MemberCount() != 1 ||
|
||||
!ub.IsObject() || ub.MemberCount() != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& kv_v = *v->MemberBegin();
|
||||
const auto& kv_lb = *lb.MemberBegin();
|
||||
const auto& kv_ub = *ub.MemberBegin();
|
||||
bool bounds_from_query = lb_from_query && ub_from_query;
|
||||
if (kv_lb.name != kv_ub.name) {
|
||||
if (bounds_from_query) {
|
||||
throw api_error::validation(
|
||||
format("BETWEEN operator requires the same type for lower and upper bound; instead got {} and {}",
|
||||
kv_lb.name, kv_ub.name));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (kv_v.name != kv_lb.name) { // Cannot compare different types, so v is NOT between lb and ub.
|
||||
return false;
|
||||
}
|
||||
if (kv_v.name == "N") {
|
||||
const char* diag = "BETWEEN operator";
|
||||
return check_BETWEEN(unwrap_number(*v, diag), unwrap_number(lb, diag), unwrap_number(ub, diag), bounds_from_query);
|
||||
}
|
||||
if (kv_v.name == "S") {
|
||||
return check_BETWEEN(std::string_view(kv_v.value.GetString(), kv_v.value.GetStringLength()),
|
||||
std::string_view(kv_lb.value.GetString(), kv_lb.value.GetStringLength()),
|
||||
std::string_view(kv_ub.value.GetString(), kv_ub.value.GetStringLength()),
|
||||
bounds_from_query);
|
||||
}
|
||||
if (kv_v.name == "B") {
|
||||
return check_BETWEEN(rjson::base64_decode(kv_v.value), rjson::base64_decode(kv_lb.value), rjson::base64_decode(kv_ub.value), bounds_from_query);
|
||||
}
|
||||
if (v_from_query) {
|
||||
throw api_error::validation(
|
||||
format("BETWEEN operator requires AttributeValueList elements to be of type String, Number, or Binary; instead got {}",
|
||||
kv_lb.name));
|
||||
} else {
|
||||
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")
|
||||
@@ -503,249 +137,109 @@ static bool verify_expected_one(const rjson::value& condition, const rjson::valu
|
||||
// and requires a different combinations of parameters in the request
|
||||
if (value) {
|
||||
if (exists && (!exists->IsBool() || exists->GetBool() != true)) {
|
||||
throw api_error::validation("Cannot combine Value with Exists!=true");
|
||||
throw api_error("ValidationException", "Cannot combine Value with Exists!=true");
|
||||
}
|
||||
if (comparison_operator) {
|
||||
throw api_error::validation("Cannot combine Value with ComparisonOperator");
|
||||
throw api_error("ValidationException", "Cannot combine Value with ComparisonOperator");
|
||||
}
|
||||
return check_EQ(got, *value);
|
||||
return got && check_EQ(*got, *value);
|
||||
} else if (exists) {
|
||||
if (comparison_operator) {
|
||||
throw api_error::validation("Cannot combine Exists with ComparisonOperator");
|
||||
throw api_error("ValidationException", "Cannot combine Exists with ComparisonOperator");
|
||||
}
|
||||
if (!exists->IsBool() || exists->GetBool() != false) {
|
||||
throw api_error::validation("Exists!=false requires Value");
|
||||
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::validation("Missing ComparisonOperator, Value or Exists");
|
||||
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:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_EQ(got, (*attribute_value_list)[0]);
|
||||
case comparison_operator_type::NE:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_NE(got, (*attribute_value_list)[0]);
|
||||
case comparison_operator_type::LT:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_compare(got, (*attribute_value_list)[0], cmp_lt{}, false, true);
|
||||
case comparison_operator_type::LE:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_compare(got, (*attribute_value_list)[0], cmp_le{}, false, true);
|
||||
case comparison_operator_type::GT:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_compare(got, (*attribute_value_list)[0], cmp_gt{}, false, true);
|
||||
case comparison_operator_type::GE:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_compare(got, (*attribute_value_list)[0], cmp_ge{}, false, true);
|
||||
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:
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
return check_BEGINS_WITH(got, (*attribute_value_list)[0], false, true);
|
||||
case comparison_operator_type::IN:
|
||||
verify_operand_count(attribute_value_list, nonempty(), *comparison_operator);
|
||||
return check_IN(got, *attribute_value_list);
|
||||
case comparison_operator_type::IS_NULL:
|
||||
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
||||
return check_NULL(got);
|
||||
case comparison_operator_type::NOT_NULL:
|
||||
verify_operand_count(attribute_value_list, empty(), *comparison_operator);
|
||||
return check_NOT_NULL(got);
|
||||
case comparison_operator_type::BETWEEN:
|
||||
verify_operand_count(attribute_value_list, exact_size(2), *comparison_operator);
|
||||
return check_BETWEEN(got, (*attribute_value_list)[0], (*attribute_value_list)[1],
|
||||
false, true, true);
|
||||
case comparison_operator_type::CONTAINS:
|
||||
{
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
// Expected's "CONTAINS" has this artificial limitation.
|
||||
// ConditionExpression's "contains()" does not...
|
||||
const rjson::value& arg = (*attribute_value_list)[0];
|
||||
const auto& argtype = (*arg.MemberBegin()).name;
|
||||
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
||||
throw api_error::validation(
|
||||
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
||||
"got {} instead", argtype));
|
||||
}
|
||||
return check_CONTAINS(got, arg);
|
||||
if (attribute_value_list->Size() != 1) {
|
||||
throw api_error("ValidationException", "BEGINS_WITH operator requires one element in AttributeValueList");
|
||||
}
|
||||
case comparison_operator_type::NOT_CONTAINS:
|
||||
{
|
||||
verify_operand_count(attribute_value_list, exact_size(1), *comparison_operator);
|
||||
// Expected's "NOT_CONTAINS" has this artificial limitation.
|
||||
// ConditionExpression's "contains()" does not...
|
||||
const rjson::value& arg = (*attribute_value_list)[0];
|
||||
const auto& argtype = (*arg.MemberBegin()).name;
|
||||
if (argtype != "S" && argtype != "N" && argtype != "B") {
|
||||
throw api_error::validation(
|
||||
format("CONTAINS operator requires a single AttributeValue of type String, Number, or Binary, "
|
||||
"got {} instead", argtype));
|
||||
}
|
||||
return check_NOT_CONTAINS(got, arg);
|
||||
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));
|
||||
}
|
||||
throw std::logic_error(format("Internal error: corrupted operator enum: {}", int(op)));
|
||||
}
|
||||
}
|
||||
|
||||
conditional_operator_type get_conditional_operator(const rjson::value& req) {
|
||||
const rjson::value* conditional_operator = rjson::find(req, "ConditionalOperator");
|
||||
if (!conditional_operator) {
|
||||
return conditional_operator_type::MISSING;
|
||||
}
|
||||
if (!conditional_operator->IsString()) {
|
||||
throw api_error::validation("'ConditionalOperator' parameter, if given, must be a string");
|
||||
}
|
||||
auto s = rjson::to_string_view(*conditional_operator);
|
||||
if (s == "AND") {
|
||||
return conditional_operator_type::AND;
|
||||
} else if (s == "OR") {
|
||||
return conditional_operator_type::OR;
|
||||
} else {
|
||||
throw api_error::validation(
|
||||
format("'ConditionalOperator' parameter must be AND, OR or missing. Found {}.", s));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the existing values of the item (previous_item) match the
|
||||
// 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 can throw an ValidationException API error if there
|
||||
// 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.
|
||||
bool verify_expected(const rjson::value& req, const rjson::value* previous_item) {
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item) {
|
||||
const rjson::value* expected = rjson::find(req, "Expected");
|
||||
auto conditional_operator = get_conditional_operator(req);
|
||||
if (conditional_operator != conditional_operator_type::MISSING &&
|
||||
(!expected || (expected->IsObject() && expected->GetObject().ObjectEmpty()))) {
|
||||
throw api_error::validation("'ConditionalOperator' parameter cannot be specified for missing or empty Expression");
|
||||
}
|
||||
if (!expected) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (!expected->IsObject()) {
|
||||
throw api_error::validation("'Expected' parameter, if given, must be an object");
|
||||
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");
|
||||
}
|
||||
}
|
||||
bool require_all = conditional_operator != conditional_operator_type::OR;
|
||||
return verify_condition(*expected, require_all, previous_item);
|
||||
}
|
||||
|
||||
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item) {
|
||||
for (auto it = condition.MemberBegin(); it != condition.MemberEnd(); ++it) {
|
||||
for (auto it = expected->MemberBegin(); it != expected->MemberEnd(); ++it) {
|
||||
const rjson::value* got = nullptr;
|
||||
if (previous_item) {
|
||||
got = rjson::find(*previous_item, rjson::to_string_view(it->name));
|
||||
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 true;
|
||||
return;
|
||||
} else if (!success && require_all) {
|
||||
// When require_all, one failure is enough!
|
||||
return false;
|
||||
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.
|
||||
return require_all;
|
||||
}
|
||||
|
||||
static bool calculate_primitive_condition(const parsed::primitive_condition& cond,
|
||||
const rjson::value* previous_item) {
|
||||
std::vector<rjson::value> calculated_values;
|
||||
calculated_values.reserve(cond._values.size());
|
||||
for (const parsed::value& v : cond._values) {
|
||||
calculated_values.push_back(calculate_value(v,
|
||||
cond._op == parsed::primitive_condition::type::VALUE ?
|
||||
calculate_value_caller::ConditionExpressionAlone :
|
||||
calculate_value_caller::ConditionExpression,
|
||||
previous_item));
|
||||
}
|
||||
switch (cond._op) {
|
||||
case parsed::primitive_condition::type::BETWEEN:
|
||||
if (calculated_values.size() != 3) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Wrong number of values {} in BETWEEN primitive_condition", cond._values.size()));
|
||||
}
|
||||
return check_BETWEEN(&calculated_values[0], calculated_values[1], calculated_values[2],
|
||||
cond._values[0].is_constant(), cond._values[1].is_constant(), cond._values[2].is_constant());
|
||||
case parsed::primitive_condition::type::IN:
|
||||
return check_IN(calculated_values);
|
||||
case parsed::primitive_condition::type::VALUE:
|
||||
if (calculated_values.size() != 1) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Unexpected values in primitive_condition", cond._values.size()));
|
||||
}
|
||||
// Unwrap the boolean wrapped as the value (if it is a boolean)
|
||||
if (calculated_values[0].IsObject() && calculated_values[0].MemberCount() == 1) {
|
||||
auto it = calculated_values[0].MemberBegin();
|
||||
if (it->name == "BOOL" && it->value.IsBool()) {
|
||||
return it->value.GetBool();
|
||||
}
|
||||
}
|
||||
throw api_error::validation(
|
||||
format("ConditionExpression: condition results in a non-boolean value: {}",
|
||||
calculated_values[0]));
|
||||
default:
|
||||
// All the rest of the operators have exactly two parameters (and unless
|
||||
// we have a bug in the parser, that's what we have in the parsed object:
|
||||
if (calculated_values.size() != 2) {
|
||||
throw std::logic_error(format("Wrong number of values {} in primitive_condition object", cond._values.size()));
|
||||
}
|
||||
}
|
||||
switch (cond._op) {
|
||||
case parsed::primitive_condition::type::EQ:
|
||||
return check_EQ(&calculated_values[0], calculated_values[1]);
|
||||
case parsed::primitive_condition::type::NE:
|
||||
return check_NE(&calculated_values[0], calculated_values[1]);
|
||||
case parsed::primitive_condition::type::GT:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_gt{},
|
||||
cond._values[0].is_constant(), cond._values[1].is_constant());
|
||||
case parsed::primitive_condition::type::GE:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_ge{},
|
||||
cond._values[0].is_constant(), cond._values[1].is_constant());
|
||||
case parsed::primitive_condition::type::LT:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_lt{},
|
||||
cond._values[0].is_constant(), cond._values[1].is_constant());
|
||||
case parsed::primitive_condition::type::LE:
|
||||
return check_compare(&calculated_values[0], calculated_values[1], cmp_le{},
|
||||
cond._values[0].is_constant(), cond._values[1].is_constant());
|
||||
default:
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error(format("Unknown type {} in primitive_condition object", (int)(cond._op)));
|
||||
if (!require_all) {
|
||||
throw api_error("ConditionalCheckFailedException", "None of ORed Expect conditions were successful.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the existing values of the item (previous_item) match the
|
||||
// conditions given by the given parsed ConditionExpression.
|
||||
bool verify_condition_expression(
|
||||
const parsed::condition_expression& condition_expression,
|
||||
const rjson::value* previous_item) {
|
||||
if (condition_expression.empty()) {
|
||||
return true;
|
||||
}
|
||||
bool ret = std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) -> bool {
|
||||
return calculate_primitive_condition(cond, previous_item);
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) -> bool {
|
||||
auto verify_condition = [&] (const parsed::condition_expression& e) {
|
||||
return verify_condition_expression(e, previous_item);
|
||||
};
|
||||
switch (list.op) {
|
||||
case '&':
|
||||
return boost::algorithm::all_of(list.conditions, verify_condition);
|
||||
case '|':
|
||||
return boost::algorithm::any_of(list.conditions, verify_condition);
|
||||
default:
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("bad operator in condition_list");
|
||||
}
|
||||
}
|
||||
}, condition_expression._expression);
|
||||
return condition_expression._negated ? !ret : ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -33,29 +33,17 @@
|
||||
|
||||
#include "cql3/restrictions/statement_restrictions.hh"
|
||||
#include "serialization.hh"
|
||||
#include "expressions_types.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
enum class comparison_operator_type {
|
||||
EQ, NE, LE, LT, GE, GT, IN, BETWEEN, CONTAINS, NOT_CONTAINS, IS_NULL, NOT_NULL, BEGINS_WITH
|
||||
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);
|
||||
|
||||
enum class conditional_operator_type {
|
||||
AND, OR, MISSING
|
||||
};
|
||||
conditional_operator_type get_conditional_operator(const rjson::value& req);
|
||||
::shared_ptr<cql3::restrictions::statement_restrictions> get_filtering_restrictions(schema_ptr schema, const column_definition& attrs_col, const rjson::value& query_filter);
|
||||
|
||||
bool verify_expected(const rjson::value& req, const rjson::value* previous_item);
|
||||
bool verify_condition(const rjson::value& condition, bool require_all, const rjson::value* previous_item);
|
||||
|
||||
bool check_CONTAINS(const rjson::value* v1, const rjson::value& v2);
|
||||
bool check_BEGINS_WITH(const rjson::value* v1, const rjson::value& v2, bool v1_from_query, bool v2_from_query);
|
||||
|
||||
bool verify_condition_expression(
|
||||
const parsed::condition_expression& condition_expression,
|
||||
const rjson::value* previous_item);
|
||||
void verify_expected(const rjson::value& req, const std::unique_ptr<rjson::value>& previous_item);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <seastar/net/dns.hh>
|
||||
#include "controller.hh"
|
||||
#include "server.hh"
|
||||
#include "executor.hh"
|
||||
#include "rmw_operation.hh"
|
||||
#include "db/config.hh"
|
||||
#include "cdc/generation_service.hh"
|
||||
#include "service/memory_limiter.hh"
|
||||
|
||||
using namespace seastar;
|
||||
|
||||
namespace alternator {
|
||||
|
||||
static logging::logger logger("alternator_controller");
|
||||
|
||||
controller::controller(
|
||||
sharded<gms::gossiper>& gossiper,
|
||||
sharded<service::storage_proxy>& proxy,
|
||||
sharded<service::migration_manager>& mm,
|
||||
sharded<db::system_distributed_keyspace>& sys_dist_ks,
|
||||
sharded<cdc::generation_service>& cdc_gen_svc,
|
||||
sharded<service::memory_limiter>& memory_limiter,
|
||||
const db::config& config)
|
||||
: _gossiper(gossiper)
|
||||
, _proxy(proxy)
|
||||
, _mm(mm)
|
||||
, _sys_dist_ks(sys_dist_ks)
|
||||
, _cdc_gen_svc(cdc_gen_svc)
|
||||
, _memory_limiter(memory_limiter)
|
||||
, _config(config)
|
||||
{
|
||||
}
|
||||
|
||||
future<> controller::start() {
|
||||
return seastar::async([this] {
|
||||
auto preferred = _config.listen_interface_prefer_ipv6() ? std::make_optional(net::inet_address::family::INET6) : std::nullopt;
|
||||
auto family = _config.enable_ipv6_dns_lookup() || preferred ? std::nullopt : std::make_optional(net::inet_address::family::INET);
|
||||
|
||||
// Create an smp_service_group to be used for limiting the
|
||||
// concurrency when forwarding Alternator request between
|
||||
// shards - if necessary for LWT.
|
||||
smp_service_group_config c;
|
||||
c.max_nonlocal_requests = 5000;
|
||||
_ssg = create_smp_service_group(c).get0();
|
||||
|
||||
rmw_operation::set_default_write_isolation(_config.alternator_write_isolation());
|
||||
executor::set_default_timeout(std::chrono::milliseconds(_config.alternator_timeout_in_ms()));
|
||||
|
||||
net::inet_address addr;
|
||||
try {
|
||||
addr = net::dns::get_host_by_name(_config.alternator_address(), family).get0().addr_list.front();
|
||||
} catch (...) {
|
||||
std::throw_with_nested(std::runtime_error(fmt::format("Unable to resolve alternator_address {}", _config.alternator_address())));
|
||||
}
|
||||
|
||||
auto get_cdc_metadata = [] (cdc::generation_service& svc) { return std::ref(svc.get_cdc_metadata()); };
|
||||
|
||||
_executor.start(std::ref(_gossiper), std::ref(_proxy), std::ref(_mm), std::ref(_sys_dist_ks), sharded_parameter(get_cdc_metadata, std::ref(_cdc_gen_svc)), _ssg.value()).get();
|
||||
_server.start(std::ref(_executor), std::ref(_proxy), std::ref(_gossiper)).get();
|
||||
std::optional<uint16_t> alternator_port;
|
||||
if (_config.alternator_port()) {
|
||||
alternator_port = _config.alternator_port();
|
||||
}
|
||||
std::optional<uint16_t> alternator_https_port;
|
||||
std::optional<tls::credentials_builder> creds;
|
||||
if (_config.alternator_https_port()) {
|
||||
alternator_https_port = _config.alternator_https_port();
|
||||
creds.emplace();
|
||||
auto opts = _config.alternator_encryption_options();
|
||||
if (opts.empty()) {
|
||||
// Earlier versions mistakenly configured Alternator's
|
||||
// HTTPS parameters via the "server_encryption_option"
|
||||
// configuration parameter. We *temporarily* continue
|
||||
// to allow this, for backward compatibility.
|
||||
opts = _config.server_encryption_options();
|
||||
if (!opts.empty()) {
|
||||
logger.warn("Setting server_encryption_options to configure "
|
||||
"Alternator's HTTPS encryption is deprecated. Please "
|
||||
"switch to setting alternator_encryption_options instead.");
|
||||
}
|
||||
}
|
||||
opts.erase("require_client_auth");
|
||||
opts.erase("truststore");
|
||||
utils::configure_tls_creds_builder(creds.value(), std::move(opts)).get();
|
||||
}
|
||||
bool alternator_enforce_authorization = _config.alternator_enforce_authorization();
|
||||
_server.invoke_on_all(
|
||||
[this, addr, alternator_port, alternator_https_port, creds = std::move(creds), alternator_enforce_authorization] (server& server) mutable {
|
||||
return server.init(addr, alternator_port, alternator_https_port, creds, alternator_enforce_authorization,
|
||||
&_memory_limiter.local().get_semaphore(),
|
||||
_config.max_concurrent_requests_per_shard);
|
||||
}).then([addr, alternator_port, alternator_https_port] {
|
||||
logger.info("Alternator server listening on {}, HTTP port {}, HTTPS port {}",
|
||||
addr, alternator_port ? std::to_string(*alternator_port) : "OFF", alternator_https_port ? std::to_string(*alternator_https_port) : "OFF");
|
||||
}).get();
|
||||
});
|
||||
}
|
||||
|
||||
future<> controller::stop() {
|
||||
return seastar::async([this] {
|
||||
_server.stop().get();
|
||||
_executor.stop().get();
|
||||
destroy_smp_service_group(_ssg.value()).get();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021-present 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/core/sharded.hh>
|
||||
#include <seastar/core/smp.hh>
|
||||
|
||||
namespace service {
|
||||
class storage_proxy;
|
||||
class migration_manager;
|
||||
class memory_limiter;
|
||||
}
|
||||
|
||||
namespace db {
|
||||
class system_distributed_keyspace;
|
||||
class config;
|
||||
}
|
||||
|
||||
namespace cdc {
|
||||
class generation_service;
|
||||
}
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
|
||||
namespace alternator {
|
||||
|
||||
using namespace seastar;
|
||||
|
||||
class executor;
|
||||
class server;
|
||||
|
||||
class controller {
|
||||
sharded<gms::gossiper>& _gossiper;
|
||||
sharded<service::storage_proxy>& _proxy;
|
||||
sharded<service::migration_manager>& _mm;
|
||||
sharded<db::system_distributed_keyspace>& _sys_dist_ks;
|
||||
sharded<cdc::generation_service>& _cdc_gen_svc;
|
||||
sharded<service::memory_limiter>& _memory_limiter;
|
||||
const db::config& _config;
|
||||
|
||||
sharded<executor> _executor;
|
||||
sharded<server> _server;
|
||||
std::optional<smp_service_group> _ssg;
|
||||
|
||||
public:
|
||||
controller(
|
||||
sharded<gms::gossiper>& gossiper,
|
||||
sharded<service::storage_proxy>& proxy,
|
||||
sharded<service::migration_manager>& mm,
|
||||
sharded<db::system_distributed_keyspace>& sys_dist_ks,
|
||||
sharded<cdc::generation_service>& cdc_gen_svc,
|
||||
sharded<service::memory_limiter>& memory_limiter,
|
||||
const db::config& config);
|
||||
|
||||
future<> start();
|
||||
future<> stop();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -26,15 +26,12 @@
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// api_error contains a DynamoDB error message to be returned to the user.
|
||||
// It can be returned by value (see executor::request_return_type) or thrown.
|
||||
// The DynamoDB's error messages are described in detail in
|
||||
// DynamoDB's error messages are described in detail in
|
||||
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html
|
||||
// An error message has an HTTP code (almost always 400), a type, e.g.,
|
||||
// "ResourceNotFoundException", and a human readable message.
|
||||
// Eventually alternator::api_handler will convert a returned or thrown
|
||||
// api_error into a JSON object, and that is returned to the user.
|
||||
class api_error final : public std::exception {
|
||||
// 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;
|
||||
@@ -45,55 +42,8 @@ public:
|
||||
, _type(std::move(type))
|
||||
, _msg(std::move(msg))
|
||||
{ }
|
||||
|
||||
// Factory functions for some common types of DynamoDB API errors
|
||||
static api_error validation(std::string msg) {
|
||||
return api_error("ValidationException", std::move(msg));
|
||||
}
|
||||
static api_error resource_not_found(std::string msg) {
|
||||
return api_error("ResourceNotFoundException", std::move(msg));
|
||||
}
|
||||
static api_error resource_in_use(std::string msg) {
|
||||
return api_error("ResourceInUseException", std::move(msg));
|
||||
}
|
||||
static api_error invalid_signature(std::string msg) {
|
||||
return api_error("InvalidSignatureException", std::move(msg));
|
||||
}
|
||||
static api_error missing_authentication_token(std::string msg) {
|
||||
return api_error("MissingAuthenticationTokenException", std::move(msg));
|
||||
}
|
||||
static api_error unrecognized_client(std::string msg) {
|
||||
return api_error("UnrecognizedClientException", std::move(msg));
|
||||
}
|
||||
static api_error unknown_operation(std::string msg) {
|
||||
return api_error("UnknownOperationException", std::move(msg));
|
||||
}
|
||||
static api_error access_denied(std::string msg) {
|
||||
return api_error("AccessDeniedException", std::move(msg));
|
||||
}
|
||||
static api_error conditional_check_failed(std::string msg) {
|
||||
return api_error("ConditionalCheckFailedException", std::move(msg));
|
||||
}
|
||||
static api_error expired_iterator(std::string msg) {
|
||||
return api_error("ExpiredIteratorException", std::move(msg));
|
||||
}
|
||||
static api_error trimmed_data_access_exception(std::string msg) {
|
||||
return api_error("TrimmedDataAccessException", std::move(msg));
|
||||
}
|
||||
static api_error request_limit_exceeded(std::string msg) {
|
||||
return api_error("RequestLimitExceeded", std::move(msg));
|
||||
}
|
||||
static api_error internal(std::string msg) {
|
||||
return api_error("InternalServerError", std::move(msg), reply::status_type::internal_server_error);
|
||||
}
|
||||
|
||||
// Provide the "std::exception" interface, to make it easier to print this
|
||||
// exception in log messages. Note that this function is *not* used to
|
||||
// format the error to send it back to the client - server.cc has
|
||||
// generate_error_reply() to format an api_error as the DynamoDB protocol
|
||||
// requires.
|
||||
virtual const char* what() const noexcept override;
|
||||
mutable std::string _what_string;
|
||||
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,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -25,212 +25,47 @@
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastar/core/sharded.hh>
|
||||
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "service/migration_manager.hh"
|
||||
#include "service/client_state.hh"
|
||||
#include "service_permit.hh"
|
||||
#include "db/timeout_clock.hh"
|
||||
|
||||
#include "alternator/error.hh"
|
||||
#include "stats.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
namespace db {
|
||||
class system_distributed_keyspace;
|
||||
}
|
||||
|
||||
namespace query {
|
||||
class partition_slice;
|
||||
class result;
|
||||
}
|
||||
|
||||
namespace cql3::selection {
|
||||
class selection;
|
||||
}
|
||||
|
||||
namespace service {
|
||||
class storage_proxy;
|
||||
}
|
||||
|
||||
namespace cdc {
|
||||
class metadata;
|
||||
}
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class rmw_operation;
|
||||
|
||||
struct make_jsonable : public json::jsonable {
|
||||
rjson::value _value;
|
||||
public:
|
||||
explicit make_jsonable(rjson::value&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
struct json_string : public json::jsonable {
|
||||
std::string _value;
|
||||
public:
|
||||
explicit json_string(std::string&& value);
|
||||
std::string to_json() const override;
|
||||
};
|
||||
|
||||
namespace parsed {
|
||||
class path;
|
||||
};
|
||||
|
||||
const std::map<sstring, sstring>& get_tags_of_table(schema_ptr schema);
|
||||
future<> update_tags(service::migration_manager& mm, schema_ptr schema, std::map<sstring, sstring>&& tags_map);
|
||||
schema_ptr get_table(service::storage_proxy& proxy, const rjson::value& request);
|
||||
|
||||
// An attribute_path_map object is used to hold data for various attributes
|
||||
// paths (parsed::path) in a hierarchy of attribute paths. Each attribute path
|
||||
// has a root attribute, and then modified by member and index operators -
|
||||
// for example in "a.b[2].c" we have "a" as the root, then ".b" member, then
|
||||
// "[2]" index, and finally ".c" member.
|
||||
// Data can be added to an attribute_path_map using the add() function, but
|
||||
// requires that attributes with data not be *overlapping* or *conflicting*:
|
||||
//
|
||||
// 1. Two attribute paths which are identical or an ancestor of one another
|
||||
// are considered *overlapping* and not allowed. If a.b.c has data,
|
||||
// we can't add more data in a.b.c or any of its descendants like a.b.c.d.
|
||||
//
|
||||
// 2. Two attribute paths which need the same parent to have both a member and
|
||||
// an index are considered *conflicting* and not allowed. E.g., if a.b has
|
||||
// data, you can't add a[1]. The meaning of adding both would be that the
|
||||
// attribute a is both a map and an array, which isn't sensible.
|
||||
//
|
||||
// These two requirements are common to the two places where Alternator uses
|
||||
// this abstraction to describe how a hierarchical item is to be transformed:
|
||||
//
|
||||
// 1. In ProjectExpression: for filtering from a full top-level attribute
|
||||
// only the parts for which user asked in ProjectionExpression.
|
||||
//
|
||||
// 2. In UpdateExpression: for taking the previous value of a top-level
|
||||
// attribute, and modifying it based on the instructions in the user
|
||||
// wrote in UpdateExpression.
|
||||
|
||||
template<typename T>
|
||||
class attribute_path_map_node {
|
||||
public:
|
||||
using data_t = T;
|
||||
// We need the extra unique_ptr<> here because libstdc++ unordered_map
|
||||
// doesn't work with incomplete types :-(
|
||||
using members_t = std::unordered_map<std::string, std::unique_ptr<attribute_path_map_node<T>>>;
|
||||
// The indexes list is sorted because DynamoDB requires handling writes
|
||||
// beyond the end of a list in index order.
|
||||
using indexes_t = std::map<unsigned, std::unique_ptr<attribute_path_map_node<T>>>;
|
||||
// The prohibition on "overlap" and "conflict" explained above means
|
||||
// That only one of data, members or indexes is non-empty.
|
||||
std::optional<std::variant<data_t, members_t, indexes_t>> _content;
|
||||
|
||||
bool is_empty() const { return !_content; }
|
||||
bool has_value() const { return _content && std::holds_alternative<data_t>(*_content); }
|
||||
bool has_members() const { return _content && std::holds_alternative<members_t>(*_content); }
|
||||
bool has_indexes() const { return _content && std::holds_alternative<indexes_t>(*_content); }
|
||||
// get_members() assumes that has_members() is true
|
||||
members_t& get_members() { return std::get<members_t>(*_content); }
|
||||
const members_t& get_members() const { return std::get<members_t>(*_content); }
|
||||
indexes_t& get_indexes() { return std::get<indexes_t>(*_content); }
|
||||
const indexes_t& get_indexes() const { return std::get<indexes_t>(*_content); }
|
||||
T& get_value() { return std::get<T>(*_content); }
|
||||
const T& get_value() const { return std::get<T>(*_content); }
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using attribute_path_map = std::unordered_map<std::string, attribute_path_map_node<T>>;
|
||||
|
||||
using attrs_to_get_node = attribute_path_map_node<std::monostate>;
|
||||
using attrs_to_get = attribute_path_map<std::monostate>;
|
||||
|
||||
|
||||
class executor : public peering_sharded_service<executor> {
|
||||
gms::gossiper& _gossiper;
|
||||
class executor {
|
||||
service::storage_proxy& _proxy;
|
||||
service::migration_manager& _mm;
|
||||
db::system_distributed_keyspace& _sdks;
|
||||
cdc::metadata& _cdc_metadata;
|
||||
// An smp_service_group to be used for limiting the concurrency when
|
||||
// forwarding Alternator request between shards - if necessary for LWT.
|
||||
smp_service_group _ssg;
|
||||
|
||||
public:
|
||||
using client_state = service::client_state;
|
||||
using request_return_type = std::variant<json::json_return_type, api_error>;
|
||||
stats _stats;
|
||||
static constexpr auto ATTRS_COLUMN_NAME = ":attrs";
|
||||
static constexpr auto KEYSPACE_NAME_PREFIX = "alternator_";
|
||||
static constexpr std::string_view INTERNAL_TABLE_PREFIX = ".scylla.alternator.";
|
||||
static constexpr auto KEYSPACE_NAME = "alternator";
|
||||
|
||||
executor(gms::gossiper& gossiper, service::storage_proxy& proxy, service::migration_manager& mm, db::system_distributed_keyspace& sdks, cdc::metadata& cdc_metadata, smp_service_group ssg)
|
||||
: _gossiper(gossiper), _proxy(proxy), _mm(mm), _sdks(sdks), _cdc_metadata(cdc_metadata), _ssg(ssg) {}
|
||||
executor(service::storage_proxy& proxy, service::migration_manager& mm) : _proxy(proxy), _mm(mm) {}
|
||||
|
||||
future<request_return_type> create_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> delete_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> update_table(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> put_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> delete_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> update_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_tables(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> scan(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_endpoints(client_state& client_state, service_permit permit, rjson::value request, std::string host_header);
|
||||
future<request_return_type> batch_write_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> batch_get_item(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> query(client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> tag_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> untag_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_tags_of_resource(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> update_time_to_live(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_time_to_live(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> list_streams(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> describe_stream(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_shard_iterator(client_state& client_state, service_permit permit, rjson::value request);
|
||||
future<request_return_type> get_records(client_state& client_state, tracing::trace_state_ptr, service_permit permit, rjson::value request);
|
||||
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<> create_keyspace(std::string_view keyspace_name);
|
||||
future<> maybe_create_keyspace();
|
||||
|
||||
static sstring table_name(const schema&);
|
||||
static db::timeout_clock::time_point default_timeout();
|
||||
static void set_default_timeout(db::timeout_clock::duration timeout);
|
||||
private:
|
||||
static db::timeout_clock::duration s_default_timeout;
|
||||
public:
|
||||
static schema_ptr find_table(service::storage_proxy&, const rjson::value& request);
|
||||
|
||||
private:
|
||||
friend class rmw_operation;
|
||||
|
||||
static bool is_alternator_keyspace(const sstring& ks_name);
|
||||
static sstring make_keyspace_name(const sstring& table_name);
|
||||
static void describe_key_schema(rjson::value& parent, const schema&, std::unordered_map<std::string,std::string> * = nullptr);
|
||||
static void describe_key_schema(rjson::value& parent, const schema& schema, std::unordered_map<std::string,std::string>&);
|
||||
|
||||
public:
|
||||
static std::optional<rjson::value> describe_single_item(schema_ptr,
|
||||
const query::partition_slice&,
|
||||
const cql3::selection::selection&,
|
||||
const query::result&,
|
||||
const attrs_to_get&);
|
||||
|
||||
static void describe_single_item(const cql3::selection::selection&,
|
||||
const std::vector<bytes_opt>&,
|
||||
const attrs_to_get&,
|
||||
rjson::value&,
|
||||
bool = false);
|
||||
|
||||
void add_stream_options(const rjson::value& stream_spec, schema_builder&) const;
|
||||
void supplement_table_info(rjson::value& descr, const schema& schema) const;
|
||||
void supplement_table_stream_info(rjson::value& descr, const schema& schema) const;
|
||||
static void maybe_trace_query(client_state& client_state, sstring_view op, sstring_view query);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -20,24 +20,15 @@
|
||||
*/
|
||||
|
||||
#include "expressions.hh"
|
||||
#include "serialization.hh"
|
||||
#include "utils/base64.hh"
|
||||
#include "conditions.hh"
|
||||
#include "alternator/expressionsLexer.hpp"
|
||||
#include "alternator/expressionsParser.hpp"
|
||||
#include "utils/overloaded_functor.hh"
|
||||
#include "error.hh"
|
||||
|
||||
#include "seastarx.hh"
|
||||
#include <seastarx.hh>
|
||||
|
||||
#include <seastar/core/print.hh>
|
||||
#include <seastar/util/log.hh>
|
||||
|
||||
#include <boost/algorithm/cxx11/any_of.hpp>
|
||||
#include <boost/algorithm/cxx11/all_of.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -74,19 +65,13 @@ parse_projection_expression(std::string query) {
|
||||
}
|
||||
}
|
||||
|
||||
parsed::condition_expression
|
||||
parse_condition_expression(std::string query) {
|
||||
try {
|
||||
return do_with_parser(query, std::mem_fn(&expressionsParser::condition_expression));
|
||||
} catch (...) {
|
||||
throw expressions_syntax_error(format("Failed parsing ConditionExpression '{}': {}", 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_functor {
|
||||
std::visit(overloaded {
|
||||
[&] (action::set&) { seen_set = true; },
|
||||
[&] (action::remove&) { seen_remove = true; },
|
||||
[&] (action::add&) { seen_add = true; },
|
||||
@@ -109,645 +94,5 @@ void update_expression::append(update_expression other) {
|
||||
seen_del |= other.seen_del;
|
||||
}
|
||||
|
||||
void condition_expression::append(condition_expression&& a, char op) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (condition_list& x) {
|
||||
// If 'a' has a single condition, we could, instead of inserting
|
||||
// it insert its single condition (possibly negated if a._negated)
|
||||
// But considering it we don't evaluate these expressions many
|
||||
// times, this optimization is not worth extra code complexity.
|
||||
if (!x.conditions.empty() && x.op != op) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("condition_expression::append called with mixed operators");
|
||||
}
|
||||
x.conditions.push_back(std::move(a));
|
||||
x.op = op;
|
||||
},
|
||||
[&] (primitive_condition& x) {
|
||||
// Shouldn't happen unless we have a bug in the parser
|
||||
throw std::logic_error("condition_expression::append called on primitive_condition");
|
||||
}
|
||||
}, _expression);
|
||||
}
|
||||
|
||||
void path::check_depth_limit() {
|
||||
if (1 + _operators.size() > depth_limit) {
|
||||
throw expressions_syntax_error(format("Document path exceeded {} nesting levels", depth_limit));
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const path& p) {
|
||||
os << p.root();
|
||||
for (const auto& op : p.operators()) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const std::string& member) {
|
||||
os << '.' << member;
|
||||
},
|
||||
[&] (unsigned index) {
|
||||
os << '[' << index << ']';
|
||||
}
|
||||
}, op);
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
} // namespace parsed
|
||||
|
||||
// The following resolve_*() functions resolve references in parsed
|
||||
// expressions of different types. Resolving a parsed expression means
|
||||
// replacing:
|
||||
// 1. In parsed::path objects, replace references like "#name" with the
|
||||
// attribute name from ExpressionAttributeNames,
|
||||
// 2. In parsed::constant objects, replace references like ":value" with
|
||||
// the value from ExpressionAttributeValues.
|
||||
// These function also track which name and value references were used, to
|
||||
// allow complaining if some remain unused.
|
||||
// Note that the resolve_*() functions modify the expressions in-place,
|
||||
// so if we ever intend to cache parsed expression, we need to pass a copy
|
||||
// into this function.
|
||||
//
|
||||
// Doing the "resolving" stage before the evaluation stage has two benefits.
|
||||
// First, it allows us to be compatible with DynamoDB in catching unused
|
||||
// names and values (see issue #6572). Second, in the FilterExpression case,
|
||||
// we need to resolve the expression just once but then use it many times
|
||||
// (once for each item to be filtered).
|
||||
|
||||
static std::optional<std::string> resolve_path_component(const std::string& column_name,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names) {
|
||||
if (column_name.size() > 0 && column_name.front() == '#') {
|
||||
if (!expression_attribute_names) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeNames missing, entry '{}' required by expression", column_name));
|
||||
}
|
||||
const rjson::value* value = rjson::find(*expression_attribute_names, column_name);
|
||||
if (!value || !value->IsString()) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeNames missing entry '{}' required by expression", column_name));
|
||||
}
|
||||
used_attribute_names.emplace(column_name);
|
||||
return std::string(rjson::to_string_view(*value));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void resolve_path(parsed::path& p,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names) {
|
||||
std::optional<std::string> r = resolve_path_component(p.root(), expression_attribute_names, used_attribute_names);
|
||||
if (r) {
|
||||
p.set_root(std::move(*r));
|
||||
}
|
||||
for (auto& op : p.operators()) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (std::string& s) {
|
||||
r = resolve_path_component(s, expression_attribute_names, used_attribute_names);
|
||||
if (r) {
|
||||
s = std::move(*r);
|
||||
}
|
||||
},
|
||||
[&] (unsigned index) {
|
||||
// nothing to resolve
|
||||
}
|
||||
}, op);
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_constant(parsed::constant& c,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const std::string& valref) {
|
||||
if (!expression_attribute_values) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues missing, entry '{}' required by expression", valref));
|
||||
}
|
||||
const rjson::value* value = rjson::find(*expression_attribute_values, valref);
|
||||
if (!value) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues missing entry '{}' required by expression", valref));
|
||||
}
|
||||
if (value->IsNull()) {
|
||||
throw api_error::validation(
|
||||
format("ExpressionAttributeValues null value for entry '{}' required by expression", valref));
|
||||
}
|
||||
validate_value(*value, "ExpressionAttributeValues");
|
||||
used_attribute_values.emplace(valref);
|
||||
c.set(*value);
|
||||
},
|
||||
[&] (const parsed::constant::literal& lit) {
|
||||
// Nothing to do, already resolved
|
||||
}
|
||||
}, c._value);
|
||||
|
||||
}
|
||||
|
||||
void resolve_value(parsed::value& rhs,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::constant& c) {
|
||||
resolve_constant(c, expression_attribute_values, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::value::function_call& f) {
|
||||
for (parsed::value& value : f._parameters) {
|
||||
resolve_value(value, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
},
|
||||
[&] (parsed::path& p) {
|
||||
resolve_path(p, expression_attribute_names, used_attribute_names);
|
||||
}
|
||||
}, rhs._value);
|
||||
}
|
||||
|
||||
void resolve_set_rhs(parsed::set_rhs& rhs,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
resolve_value(rhs._v1, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
if (rhs._op != 'v') {
|
||||
resolve_value(rhs._v2, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
}
|
||||
|
||||
void resolve_update_expression(parsed::update_expression& ue,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
for (parsed::update_expression::action& action : ue.actions()) {
|
||||
resolve_path(action._path, expression_attribute_names, used_attribute_names);
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::update_expression::action::set& a) {
|
||||
resolve_set_rhs(a._rhs, expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::update_expression::action::remove& a) {
|
||||
// nothing to do
|
||||
},
|
||||
[&] (parsed::update_expression::action::add& a) {
|
||||
resolve_constant(a._valref, expression_attribute_values, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::update_expression::action::del& a) {
|
||||
resolve_constant(a._valref, expression_attribute_values, used_attribute_values);
|
||||
}
|
||||
}, action._action);
|
||||
}
|
||||
}
|
||||
|
||||
static void resolve_primitive_condition(parsed::primitive_condition& pc,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
for (parsed::value& value : pc._values) {
|
||||
resolve_value(value,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
}
|
||||
}
|
||||
|
||||
void resolve_condition_expression(parsed::condition_expression& ce,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (parsed::primitive_condition& cond) {
|
||||
resolve_primitive_condition(cond,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
},
|
||||
[&] (parsed::condition_expression::condition_list& list) {
|
||||
for (parsed::condition_expression& cond : list.conditions) {
|
||||
resolve_condition_expression(cond,
|
||||
expression_attribute_names, expression_attribute_values,
|
||||
used_attribute_names, used_attribute_values);
|
||||
|
||||
}
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
void resolve_projection_expression(std::vector<parsed::path>& pe,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names) {
|
||||
for (parsed::path& p : pe) {
|
||||
resolve_path(p, expression_attribute_names, used_attribute_names);
|
||||
}
|
||||
}
|
||||
|
||||
// condition_expression_on() checks whether a condition_expression places any
|
||||
// condition on the given attribute. It can be useful, for example, for
|
||||
// checking whether the condition tries to restrict a key column.
|
||||
|
||||
static bool value_on(const parsed::value& v, std::string_view attribute) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) {
|
||||
return false;
|
||||
},
|
||||
[&] (const parsed::value::function_call& f) {
|
||||
for (const parsed::value& value : f._parameters) {
|
||||
if (value_on(value, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[&] (const parsed::path& p) {
|
||||
return p.root() == attribute;
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
static bool primitive_condition_on(const parsed::primitive_condition& pc, std::string_view attribute) {
|
||||
for (const parsed::value& value : pc._values) {
|
||||
if (value_on(value, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool condition_expression_on(const parsed::condition_expression& ce, std::string_view attribute) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) {
|
||||
return primitive_condition_on(cond, attribute);
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) {
|
||||
for (const parsed::condition_expression& cond : list.conditions) {
|
||||
if (condition_expression_on(cond, attribute)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
// for_condition_expression_on() runs a given function over all the attributes
|
||||
// mentioned in the expression. If the same attribute is mentioned more than
|
||||
// once, the function will be called more than once for the same attribute.
|
||||
|
||||
static void for_value_on(const parsed::value& v, const noncopyable_function<void(std::string_view)>& func) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) { },
|
||||
[&] (const parsed::value::function_call& f) {
|
||||
for (const parsed::value& value : f._parameters) {
|
||||
for_value_on(value, func);
|
||||
}
|
||||
},
|
||||
[&] (const parsed::path& p) {
|
||||
func(p.root());
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
void for_condition_expression_on(const parsed::condition_expression& ce, const noncopyable_function<void(std::string_view)>& func) {
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const parsed::primitive_condition& cond) {
|
||||
for (const parsed::value& value : cond._values) {
|
||||
for_value_on(value, func);
|
||||
}
|
||||
},
|
||||
[&] (const parsed::condition_expression::condition_list& list) {
|
||||
for (const parsed::condition_expression& cond : list.conditions) {
|
||||
for_condition_expression_on(cond, func);
|
||||
}
|
||||
}
|
||||
}, ce._expression);
|
||||
}
|
||||
|
||||
// The following calculate_value() functions calculate, or evaluate, a parsed
|
||||
// expression. The parsed expression is assumed to have been "resolved", with
|
||||
// the matching resolve_* function.
|
||||
|
||||
// calculate_size() is ConditionExpression's size() function, i.e., it takes
|
||||
// a JSON-encoded value and returns its "size" as defined differently for the
|
||||
// different types - also as a JSON-encoded number.
|
||||
// It return a JSON-encoded "null" value if this value's type has no size
|
||||
// defined. Comparisons against this non-numeric value will later fail.
|
||||
static rjson::value calculate_size(const rjson::value& v) {
|
||||
// NOTE: If v is improperly formatted for our JSON value encoding, it
|
||||
// must come from the request itself, not from the database, so it makes
|
||||
// sense to throw a ValidationException if we see such a problem.
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
throw api_error::validation(format("invalid object: {}", v));
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
int ret;
|
||||
if (it->name == "S") {
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("invalid string: {}", v));
|
||||
}
|
||||
ret = it->value.GetStringLength();
|
||||
} else if (it->name == "NS" || it->name == "SS" || it->name == "BS" || it->name == "L") {
|
||||
if (!it->value.IsArray()) {
|
||||
throw api_error::validation(format("invalid set: {}", v));
|
||||
}
|
||||
ret = it->value.Size();
|
||||
} else if (it->name == "M") {
|
||||
if (!it->value.IsObject()) {
|
||||
throw api_error::validation(format("invalid map: {}", v));
|
||||
}
|
||||
ret = it->value.MemberCount();
|
||||
} else if (it->name == "B") {
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("invalid byte string: {}", v));
|
||||
}
|
||||
ret = base64_decoded_len(rjson::to_string_view(it->value));
|
||||
} else {
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::add(json_ret, "null", rjson::value(true));
|
||||
return json_ret;
|
||||
}
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::add(json_ret, "N", rjson::from_string(std::to_string(ret)));
|
||||
return json_ret;
|
||||
}
|
||||
|
||||
static const rjson::value& calculate_value(const parsed::constant& c) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant::literal& v) -> const rjson::value& {
|
||||
return *v;
|
||||
},
|
||||
[&] (const std::string& valref) -> const rjson::value& {
|
||||
// Shouldn't happen, we should have called resolve_value() earlier
|
||||
// and replaced the value reference by the literal constant.
|
||||
throw std::logic_error("calculate_value() called before resolve_value()");
|
||||
}
|
||||
}, c._value);
|
||||
}
|
||||
|
||||
static rjson::value to_bool_json(bool b) {
|
||||
rjson::value json_ret = rjson::empty_object();
|
||||
rjson::add(json_ret, "BOOL", rjson::value(b));
|
||||
return json_ret;
|
||||
}
|
||||
|
||||
static bool known_type(std::string_view type) {
|
||||
static thread_local const std::unordered_set<std::string_view> types = {
|
||||
"N", "S", "B", "NS", "SS", "BS", "L", "M", "NULL", "BOOL"
|
||||
};
|
||||
return types.contains(type);
|
||||
}
|
||||
|
||||
using function_handler_type = rjson::value(calculate_value_caller, const rjson::value*, const parsed::value::function_call&);
|
||||
static const
|
||||
std::unordered_map<std::string_view, function_handler_type*> function_handlers {
|
||||
{"list_append", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::UpdateExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: list_append() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: list_append() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
rjson::value ret = list_concatenate(v1, v2);
|
||||
if (ret.IsNull()) {
|
||||
throw api_error::validation("UpdateExpression: list_append() given a non-list");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
{"if_not_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::UpdateExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: if_not_exists() must include path as its first argument", caller));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return v1.IsNull() ? std::move(v2) : std::move(v1);
|
||||
}
|
||||
},
|
||||
{"size", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpression) {
|
||||
throw api_error::validation(
|
||||
format("{}: size() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: size() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return calculate_size(v);
|
||||
}
|
||||
},
|
||||
{"attribute_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_exists()'s parameter must be a path", caller));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return to_bool_json(!v.IsNull());
|
||||
}
|
||||
},
|
||||
{"attribute_not_exists", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 1) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists() accepts 1 parameter, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
if (!std::holds_alternative<parsed::path>(f._parameters[0]._value)) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_not_exists()'s parameter must be a path", caller));
|
||||
}
|
||||
rjson::value v = calculate_value(f._parameters[0], caller, previous_item);
|
||||
return to_bool_json(v.IsNull());
|
||||
}
|
||||
},
|
||||
{"attribute_type", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
// There is no real reason for the following check (not
|
||||
// allowing the type to come from a document attribute), but
|
||||
// DynamoDB does this check, so we do too...
|
||||
if (!f._parameters[1].is_constant()) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_types()'s first parameter must be an expression attribute", caller));
|
||||
}
|
||||
rjson::value v0 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v1 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
if (v1.IsObject() && v1.MemberCount() == 1 && v1.MemberBegin()->name == "S") {
|
||||
// If the type parameter is not one of the legal types
|
||||
// we should generate an error, not a failed condition:
|
||||
if (!known_type(rjson::to_string_view(v1.MemberBegin()->value))) {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_types()'s second parameter, {}, is not a known type",
|
||||
caller, v1.MemberBegin()->value));
|
||||
}
|
||||
if (v0.IsObject() && v0.MemberCount() == 1) {
|
||||
return to_bool_json(v1.MemberBegin()->value == v0.MemberBegin()->name);
|
||||
} else {
|
||||
return to_bool_json(false);
|
||||
}
|
||||
} else {
|
||||
throw api_error::validation(
|
||||
format("{}: attribute_type() second parameter must refer to a string, got {}", caller, v1));
|
||||
}
|
||||
}
|
||||
},
|
||||
{"begins_with", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: begins_with() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: begins_with() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return to_bool_json(check_BEGINS_WITH(v1.IsNull() ? nullptr : &v1, v2,
|
||||
f._parameters[0].is_constant(), f._parameters[1].is_constant()));
|
||||
}
|
||||
},
|
||||
{"contains", [] (calculate_value_caller caller, const rjson::value* previous_item, const parsed::value::function_call& f) {
|
||||
if (caller != calculate_value_caller::ConditionExpressionAlone) {
|
||||
throw api_error::validation(
|
||||
format("{}: contains() not allowed here", caller));
|
||||
}
|
||||
if (f._parameters.size() != 2) {
|
||||
throw api_error::validation(
|
||||
format("{}: contains() accepts 2 parameters, got {}", caller, f._parameters.size()));
|
||||
}
|
||||
rjson::value v1 = calculate_value(f._parameters[0], caller, previous_item);
|
||||
rjson::value v2 = calculate_value(f._parameters[1], caller, previous_item);
|
||||
return to_bool_json(check_CONTAINS(v1.IsNull() ? nullptr : &v1, v2));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Given a parsed::path and an item read from the table, extract the value
|
||||
// of a certain attribute path, such as "a" or "a.b.c[3]". Returns a null
|
||||
// value if the item or the requested attribute does not exist.
|
||||
// Note that the item is assumed to be encoded in JSON using DynamoDB
|
||||
// conventions - each level of a nested document is a map with one key -
|
||||
// a type (e.g., "M" for map) - and its value is the representation of
|
||||
// that value.
|
||||
static rjson::value extract_path(const rjson::value* item,
|
||||
const parsed::path& p, calculate_value_caller caller) {
|
||||
if (!item) {
|
||||
return rjson::null_value();
|
||||
}
|
||||
const rjson::value* v = rjson::find(*item, p.root());
|
||||
if (!v) {
|
||||
return rjson::null_value();
|
||||
}
|
||||
for (const auto& op : p.operators()) {
|
||||
if (!v->IsObject() || v->MemberCount() != 1) {
|
||||
// This shouldn't happen. We shouldn't have stored malformed
|
||||
// objects. But today Alternator does not validate the structure
|
||||
// of nested documents before storing them, so this can happen on
|
||||
// read.
|
||||
throw api_error::validation(format("{}: malformed item read: {}", *item));
|
||||
}
|
||||
const char* type = v->MemberBegin()->name.GetString();
|
||||
v = &(v->MemberBegin()->value);
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const std::string& member) {
|
||||
if (type[0] == 'M' && v->IsObject()) {
|
||||
v = rjson::find(*v, member);
|
||||
} else {
|
||||
v = nullptr;
|
||||
}
|
||||
},
|
||||
[&] (unsigned index) {
|
||||
if (type[0] == 'L' && v->IsArray() && index < v->Size()) {
|
||||
v = &(v->GetArray()[index]);
|
||||
} else {
|
||||
v = nullptr;
|
||||
}
|
||||
}
|
||||
}, op);
|
||||
if (!v) {
|
||||
return rjson::null_value();
|
||||
}
|
||||
}
|
||||
return rjson::copy(*v);
|
||||
}
|
||||
|
||||
// Given a parsed::value, which can refer either to a constant value from
|
||||
// ExpressionAttributeValues, to the value of some attribute, or to a function
|
||||
// of other values, this function calculates the resulting value.
|
||||
// "caller" determines which expression - ConditionExpression or
|
||||
// UpdateExpression - is asking for this value. We need to know this because
|
||||
// DynamoDB allows a different choice of functions for different expressions.
|
||||
rjson::value calculate_value(const parsed::value& v,
|
||||
calculate_value_caller caller,
|
||||
const rjson::value* previous_item) {
|
||||
return std::visit(overloaded_functor {
|
||||
[&] (const parsed::constant& c) -> rjson::value {
|
||||
return rjson::copy(calculate_value(c));
|
||||
},
|
||||
[&] (const parsed::value::function_call& f) -> rjson::value {
|
||||
auto function_it = function_handlers.find(std::string_view(f._function_name));
|
||||
if (function_it == function_handlers.end()) {
|
||||
throw api_error::validation(
|
||||
format("{}: unknown function '{}' called.", caller, f._function_name));
|
||||
}
|
||||
return function_it->second(caller, previous_item, f);
|
||||
},
|
||||
[&] (const parsed::path& p) -> rjson::value {
|
||||
return extract_path(previous_item, p, caller);
|
||||
}
|
||||
}, v._value);
|
||||
}
|
||||
|
||||
// Same as calculate_value() above, except takes a set_rhs, which may be
|
||||
// either a single value, or v1+v2 or v1-v2.
|
||||
rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
const rjson::value* previous_item) {
|
||||
switch (rhs._op) {
|
||||
case 'v':
|
||||
return calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
case '+': {
|
||||
rjson::value v1 = calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
rjson::value v2 = calculate_value(rhs._v2, calculate_value_caller::UpdateExpression, previous_item);
|
||||
return number_add(v1, v2);
|
||||
}
|
||||
case '-': {
|
||||
rjson::value v1 = calculate_value(rhs._v1, calculate_value_caller::UpdateExpression, previous_item);
|
||||
rjson::value v2 = calculate_value(rhs._v2, calculate_value_caller::UpdateExpression, previous_item);
|
||||
return number_subtract(v1, v2);
|
||||
}
|
||||
}
|
||||
// Can't happen
|
||||
return rjson::null_value();
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*
|
||||
* This file is part of Scylla. See the LICENSE.PROPRIETARY file in the
|
||||
* top-level directory for licensing information.
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -142,12 +145,6 @@ REMOVE: R E M O V E;
|
||||
ADD: A D D;
|
||||
DELETE: D E L E T E;
|
||||
|
||||
AND: A N D;
|
||||
OR: O R;
|
||||
NOT: N O T;
|
||||
BETWEEN: B E T W E E N;
|
||||
IN: I N;
|
||||
|
||||
fragment ALPHA: 'A'..'Z' | 'a'..'z';
|
||||
fragment DIGIT: '0'..'9';
|
||||
fragment ALNUM: ALPHA | DIGIT | '_';
|
||||
@@ -168,19 +165,19 @@ path returns [parsed::path p]:
|
||||
| '[' INTEGER ']' { $p.add_index(std::stoi($INTEGER.text)); }
|
||||
)*;
|
||||
|
||||
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=value { $v.add_func_parameter($x.v); }
|
||||
(',' x=value { $v.add_func_parameter($x.v); })*
|
||||
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=value { $rhs.set_value(std::move($v.v)); }
|
||||
( '+' v=value { $rhs.set_plus(std::move($v.v)); }
|
||||
| '-' v=value { $rhs.set_minus(std::move($v.v)); }
|
||||
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)); }
|
||||
)?
|
||||
;
|
||||
|
||||
@@ -215,48 +212,3 @@ update_expression returns [parsed::update_expression e]:
|
||||
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;
|
||||
|
||||
|
||||
primitive_condition returns [parsed::primitive_condition c]:
|
||||
v=value { $c.add_value(std::move($v.v));
|
||||
$c.set_operator(parsed::primitive_condition::type::VALUE); }
|
||||
( ( '=' { $c.set_operator(parsed::primitive_condition::type::EQ); }
|
||||
| '<' '>' { $c.set_operator(parsed::primitive_condition::type::NE); }
|
||||
| '<' { $c.set_operator(parsed::primitive_condition::type::LT); }
|
||||
| '<' '=' { $c.set_operator(parsed::primitive_condition::type::LE); }
|
||||
| '>' { $c.set_operator(parsed::primitive_condition::type::GT); }
|
||||
| '>' '=' { $c.set_operator(parsed::primitive_condition::type::GE); }
|
||||
)
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
| BETWEEN { $c.set_operator(parsed::primitive_condition::type::BETWEEN); }
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
AND
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
| IN '(' { $c.set_operator(parsed::primitive_condition::type::IN); }
|
||||
v=value { $c.add_value(std::move($v.v)); }
|
||||
(',' v=value { $c.add_value(std::move($v.v)); })*
|
||||
')'
|
||||
)?
|
||||
;
|
||||
|
||||
// The following rules for parsing boolean expressions are verbose and
|
||||
// somewhat strange because of Antlr 3's limitations on recursive rules,
|
||||
// common rule prefixes, and (lack of) support for operator precedence.
|
||||
// These rules could have been written more clearly using a more powerful
|
||||
// parser generator - such as Yacc.
|
||||
boolean_expression returns [parsed::condition_expression e]:
|
||||
b=boolean_expression_1 { $e.append(std::move($b.e), '|'); }
|
||||
(OR b=boolean_expression_1 { $e.append(std::move($b.e), '|'); } )*
|
||||
;
|
||||
boolean_expression_1 returns [parsed::condition_expression e]:
|
||||
b=boolean_expression_2 { $e.append(std::move($b.e), '&'); }
|
||||
(AND b=boolean_expression_2 { $e.append(std::move($b.e), '&'); } )*
|
||||
;
|
||||
boolean_expression_2 returns [parsed::condition_expression e]:
|
||||
p=primitive_condition { $e.set_primitive(std::move($p.c)); }
|
||||
| NOT b=boolean_expression_2 { $e = std::move($b.e); $e.apply_not(); }
|
||||
| '(' b=boolean_expression ')' { $e = std::move($b.e); }
|
||||
;
|
||||
|
||||
condition_expression returns [parsed::condition_expression e]:
|
||||
boolean_expression { e=std::move($boolean_expression.e); } EOF;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -24,13 +24,8 @@
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <string_view>
|
||||
|
||||
#include <seastar/util/noncopyable_function.hh>
|
||||
|
||||
#include "expressions_types.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -41,62 +36,6 @@ public:
|
||||
|
||||
parsed::update_expression parse_update_expression(std::string query);
|
||||
std::vector<parsed::path> parse_projection_expression(std::string query);
|
||||
parsed::condition_expression parse_condition_expression(std::string query);
|
||||
|
||||
void resolve_update_expression(parsed::update_expression& ue,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values);
|
||||
void resolve_projection_expression(std::vector<parsed::path>& pe,
|
||||
const rjson::value* expression_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_names);
|
||||
void resolve_condition_expression(parsed::condition_expression& ce,
|
||||
const rjson::value* expression_attribute_names,
|
||||
const rjson::value* expression_attribute_values,
|
||||
std::unordered_set<std::string>& used_attribute_names,
|
||||
std::unordered_set<std::string>& used_attribute_values);
|
||||
|
||||
void validate_value(const rjson::value& v, const char* caller);
|
||||
|
||||
bool condition_expression_on(const parsed::condition_expression& ce, std::string_view attribute);
|
||||
|
||||
// for_condition_expression_on() runs the given function on the attributes
|
||||
// that the expression uses. It may run for the same attribute more than once
|
||||
// if the same attribute is used more than once in the expression.
|
||||
void for_condition_expression_on(const parsed::condition_expression& ce, const noncopyable_function<void(std::string_view)>& func);
|
||||
|
||||
// calculate_value() behaves slightly different (especially, different
|
||||
// functions supported) when used in different types of expressions, as
|
||||
// enumerated in this enum:
|
||||
enum class calculate_value_caller {
|
||||
UpdateExpression, ConditionExpression, ConditionExpressionAlone
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& out, calculate_value_caller caller) {
|
||||
switch (caller) {
|
||||
case calculate_value_caller::UpdateExpression:
|
||||
out << "UpdateExpression";
|
||||
break;
|
||||
case calculate_value_caller::ConditionExpression:
|
||||
out << "ConditionExpression";
|
||||
break;
|
||||
case calculate_value_caller::ConditionExpressionAlone:
|
||||
out << "ConditionExpression";
|
||||
break;
|
||||
default:
|
||||
out << "unknown type of expression";
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
rjson::value calculate_value(const parsed::value& v,
|
||||
calculate_value_caller caller,
|
||||
const rjson::value* previous_item);
|
||||
|
||||
rjson::value calculate_value(const parsed::set_rhs& rhs,
|
||||
const rjson::value* previous_item);
|
||||
|
||||
|
||||
} /* namespace alternator */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -25,10 +25,6 @@
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include <seastar/core/shared_ptr.hh>
|
||||
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
/*
|
||||
* Parsed representation of expressions and their components.
|
||||
*
|
||||
@@ -49,23 +45,15 @@ class path {
|
||||
// dot (e.g., ".xyz").
|
||||
std::string _root;
|
||||
std::vector<std::variant<std::string, unsigned>> _operators;
|
||||
// It is useful to limit the depth of a user-specified path, because is
|
||||
// allows us to use recursive algorithms without worrying about recursion
|
||||
// depth. DynamoDB officially limits the length of paths to 32 components
|
||||
// (including the root) so let's use the same limit.
|
||||
static constexpr unsigned depth_limit = 32;
|
||||
void check_depth_limit();
|
||||
public:
|
||||
void set_root(std::string root) {
|
||||
_root = std::move(root);
|
||||
}
|
||||
void add_index(unsigned i) {
|
||||
_operators.emplace_back(i);
|
||||
check_depth_limit();
|
||||
}
|
||||
void add_dot(std::string(name)) {
|
||||
_operators.emplace_back(std::move(name));
|
||||
check_depth_limit();
|
||||
}
|
||||
const std::string& root() const {
|
||||
return _root;
|
||||
@@ -73,36 +61,12 @@ public:
|
||||
bool has_operators() const {
|
||||
return !_operators.empty();
|
||||
}
|
||||
const std::vector<std::variant<std::string, unsigned>>& operators() const {
|
||||
return _operators;
|
||||
}
|
||||
std::vector<std::variant<std::string, unsigned>>& operators() {
|
||||
return _operators;
|
||||
}
|
||||
friend std::ostream& operator<<(std::ostream&, const path&);
|
||||
};
|
||||
|
||||
// When an expression is first parsed, all constants are references, like
|
||||
// ":val1", into ExpressionAttributeValues. This uses std::string() variant.
|
||||
// The resolve_value() function replaces these constants by the JSON item
|
||||
// extracted from the ExpressionAttributeValues.
|
||||
struct constant {
|
||||
// We use lw_shared_ptr<rjson::value> just to make rjson::value copyable,
|
||||
// to make this entire object copyable as ANTLR needs.
|
||||
using literal = lw_shared_ptr<rjson::value>;
|
||||
std::variant<std::string, literal> _value;
|
||||
void set(const rjson::value& v) {
|
||||
_value = make_lw_shared<rjson::value>(rjson::copy(v));
|
||||
}
|
||||
void set(std::string& s) {
|
||||
_value = s;
|
||||
}
|
||||
};
|
||||
|
||||
// "value" is is a value used in the right hand side of an assignment
|
||||
// expression, "SET a = ...". It can be a constant (a reference to a value
|
||||
// included in the request, e.g., ":val"), a path to an attribute from the
|
||||
// existing item (e.g., "a.b[3].c"), or a function of other such values.
|
||||
// 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.
|
||||
@@ -111,12 +75,9 @@ struct value {
|
||||
std::string _function_name;
|
||||
std::vector<value> _parameters;
|
||||
};
|
||||
std::variant<constant, path, function_call> _value;
|
||||
void set_constant(constant c) {
|
||||
_value = std::move(c);
|
||||
}
|
||||
std::variant<std::string, path, function_call> _value;
|
||||
void set_valref(std::string s) {
|
||||
_value = constant { std::move(s) };
|
||||
_value = std::move(s);
|
||||
}
|
||||
void set_path(path p) {
|
||||
_value = std::move(p);
|
||||
@@ -127,15 +88,6 @@ struct value {
|
||||
void add_func_parameter(value v) {
|
||||
std::get<function_call>(_value)._parameters.emplace_back(std::move(v));
|
||||
}
|
||||
bool is_constant() const {
|
||||
return std::holds_alternative<constant>(_value);
|
||||
}
|
||||
bool is_path() const {
|
||||
return std::holds_alternative<path>(_value);
|
||||
}
|
||||
bool is_func() const {
|
||||
return std::holds_alternative<function_call>(_value);
|
||||
}
|
||||
};
|
||||
|
||||
// The right-hand-side of a SET in an update expression can be either a
|
||||
@@ -169,10 +121,10 @@ public:
|
||||
struct remove {
|
||||
};
|
||||
struct add {
|
||||
constant _valref;
|
||||
std::string _valref;
|
||||
};
|
||||
struct del {
|
||||
constant _valref;
|
||||
std::string _valref;
|
||||
};
|
||||
std::variant<set, remove, add, del> _action;
|
||||
|
||||
@@ -186,11 +138,11 @@ public:
|
||||
}
|
||||
void assign_add(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = add { constant { std::move(v) } };
|
||||
_action = add { std::move(v) };
|
||||
}
|
||||
void assign_del(path p, std::string v) {
|
||||
_path = std::move(p);
|
||||
_action = del { constant { std::move(v) } };
|
||||
_action = del { std::move(v) };
|
||||
}
|
||||
};
|
||||
private:
|
||||
@@ -208,62 +160,6 @@ public:
|
||||
const std::vector<action>& actions() const {
|
||||
return _actions;
|
||||
}
|
||||
std::vector<action>& actions() {
|
||||
return _actions;
|
||||
}
|
||||
};
|
||||
|
||||
// A primitive_condition is a condition expression involving one condition,
|
||||
// while the full condition_expression below adds boolean logic over these
|
||||
// primitive conditions.
|
||||
// The supported primitive conditions are:
|
||||
// 1. Binary operators - v1 OP v2, where OP is =, <>, <, <=, >, or >= and
|
||||
// v1 and v2 are values - from the item (an attribute path), the query
|
||||
// (a ":val" reference), or a function of the the above (only the size()
|
||||
// function is supported).
|
||||
// 2. Ternary operator - v1 BETWEEN v2 and v3 (means v1 >= v2 AND v1 <= v3).
|
||||
// 3. N-ary operator - v1 IN ( v2, v3, ... )
|
||||
// 4. A single function call (attribute_exists etc.). The parser actually
|
||||
// accepts a more general "value" here but later stages reject a value
|
||||
// which is not a function call (because DynamoDB does it too).
|
||||
class primitive_condition {
|
||||
public:
|
||||
enum class type {
|
||||
UNDEFINED, VALUE, EQ, NE, LT, LE, GT, GE, BETWEEN, IN
|
||||
};
|
||||
type _op = type::UNDEFINED;
|
||||
std::vector<value> _values;
|
||||
void set_operator(type op) {
|
||||
_op = op;
|
||||
}
|
||||
void add_value(value&& v) {
|
||||
_values.push_back(std::move(v));
|
||||
}
|
||||
bool empty() const {
|
||||
return _op == type::UNDEFINED;
|
||||
}
|
||||
};
|
||||
|
||||
class condition_expression {
|
||||
public:
|
||||
bool _negated = false; // If true, the entire condition is negated
|
||||
struct condition_list {
|
||||
char op = '|'; // '&' or '|'
|
||||
std::vector<condition_expression> conditions;
|
||||
};
|
||||
std::variant<primitive_condition, condition_list> _expression = condition_list();
|
||||
|
||||
void set_primitive(primitive_condition&& p) {
|
||||
_expression = std::move(p);
|
||||
}
|
||||
void append(condition_expression&& c, char op);
|
||||
void apply_not() {
|
||||
_negated = !_negated;
|
||||
}
|
||||
bool empty() const {
|
||||
return std::holds_alternative<condition_list>(_expression) &&
|
||||
std::get<condition_list>(_expression).conditions.empty();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace parsed
|
||||
|
||||
120
alternator/rjson.cc
Normal file
120
alternator/rjson.cc
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
159
alternator/rjson.hh
Normal file
159
alternator/rjson.hh
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020-present 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 "seastarx.hh"
|
||||
#include "service/paxos/cas_request.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "executor.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// An rmw_operation encapsulates the common logic of all the item update
|
||||
// operations which may involve a read of the item before the write
|
||||
// (so-called Read-Modify-Write operations). These operations include PutItem,
|
||||
// UpdateItem and DeleteItem: All of these may be conditional operations (the
|
||||
// "Expected" parameter) which requir a read before the write, and UpdateItem
|
||||
// may also have an update expression which refers to the item's old value.
|
||||
//
|
||||
// The code below supports running the read and the write together as one
|
||||
// transaction using LWT (this is why rmw_operation is a subclass of
|
||||
// cas_request, as required by storage_proxy::cas()), but also has optional
|
||||
// modes not using LWT.
|
||||
class rmw_operation : public service::cas_request, public enable_shared_from_this<rmw_operation> {
|
||||
public:
|
||||
// The following options choose which mechanism to use for isolating
|
||||
// parallel write operations:
|
||||
// * The FORBID_RMW option forbids RMW (read-modify-write) operations
|
||||
// such as conditional updates. For the remaining write-only
|
||||
// operations, ordinary quorum writes are isolated enough.
|
||||
// * The LWT_ALWAYS option always uses LWT (lightweight transactions)
|
||||
// for any write operation - whether or not it also has a read.
|
||||
// * The LWT_RMW_ONLY option uses LWT only for RMW operations, and uses
|
||||
// ordinary quorum writes for write-only operations.
|
||||
// This option is not safe if the user may send both RMW and write-only
|
||||
// operations on the same item.
|
||||
// * The UNSAFE_RMW option does read-modify-write operations as separate
|
||||
// read and write. It is unsafe - concurrent RMW operations are not
|
||||
// isolated at all. This option will likely be removed in the future.
|
||||
enum class write_isolation {
|
||||
FORBID_RMW, LWT_ALWAYS, LWT_RMW_ONLY, UNSAFE_RMW
|
||||
};
|
||||
static constexpr auto WRITE_ISOLATION_TAG_KEY = "system:write_isolation";
|
||||
|
||||
static write_isolation get_write_isolation_for_schema(schema_ptr schema);
|
||||
|
||||
static write_isolation default_write_isolation;
|
||||
public:
|
||||
static void set_default_write_isolation(std::string_view mode);
|
||||
|
||||
protected:
|
||||
// The full request JSON
|
||||
rjson::value _request;
|
||||
// All RMW operations involve a single item with a specific partition
|
||||
// and optional clustering key, in a single table, so the following
|
||||
// information is common to all of them:
|
||||
schema_ptr _schema;
|
||||
partition_key _pk = partition_key::make_empty();
|
||||
clustering_key _ck = clustering_key::make_empty();
|
||||
write_isolation _write_isolation;
|
||||
|
||||
// All RMW operations can have a ReturnValues parameter from the following
|
||||
// choices. But note that only UpdateItem actually supports all of them:
|
||||
enum class returnvalues {
|
||||
NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW
|
||||
} _returnvalues;
|
||||
static returnvalues parse_returnvalues(const rjson::value& request);
|
||||
// When _returnvalues != NONE, apply() should store here, in JSON form,
|
||||
// the values which are to be returned in the "Attributes" field.
|
||||
// The default null JSON means do not return an Attributes field at all.
|
||||
// This field is marked "mutable" so that the const apply() can modify
|
||||
// it (see explanation below), but note that because apply() may be
|
||||
// called more than once, if apply() will sometimes set this field it
|
||||
// must set it (even if just to the default empty value) every time.
|
||||
mutable rjson::value _return_attributes;
|
||||
public:
|
||||
// The constructor of a rmw_operation subclass should parse the request
|
||||
// and try to discover as many input errors as it can before really
|
||||
// attempting the read or write operations.
|
||||
rmw_operation(service::storage_proxy& proxy, rjson::value&& request);
|
||||
// rmw_operation subclasses (update_item_operation, put_item_operation
|
||||
// and delete_item_operation) shall implement an apply() function which
|
||||
// takes the previous value of the item (if it was read) and creates the
|
||||
// write mutation. If the previous value of item does not pass the needed
|
||||
// conditional expression, apply() should return an empty optional.
|
||||
// apply() may throw if it encounters input errors not discovered during
|
||||
// the constructor.
|
||||
// apply() may be called more than once in case of contention, so it must
|
||||
// not change the state saved in the object (issue #7218 was caused by
|
||||
// violating this). We mark apply() "const" to let the compiler validate
|
||||
// this for us. The output-only field _return_attributes is marked
|
||||
// "mutable" above so that apply() can still write to it.
|
||||
virtual std::optional<mutation> apply(std::unique_ptr<rjson::value> previous_item, api::timestamp_type ts) const = 0;
|
||||
// Convert the above apply() into the signature needed by cas_request:
|
||||
virtual std::optional<mutation> apply(foreign_ptr<lw_shared_ptr<query::result>> qr, const query::partition_slice& slice, api::timestamp_type ts) override;
|
||||
virtual ~rmw_operation() = default;
|
||||
schema_ptr schema() const { return _schema; }
|
||||
const rjson::value& request() const { return _request; }
|
||||
rjson::value&& move_request() && { return std::move(_request); }
|
||||
future<executor::request_return_type> execute(service::storage_proxy& proxy,
|
||||
service::client_state& client_state,
|
||||
tracing::trace_state_ptr trace_state,
|
||||
service_permit permit,
|
||||
bool needs_read_before_write,
|
||||
stats& stats);
|
||||
std::optional<shard_id> shard_for_execute(bool needs_read_before_write);
|
||||
};
|
||||
|
||||
} // namespace alternator
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -19,21 +19,19 @@
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "utils/base64.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "base64.hh"
|
||||
#include "log.hh"
|
||||
#include "serialization.hh"
|
||||
#include "error.hh"
|
||||
#include "rapidjson/writer.h"
|
||||
#include "concrete_types.hh"
|
||||
#include "cql3/type_json.hh"
|
||||
|
||||
static logging::logger slogger("alternator-serialization");
|
||||
|
||||
namespace alternator {
|
||||
|
||||
type_info type_info_from_string(std::string_view type) {
|
||||
static thread_local const std::unordered_map<std::string_view, type_info> type_infos = {
|
||||
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}},
|
||||
@@ -66,36 +64,32 @@ struct from_json_visitor {
|
||||
|
||||
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(rjson::to_string_view(v)));
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
void operator()(const bytes_type_impl& t) const {
|
||||
bo.write(rjson::base64_decode(v));
|
||||
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 {
|
||||
try {
|
||||
bo.write(t.from_string(rjson::to_string_view(v)));
|
||||
} catch (const marshal_exception& e) {
|
||||
throw api_error::validation(format("The parameter cannot be converted to a numeric value: {}", v));
|
||||
}
|
||||
bo.write(t.from_string(sstring_view(v.GetString(), v.GetStringLength())));
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
bo.write(from_json_object(t, v, cql_serialization_format::internal()));
|
||||
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::validation(format("An item can contain only one attribute definition: {}", item));
|
||||
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(rjson::to_string_view(it->name)); // JSON keys are guaranteed to be strings
|
||||
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);
|
||||
slogger.trace("Non-optimal serialization of type {}", it->name.GetString());
|
||||
return bytes{int8_t(type_info.atype)} + to_bytes(rjson::print(item));
|
||||
}
|
||||
|
||||
@@ -113,27 +107,27 @@ struct to_json_visitor {
|
||||
|
||||
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 = to_json_string(*decimal_type, bytes(bv));
|
||||
auto s = decimal_type->to_json_string(bytes(bv));
|
||||
//FIXME(sarna): unnecessary copy
|
||||
rjson::add_with_string_name(deserialized, type_ident, rjson::from_string(s));
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::from_string(s));
|
||||
}
|
||||
void operator()(const string_type_impl& t) {
|
||||
rjson::add_with_string_name(deserialized, type_ident, rjson::from_string(reinterpret_cast<const char *>(bv.data()), bv.size()));
|
||||
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::add_with_string_name(deserialized, type_ident, rjson::from_string(b64));
|
||||
rjson::set_with_string_name(deserialized, type_ident, rjson::from_string(b64));
|
||||
}
|
||||
// default
|
||||
void operator()(const abstract_type& t) const {
|
||||
rjson::add_with_string_name(deserialized, type_ident, rjson::parse(to_json_string(t, bytes(bv))));
|
||||
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::validation("Serialized value empty");
|
||||
throw api_error("ValidationException", "Serialized value empty");
|
||||
}
|
||||
|
||||
alternator_type atype = alternator_type(bv[0]);
|
||||
@@ -141,7 +135,7 @@ rjson::value deserialize_item(bytes_view bv) {
|
||||
|
||||
if (atype == alternator_type::NOT_SUPPORTED_YET) {
|
||||
slogger.trace("Non-optimal deserialization of alternator type {}", int8_t(atype));
|
||||
return rjson::parse(std::string_view(reinterpret_cast<const char *>(bv.data()), bv.size()));
|
||||
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});
|
||||
@@ -158,48 +152,34 @@ std::string type_to_string(data_type type) {
|
||||
};
|
||||
auto it = types.find(type);
|
||||
if (it == types.end()) {
|
||||
// fall back to string, in order to be able to present
|
||||
// internal Scylla types in a human-readable way
|
||||
return "S";
|
||||
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();
|
||||
const rjson::value* key_typed_value = rjson::find(item, column_name);
|
||||
if (!key_typed_value) {
|
||||
throw api_error::validation(format("Key column {} not found", column_name));
|
||||
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);
|
||||
return get_key_from_typed_value(key_typed_value, column, expected_type);
|
||||
}
|
||||
|
||||
// Parses the JSON encoding for a key value, which is a map with a single
|
||||
// entry, whose key is the type (expected to match the key column's type)
|
||||
// and the value is the encoded value.
|
||||
bytes get_key_from_typed_value(const rjson::value& key_typed_value, const column_definition& column) {
|
||||
if (!key_typed_value.IsObject() || key_typed_value.MemberCount() != 1 ||
|
||||
!key_typed_value.MemberBegin()->value.IsString()) {
|
||||
throw api_error::validation(
|
||||
format("Malformed value object for key column {}: {}",
|
||||
column.name_as_text(), key_typed_value));
|
||||
}
|
||||
|
||||
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 != type_to_string(column.type)) {
|
||||
throw api_error::validation(
|
||||
if (it->name.GetString() != expected_type) {
|
||||
throw api_error("ValidationException",
|
||||
format("Type mismatch: expected type {} for key column {}, got type {}",
|
||||
type_to_string(column.type), column.name_as_text(), it->name));
|
||||
}
|
||||
std::string_view value_view = rjson::to_string_view(it->value);
|
||||
if (value_view.empty()) {
|
||||
throw api_error::validation(
|
||||
format("The AttributeValue for a key attribute cannot contain an empty string value. Key: {}", column.name_as_text()));
|
||||
expected_type, column.name_as_text(), it->name.GetString()));
|
||||
}
|
||||
if (column.type == bytes_type) {
|
||||
return rjson::base64_decode(it->value);
|
||||
return base64_decode(it->value.GetString());
|
||||
} else {
|
||||
return column.type->from_string(rjson::to_string_view(it->value));
|
||||
return column.type->from_string(it->value.GetString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -214,14 +194,11 @@ rjson::value json_key_column_value(bytes_view cell, const column_definition& col
|
||||
// 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 = to_json_string(*decimal_type, bytes(cell));
|
||||
auto s = decimal_type->to_json_string(bytes(cell));
|
||||
return rjson::from_string(s);
|
||||
} else {
|
||||
// Support for arbitrary key types is useful for parsing values of virtual tables,
|
||||
// which can involve any type supported by Scylla.
|
||||
// In order to guarantee that the returned type is parsable by alternator clients,
|
||||
// they are represented simply as strings.
|
||||
return rjson::from_string(column.type->to_string(bytes(cell)));
|
||||
// We shouldn't get here, we shouldn't see such key columns.
|
||||
throw std::runtime_error(format("Unexpected key type: {}", column.type->name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,146 +227,4 @@ clustering_key ck_from_json(const rjson::value& item, schema_ptr schema) {
|
||||
return clustering_key::from_exploded(raw_ck);
|
||||
}
|
||||
|
||||
big_decimal unwrap_number(const rjson::value& v, std::string_view diagnostic) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
throw api_error::validation(format("{}: invalid number object", diagnostic));
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
if (it->name != "N") {
|
||||
throw api_error::validation(format("{}: expected number, found type '{}'", diagnostic, it->name));
|
||||
}
|
||||
try {
|
||||
if (it->value.IsNumber()) {
|
||||
// FIXME(sarna): should use big_decimal constructor with numeric values directly:
|
||||
return big_decimal(rjson::print(it->value));
|
||||
}
|
||||
if (!it->value.IsString()) {
|
||||
throw api_error::validation(format("{}: improperly formatted number constant", diagnostic));
|
||||
}
|
||||
return big_decimal(rjson::to_string_view(it->value));
|
||||
} catch (const marshal_exception& e) {
|
||||
throw api_error::validation(format("The parameter cannot be converted to a numeric value: {}", it->value));
|
||||
}
|
||||
}
|
||||
|
||||
const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value& v) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
return {"", nullptr};
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
const std::string it_key = it->name.GetString();
|
||||
if (it_key != "SS" && it_key != "BS" && it_key != "NS") {
|
||||
return {"", nullptr};
|
||||
}
|
||||
return std::make_pair(it_key, &(it->value));
|
||||
}
|
||||
|
||||
const rjson::value* unwrap_list(const rjson::value& v) {
|
||||
if (!v.IsObject() || v.MemberCount() != 1) {
|
||||
return nullptr;
|
||||
}
|
||||
auto it = v.MemberBegin();
|
||||
if (it->name != std::string("L")) {
|
||||
return nullptr;
|
||||
}
|
||||
return &(it->value);
|
||||
}
|
||||
|
||||
// Take two JSON-encoded numeric values ({"N": "thenumber"}) and return the
|
||||
// sum, again as a JSON-encoded number.
|
||||
rjson::value number_add(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto n1 = unwrap_number(v1, "UpdateExpression");
|
||||
auto n2 = unwrap_number(v2, "UpdateExpression");
|
||||
rjson::value ret = rjson::empty_object();
|
||||
std::string str_ret = std::string((n1 + n2).to_string());
|
||||
rjson::add(ret, "N", rjson::from_string(str_ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
rjson::value number_subtract(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto n1 = unwrap_number(v1, "UpdateExpression");
|
||||
auto n2 = unwrap_number(v2, "UpdateExpression");
|
||||
rjson::value ret = rjson::empty_object();
|
||||
std::string str_ret = std::string((n1 - n2).to_string());
|
||||
rjson::add(ret, "N", rjson::from_string(str_ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual set]}) and
|
||||
// return the sum of both sets, again as a set value.
|
||||
rjson::value set_sum(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto [set1_type, set1] = unwrap_set(v1);
|
||||
auto [set2_type, set2] = unwrap_set(v2);
|
||||
if (set1_type != set2_type) {
|
||||
throw api_error::validation(format("Mismatched set types: {} and {}", set1_type, set2_type));
|
||||
}
|
||||
if (!set1 || !set2) {
|
||||
throw api_error::validation("UpdateExpression: ADD operation for sets must be given sets as arguments");
|
||||
}
|
||||
rjson::value sum = rjson::copy(*set1);
|
||||
std::set<rjson::value, rjson::single_value_comp> set1_raw;
|
||||
for (auto it = sum.Begin(); it != sum.End(); ++it) {
|
||||
set1_raw.insert(rjson::copy(*it));
|
||||
}
|
||||
for (const auto& a : set2->GetArray()) {
|
||||
if (!set1_raw.contains(a)) {
|
||||
rjson::push_back(sum, rjson::copy(a));
|
||||
}
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::add_with_string_name(ret, set1_type, std::move(sum));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual list]}) and
|
||||
// return the difference of s1 - s2, again as a set value.
|
||||
// DynamoDB does not allow empty sets, so if resulting set is empty, return
|
||||
// an unset optional instead.
|
||||
std::optional<rjson::value> set_diff(const rjson::value& v1, const rjson::value& v2) {
|
||||
auto [set1_type, set1] = unwrap_set(v1);
|
||||
auto [set2_type, set2] = unwrap_set(v2);
|
||||
if (set1_type != set2_type) {
|
||||
throw api_error::validation(format("Mismatched set types: {} and {}", set1_type, set2_type));
|
||||
}
|
||||
if (!set1 || !set2) {
|
||||
throw api_error::validation("UpdateExpression: DELETE operation can only be performed on a set");
|
||||
}
|
||||
std::set<rjson::value, rjson::single_value_comp> set1_raw;
|
||||
for (auto it = set1->Begin(); it != set1->End(); ++it) {
|
||||
set1_raw.insert(rjson::copy(*it));
|
||||
}
|
||||
for (const auto& a : set2->GetArray()) {
|
||||
set1_raw.erase(a);
|
||||
}
|
||||
if (set1_raw.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::add_with_string_name(ret, set1_type, rjson::empty_array());
|
||||
rjson::value& result_set = ret[set1_type];
|
||||
for (const auto& a : set1_raw) {
|
||||
rjson::push_back(result_set, rjson::copy(a));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take two JSON-encoded list values (remember that a list value is
|
||||
// {"L": [...the actual list]}) and return the concatenation, again as
|
||||
// a list value.
|
||||
// Returns a null value if one of the arguments is not actually a list.
|
||||
rjson::value list_concatenate(const rjson::value& v1, const rjson::value& v2) {
|
||||
const rjson::value* list1 = unwrap_list(v1);
|
||||
const rjson::value* list2 = unwrap_list(v2);
|
||||
if (!list1 || !list2) {
|
||||
return rjson::null_value();
|
||||
}
|
||||
rjson::value cat = rjson::copy(*list1);
|
||||
for (const auto& a : list2->GetArray()) {
|
||||
rjson::push_back(cat, rjson::copy(a));
|
||||
}
|
||||
rjson::value ret = rjson::empty_object();
|
||||
rjson::add(ret, "L", std::move(cat));
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -22,12 +22,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "types.hh"
|
||||
#include "schema_fwd.hh"
|
||||
#include "schema.hh"
|
||||
#include "keys.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "utils/big_decimal.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
@@ -45,7 +43,7 @@ struct type_representation {
|
||||
data_type dtype;
|
||||
};
|
||||
|
||||
type_info type_info_from_string(std::string_view type);
|
||||
type_info type_info_from_string(std::string type);
|
||||
type_representation represent_type(alternator_type atype);
|
||||
|
||||
bytes serialize_item(const rjson::value& item);
|
||||
@@ -54,42 +52,10 @@ 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);
|
||||
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);
|
||||
|
||||
// If v encodes a number (i.e., it is a {"N": [...]}, returns an object representing it. Otherwise,
|
||||
// raises ValidationException with diagnostic.
|
||||
big_decimal unwrap_number(const rjson::value& v, std::string_view diagnostic);
|
||||
|
||||
// Check if a given JSON object encodes a set (i.e., it is a {"SS": [...]}, or "NS", "BS"
|
||||
// and returns set's type and a pointer to that set. If the object does not encode a set,
|
||||
// returned value is {"", nullptr}
|
||||
const std::pair<std::string, const rjson::value*> unwrap_set(const rjson::value& v);
|
||||
|
||||
// Check if a given JSON object encodes a list (i.e., it is a {"L": [...]}
|
||||
// and returns a pointer to that list.
|
||||
const rjson::value* unwrap_list(const rjson::value& v);
|
||||
|
||||
// Take two JSON-encoded numeric values ({"N": "thenumber"}) and return the
|
||||
// sum, again as a JSON-encoded number.
|
||||
rjson::value number_add(const rjson::value& v1, const rjson::value& v2);
|
||||
rjson::value number_subtract(const rjson::value& v1, const rjson::value& v2);
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual set]}) and
|
||||
// return the sum of both sets, again as a set value.
|
||||
rjson::value set_sum(const rjson::value& v1, const rjson::value& v2);
|
||||
// Take two JSON-encoded set values (e.g. {"SS": [...the actual list]}) and
|
||||
// return the difference of s1 - s2, again as a set value.
|
||||
// DynamoDB does not allow empty sets, so if resulting set is empty, return
|
||||
// an unset optional instead.
|
||||
std::optional<rjson::value> set_diff(const rjson::value& v1, const rjson::value& v2);
|
||||
// Take two JSON-encoded list values (remember that a list value is
|
||||
// {"L": [...the actual list]}) and return the concatenation, again as
|
||||
// a list value.
|
||||
// Returns a null value if one of the arguments is not actually a list.
|
||||
rjson::value list_concatenate(const rjson::value& v1, const rjson::value& v2);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -22,20 +22,12 @@
|
||||
#include "alternator/server.hh"
|
||||
#include "log.hh"
|
||||
#include <seastar/http/function_handlers.hh>
|
||||
#include <seastar/http/short_streams.hh>
|
||||
#include <seastar/core/coroutine.hh>
|
||||
#include <seastar/json/json_elements.hh>
|
||||
#include <seastar/util/defer.hh>
|
||||
#include "seastarx.hh"
|
||||
#include <seastarx.hh>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include "error.hh"
|
||||
#include "utils/rjson.hh"
|
||||
#include "auth.hh"
|
||||
#include <cctype>
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "locator/snitch_base.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include "utils/overloaded_functor.hh"
|
||||
#include "utils/fb_utilities.hh"
|
||||
#include "rjson.hh"
|
||||
|
||||
static logging::logger slogger("alternator-server");
|
||||
|
||||
@@ -45,57 +37,12 @@ namespace alternator {
|
||||
|
||||
static constexpr auto TARGET = "X-Amz-Target";
|
||||
|
||||
inline std::vector<std::string_view> split(std::string_view text, char separator) {
|
||||
std::vector<std::string_view> tokens;
|
||||
inline std::vector<sstring> split(const sstring& text, const char* separator) {
|
||||
if (text == "") {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
auto pos = text.find_first_of(separator);
|
||||
if (pos != std::string_view::npos) {
|
||||
tokens.emplace_back(text.data(), pos);
|
||||
text.remove_prefix(pos + 1);
|
||||
} else {
|
||||
tokens.emplace_back(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Handle CORS (Cross-origin resource sharing) in the HTTP request:
|
||||
// If the request has the "Origin" header specifying where the script which
|
||||
// makes this request comes from, we need to reply with the header
|
||||
// "Access-Control-Allow-Origin: *" saying that this (and any) origin is fine.
|
||||
// Additionally, if preflight==true (i.e., this is an OPTIONS request),
|
||||
// the script can also "request" in headers that the server allows it to use
|
||||
// some HTTP methods and headers in the followup request, and the server
|
||||
// should respond by "allowing" them in the response headers.
|
||||
// We also add the header "Access-Control-Expose-Headers" to let the script
|
||||
// access additional headers in the response.
|
||||
// This handle_CORS() should be used when handling any HTTP method - both the
|
||||
// usual GET and POST, and also the "preflight" OPTIONS method.
|
||||
static void handle_CORS(const request& req, reply& rep, bool preflight) {
|
||||
if (!req.get_header("origin").empty()) {
|
||||
rep.add_header("Access-Control-Allow-Origin", "*");
|
||||
// This is the list that DynamoDB returns for expose headers. I am
|
||||
// not sure why not just return "*" here, what's the risk?
|
||||
rep.add_header("Access-Control-Expose-Headers", "x-amzn-RequestId,x-amzn-ErrorType,x-amzn-ErrorMessage,Date");
|
||||
if (preflight) {
|
||||
sstring s = req.get_header("Access-Control-Request-Headers");
|
||||
if (!s.empty()) {
|
||||
rep.add_header("Access-Control-Allow-Headers", std::move(s));
|
||||
}
|
||||
s = req.get_header("Access-Control-Request-Method");
|
||||
if (!s.empty()) {
|
||||
rep.add_header("Access-Control-Allow-Methods", std::move(s));
|
||||
}
|
||||
// Our CORS response never change anyway, let the browser cache it
|
||||
// for two hours (Chrome's maximum):
|
||||
rep.add_header("Access-Control-Max-Age", "7200");
|
||||
}
|
||||
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
|
||||
@@ -106,534 +53,119 @@ static void handle_CORS(const request& req, reply& rep, bool preflight) {
|
||||
// Internal Server Error.
|
||||
class api_handler : public handler_base {
|
||||
public:
|
||||
api_handler(const std::function<future<executor::request_return_type>(std::unique_ptr<request> req)>& _handle) : _f_handle(
|
||||
[this, _handle](std::unique_ptr<request> req, std::unique_ptr<reply> rep) {
|
||||
return seastar::futurize_invoke(_handle, std::move(req)).then_wrapped([this, rep = std::move(rep)](future<executor::request_return_type> resf) mutable {
|
||||
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) {
|
||||
generate_error_reply(*rep, ae);
|
||||
ret = ae;
|
||||
} catch (rjson::error & re) {
|
||||
generate_error_reply(*rep,
|
||||
api_error::validation(re.what()));
|
||||
ret = api_error("ValidationException", re.what());
|
||||
} catch (...) {
|
||||
generate_error_reply(*rep,
|
||||
api_error::internal(format("Internal server error: {}", std::current_exception())));
|
||||
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();
|
||||
std::visit(overloaded_functor {
|
||||
[&] (const json::json_return_type& json_return_value) {
|
||||
slogger.trace("api_handler success case");
|
||||
if (json_return_value._body_writer) {
|
||||
// Unfortunately, write_body() forces us to choose
|
||||
// from a fixed and irrelevant list of "mime-types"
|
||||
// at this point. But we'll override it with the
|
||||
// one (application/x-amz-json-1.0) below.
|
||||
rep->write_body("json", std::move(json_return_value._body_writer));
|
||||
} else {
|
||||
rep->_content += json_return_value._res;
|
||||
}
|
||||
},
|
||||
[&] (const api_error& err) {
|
||||
generate_error_reply(*rep, err);
|
||||
}
|
||||
}, res);
|
||||
|
||||
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 {
|
||||
handle_CORS(*req, *rep, false);
|
||||
return _f_handle(std::move(req), std::move(rep)).then(
|
||||
[this](std::unique_ptr<reply> rep) {
|
||||
rep->set_mime_type("application/x-amz-json-1.0");
|
||||
rep->done();
|
||||
rep->done(_type);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
void generate_error_reply(reply& rep, const api_error& err) {
|
||||
rep._content += "{\"__type\":\"com.amazonaws.dynamodb.v20120810#" + err._type + "\"," +
|
||||
"\"message\":\"" + err._msg + "\"}";
|
||||
rep._status = err._http_code;
|
||||
slogger.trace("api_handler error case: {}", rep._content);
|
||||
}
|
||||
|
||||
future_handler_function _f_handle;
|
||||
sstring _type;
|
||||
};
|
||||
|
||||
class gated_handler : public handler_base {
|
||||
seastar::gate& _gate;
|
||||
public:
|
||||
gated_handler(seastar::gate& gate) : _gate(gate) {}
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) = 0;
|
||||
virtual future<std::unique_ptr<reply>> handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) final override {
|
||||
return with_gate(_gate, [this, &path, req = std::move(req), rep = std::move(rep)] () mutable {
|
||||
return do_handle(path, std::move(req), std::move(rep));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class health_handler : public gated_handler {
|
||||
public:
|
||||
health_handler(seastar::gate& pending_requests) : gated_handler(pending_requests) {}
|
||||
protected:
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
handle_CORS(*req, *rep, false);
|
||||
rep->set_status(reply::status_type::ok);
|
||||
rep->write_body("txt", format("healthy: {}", req->get_header("Host")));
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
};
|
||||
|
||||
class local_nodelist_handler : public gated_handler {
|
||||
service::storage_proxy& _proxy;
|
||||
gms::gossiper& _gossiper;
|
||||
public:
|
||||
local_nodelist_handler(seastar::gate& pending_requests, service::storage_proxy& proxy, gms::gossiper& gossiper)
|
||||
: gated_handler(pending_requests)
|
||||
, _proxy(proxy)
|
||||
, _gossiper(gossiper) {}
|
||||
protected:
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
rjson::value results = rjson::empty_array();
|
||||
// It's very easy to get a list of all live nodes on the cluster,
|
||||
// using _gossiper().get_live_members(). But getting
|
||||
// just the list of live nodes in this DC needs more elaborate code:
|
||||
sstring local_dc = locator::i_endpoint_snitch::get_local_snitch_ptr()->get_datacenter(
|
||||
utils::fb_utilities::get_broadcast_address());
|
||||
std::unordered_set<gms::inet_address> local_dc_nodes =
|
||||
_proxy.get_token_metadata_ptr()->get_topology().get_datacenter_endpoints().at(local_dc);
|
||||
for (auto& ip : local_dc_nodes) {
|
||||
if (_gossiper.is_alive(ip)) {
|
||||
rjson::push_back(results, rjson::from_string(ip.to_sstring()));
|
||||
}
|
||||
}
|
||||
rep->set_status(reply::status_type::ok);
|
||||
rep->set_content_type("json");
|
||||
rep->_content = rjson::print(results);
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
};
|
||||
|
||||
// The CORS (Cross-origin resource sharing) protocol can send an OPTIONS
|
||||
// request before ("pre-flight") the main request. The response to this
|
||||
// request can be empty, but needs to have the right headers (which we
|
||||
// fill with handle_CORS())
|
||||
class options_handler : public gated_handler {
|
||||
public:
|
||||
options_handler(seastar::gate& pending_requests) : gated_handler(pending_requests) {}
|
||||
protected:
|
||||
virtual future<std::unique_ptr<reply>> do_handle(const sstring& path, std::unique_ptr<request> req, std::unique_ptr<reply> rep) override {
|
||||
handle_CORS(*req, *rep, true);
|
||||
rep->set_status(reply::status_type::ok);
|
||||
rep->write_body("txt", sstring(""));
|
||||
return make_ready_future<std::unique_ptr<reply>>(std::move(rep));
|
||||
}
|
||||
};
|
||||
|
||||
future<std::string> server::verify_signature(const request& req, const chunked_content& content) {
|
||||
if (!_enforce_authorization) {
|
||||
slogger.debug("Skipping authorization");
|
||||
return make_ready_future<std::string>("<unauthenticated request>");
|
||||
}
|
||||
auto host_it = req._headers.find("Host");
|
||||
if (host_it == req._headers.end()) {
|
||||
throw api_error::invalid_signature("Host header is mandatory for signature verification");
|
||||
}
|
||||
auto authorization_it = req._headers.find("Authorization");
|
||||
if (authorization_it == req._headers.end()) {
|
||||
throw api_error::missing_authentication_token("Authorization header is mandatory for signature verification");
|
||||
}
|
||||
std::string host = host_it->second;
|
||||
std::string_view authorization_header = authorization_it->second;
|
||||
auto pos = authorization_header.find_first_of(' ');
|
||||
if (pos == std::string_view::npos || authorization_header.substr(0, pos) != "AWS4-HMAC-SHA256") {
|
||||
throw api_error::invalid_signature(format("Authorization header must use AWS4-HMAC-SHA256 algorithm: {}", authorization_header));
|
||||
}
|
||||
authorization_header.remove_prefix(pos+1);
|
||||
std::string credential;
|
||||
std::string user_signature;
|
||||
std::string signed_headers_str;
|
||||
std::vector<std::string_view> signed_headers;
|
||||
do {
|
||||
// Either one of a comma or space can mark the end of an entry
|
||||
pos = authorization_header.find_first_of(" ,");
|
||||
std::string_view entry = authorization_header.substr(0, pos);
|
||||
if (pos != std::string_view::npos) {
|
||||
authorization_header.remove_prefix(pos + 1);
|
||||
}
|
||||
if (entry.empty()) {
|
||||
continue;
|
||||
}
|
||||
std::vector<std::string_view> entry_split = split(entry, '=');
|
||||
if (entry_split.size() != 2) {
|
||||
continue;
|
||||
}
|
||||
std::string_view auth_value = entry_split[1];
|
||||
if (entry_split[0] == "Credential") {
|
||||
credential = std::string(auth_value);
|
||||
} else if (entry_split[0] == "Signature") {
|
||||
user_signature = std::string(auth_value);
|
||||
} else if (entry_split[0] == "SignedHeaders") {
|
||||
signed_headers_str = std::string(auth_value);
|
||||
signed_headers = split(auth_value, ';');
|
||||
std::sort(signed_headers.begin(), signed_headers.end());
|
||||
}
|
||||
} while (pos != std::string_view::npos);
|
||||
|
||||
std::vector<std::string_view> credential_split = split(credential, '/');
|
||||
if (credential_split.size() != 5) {
|
||||
throw api_error::validation(format("Incorrect credential information format: {}", credential));
|
||||
}
|
||||
std::string user(credential_split[0]);
|
||||
std::string datestamp(credential_split[1]);
|
||||
std::string region(credential_split[2]);
|
||||
std::string service(credential_split[3]);
|
||||
|
||||
std::map<std::string_view, std::string_view> signed_headers_map;
|
||||
for (const auto& header : signed_headers) {
|
||||
signed_headers_map.emplace(header, std::string_view());
|
||||
}
|
||||
for (auto& header : req._headers) {
|
||||
std::string header_str;
|
||||
header_str.resize(header.first.size());
|
||||
std::transform(header.first.begin(), header.first.end(), header_str.begin(), ::tolower);
|
||||
auto it = signed_headers_map.find(header_str);
|
||||
if (it != signed_headers_map.end()) {
|
||||
it->second = std::string_view(header.second);
|
||||
}
|
||||
}
|
||||
|
||||
auto cache_getter = [&proxy = _proxy] (std::string username) {
|
||||
return get_key_from_roles(proxy, std::move(username));
|
||||
};
|
||||
return _key_cache.get_ptr(user, cache_getter).then([this, &req, &content,
|
||||
user = std::move(user),
|
||||
host = std::move(host),
|
||||
datestamp = std::move(datestamp),
|
||||
signed_headers_str = std::move(signed_headers_str),
|
||||
signed_headers_map = std::move(signed_headers_map),
|
||||
region = std::move(region),
|
||||
service = std::move(service),
|
||||
user_signature = std::move(user_signature)] (key_cache::value_ptr key_ptr) {
|
||||
std::string signature = get_signature(user, *key_ptr, std::string_view(host), req._method,
|
||||
datestamp, signed_headers_str, signed_headers_map, content, region, service, "");
|
||||
|
||||
if (signature != std::string_view(user_signature)) {
|
||||
_key_cache.remove(user);
|
||||
throw api_error::unrecognized_client("The security token included in the request is invalid.");
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
static tracing::trace_state_ptr create_tracing_session(tracing::tracing& tracing_instance) {
|
||||
tracing::trace_state_props_set props;
|
||||
props.set<tracing::trace_state_props::full_tracing>();
|
||||
props.set_if<tracing::trace_state_props::log_slow_query>(tracing_instance.slow_query_tracing_enabled());
|
||||
return tracing_instance.create_session(tracing::trace_type::QUERY, props);
|
||||
}
|
||||
|
||||
// truncated_content_view() prints a potentially long chunked_content for
|
||||
// debugging purposes. In the common case when the content is not excessively
|
||||
// long, it just returns a view into the given content, without any copying.
|
||||
// But when the content is very long, it is truncated after some arbitrary
|
||||
// max_len (or one chunk, whichever comes first), with "<truncated>" added at
|
||||
// the end. To do this modification to the string, we need to create a new
|
||||
// std::string, so the caller must pass us a reference to one, "buf", where
|
||||
// we can store the content. The returned view is only alive for as long this
|
||||
// buf is kept alive.
|
||||
static std::string_view truncated_content_view(const chunked_content& content, std::string& buf) {
|
||||
constexpr size_t max_len = 1024;
|
||||
if (content.empty()) {
|
||||
return std::string_view();
|
||||
} else if (content.size() == 1 && content.begin()->size() <= max_len) {
|
||||
return std::string_view(content.begin()->get(), content.begin()->size());
|
||||
} else {
|
||||
buf = std::string(content.begin()->get(), std::min(content.begin()->size(), max_len)) + "<truncated>";
|
||||
return std::string_view(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static tracing::trace_state_ptr maybe_trace_query(service::client_state& client_state, std::string_view username, sstring_view op, const chunked_content& query) {
|
||||
tracing::trace_state_ptr trace_state;
|
||||
tracing::tracing& tracing_instance = tracing::tracing::get_local_tracing_instance();
|
||||
if (tracing_instance.trace_next_query() || tracing_instance.slow_query_tracing_enabled()) {
|
||||
trace_state = create_tracing_session(tracing_instance);
|
||||
std::string buf;
|
||||
tracing::add_session_param(trace_state, "alternator_op", op);
|
||||
tracing::add_query(trace_state, truncated_content_view(query, buf));
|
||||
tracing::begin(trace_state, format("Alternator {}", op), client_state.get_client_address());
|
||||
tracing::set_username(trace_state, auth::authenticated_user(username));
|
||||
}
|
||||
return trace_state;
|
||||
}
|
||||
|
||||
future<executor::request_return_type> server::handle_api_request(std::unique_ptr<request> req) {
|
||||
_executor._stats.total_operations++;
|
||||
sstring target = req->get_header(TARGET);
|
||||
std::vector<std::string_view> split_target = split(target, '.');
|
||||
//NOTICE(sarna): Target consists of Dynamo API version followed by a dot '.' and operation type (e.g. CreateTable)
|
||||
std::string op = split_target.empty() ? std::string() : std::string(split_target.back());
|
||||
// JSON parsing can allocate up to roughly 2x the size of the raw
|
||||
// document, + a couple of bytes for maintenance.
|
||||
// TODO: consider the case where req->content_length is missing. Maybe
|
||||
// we need to take the content_length_limit and return some of the units
|
||||
// when we finish read_content_and_verify_signature?
|
||||
size_t mem_estimate = req->content_length * 2 + 8000;
|
||||
auto units_fut = get_units(*_memory_limiter, mem_estimate);
|
||||
if (_memory_limiter->waiters()) {
|
||||
++_executor._stats.requests_blocked_memory;
|
||||
}
|
||||
auto units = co_await std::move(units_fut);
|
||||
assert(req->content_stream);
|
||||
chunked_content content = co_await httpd::read_entire_stream(*req->content_stream);
|
||||
auto username = co_await verify_signature(*req, content);
|
||||
|
||||
if (slogger.is_enabled(log_level::trace)) {
|
||||
std::string buf;
|
||||
slogger.trace("Request: {} {} {}", op, truncated_content_view(content, buf), req->_headers);
|
||||
}
|
||||
auto callback_it = _callbacks.find(op);
|
||||
if (callback_it == _callbacks.end()) {
|
||||
_executor._stats.unsupported_operations++;
|
||||
co_return api_error::unknown_operation(format("Unsupported operation {}", op));
|
||||
}
|
||||
if (_pending_requests.get_count() >= _max_concurrent_requests) {
|
||||
_executor._stats.requests_shed++;
|
||||
co_return api_error::request_limit_exceeded(format("too many in-flight requests (configured via max_concurrent_requests_per_shard): {}", _pending_requests.get_count()));
|
||||
}
|
||||
_pending_requests.enter();
|
||||
auto leave = defer([this] () noexcept { _pending_requests.leave(); });
|
||||
//FIXME: Client state can provide more context, e.g. client's endpoint address
|
||||
// We use unique_ptr because client_state cannot be moved or copied
|
||||
executor::client_state client_state{executor::client_state::internal_tag()};
|
||||
tracing::trace_state_ptr trace_state = maybe_trace_query(client_state, username, op, content);
|
||||
tracing::trace(trace_state, op);
|
||||
rjson::value json_request = co_await _json_parser.parse(std::move(content));
|
||||
co_return co_await callback_it->second(_executor, client_state, trace_state,
|
||||
make_service_permit(std::move(units)), std::move(json_request), std::move(req));
|
||||
}
|
||||
|
||||
void server::set_routes(routes& r) {
|
||||
api_handler* req_handler = new api_handler([this] (std::unique_ptr<request> req) mutable {
|
||||
return handle_api_request(std::move(req));
|
||||
});
|
||||
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); }},
|
||||
};
|
||||
|
||||
r.put(operation_type::POST, "/", req_handler);
|
||||
r.put(operation_type::GET, "/", new health_handler(_pending_requests));
|
||||
// The "/localnodes" request is a new Alternator feature, not supported by
|
||||
// DynamoDB and not required for DynamoDB compatibility. It allows a
|
||||
// client to enquire - using a trivial HTTP request without requiring
|
||||
// authentication - the list of all live nodes in the same data center of
|
||||
// the Alternator cluster. The client can use this list to balance its
|
||||
// request load to all the nodes in the same geographical region.
|
||||
// Note that this API exposes - openly without authentication - the
|
||||
// information on the cluster's members inside one data center. We do not
|
||||
// consider this to be a security risk, because an attacker can already
|
||||
// scan an entire subnet for nodes responding to the health request,
|
||||
// or even just scan for open ports.
|
||||
r.put(operation_type::GET, "/localnodes", new local_nodelist_handler(_pending_requests, _proxy, _gossiper));
|
||||
r.put(operation_type::OPTIONS, "/", new options_handler(_pending_requests));
|
||||
}
|
||||
|
||||
//FIXME: A way to immediately invalidate the cache should be considered,
|
||||
// e.g. when the system table which stores the keys is changed.
|
||||
// For now, this propagation may take up to 1 minute.
|
||||
server::server(executor& exec, service::storage_proxy& proxy, gms::gossiper& gossiper)
|
||||
: _http_server("http-alternator")
|
||||
, _https_server("https-alternator")
|
||||
, _executor(exec)
|
||||
, _proxy(proxy)
|
||||
, _gossiper(gossiper)
|
||||
, _key_cache(1024, 1min, slogger)
|
||||
, _enforce_authorization(false)
|
||||
, _enabled_servers{}
|
||||
, _pending_requests{}
|
||||
, _callbacks{
|
||||
{"CreateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.create_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DeleteTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.delete_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UpdateTable", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.update_table(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"PutItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.put_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UpdateItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.update_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DeleteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.delete_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListTables", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_tables(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"Scan", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.scan(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeEndpoints", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_endpoints(client_state, std::move(permit), std::move(json_request), req->get_header("Host"));
|
||||
}},
|
||||
{"BatchWriteItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.batch_write_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"BatchGetItem", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.batch_get_item(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"Query", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.query(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"TagResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.tag_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UntagResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.untag_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListTagsOfResource", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_tags_of_resource(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"UpdateTimeToLive", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.update_time_to_live(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeTimeToLive", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_time_to_live(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"ListStreams", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.list_streams(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"DescribeStream", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.describe_stream(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetShardIterator", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_shard_iterator(client_state, std::move(permit), std::move(json_request));
|
||||
}},
|
||||
{"GetRecords", [] (executor& e, executor::client_state& client_state, tracing::trace_state_ptr trace_state, service_permit permit, rjson::value json_request, std::unique_ptr<request> req) {
|
||||
return e.get_records(client_state, std::move(trace_state), std::move(permit), std::move(json_request));
|
||||
}},
|
||||
} {
|
||||
}
|
||||
|
||||
future<> server::init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
bool enforce_authorization, semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests) {
|
||||
_memory_limiter = memory_limiter;
|
||||
_enforce_authorization = enforce_authorization;
|
||||
_max_concurrent_requests = std::move(max_concurrent_requests);
|
||||
if (!port && !https_port) {
|
||||
return make_exception_future<>(std::runtime_error("Either regular port or TLS port"
|
||||
" must be specified in order to init an alternator HTTP server instance"));
|
||||
}
|
||||
return seastar::async([this, addr, port, https_port, creds] {
|
||||
try {
|
||||
_executor.start().get();
|
||||
|
||||
if (port) {
|
||||
set_routes(_http_server._routes);
|
||||
_http_server.set_content_length_limit(server::content_length_limit);
|
||||
_http_server.set_content_streaming(true);
|
||||
_http_server.listen(socket_address{addr, *port}).get();
|
||||
_enabled_servers.push_back(std::ref(_http_server));
|
||||
}
|
||||
if (https_port) {
|
||||
set_routes(_https_server._routes);
|
||||
_https_server.set_content_length_limit(server::content_length_limit);
|
||||
_https_server.set_content_streaming(true);
|
||||
_https_server.set_tls_credentials(creds->build_reloadable_server_credentials([](const std::unordered_set<sstring>& files, std::exception_ptr ep) {
|
||||
if (ep) {
|
||||
slogger.warn("Exception loading {}: {}", files, ep);
|
||||
} else {
|
||||
slogger.info("Reloaded {}", files);
|
||||
}
|
||||
}).get0());
|
||||
_https_server.listen(socket_address{addr, *https_port}).get();
|
||||
_enabled_servers.push_back(std::ref(_https_server));
|
||||
}
|
||||
} catch (...) {
|
||||
slogger.error("Failed to set up Alternator HTTP server on {} port {}, TLS port {}: {}",
|
||||
addr, port ? std::to_string(*port) : "OFF", https_port ? std::to_string(*https_port) : "OFF", std::current_exception());
|
||||
std::throw_with_nested(std::runtime_error(
|
||||
format("Failed to set up Alternator HTTP server on {} port {}, TLS port {}",
|
||||
addr, port ? std::to_string(*port) : "OFF", https_port ? std::to_string(*https_port) : "OFF")));
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
future<> server::stop() {
|
||||
return parallel_for_each(_enabled_servers, [] (http_server& server) {
|
||||
return server.stop();
|
||||
}).then([this] {
|
||||
return _pending_requests.close();
|
||||
}).then([this] {
|
||||
return _json_parser.stop();
|
||||
});
|
||||
}
|
||||
|
||||
server::json_parser::json_parser() : _run_parse_json_thread(async([this] {
|
||||
while (true) {
|
||||
_document_waiting.wait().get();
|
||||
if (_as.abort_requested()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_parsed_document = rjson::parse_yieldable(std::move(_raw_document));
|
||||
_current_exception = nullptr;
|
||||
} catch (...) {
|
||||
_current_exception = std::current_exception();
|
||||
}
|
||||
_document_parsed.signal();
|
||||
}
|
||||
})) {
|
||||
}
|
||||
|
||||
future<rjson::value> server::json_parser::parse(chunked_content&& content) {
|
||||
if (content.size() < yieldable_parsing_threshold) {
|
||||
return make_ready_future<rjson::value>(rjson::parse(std::move(content)));
|
||||
}
|
||||
return with_semaphore(_parsing_sem, 1, [this, content = std::move(content)] () mutable {
|
||||
_raw_document = std::move(content);
|
||||
_document_waiting.signal();
|
||||
return _document_parsed.wait().then([this] {
|
||||
if (_current_exception) {
|
||||
return make_exception_future<rjson::value>(_current_exception);
|
||||
}
|
||||
return make_ready_future<rjson::value>(std::move(_parsed_document));
|
||||
//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::json_parser::stop() {
|
||||
_as.request_abort();
|
||||
_document_waiting.signal();
|
||||
_document_parsed.broken();
|
||||
return std::move(_run_parse_json_thread);
|
||||
}
|
||||
|
||||
const char* api_error::what() const noexcept {
|
||||
if (_what_string.empty()) {
|
||||
_what_string = format("{} {}: {}", _http_code, _type, _msg);
|
||||
}
|
||||
return _what_string.c_str();
|
||||
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,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -23,71 +23,19 @@
|
||||
|
||||
#include "alternator/executor.hh"
|
||||
#include <seastar/core/future.hh>
|
||||
#include <seastar/core/condition-variable.hh>
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include <seastar/net/tls.hh>
|
||||
#include <optional>
|
||||
#include "alternator/auth.hh"
|
||||
#include "utils/small_vector.hh"
|
||||
#include "utils/updateable_value.hh"
|
||||
#include <seastar/core/units.hh>
|
||||
|
||||
namespace alternator {
|
||||
|
||||
using chunked_content = rjson::chunked_content;
|
||||
|
||||
class server {
|
||||
static constexpr size_t content_length_limit = 16*MB;
|
||||
using alternator_callback = std::function<future<executor::request_return_type>(executor&, executor::client_state&,
|
||||
tracing::trace_state_ptr, service_permit, rjson::value, std::unique_ptr<request>)>;
|
||||
using alternator_callbacks_map = std::unordered_map<std::string_view, alternator_callback>;
|
||||
|
||||
http_server _http_server;
|
||||
http_server _https_server;
|
||||
executor& _executor;
|
||||
service::storage_proxy& _proxy;
|
||||
gms::gossiper& _gossiper;
|
||||
|
||||
key_cache _key_cache;
|
||||
bool _enforce_authorization;
|
||||
utils::small_vector<std::reference_wrapper<seastar::httpd::http_server>, 2> _enabled_servers;
|
||||
gate _pending_requests;
|
||||
alternator_callbacks_map _callbacks;
|
||||
|
||||
semaphore* _memory_limiter;
|
||||
utils::updateable_value<uint32_t> _max_concurrent_requests;
|
||||
|
||||
class json_parser {
|
||||
static constexpr size_t yieldable_parsing_threshold = 16*KB;
|
||||
chunked_content _raw_document;
|
||||
rjson::value _parsed_document;
|
||||
std::exception_ptr _current_exception;
|
||||
semaphore _parsing_sem{1};
|
||||
condition_variable _document_waiting;
|
||||
condition_variable _document_parsed;
|
||||
abort_source _as;
|
||||
future<> _run_parse_json_thread;
|
||||
public:
|
||||
json_parser();
|
||||
// Moving a chunked_content into parse() allows parse() to free each
|
||||
// chunk as soon as it is parsed, so when chunks are relatively small,
|
||||
// we don't need to store the sum of unparsed and parsed sizes.
|
||||
future<rjson::value> parse(chunked_content&& content);
|
||||
future<> stop();
|
||||
};
|
||||
json_parser _json_parser;
|
||||
|
||||
seastar::httpd::http_server_control _control;
|
||||
seastar::sharded<executor>& _executor;
|
||||
public:
|
||||
server(executor& executor, service::storage_proxy& proxy, gms::gossiper& gossiper);
|
||||
server(seastar::sharded<executor>& executor) : _executor(executor) {}
|
||||
|
||||
future<> init(net::inet_address addr, std::optional<uint16_t> port, std::optional<uint16_t> https_port, std::optional<tls::credentials_builder> creds,
|
||||
bool enforce_authorization, semaphore* memory_limiter, utils::updateable_value<uint32_t> max_concurrent_requests);
|
||||
future<> stop();
|
||||
seastar::future<> init(net::inet_address addr, uint16_t port);
|
||||
private:
|
||||
void set_routes(seastar::httpd::routes& r);
|
||||
// If verification succeeds, returns the authenticated user's username
|
||||
future<std::string> verify_signature(const seastar::httpd::request&, const chunked_content&);
|
||||
future<executor::request_return_type> handle_api_request(std::unique_ptr<request> req);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "stats.hh"
|
||||
#include "utils/histogram_metrics_helper.hh"
|
||||
|
||||
#include <seastar/core/metrics.hh>
|
||||
|
||||
namespace alternator {
|
||||
@@ -37,8 +37,7 @@ stats::stats() : api_operations{} {
|
||||
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 to_metrics_histogram(api_operations.name);}),
|
||||
OPERATION(batch_get_item, "BatchGetItem")
|
||||
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")
|
||||
@@ -78,11 +77,6 @@ stats::stats() : api_operations{} {
|
||||
OPERATION_LATENCY(get_item_latency, "GetItem")
|
||||
OPERATION_LATENCY(delete_item_latency, "DeleteItem")
|
||||
OPERATION_LATENCY(update_item_latency, "UpdateItem")
|
||||
OPERATION(list_streams, "ListStreams")
|
||||
OPERATION(describe_stream, "DescribeStream")
|
||||
OPERATION(get_shard_iterator, "GetShardIterator")
|
||||
OPERATION(get_records, "GetRecords")
|
||||
OPERATION_LATENCY(get_records_latency, "GetRecords")
|
||||
});
|
||||
_metrics.add_group("alternator", {
|
||||
seastar::metrics::make_total_operations("unsupported_operations", unsupported_operations,
|
||||
@@ -91,14 +85,6 @@ stats::stats() : api_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("write_using_lwt", write_using_lwt,
|
||||
seastar::metrics::description("number of writes that used LWT")),
|
||||
seastar::metrics::make_total_operations("shard_bounce_for_lwt", shard_bounce_for_lwt,
|
||||
seastar::metrics::description("number writes that had to be bounced from this shard because of LWT requirements")),
|
||||
seastar::metrics::make_total_operations("requests_blocked_memory", requests_blocked_memory,
|
||||
seastar::metrics::description("Counts a number of requests blocked due to memory pressure.")),
|
||||
seastar::metrics::make_total_operations("requests_shed", requests_shed,
|
||||
seastar::metrics::description("Counts a number of requests shed due to overload.")),
|
||||
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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-present ScyllaDB
|
||||
* Copyright 2019 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -74,25 +74,16 @@ public:
|
||||
uint64_t update_item = 0;
|
||||
uint64_t update_table = 0;
|
||||
uint64_t update_time_to_live = 0;
|
||||
uint64_t list_streams = 0;
|
||||
uint64_t describe_stream = 0;
|
||||
uint64_t get_shard_iterator = 0;
|
||||
uint64_t get_records = 0;
|
||||
|
||||
utils::time_estimated_histogram put_item_latency;
|
||||
utils::time_estimated_histogram get_item_latency;
|
||||
utils::time_estimated_histogram delete_item_latency;
|
||||
utils::time_estimated_histogram update_item_latency;
|
||||
utils::time_estimated_histogram get_records_latency;
|
||||
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;
|
||||
uint64_t write_using_lwt = 0;
|
||||
uint64_t shard_bounce_for_lwt = 0;
|
||||
uint64_t requests_blocked_memory = 0;
|
||||
uint64_t requests_shed = 0;
|
||||
// CQL-derived stats
|
||||
cql3::cql_stats cql_stats;
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-present 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 "serializer.hh"
|
||||
#include "schema.hh"
|
||||
#include "db/extensions.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
class tags_extension : public schema_extension {
|
||||
public:
|
||||
static constexpr auto NAME = "scylla_tags";
|
||||
|
||||
tags_extension() = default;
|
||||
explicit tags_extension(const std::map<sstring, sstring>& tags) : _tags(std::move(tags)) {}
|
||||
explicit tags_extension(bytes b) : _tags(tags_extension::deserialize(b)) {}
|
||||
explicit tags_extension(const sstring& s) {
|
||||
throw std::logic_error("Cannot create tags from string");
|
||||
}
|
||||
bytes serialize() const override {
|
||||
return ser::serialize_to_buffer<bytes>(_tags);
|
||||
}
|
||||
static std::map<sstring, sstring> deserialize(bytes_view buffer) {
|
||||
return ser::deserialize_from_buffer(buffer, boost::type<std::map<sstring, sstring>>());
|
||||
}
|
||||
const std::map<sstring, sstring>& tags() const {
|
||||
return _tags;
|
||||
}
|
||||
private:
|
||||
std::map<sstring, sstring> _tags;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-present 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 <seastar/core/sstring.hh>
|
||||
#include <seastar/core/coroutine.hh>
|
||||
|
||||
#include "executor.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
#include "gms/feature_service.hh"
|
||||
#include "database.hh"
|
||||
#include "utils/rjson.hh"
|
||||
|
||||
namespace alternator {
|
||||
|
||||
// We write the expiration-time attribute enabled on a table using a
|
||||
// tag TTL_TAG_KEY.
|
||||
// Currently, the *value* of this tag is simply the name of the attribute,
|
||||
// and the expiration scanner interprets it as an Alternator attribute name -
|
||||
// It can refer to a real column or if that doesn't exist, to a member of
|
||||
// the ":attrs" map column. Although this is designed for Alternator, it may
|
||||
// be good enough for CQL as well (there, the ":attrs" column won't exist).
|
||||
static const sstring TTL_TAG_KEY("system:ttl_attribute");
|
||||
|
||||
future<executor::request_return_type> executor::update_time_to_live(client_state& client_state, service_permit permit, rjson::value request) {
|
||||
_stats.api_operations.update_time_to_live++;
|
||||
if (!_proxy.get_db().local().features().cluster_supports_alternator_ttl()) {
|
||||
co_return api_error::unknown_operation("UpdateTimeToLive not yet supported. Experimental support is available if the 'alternator_ttl' experimental feature is enabled on all nodes.");
|
||||
}
|
||||
|
||||
schema_ptr schema = get_table(_proxy, request);
|
||||
rjson::value* spec = rjson::find(request, "TimeToLiveSpecification");
|
||||
if (!spec || !spec->IsObject()) {
|
||||
co_return api_error::validation("UpdateTimeToLive missing mandatory TimeToLiveSpecification");
|
||||
}
|
||||
const rjson::value* v = rjson::find(*spec, "Enabled");
|
||||
if (!v || !v->IsBool()) {
|
||||
co_return api_error::validation("UpdateTimeToLive requires boolean Enabled");
|
||||
}
|
||||
bool enabled = v->GetBool();
|
||||
v = rjson::find(*spec, "AttributeName");
|
||||
if (!v || !v->IsString()) {
|
||||
co_return api_error::validation("UpdateTimeToLive requires string AttributeName");
|
||||
}
|
||||
// Although the DynamoDB documentation specifies that attribute names
|
||||
// should be between 1 and 64K bytes, in practice, it only allows
|
||||
// between 1 and 255 bytes. There are no other limitations on which
|
||||
// characters are allowed in the name.
|
||||
if (v->GetStringLength() < 1 || v->GetStringLength() > 255) {
|
||||
co_return api_error::validation("The length of AttributeName must be between 1 and 255");
|
||||
}
|
||||
sstring attribute_name(v->GetString(), v->GetStringLength());
|
||||
|
||||
std::map<sstring, sstring> tags_map = get_tags_of_table(schema);
|
||||
if (enabled) {
|
||||
if (tags_map.contains(TTL_TAG_KEY)) {
|
||||
co_return api_error::validation("TTL is already enabled");
|
||||
}
|
||||
tags_map[TTL_TAG_KEY] = attribute_name;
|
||||
} else {
|
||||
auto i = tags_map.find(TTL_TAG_KEY);
|
||||
if (i == tags_map.end()) {
|
||||
co_return api_error::validation("TTL is already disabled");
|
||||
} else if (i->second != attribute_name) {
|
||||
co_return api_error::validation(format(
|
||||
"Requested to disable TTL on attribute {}, but a different attribute {} is enabled.",
|
||||
attribute_name, i->second));
|
||||
}
|
||||
tags_map.erase(TTL_TAG_KEY);
|
||||
}
|
||||
co_await update_tags(_mm, schema, std::move(tags_map));
|
||||
// Prepare the response, which contains a TimeToLiveSpecification
|
||||
// basically identical to the request's
|
||||
rjson::value response = rjson::empty_object();
|
||||
rjson::add(response, "TimeToLiveSpecification", std::move(*spec));
|
||||
co_return make_jsonable(std::move(response));
|
||||
}
|
||||
|
||||
future<executor::request_return_type> executor::describe_time_to_live(client_state& client_state, service_permit permit, rjson::value request) {
|
||||
_stats.api_operations.describe_time_to_live++;
|
||||
schema_ptr schema = get_table(_proxy, request);
|
||||
std::map<sstring, sstring> tags_map = get_tags_of_table(schema);
|
||||
rjson::value desc = rjson::empty_object();
|
||||
auto i = tags_map.find(TTL_TAG_KEY);
|
||||
if (i == tags_map.end()) {
|
||||
rjson::add(desc, "TimeToLiveStatus", "DISABLED");
|
||||
} else {
|
||||
rjson::add(desc, "TimeToLiveStatus", "ENABLED");
|
||||
rjson::add(desc, "AttributeName", rjson::from_string(i->second));
|
||||
}
|
||||
rjson::value response = rjson::empty_object();
|
||||
rjson::add(response, "TimeToLiveDescription", std::move(desc));
|
||||
co_return make_jsonable(std::move(response));
|
||||
}
|
||||
|
||||
} // namespace alternator
|
||||
@@ -13,7 +13,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get row cache save period in seconds",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_row_cache_save_period_in_seconds",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -35,7 +35,7 @@
|
||||
"description":"row cache save period in seconds",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get key cache save period in seconds",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_key_cache_save_period_in_seconds",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -70,7 +70,7 @@
|
||||
"description":"key cache save period in seconds",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -83,7 +83,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get counter cache save period in seconds",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_counter_cache_save_period_in_seconds",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -105,7 +105,7 @@
|
||||
"description":"counter cache save period in seconds",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -118,7 +118,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get row cache keys to save",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_row_cache_keys_to_save",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -140,7 +140,7 @@
|
||||
"description":"row cache keys to save",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -153,7 +153,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get key cache keys to save",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_key_cache_keys_to_save",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -175,7 +175,7 @@
|
||||
"description":"key cache keys to save",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -188,7 +188,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get counter cache keys to save",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_counter_cache_keys_to_save",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -210,7 +210,7 @@
|
||||
"description":"counter cache keys to save",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -448,7 +448,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get key entries",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_key_entries",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -568,7 +568,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get row entries",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_row_entries",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -688,7 +688,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get counter entries",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_counter_entries",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Force a major compaction of this column family",
|
||||
"type":"void",
|
||||
"type":"string",
|
||||
"nickname":"force_major_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -89,7 +89,7 @@
|
||||
"description":"true if the output of the major compaction should be split in several sstables",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"type":"bool",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -121,7 +121,7 @@
|
||||
"description":"The minimum number of sstables in queue before compaction kicks off",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -172,7 +172,7 @@
|
||||
"description":"The maximum number of sstables in queue before compaction kicks off",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -223,7 +223,7 @@
|
||||
"description":"The maximum number of sstables in queue before compaction kicks off",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
@@ -231,7 +231,7 @@
|
||||
"description":"The minimum number of sstables in queue before compaction kicks off",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -380,54 +380,16 @@
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"check if the auto_compaction property is enabled for a given table",
|
||||
"summary":"check if the auto compaction disabled",
|
||||
"type":"boolean",
|
||||
"nickname":"get_auto_compaction",
|
||||
"nickname":"is_auto_compaction_disabled",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Enable table auto compaction",
|
||||
"type":"void",
|
||||
"nickname":"enable_auto_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Disable table auto compaction",
|
||||
"type":"void",
|
||||
"nickname":"disable_auto_compaction",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"name",
|
||||
"description":"The table name in keyspace:name format",
|
||||
"description":"The column family name in keyspace:name format",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
@@ -582,7 +544,7 @@
|
||||
"summary":"sstable count for each level. empty unless leveled compaction is used",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type": "long"
|
||||
"type":"int"
|
||||
},
|
||||
"nickname":"get_sstable_count_per_level",
|
||||
"produces":[
|
||||
@@ -674,7 +636,7 @@
|
||||
"description":"Duration (in milliseconds) of monitoring operation",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
@@ -682,7 +644,7 @@
|
||||
"description":"number of the top partitions to list",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
@@ -690,7 +652,7 @@
|
||||
"description":"capacity of stream summary: determines amount of resources used in query processing",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -959,7 +921,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get memtable switch count",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_memtable_switch_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -983,7 +945,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all memtable switch count",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_memtable_switch_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1120,7 +1082,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get read latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_read_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1273,7 +1235,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all read latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_read_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1289,7 +1251,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get range latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_range_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1313,7 +1275,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all range latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_range_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1329,7 +1291,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get write latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_write_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1482,7 +1444,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all write latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_write_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1498,7 +1460,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get pending flushes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_pending_flushes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1522,7 +1484,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all pending flushes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_pending_flushes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1538,7 +1500,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get pending compactions",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_pending_compactions",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1562,7 +1524,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all pending compactions",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_pending_compactions",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1578,7 +1540,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get live ss table count",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_live_ss_table_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1602,7 +1564,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all live ss table count",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_live_ss_table_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1618,7 +1580,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get live disk space used",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_live_disk_space_used",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1642,7 +1604,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all live disk space used",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_live_disk_space_used",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1658,7 +1620,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get total disk space used",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_total_disk_space_used",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1682,7 +1644,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all total disk space used",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_total_disk_space_used",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2138,7 +2100,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get speculative retries",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_speculative_retries",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2162,7 +2124,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all speculative retries",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_speculative_retries",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2242,7 +2204,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get row cache hit out of range",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_row_cache_hit_out_of_range",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2266,7 +2228,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all row cache hit out of range",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_row_cache_hit_out_of_range",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2282,7 +2244,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get row cache hit",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_row_cache_hit",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2306,7 +2268,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all row cache hit",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_row_cache_hit",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2322,7 +2284,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get row cache miss",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_row_cache_miss",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2346,7 +2308,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all row cache miss",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_row_cache_miss",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2362,7 +2324,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get cas prepare",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_cas_prepare",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2386,7 +2348,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get cas propose",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_cas_propose",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2410,7 +2372,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get cas commit",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_cas_commit",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2925,10 +2887,6 @@
|
||||
"id":"toppartitions_query_results",
|
||||
"description":"nodetool toppartitions query results",
|
||||
"properties":{
|
||||
"read_cardinality":{
|
||||
"type":"long",
|
||||
"description":"Number of the unique operations in the sample set"
|
||||
},
|
||||
"read":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
@@ -2936,10 +2894,6 @@
|
||||
},
|
||||
"description":"Read results"
|
||||
},
|
||||
"write_cardinality":{
|
||||
"type":"long",
|
||||
"description":"Number of the unique operations in the sample set"
|
||||
},
|
||||
"write":{
|
||||
"type":"array",
|
||||
"items":{
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get pending tasks",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_pending_tasks",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -181,7 +181,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get bytes compacted",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_bytes_compacted",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -197,7 +197,7 @@
|
||||
"description":"A row merged information",
|
||||
"properties":{
|
||||
"key":{
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"description":"The number of sstable"
|
||||
},
|
||||
"value":{
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"apiVersion":"0.0.1",
|
||||
"swaggerVersion":"1.2",
|
||||
"basePath":"{{Protocol}}://{{Host}}",
|
||||
"resourcePath":"/error_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"apis":[
|
||||
{
|
||||
"path":"/v2/error_injection/injection/{injection}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Activate an injection that triggers an error in code",
|
||||
"type":"void",
|
||||
"nickname":"enable_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"injection",
|
||||
"description":"injection name, should correspond to an injection added in code",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
},
|
||||
{
|
||||
"name":"one_shot",
|
||||
"description":"boolean flag indicating whether the injection should be enabled to trigger only once",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Deactivate an injection previously activated by the API",
|
||||
"type":"void",
|
||||
"nickname":"disable_injection",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"injection",
|
||||
"description":"injection name",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/v2/error_injection/injection",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"List all enabled injections on all shards, i.e. injections that will trigger an error in the code",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string"
|
||||
},
|
||||
"nickname":"get_enabled_injections_on_all",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
},
|
||||
{
|
||||
"method":"DELETE",
|
||||
"summary":"Deactivate all injections previously activated on all shards by the API",
|
||||
"type":"void",
|
||||
"nickname":"disable_on_all",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -110,7 +110,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get count down endpoint",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_down_endpoint_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get count up endpoint",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_up_endpoint_count",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -180,11 +180,11 @@
|
||||
"description": "The endpoint address"
|
||||
},
|
||||
"generation": {
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"description": "The heart beat generation"
|
||||
},
|
||||
"version": {
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"description": "The heart beat version"
|
||||
},
|
||||
"update_time": {
|
||||
@@ -209,7 +209,7 @@
|
||||
"description": "Holds a version value for an application state",
|
||||
"properties": {
|
||||
"application_state": {
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"description": "The application state enum index"
|
||||
},
|
||||
"value": {
|
||||
@@ -217,7 +217,7 @@
|
||||
"description": "The version value"
|
||||
},
|
||||
"version": {
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"description": "The application state version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Returns files which are pending for archival attempt. Does NOT include failed archive attempts",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_current_generation_number",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -99,7 +99,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get heart beat version for a node",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_current_heart_beat_version",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -148,30 +148,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/gossiper/force_remove_endpoint/{addr}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Force remove an endpoint from gossip",
|
||||
"type":"void",
|
||||
"nickname":"force_remove_endpoint",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"addr",
|
||||
"description":"The endpoint address",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,61 +7,6 @@
|
||||
"application/json"
|
||||
],
|
||||
"apis":[
|
||||
{
|
||||
"path":"/hinted_handoff/sync_point",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Creates a hints sync point. It can be used to wait until hints between given nodes are replayed. A sync point allows you to wait for hints accumulated at the moment of its creation - it won't wait for hints generated later. A sync point is described entirely by its ID - there is no state kept server-side, so there is no need to delete it.",
|
||||
"type":"string",
|
||||
"nickname":"create_hints_sync_point",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"target_hosts",
|
||||
"description":"A list of nodes towards which hints should be replayed. Multiple hosts can be listed by separating them with commas. If not provided or empty, the point will resolve when current hints towards all nodes in the cluster are sent.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get the status of a hints sync point, possibly waiting for it to be reached.",
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"DONE",
|
||||
"IN_PROGRESS"
|
||||
],
|
||||
"nickname":"get_hints_sync_point",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"id",
|
||||
"description":"The ID of the hint sync point which should be checked or waited on",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"timeout",
|
||||
"description":"Timeout in seconds after which the query returns even if hints are still being replayed. No value or 0 will cause the query to return immediately. A negative value will cause the query to wait until the sync point is reached",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"long",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/hinted_handoff/hints",
|
||||
"operations":[
|
||||
@@ -154,7 +99,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get create hint count",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_create_hint_count",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -178,7 +123,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get not stored hints count",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_not_stored_hints_count",
|
||||
"produces": [
|
||||
"application/json"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"items":{
|
||||
"type":"message_counter"
|
||||
},
|
||||
"nickname":"get_replied_messages",
|
||||
"nickname":"get_completed_messages",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
@@ -191,7 +191,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get the version number",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_version",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -249,10 +249,10 @@
|
||||
"MIGRATION_REQUEST",
|
||||
"PREPARE_MESSAGE",
|
||||
"PREPARE_DONE_MESSAGE",
|
||||
"UNUSED__STREAM_MUTATION",
|
||||
"STREAM_MUTATION",
|
||||
"STREAM_MUTATION_DONE",
|
||||
"COMPLETE_MESSAGE",
|
||||
"UNUSED__REPAIR_CHECKSUM_RANGE",
|
||||
"REPAIR_CHECKSUM_RANGE",
|
||||
"GET_SCHEMA_VERSION"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"summary":"Get the hinted handoff enabled by dc",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"array"
|
||||
"type":"mapper_list"
|
||||
},
|
||||
"nickname":"get_hinted_handoff_enabled_by_dc",
|
||||
"produces":[
|
||||
@@ -105,7 +105,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get the max hint window",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_max_hint_window",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -128,7 +128,7 @@
|
||||
"description":"max hint window in ms",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -141,7 +141,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get max hints in progress",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_max_hints_in_progress",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -164,7 +164,7 @@
|
||||
"description":"max hints in progress",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -177,7 +177,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get hints in progress",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_hints_in_progress",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -602,7 +602,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas write metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_cas_write_metrics_unfinished_commit",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -632,7 +632,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas write metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_cas_write_metrics_condition_not_met",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -641,28 +641,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_write/failed_read_round_optimization",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas write metrics",
|
||||
"type": "long",
|
||||
"nickname": "get_cas_write_metrics_failed_read_round_optimization",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_read/unfinished_commit",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas read metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_cas_read_metrics_unfinished_commit",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -686,13 +671,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/cas_read/condition_not_met",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get cas read metrics",
|
||||
"type": "int",
|
||||
"nickname": "get_cas_read_metrics_condition_not_met",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/storage_proxy/metrics/read/timeouts",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get read metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_read_metrics_timeouts",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -707,7 +707,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get read metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_read_metrics_unavailables",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -842,7 +842,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get range metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_range_metrics_timeouts",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -857,7 +857,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get range metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_range_metrics_unavailables",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -902,7 +902,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get write metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_write_metrics_timeouts",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -917,7 +917,7 @@
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get write metrics",
|
||||
"type": "long",
|
||||
"type": "int",
|
||||
"nickname": "get_write_metrics_unavailables",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1023,7 +1023,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get read latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_read_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1055,7 +1055,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get write latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_write_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1087,7 +1087,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get range latency",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_range_latency",
|
||||
"produces":[
|
||||
"application/json"
|
||||
|
||||
@@ -104,68 +104,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/toppartitions/",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Toppartitions query",
|
||||
"type":"toppartitions_query_results",
|
||||
"nickname":"toppartitions_generic",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"table_filters",
|
||||
"description":"Optional list of table name filters in keyspace:name format",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string"
|
||||
},
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"keyspace_filters",
|
||||
"description":"Optional list of keyspace filters",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string"
|
||||
},
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"duration",
|
||||
"description":"Duration (in milliseconds) of monitoring operation",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"list_size",
|
||||
"description":"number of the top partitions to list",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"capacity",
|
||||
"description":"capacity of stream summary: determines amount of resources used in query processing",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/nodes/leaving",
|
||||
"operations":[
|
||||
@@ -520,7 +458,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Return the generation value for this node.",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_current_generation_number",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -573,21 +511,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/cdc_streams_check_and_repair",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Checks that CDC streams reflect current cluster topology and regenerates them if not.",
|
||||
"type":"void",
|
||||
"nickname":"cdc_streams_check_and_repair",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/snapshots",
|
||||
"operations":[
|
||||
@@ -637,14 +560,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"sf",
|
||||
"description":"Skip flush. When set to \"true\", do not flush memtables before snapshotting (snapshot will not contain unflushed data)",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -667,15 +582,7 @@
|
||||
},
|
||||
{
|
||||
"name":"kn",
|
||||
"description":"Comma seperated keyspaces name that their snapshot will be deleted",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"cf",
|
||||
"description":"an optional table name that its snapshot will be deleted",
|
||||
"description":"Comma seperated keyspaces name to snapshot",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
@@ -739,7 +646,7 @@
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Trigger a cleanup of keys on a single keyspace",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"force_keyspace_cleanup",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -770,8 +677,8 @@
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Scrub (deserialize + reserialize at the latest version, resolving corruptions if any) the given keyspace. If columnFamilies array is empty, all CFs are scrubbed. Scrubbed CFs will be snapshotted first, if disableSnapshot is false. Scrub has the following modes: Abort (default) - abort scrub if corruption is detected; Skip (same as `skip_corrupted=true`) skip over corrupt data, omitting them from the output; Segregate - segregate data into multiple sstables if needed, such that each sstable contains data with valid order; Validate - read (no rewrite) and validate data, logging any problems found.",
|
||||
"type": "long",
|
||||
"summary":"Scrub (deserialize + reserialize at the latest version, skipping bad rows if any) the given keyspace. If columnFamilies array is empty, all CFs are scrubbed. Scrubbed CFs will be snapshotted first, if disableSnapshot is false",
|
||||
"type":"int",
|
||||
"nickname":"scrub",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -793,20 +700,6 @@
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"scrub_mode",
|
||||
"description":"How to handle corrupt data (overrides 'skip_corrupted'); ",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"ABORT",
|
||||
"SKIP",
|
||||
"SEGREGATE",
|
||||
"VALIDATE"
|
||||
],
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"keyspace",
|
||||
"description":"The keyspace to query about",
|
||||
@@ -833,7 +726,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Rewrite all sstables to the latest version. Unlike scrub, it doesn't skip bad rows and do not snapshot sstables first.",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"upgrade_sstables",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -907,7 +800,7 @@
|
||||
"summary":"Return an array with the ids of the currently active repairs",
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type": "long"
|
||||
"type":"int"
|
||||
},
|
||||
"nickname":"get_active_repair_async",
|
||||
"produces":[
|
||||
@@ -917,50 +810,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/repair_status/",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Query the repair status and return when the repair is finished or timeout",
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"RUNNING",
|
||||
"SUCCESSFUL",
|
||||
"FAILED"
|
||||
],
|
||||
"nickname":"repair_await_completion",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
{
|
||||
"name":"id",
|
||||
"description":"The repair ID to check for status",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"timeout",
|
||||
"description":"Seconds to wait before the query returns even if the repair is not finished. The value -1 or not providing this parameter means no timeout",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/storage_service/repair_async/{keyspace}",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Invoke repair asynchronously. You can track repair progress by using the get supplying id",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"repair_async",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1054,14 +910,6 @@
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"ignore_nodes",
|
||||
"description":"Which hosts are to ignore in this repair. Multiple hosts can be listed separated by commas.",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"trace",
|
||||
"description":"If the value is the string 'true' with any capitalization, enable tracing of the repair.",
|
||||
@@ -1099,7 +947,7 @@
|
||||
"description":"The repair ID to check for status",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -1197,14 +1045,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"ignore_nodes",
|
||||
"description":"List of dead nodes to ingore in removenode operation",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1437,18 +1277,18 @@
|
||||
},
|
||||
{
|
||||
"name":"dynamic_update_interval",
|
||||
"description":"interval in ms (default 100)",
|
||||
"description":"integer, in ms (default 100)",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"long",
|
||||
"type":"integer",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"dynamic_reset_interval",
|
||||
"description":"interval in ms (default 600,000)",
|
||||
"description":"integer, in ms (default 600,000)",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"long",
|
||||
"type":"integer",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
@@ -1653,7 +1493,7 @@
|
||||
"description":"Stream throughput",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -1661,7 +1501,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get stream throughput mb per sec",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_stream_throughput_mb_per_sec",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1677,7 +1517,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"get compaction throughput mb per sec",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_compaction_throughput_mb_per_sec",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -1699,7 +1539,7 @@
|
||||
"description":"compaction throughput",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -1856,22 +1696,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"load_and_stream",
|
||||
"description":"Load the sstables and stream to all replica nodes that owns the data",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"primary_replica_only",
|
||||
"description":"Load the sstables and stream to primary replica node that owns the data. Repair is needed after the load and stream process",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"string",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1982,14 +1806,6 @@
|
||||
"allowMultiple":false,
|
||||
"type":"long",
|
||||
"paramType":"query"
|
||||
},
|
||||
{
|
||||
"name":"fast",
|
||||
"description":"Lightweight tracing mode: if true, slow queries tracing records only session headers",
|
||||
"required":false,
|
||||
"allowMultiple":false,
|
||||
"type":"boolean",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2127,7 +1943,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Returns the threshold for warning of queries with many tombstones",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_tombstone_warn_threshold",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2149,7 +1965,7 @@
|
||||
"description":"tombstone debug threshold",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -2162,7 +1978,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_tombstone_failure_threshold",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2184,7 +2000,7 @@
|
||||
"description":"tombstone debug threshold",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -2197,7 +2013,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Returns the threshold for rejecting queries due to a large batch size",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_batch_size_failure_threshold",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2219,7 +2035,7 @@
|
||||
"description":"batch size debug threshold",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -2243,7 +2059,7 @@
|
||||
"description":"throttle in kb",
|
||||
"required":true,
|
||||
"allowMultiple":false,
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"paramType":"query"
|
||||
}
|
||||
]
|
||||
@@ -2256,7 +2072,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get load",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_metrics_load",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2272,7 +2088,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get exceptions",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_exceptions",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2288,7 +2104,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get total hints in progress",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_total_hints_in_progress",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2304,7 +2120,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get total hints",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_total_hints",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -2488,10 +2304,6 @@
|
||||
"threshold":{
|
||||
"type":"long",
|
||||
"description":"The slow query logging threshold in microseconds. Queries that takes longer, will be logged"
|
||||
},
|
||||
"fast":{
|
||||
"type":"boolean",
|
||||
"description":"Is lightweight tracing mode enabled. In that mode tracing ignore events and tracks only sessions."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2596,7 +2408,7 @@
|
||||
"version":{
|
||||
"type":"string",
|
||||
"enum":[
|
||||
"ka", "la", "mc", "md"
|
||||
"ka", "la", "mc"
|
||||
],
|
||||
"description":"SSTable version"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get number of active outbound streams",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_active_streams_outbound",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -48,7 +48,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get total incoming bytes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_total_incoming_bytes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -72,7 +72,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all total incoming bytes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_total_incoming_bytes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -88,7 +88,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get total outgoing bytes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_total_outgoing_bytes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -112,7 +112,7 @@
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get all total outgoing bytes",
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"nickname":"get_all_total_outgoing_bytes",
|
||||
"produces":[
|
||||
"application/json"
|
||||
@@ -154,7 +154,7 @@
|
||||
"description":"The peer"
|
||||
},
|
||||
"session_index":{
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"description":"The session index"
|
||||
},
|
||||
"connecting":{
|
||||
@@ -211,7 +211,7 @@
|
||||
"description":"The ID"
|
||||
},
|
||||
"files":{
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"description":"Number of files to transfer. Can be 0 if nothing to transfer for some streaming request."
|
||||
},
|
||||
"total_size":{
|
||||
@@ -242,7 +242,7 @@
|
||||
"description":"The peer address"
|
||||
},
|
||||
"session_index":{
|
||||
"type": "long",
|
||||
"type":"int",
|
||||
"description":"The session index"
|
||||
},
|
||||
"file_name":{
|
||||
|
||||
@@ -52,37 +52,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/system/drop_sstable_caches",
|
||||
"operations":[
|
||||
{
|
||||
"method":"POST",
|
||||
"summary":"Drop in-memory caches for data which is in sstables",
|
||||
"type":"void",
|
||||
"nickname":"drop_sstable_caches",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/system/uptime_ms",
|
||||
"operations":[
|
||||
{
|
||||
"method":"GET",
|
||||
"summary":"Get system uptime, in milliseconds",
|
||||
"type":"long",
|
||||
"nickname":"get_system_uptime",
|
||||
"produces":[
|
||||
"application/json"
|
||||
],
|
||||
"parameters":[]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path":"/system/logger/{name}",
|
||||
"operations":[
|
||||
|
||||
124
api/api.cc
124
api/api.cc
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-present ScyllaDB
|
||||
* Copyright 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -36,7 +36,6 @@
|
||||
#include "endpoint_snitch.hh"
|
||||
#include "compaction_manager.hh"
|
||||
#include "hinted_handoff.hh"
|
||||
#include "error_injection.hh"
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "stream_manager.hh"
|
||||
#include "system.hh"
|
||||
@@ -69,19 +68,13 @@ future<> set_server_init(http_context& ctx) {
|
||||
rb->set_api_doc(r);
|
||||
rb02->set_api_doc(r);
|
||||
rb02->register_api_file(r, "swagger20_header");
|
||||
set_config(rb02, ctx, r);
|
||||
rb->register_function(r, "system",
|
||||
"The system related API");
|
||||
set_system(ctx, r);
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_config(http_context& ctx, const db::config& cfg) {
|
||||
auto rb02 = std::make_shared < api_registry_builder20 > (ctx.api_doc, "/v2");
|
||||
return ctx.http_server.set_routes([&ctx, &cfg, rb02](routes& r) {
|
||||
set_config(rb02, ctx, r, cfg);
|
||||
});
|
||||
}
|
||||
|
||||
static future<> register_api(http_context& ctx, const sstring& api_name,
|
||||
const sstring api_desc,
|
||||
std::function<void(http_context& ctx, routes& r)> f) {
|
||||
@@ -93,69 +86,17 @@ static future<> register_api(http_context& ctx, const sstring& api_name,
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_transport_controller(http_context& ctx, cql_transport::controller& ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &ctl] (routes& r) { set_transport_controller(ctx, r, ctl); });
|
||||
}
|
||||
|
||||
future<> unset_transport_controller(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_transport_controller(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_rpc_controller(http_context& ctx, thrift_controller& ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &ctl] (routes& r) { set_rpc_controller(ctx, r, ctl); });
|
||||
}
|
||||
|
||||
future<> unset_rpc_controller(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_rpc_controller(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, sharded<gms::gossiper>& g, sharded<cdc::generation_service>& cdc_gs) {
|
||||
return register_api(ctx, "storage_service", "The storage service API", [&ss, &g, &cdc_gs] (http_context& ctx, routes& r) {
|
||||
set_storage_service(ctx, r, ss, g.local(), cdc_gs);
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_sstables_loader(http_context& ctx, sharded<sstables_loader>& sst_loader) {
|
||||
return ctx.http_server.set_routes([&ctx, &sst_loader] (routes& r) { set_sstables_loader(ctx, r, sst_loader); });
|
||||
}
|
||||
|
||||
future<> unset_server_sstables_loader(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_sstables_loader(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb) {
|
||||
return ctx.http_server.set_routes([&ctx, &vb] (routes& r) { set_view_builder(ctx, r, vb); });
|
||||
}
|
||||
|
||||
future<> unset_server_view_builder(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_view_builder(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_repair(http_context& ctx, sharded<repair_service>& repair) {
|
||||
return ctx.http_server.set_routes([&ctx, &repair] (routes& r) { set_repair(ctx, r, repair); });
|
||||
}
|
||||
|
||||
future<> unset_server_repair(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_repair(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_snapshot(http_context& ctx, sharded<db::snapshot_ctl>& snap_ctl) {
|
||||
return ctx.http_server.set_routes([&ctx, &snap_ctl] (routes& r) { set_snapshot(ctx, r, snap_ctl); });
|
||||
}
|
||||
|
||||
future<> unset_server_snapshot(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_snapshot(ctx, r); });
|
||||
future<> set_server_storage_service(http_context& ctx) {
|
||||
return register_api(ctx, "storage_service", "The storage service API", set_storage_service);
|
||||
}
|
||||
|
||||
future<> set_server_snitch(http_context& ctx) {
|
||||
return register_api(ctx, "endpoint_snitch_info", "The endpoint snitch info API", set_endpoint_snitch);
|
||||
}
|
||||
|
||||
future<> set_server_gossip(http_context& ctx, sharded<gms::gossiper>& g) {
|
||||
future<> set_server_gossip(http_context& ctx) {
|
||||
return register_api(ctx, "gossiper",
|
||||
"The gossiper API", [&g] (http_context& ctx, routes& r) {
|
||||
set_gossiper(ctx, r, g.local());
|
||||
});
|
||||
"The gossiper API", set_gossiper);
|
||||
}
|
||||
|
||||
future<> set_server_load_sstable(http_context& ctx) {
|
||||
@@ -163,21 +104,14 @@ future<> set_server_load_sstable(http_context& ctx) {
|
||||
"The column family API", set_column_family);
|
||||
}
|
||||
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms) {
|
||||
future<> set_server_messaging_service(http_context& ctx) {
|
||||
return register_api(ctx, "messaging_service",
|
||||
"The messaging service API", [&ms] (http_context& ctx, routes& r) {
|
||||
set_messaging_service(ctx, r, ms);
|
||||
});
|
||||
}
|
||||
future<> unset_server_messaging_service(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_messaging_service(ctx, r); });
|
||||
"The messaging service API", set_messaging_service);
|
||||
}
|
||||
|
||||
future<> set_server_storage_proxy(http_context& ctx, sharded<service::storage_service>& ss) {
|
||||
future<> set_server_storage_proxy(http_context& ctx) {
|
||||
return register_api(ctx, "storage_proxy",
|
||||
"The storage proxy API", [&ss] (http_context& ctx, routes& r) {
|
||||
set_storage_proxy(ctx, r, ss);
|
||||
});
|
||||
"The storage proxy API", set_storage_proxy);
|
||||
}
|
||||
|
||||
future<> set_server_stream_manager(http_context& ctx) {
|
||||
@@ -190,34 +124,13 @@ future<> set_server_cache(http_context& ctx) {
|
||||
"The cache service API", set_cache_service);
|
||||
}
|
||||
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<gms::gossiper>& g) {
|
||||
return register_api(ctx, "hinted_handoff",
|
||||
"The hinted handoff API", [&g] (http_context& ctx, routes& r) {
|
||||
set_hinted_handoff(ctx, r, g.local());
|
||||
});
|
||||
}
|
||||
|
||||
future<> unset_hinted_handoff(http_context& ctx) {
|
||||
return ctx.http_server.set_routes([&ctx] (routes& r) { unset_hinted_handoff(ctx, r); });
|
||||
}
|
||||
|
||||
future<> set_server_gossip_settle(http_context& ctx, sharded<gms::gossiper>& g) {
|
||||
auto rb = std::make_shared < api_registry_builder > (ctx.api_doc);
|
||||
|
||||
return ctx.http_server.set_routes([rb, &ctx, &g](routes& r) {
|
||||
rb->register_function(r, "failure_detector",
|
||||
"The failure detector API");
|
||||
set_failure_detector(ctx, r, g.local());
|
||||
});
|
||||
}
|
||||
|
||||
future<> set_server_compaction_manager(http_context& ctx) {
|
||||
future<> set_server_gossip_settle(http_context& ctx) {
|
||||
auto rb = std::make_shared < api_registry_builder > (ctx.api_doc);
|
||||
|
||||
return ctx.http_server.set_routes([rb, &ctx](routes& r) {
|
||||
rb->register_function(r, "compaction_manager",
|
||||
"The Compaction manager API");
|
||||
set_compaction_manager(ctx, r);
|
||||
rb->register_function(r, "failure_detector",
|
||||
"The failure detector API");
|
||||
set_failure_detector(ctx,r);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,18 +138,21 @@ future<> set_server_done(http_context& ctx) {
|
||||
auto rb = std::make_shared < api_registry_builder > (ctx.api_doc);
|
||||
|
||||
return ctx.http_server.set_routes([rb, &ctx](routes& r) {
|
||||
rb->register_function(r, "compaction_manager",
|
||||
"The Compaction manager API");
|
||||
set_compaction_manager(ctx, r);
|
||||
rb->register_function(r, "lsa", "Log-structured allocator API");
|
||||
set_lsa(ctx, r);
|
||||
|
||||
rb->register_function(r, "commitlog",
|
||||
"The commit log API");
|
||||
set_commitlog(ctx,r);
|
||||
rb->register_function(r, "hinted_handoff",
|
||||
"The hinted handoff API");
|
||||
set_hinted_handoff(ctx, r);
|
||||
rb->register_function(r, "collectd",
|
||||
"The collectd API");
|
||||
set_collectd(ctx, r);
|
||||
rb->register_function(r, "error_injection",
|
||||
"The error injection API");
|
||||
set_error_injection(ctx, r);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-present ScyllaDB
|
||||
* Copyright 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <boost/units/detail/utility.hpp>
|
||||
#include "api/api-doc/utils.json.hh"
|
||||
#include "utils/histogram.hh"
|
||||
#include "utils/estimated_histogram.hh"
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "api_init.hh"
|
||||
#include "seastarx.hh"
|
||||
@@ -71,7 +70,7 @@ T map_sum(T&& dest, const S& src) {
|
||||
for (auto i : src) {
|
||||
dest[i.first] += i.second;
|
||||
}
|
||||
return std::move(dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
template <typename MAP>
|
||||
@@ -257,6 +256,4 @@ public:
|
||||
operator T() const { return value; }
|
||||
};
|
||||
|
||||
utils_json::estimated_histogram time_to_json_histogram(const utils::time_estimated_histogram& val);
|
||||
|
||||
}
|
||||
|
||||
@@ -19,48 +19,9 @@
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <seastar/http/httpd.hh>
|
||||
#include <seastar/core/future.hh>
|
||||
|
||||
#include "database_fwd.hh"
|
||||
#include "seastarx.hh"
|
||||
|
||||
namespace service {
|
||||
|
||||
class load_meter;
|
||||
class storage_proxy;
|
||||
class storage_service;
|
||||
|
||||
} // namespace service
|
||||
|
||||
class sstables_loader;
|
||||
|
||||
namespace locator {
|
||||
|
||||
class token_metadata;
|
||||
class shared_token_metadata;
|
||||
|
||||
} // namespace locator
|
||||
|
||||
namespace cql_transport { class controller; }
|
||||
class thrift_controller;
|
||||
namespace db {
|
||||
class snapshot_ctl;
|
||||
class config;
|
||||
namespace view {
|
||||
class view_builder;
|
||||
}
|
||||
}
|
||||
namespace netw { class messaging_service; }
|
||||
class repair_service;
|
||||
namespace cdc { class generation_service; }
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
#include "service/storage_proxy.hh"
|
||||
#include <seastar/http/httpd.hh>
|
||||
|
||||
namespace api {
|
||||
|
||||
@@ -70,45 +31,22 @@ struct http_context {
|
||||
httpd::http_server_control http_server;
|
||||
distributed<database>& db;
|
||||
distributed<service::storage_proxy>& sp;
|
||||
service::load_meter& lmeter;
|
||||
const sharded<locator::shared_token_metadata>& shared_token_metadata;
|
||||
|
||||
http_context(distributed<database>& _db,
|
||||
distributed<service::storage_proxy>& _sp,
|
||||
service::load_meter& _lm, const sharded<locator::shared_token_metadata>& _stm)
|
||||
: db(_db), sp(_sp), lmeter(_lm), shared_token_metadata(_stm) {
|
||||
distributed<service::storage_proxy>& _sp)
|
||||
: db(_db), sp(_sp) {
|
||||
}
|
||||
|
||||
const locator::token_metadata& get_token_metadata();
|
||||
};
|
||||
|
||||
future<> set_server_init(http_context& ctx);
|
||||
future<> set_server_config(http_context& ctx, const db::config& cfg);
|
||||
future<> set_server_snitch(http_context& ctx);
|
||||
future<> set_server_storage_service(http_context& ctx, sharded<service::storage_service>& ss, sharded<gms::gossiper>& g, sharded<cdc::generation_service>& cdc_gs);
|
||||
future<> set_server_sstables_loader(http_context& ctx, sharded<sstables_loader>& sst_loader);
|
||||
future<> unset_server_sstables_loader(http_context& ctx);
|
||||
future<> set_server_view_builder(http_context& ctx, sharded<db::view::view_builder>& vb);
|
||||
future<> unset_server_view_builder(http_context& ctx);
|
||||
future<> set_server_repair(http_context& ctx, sharded<repair_service>& repair);
|
||||
future<> unset_server_repair(http_context& ctx);
|
||||
future<> set_transport_controller(http_context& ctx, cql_transport::controller& ctl);
|
||||
future<> unset_transport_controller(http_context& ctx);
|
||||
future<> set_rpc_controller(http_context& ctx, thrift_controller& ctl);
|
||||
future<> unset_rpc_controller(http_context& ctx);
|
||||
future<> set_server_snapshot(http_context& ctx, sharded<db::snapshot_ctl>& snap_ctl);
|
||||
future<> unset_server_snapshot(http_context& ctx);
|
||||
future<> set_server_gossip(http_context& ctx, sharded<gms::gossiper>& g);
|
||||
future<> set_server_storage_service(http_context& ctx);
|
||||
future<> set_server_gossip(http_context& ctx);
|
||||
future<> set_server_load_sstable(http_context& ctx);
|
||||
future<> set_server_messaging_service(http_context& ctx, sharded<netw::messaging_service>& ms);
|
||||
future<> unset_server_messaging_service(http_context& ctx);
|
||||
future<> set_server_storage_proxy(http_context& ctx, sharded<service::storage_service>& ss);
|
||||
future<> set_server_messaging_service(http_context& ctx);
|
||||
future<> set_server_storage_proxy(http_context& ctx);
|
||||
future<> set_server_stream_manager(http_context& ctx);
|
||||
future<> set_hinted_handoff(http_context& ctx, sharded<gms::gossiper>& g);
|
||||
future<> unset_hinted_handoff(http_context& ctx);
|
||||
future<> set_server_gossip_settle(http_context& ctx, sharded<gms::gossiper>& g);
|
||||
future<> set_server_gossip_settle(http_context& ctx);
|
||||
future<> set_server_cache(http_context& ctx);
|
||||
future<> set_server_compaction_manager(http_context& ctx);
|
||||
future<> set_server_done(http_context& ctx);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -208,11 +208,9 @@ void set_cache_service(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cs::get_row_capacity.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().region().occupancy().used_space();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, uint64_t(0), [](const column_family& cf) {
|
||||
return cf.get_row_cache().get_cache_tracker().region().occupancy().used_space();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_hits.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -253,19 +251,15 @@ void set_cache_service(http_context& ctx, routes& r) {
|
||||
cs::get_row_size.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
// In origin row size is the weighted size.
|
||||
// We currently do not support weights, so we use num entries instead
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().partitions();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, 0, [](const column_family& cf) {
|
||||
return cf.get_row_cache().partitions();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_row_entries.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return ctx.db.map_reduce0([](database& db) -> uint64_t {
|
||||
return db.row_cache_tracker().partitions();
|
||||
}, uint64_t(0), std::plus<uint64_t>()).then([](const int64_t& res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
return map_reduce_cf(ctx, 0, [](const column_family& cf) {
|
||||
return cf.get_row_cache().partitions();
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cs::get_counter_capacity.set(r, [] (std::unique_ptr<request> req) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -64,7 +64,7 @@ static const char* str_to_regex(const sstring& v) {
|
||||
void set_collectd(http_context& ctx, routes& r) {
|
||||
cd::get_collectd.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
|
||||
auto id = ::make_shared<scollectd::type_instance_id>(req->param["pluginid"],
|
||||
auto id = make_shared<scollectd::type_instance_id>(req->param["pluginid"],
|
||||
req->get_query_param("instance"), req->get_query_param("type"),
|
||||
req->get_query_param("type_instance"));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -24,13 +24,10 @@
|
||||
#include <vector>
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "sstables/sstables.hh"
|
||||
#include "sstables/metadata_collector.hh"
|
||||
#include "utils/estimated_histogram.hh"
|
||||
#include <algorithm>
|
||||
#include "db/system_keyspace_view_types.hh"
|
||||
|
||||
#include "db/data_listeners.hh"
|
||||
#include "storage_service.hh"
|
||||
#include "unimplemented.hh"
|
||||
|
||||
extern logging::logger apilog;
|
||||
|
||||
@@ -56,7 +53,8 @@ std::tuple<sstring, sstring> parse_fully_qualified_cf_name(sstring name) {
|
||||
return std::make_tuple(name.substr(0, pos), name.substr(end));
|
||||
}
|
||||
|
||||
const utils::UUID& get_uuid(const sstring& ks, const sstring& cf, const database& db) {
|
||||
const utils::UUID& get_uuid(const sstring& name, const database& db) {
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name);
|
||||
try {
|
||||
return db.find_uuid(ks, cf);
|
||||
} catch (std::out_of_range& e) {
|
||||
@@ -64,11 +62,6 @@ const utils::UUID& get_uuid(const sstring& ks, const sstring& cf, const database
|
||||
}
|
||||
}
|
||||
|
||||
const utils::UUID& get_uuid(const sstring& name, const database& db) {
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name);
|
||||
return get_uuid(ks, cf, db);
|
||||
}
|
||||
|
||||
future<> foreach_column_family(http_context& ctx, const sstring& name, function<void(column_family&)> f) {
|
||||
auto uuid = get_uuid(name, ctx.db.local());
|
||||
|
||||
@@ -78,28 +71,28 @@ future<> foreach_column_family(http_context& ctx, const sstring& name, function<
|
||||
}
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx, const sstring& name,
|
||||
int64_t column_family_stats::*f) {
|
||||
int64_t column_family::stats::*f) {
|
||||
return map_reduce_cf(ctx, name, int64_t(0), [f](const column_family& cf) {
|
||||
return cf.get_stats().*f;
|
||||
}, std::plus<int64_t>());
|
||||
}
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx,
|
||||
int64_t column_family_stats::*f) {
|
||||
int64_t column_family::stats::*f) {
|
||||
return map_reduce_cf(ctx, int64_t(0), [f](const column_family& cf) {
|
||||
return cf.get_stats().*f;
|
||||
}, std::plus<int64_t>());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_stats_count(http_context& ctx, const sstring& name,
|
||||
utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
return map_reduce_cf(ctx, name, int64_t(0), [f](const column_family& cf) {
|
||||
return (cf.get_stats().*f).hist.count;
|
||||
}, std::plus<int64_t>());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_stats_sum(http_context& ctx, const sstring& name,
|
||||
utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
auto uuid = get_uuid(name, ctx.db.local());
|
||||
return ctx.db.map_reduce0([uuid, f](database& db) {
|
||||
// Histograms information is sample of the actual load
|
||||
@@ -115,14 +108,14 @@ static future<json::json_return_type> get_cf_stats_sum(http_context& ctx, const
|
||||
|
||||
|
||||
static future<json::json_return_type> get_cf_stats_count(http_context& ctx,
|
||||
utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
return map_reduce_cf(ctx, int64_t(0), [f](const column_family& cf) {
|
||||
return (cf.get_stats().*f).hist.count;
|
||||
}, std::plus<int64_t>());
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_histogram(http_context& ctx, const sstring& name,
|
||||
utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
utils::UUID uuid = get_uuid(name, ctx.db.local());
|
||||
return ctx.db.map_reduce0([f, uuid](const database& p) {
|
||||
return (p.find_column_family(uuid).get_stats().*f).hist;},
|
||||
@@ -133,7 +126,7 @@ static future<json::json_return_type> get_cf_histogram(http_context& ctx, const
|
||||
});
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_histogram(http_context& ctx, utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
static future<json::json_return_type> get_cf_histogram(http_context& ctx, utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
std::function<utils::ihistogram(const database&)> fun = [f] (const database& db) {
|
||||
utils::ihistogram res;
|
||||
for (auto i : db.get_column_families()) {
|
||||
@@ -149,7 +142,7 @@ static future<json::json_return_type> get_cf_histogram(http_context& ctx, utils:
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_rate_and_histogram(http_context& ctx, const sstring& name,
|
||||
utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
utils::UUID uuid = get_uuid(name, ctx.db.local());
|
||||
return ctx.db.map_reduce0([f, uuid](const database& p) {
|
||||
return (p.find_column_family(uuid).get_stats().*f).rate();},
|
||||
@@ -160,7 +153,7 @@ static future<json::json_return_type> get_cf_rate_and_histogram(http_context& c
|
||||
});
|
||||
}
|
||||
|
||||
static future<json::json_return_type> get_cf_rate_and_histogram(http_context& ctx, utils::timed_rate_moving_average_and_histogram column_family_stats::*f) {
|
||||
static future<json::json_return_type> get_cf_rate_and_histogram(http_context& ctx, utils::timed_rate_moving_average_and_histogram column_family::stats::*f) {
|
||||
std::function<utils::rate_moving_average_and_histogram(const database&)> fun = [f] (const database& db) {
|
||||
utils::rate_moving_average_and_histogram res;
|
||||
for (auto i : db.get_column_families()) {
|
||||
@@ -183,7 +176,7 @@ static future<json::json_return_type> get_cf_unleveled_sstables(http_context& ct
|
||||
|
||||
static int64_t min_partition_size(column_family& cf) {
|
||||
int64_t res = INT64_MAX;
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res = std::min(res, i->get_stats_metadata().estimated_partition_size.min());
|
||||
}
|
||||
return (res == INT64_MAX) ? 0 : res;
|
||||
@@ -191,7 +184,7 @@ static int64_t min_partition_size(column_family& cf) {
|
||||
|
||||
static int64_t max_partition_size(column_family& cf) {
|
||||
int64_t res = 0;
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res = std::max(i->get_stats_metadata().estimated_partition_size.max(), res);
|
||||
}
|
||||
return res;
|
||||
@@ -199,7 +192,7 @@ static int64_t max_partition_size(column_family& cf) {
|
||||
|
||||
static integral_ratio_holder mean_partition_size(column_family& cf) {
|
||||
integral_ratio_holder res;
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
auto c = i->get_stats_metadata().estimated_partition_size.count();
|
||||
res.sub += i->get_stats_metadata().estimated_partition_size.mean() * c;
|
||||
res.total += c;
|
||||
@@ -252,22 +245,17 @@ static future<json::json_return_type> sum_sstable(http_context& ctx, bool total)
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> map_reduce_cf_time_histogram(http_context& ctx, const sstring& name, std::function<utils::time_estimated_histogram(const column_family&)> f) {
|
||||
return map_reduce_cf_raw(ctx, name, utils::time_estimated_histogram(), f, utils::time_estimated_histogram_merge).then([](const utils::time_estimated_histogram& res) {
|
||||
return make_ready_future<json::json_return_type>(time_to_json_histogram(res));
|
||||
});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class sum_ratio {
|
||||
uint64_t _n = 0;
|
||||
T _total = 0;
|
||||
public:
|
||||
void operator()(T value) {
|
||||
future<> operator()(T value) {
|
||||
if (value > 0) {
|
||||
_total += value;
|
||||
_n++;
|
||||
}
|
||||
return make_ready_future<>();
|
||||
}
|
||||
// Returns average value of all registered ratios.
|
||||
T get() && {
|
||||
@@ -277,7 +265,7 @@ public:
|
||||
|
||||
static double get_compression_ratio(column_family& cf) {
|
||||
sum_ratio<double> result;
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i : *cf.get_sstables()) {
|
||||
auto compression_ratio = i->get_compression_ratio();
|
||||
if (compression_ratio != sstables::metadata_collector::NO_COMPRESSION_RATIO) {
|
||||
result(compression_ratio);
|
||||
@@ -313,8 +301,8 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return res;
|
||||
});
|
||||
|
||||
cf::get_column_family.set(r, [&ctx] (std::unique_ptr<request> req){
|
||||
std::list<cf::column_family_info> res;
|
||||
cf::get_column_family.set(r, [&ctx] (const_req req){
|
||||
vector<cf::column_family_info> res;
|
||||
for (auto i: ctx.db.local().get_column_families_mapping()) {
|
||||
cf::column_family_info info;
|
||||
info.ks = i.first.first;
|
||||
@@ -322,7 +310,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
info.type = "ColumnFamilies";
|
||||
res.push_back(info);
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(json::stream_range_as_array(std::move(res), std::identity()));
|
||||
return res;
|
||||
});
|
||||
|
||||
cf::get_column_family_name_keyspace.set(r, [&ctx] (const_req req){
|
||||
@@ -334,15 +322,15 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_memtable_columns_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t{0}, [](column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], 0, [](column_family& cf) {
|
||||
return cf.active_memtable().partition_count();
|
||||
}, std::plus<>());
|
||||
}, std::plus<int>());
|
||||
});
|
||||
|
||||
cf::get_all_memtable_columns_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t{0}, [](column_family& cf) {
|
||||
return map_reduce_cf(ctx, 0, [](column_family& cf) {
|
||||
return cf.active_memtable().partition_count();
|
||||
}, std::plus<>());
|
||||
}, std::plus<int>());
|
||||
});
|
||||
|
||||
cf::get_memtable_on_heap_size.set(r, [] (const_req req) {
|
||||
@@ -416,18 +404,18 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_memtable_switch_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx,req->param["name"] ,&column_family_stats::memtable_switch_count);
|
||||
return get_cf_stats(ctx,req->param["name"] ,&column_family::stats::memtable_switch_count);
|
||||
});
|
||||
|
||||
cf::get_all_memtable_switch_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx, &column_family_stats::memtable_switch_count);
|
||||
return get_cf_stats(ctx, &column_family::stats::memtable_switch_count);
|
||||
});
|
||||
|
||||
// FIXME: this refers to partitions, not rows.
|
||||
cf::get_estimated_row_size_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
utils::estimated_histogram res(0);
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res.merge(i->get_stats_metadata().estimated_partition_size);
|
||||
}
|
||||
return res;
|
||||
@@ -439,7 +427,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
cf::get_estimated_row_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], int64_t(0), [](column_family& cf) {
|
||||
uint64_t res = 0;
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res += i->get_stats_metadata().estimated_partition_size.count();
|
||||
}
|
||||
return res;
|
||||
@@ -450,7 +438,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
cf::get_estimated_column_count_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
utils::estimated_histogram res(0);
|
||||
for (auto sstables = cf.get_sstables(); auto& i : *sstables) {
|
||||
for (auto i: *cf.get_sstables() ) {
|
||||
res.merge(i->get_stats_metadata().estimated_cells_count);
|
||||
}
|
||||
return res;
|
||||
@@ -465,67 +453,67 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_pending_flushes.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx,req->param["name"] ,&column_family_stats::pending_flushes);
|
||||
return get_cf_stats(ctx,req->param["name"] ,&column_family::stats::pending_flushes);
|
||||
});
|
||||
|
||||
cf::get_all_pending_flushes.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx, &column_family_stats::pending_flushes);
|
||||
return get_cf_stats(ctx, &column_family::stats::pending_flushes);
|
||||
});
|
||||
|
||||
cf::get_read.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_count(ctx,req->param["name"] ,&column_family_stats::reads);
|
||||
return get_cf_stats_count(ctx,req->param["name"] ,&column_family::stats::reads);
|
||||
});
|
||||
|
||||
cf::get_all_read.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_count(ctx, &column_family_stats::reads);
|
||||
return get_cf_stats_count(ctx, &column_family::stats::reads);
|
||||
});
|
||||
|
||||
cf::get_write.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_count(ctx, req->param["name"] ,&column_family_stats::writes);
|
||||
return get_cf_stats_count(ctx, req->param["name"] ,&column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_all_write.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_count(ctx, &column_family_stats::writes);
|
||||
return get_cf_stats_count(ctx, &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_read_latency_histogram_depricated.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family_stats::reads);
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family::stats::reads);
|
||||
});
|
||||
|
||||
cf::get_read_latency_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_rate_and_histogram(ctx, req->param["name"], &column_family_stats::reads);
|
||||
return get_cf_rate_and_histogram(ctx, req->param["name"], &column_family::stats::reads);
|
||||
});
|
||||
|
||||
cf::get_read_latency.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_sum(ctx,req->param["name"] ,&column_family_stats::reads);
|
||||
return get_cf_stats_sum(ctx,req->param["name"] ,&column_family::stats::reads);
|
||||
});
|
||||
|
||||
cf::get_write_latency.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats_sum(ctx, req->param["name"] ,&column_family_stats::writes);
|
||||
return get_cf_stats_sum(ctx, req->param["name"] ,&column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_all_read_latency_histogram_depricated.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, &column_family_stats::writes);
|
||||
return get_cf_histogram(ctx, &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_all_read_latency_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_rate_and_histogram(ctx, &column_family_stats::writes);
|
||||
return get_cf_rate_and_histogram(ctx, &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_write_latency_histogram_depricated.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family_stats::writes);
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_write_latency_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_rate_and_histogram(ctx, req->param["name"], &column_family_stats::writes);
|
||||
return get_cf_rate_and_histogram(ctx, req->param["name"], &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_all_write_latency_histogram_depricated.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, &column_family_stats::writes);
|
||||
return get_cf_histogram(ctx, &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_all_write_latency_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_rate_and_histogram(ctx, &column_family_stats::writes);
|
||||
return get_cf_rate_and_histogram(ctx, &column_family::stats::writes);
|
||||
});
|
||||
|
||||
cf::get_pending_compactions.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -541,11 +529,11 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_live_ss_table_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx, req->param["name"], &column_family_stats::live_sstable_count);
|
||||
return get_cf_stats(ctx, req->param["name"], &column_family::stats::live_sstable_count);
|
||||
});
|
||||
|
||||
cf::get_all_live_ss_table_count.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_stats(ctx, &column_family_stats::live_sstable_count);
|
||||
return get_cf_stats(ctx, &column_family::stats::live_sstable_count);
|
||||
});
|
||||
|
||||
cf::get_unleveled_sstables.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -602,8 +590,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
cf::get_bloom_filter_false_positives.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_get_false_positive();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
@@ -611,8 +598,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
cf::get_all_bloom_filter_false_positives.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_get_false_positive();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
@@ -620,8 +606,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
cf::get_recent_bloom_filter_false_positives.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_get_recent_false_positive();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
@@ -629,8 +614,7 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
cf::get_all_recent_bloom_filter_false_positives.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_get_recent_false_positive();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
@@ -662,54 +646,48 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
cf::get_bloom_filter_disk_space_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_size();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->filter_size();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cf::get_all_bloom_filter_disk_space_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_size();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->filter_size();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cf::get_bloom_filter_off_heap_memory_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_memory_size();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->filter_memory_size();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cf::get_all_bloom_filter_off_heap_memory_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->filter_memory_size();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->filter_memory_size();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cf::get_index_summary_off_heap_memory_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, req->param["name"], uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->get_summary().memory_footprint();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->get_summary().memory_footprint();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
|
||||
cf::get_all_index_summary_off_heap_memory_used.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf(ctx, uint64_t(0), [] (column_family& cf) {
|
||||
auto sstables = cf.get_sstables();
|
||||
return std::accumulate(sstables->begin(), sstables->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return s + sst->get_summary().memory_footprint();
|
||||
return std::accumulate(cf.get_sstables()->begin(), cf.get_sstables()->end(), uint64_t(0), [](uint64_t s, auto& sst) {
|
||||
return sst->get_summary().memory_footprint();
|
||||
});
|
||||
}, std::plus<uint64_t>());
|
||||
});
|
||||
@@ -814,22 +792,25 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
|
||||
});
|
||||
|
||||
cf::get_cas_prepare.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_prepare;
|
||||
});
|
||||
cf::get_cas_prepare.set(r, [] (std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
//auto id = get_uuid(req->param["name"], ctx.db.local());
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
|
||||
cf::get_cas_propose.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_accept;
|
||||
});
|
||||
cf::get_cas_propose.set(r, [] (std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
//auto id = get_uuid(req->param["name"], ctx.db.local());
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
|
||||
cf::get_cas_commit.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return cf.get_stats().estimated_cas_learn;
|
||||
});
|
||||
cf::get_cas_commit.set(r, [] (std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
//auto id = get_uuid(req->param["name"], ctx.db.local());
|
||||
return make_ready_future<json::json_return_type>(0);
|
||||
});
|
||||
|
||||
cf::get_sstables_per_read_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
@@ -840,11 +821,11 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_tombstone_scanned_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family_stats::tombstone_scanned);
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family::stats::tombstone_scanned);
|
||||
});
|
||||
|
||||
cf::get_live_scanned_histogram.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family_stats::live_scanned);
|
||||
return get_cf_histogram(ctx, req->param["name"], &column_family::stats::live_scanned);
|
||||
});
|
||||
|
||||
cf::get_col_update_time_delta_histogram.set(r, [] (std::unique_ptr<request> req) {
|
||||
@@ -855,57 +836,19 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
|
||||
cf::get_auto_compaction.set(r, [&ctx] (const_req req) {
|
||||
const utils::UUID& uuid = get_uuid(req.param["name"], ctx.db.local());
|
||||
column_family& cf = ctx.db.local().find_column_family(uuid);
|
||||
return !cf.is_auto_compaction_disabled_by_user();
|
||||
cf::is_auto_compaction_disabled.set(r, [] (const_req req) {
|
||||
// FIXME
|
||||
// currently auto compaction is disable
|
||||
// it should be changed when it would have an API
|
||||
return true;
|
||||
});
|
||||
|
||||
cf::enable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return ctx.db.invoke_on(0, [&ctx, req = std::move(req)] (database& db) {
|
||||
auto g = database::autocompaction_toggle_guard(db);
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
cf.enable_auto_compaction();
|
||||
}).then([g = std::move(g)] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
cf::get_built_indexes.set(r, [](const_req) {
|
||||
// FIXME
|
||||
// Currently there are no index support
|
||||
return std::vector<sstring>();
|
||||
});
|
||||
|
||||
cf::disable_auto_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return ctx.db.invoke_on(0, [&ctx, req = std::move(req)] (database& db) {
|
||||
auto g = database::autocompaction_toggle_guard(db);
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
cf.disable_auto_compaction();
|
||||
}).then([g = std::move(g)] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cf::get_built_indexes.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
auto ks_cf = parse_fully_qualified_cf_name(req->param["name"]);
|
||||
auto&& ks = std::get<0>(ks_cf);
|
||||
auto&& cf_name = std::get<1>(ks_cf);
|
||||
return db::system_keyspace::load_view_build_progress().then([ks, cf_name, &ctx](const std::vector<db::system_keyspace_view_build_progress>& vb) mutable {
|
||||
std::set<sstring> vp;
|
||||
for (auto b : vb) {
|
||||
if (b.view.first == ks) {
|
||||
vp.insert(b.view.second);
|
||||
}
|
||||
}
|
||||
std::vector<sstring> res;
|
||||
auto uuid = get_uuid(ks, cf_name, ctx.db.local());
|
||||
column_family& cf = ctx.db.local().find_column_family(uuid);
|
||||
res.reserve(cf.get_index_manager().list_indexes().size());
|
||||
for (auto&& i : cf.get_index_manager().list_indexes()) {
|
||||
if (!vp.contains(secondary_index::index_table_name(i.metadata().name()))) {
|
||||
res.emplace_back(i.metadata().name());
|
||||
}
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
cf::get_compression_metadata_off_heap_memory_used.set(r, [](const_req) {
|
||||
// FIXME
|
||||
@@ -933,15 +876,17 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
|
||||
cf::get_read_latency_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_read;
|
||||
});
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::get_write_latency_estimated_histogram.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
return map_reduce_cf_time_histogram(ctx, req->param["name"], [](const column_family& cf) {
|
||||
return map_reduce_cf(ctx, req->param["name"], utils::estimated_histogram(0), [](column_family& cf) {
|
||||
return cf.get_stats().estimated_write;
|
||||
});
|
||||
},
|
||||
utils::estimated_histogram_merge, utils_json::estimated_histogram());
|
||||
});
|
||||
|
||||
cf::set_compaction_strategy_class.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
@@ -992,32 +937,44 @@ void set_column_family(http_context& ctx, routes& r) {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
cf::toppartitions.set(r, [&ctx] (std::unique_ptr<request> req) {
|
||||
auto name = req->param["name"];
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name);
|
||||
auto name_param = req->param["name"];
|
||||
auto [ks, cf] = parse_fully_qualified_cf_name(name_param);
|
||||
|
||||
api::req_param<std::chrono::milliseconds, unsigned> duration{*req, "duration", 1000ms};
|
||||
api::req_param<unsigned> capacity(*req, "capacity", 256);
|
||||
api::req_param<unsigned> list_size(*req, "list_size", 10);
|
||||
|
||||
apilog.info("toppartitions query: name={} duration={} list_size={} capacity={}",
|
||||
name, duration.param, list_size.param, capacity.param);
|
||||
name_param, duration.param, list_size.param, capacity.param);
|
||||
|
||||
return seastar::do_with(db::toppartitions_query(ctx.db, {{ks, cf}}, {}, duration.value, list_size, capacity), [&ctx] (db::toppartitions_query& q) {
|
||||
return run_toppartitions_query(q, ctx, true);
|
||||
return seastar::do_with(db::toppartitions_query(ctx.db, ks, cf, duration.value, list_size, capacity), [&ctx](auto& q) {
|
||||
return q.scatter().then([&q] {
|
||||
return sleep(q.duration()).then([&q] {
|
||||
return q.gather(q.capacity()).then([&q] (auto topk_results) {
|
||||
apilog.debug("toppartitions query: processing results");
|
||||
cf::toppartitions_query_results results;
|
||||
|
||||
for (auto& d: topk_results.read.top(q.list_size())) {
|
||||
cf::toppartitions_record r;
|
||||
r.partition = sstring(d.item);
|
||||
r.count = d.count;
|
||||
r.error = d.error;
|
||||
results.read.push(r);
|
||||
}
|
||||
for (auto& d: topk_results.write.top(q.list_size())) {
|
||||
cf::toppartitions_record r;
|
||||
r.partition = sstring(d.item);
|
||||
r.count = d.count;
|
||||
r.error = d.error;
|
||||
results.write.push(r);
|
||||
}
|
||||
return make_ready_future<json::json_return_type>(results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cf::force_major_compaction.set(r, [&ctx](std::unique_ptr<request> req) {
|
||||
if (req->get_query_param("split_output") != "") {
|
||||
fail(unimplemented::cause::API);
|
||||
}
|
||||
return foreach_column_family(ctx, req->param["name"], [](column_family &cf) {
|
||||
return cf.compact_all_sstables();
|
||||
}).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -68,8 +68,6 @@ future<json::json_return_type> map_reduce_cf(http_context& ctx, const sstring& n
|
||||
});
|
||||
}
|
||||
|
||||
future<json::json_return_type> map_reduce_cf_time_histogram(http_context& ctx, const sstring& name, std::function<utils::time_estimated_histogram(const column_family&)> f);
|
||||
|
||||
struct map_reduce_column_families_locally {
|
||||
std::any init;
|
||||
std::function<std::unique_ptr<std::any>(column_family&)> mapper;
|
||||
@@ -77,7 +75,7 @@ struct map_reduce_column_families_locally {
|
||||
future<std::unique_ptr<std::any>> operator()(database& db) const {
|
||||
auto res = seastar::make_lw_shared<std::unique_ptr<std::any>>(std::make_unique<std::any>(init));
|
||||
return do_for_each(db.get_column_families(), [res, this](const std::pair<utils::UUID, seastar::lw_shared_ptr<table>>& i) {
|
||||
*res = reducer(std::move(*res), mapper(*i.second.get()));
|
||||
*res = std::move(reducer(std::move(*res), mapper(*i.second.get())));
|
||||
}).then([res] {
|
||||
return std::move(*res);
|
||||
});
|
||||
@@ -111,12 +109,9 @@ future<json::json_return_type> map_reduce_cf(http_context& ctx, I init,
|
||||
}
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx, const sstring& name,
|
||||
int64_t column_family_stats::*f);
|
||||
int64_t column_family::stats::*f);
|
||||
|
||||
future<json::json_return_type> get_cf_stats(http_context& ctx,
|
||||
int64_t column_family_stats::*f);
|
||||
|
||||
|
||||
std::tuple<sstring, sstring> parse_fully_qualified_cf_name(sstring name);
|
||||
int64_t column_family::stats::*f);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "commitlog.hh"
|
||||
#include "db/commitlog/commitlog.hh"
|
||||
#include <db/commitlog/commitlog.hh>
|
||||
#include "api/api-doc/commitlog.json.hh"
|
||||
#include "database.hh"
|
||||
#include <vector>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -20,11 +20,10 @@
|
||||
*/
|
||||
|
||||
#include "compaction_manager.hh"
|
||||
#include "compaction/compaction_manager.hh"
|
||||
#include "sstables/compaction_manager.hh"
|
||||
#include "api/api-doc/compaction_manager.json.hh"
|
||||
#include "db/system_keyspace.hh"
|
||||
#include "column_family.hh"
|
||||
#include "unimplemented.hh"
|
||||
#include <utility>
|
||||
|
||||
namespace api {
|
||||
@@ -59,13 +58,12 @@ void set_compaction_manager(http_context& ctx, routes& r) {
|
||||
|
||||
for (const auto& c : cm.get_compactions()) {
|
||||
cm::summary s;
|
||||
s.id = c.compaction_uuid.to_sstring();
|
||||
s.ks = c.ks_name;
|
||||
s.cf = c.cf_name;
|
||||
s.ks = c->ks_name;
|
||||
s.cf = c->cf_name;
|
||||
s.unit = "keys";
|
||||
s.task_type = sstables::compaction_name(c.type);
|
||||
s.completed = c.total_keys_written;
|
||||
s.total = c.total_partitions;
|
||||
s.task_type = sstables::compaction_name(c->type);
|
||||
s.completed = c->total_keys_written;
|
||||
s.total = c->total_partitions;
|
||||
summaries.push_back(std::move(s));
|
||||
}
|
||||
return summaries;
|
||||
@@ -76,14 +74,13 @@ 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) {
|
||||
return do_with(std::unordered_map<std::pair<sstring, sstring>, uint64_t, utils::tuple_hash>(), [&ctx, &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 std::move(tasks);
|
||||
});
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-present ScyllaDB
|
||||
* Copyright 2018 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "api/config.hh"
|
||||
#include "api/api-doc/config.json.hh"
|
||||
#include "db/config.hh"
|
||||
#include "database.hh"
|
||||
#include <sstream>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
|
||||
@@ -88,11 +89,11 @@ future<> get_config_swagger_entry(std::string_view name, const std::string& desc
|
||||
|
||||
namespace cs = httpd::config_json;
|
||||
|
||||
void set_config(std::shared_ptr < api_registry_builder20 > rb, http_context& ctx, routes& r, const db::config& cfg) {
|
||||
rb->register_function(r, [&cfg] (output_stream<char>& os) {
|
||||
return do_with(true, [&os, &cfg] (bool& first) {
|
||||
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) {
|
||||
auto f = make_ready_future();
|
||||
for (auto&& cfg_ref : cfg.values()) {
|
||||
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);
|
||||
@@ -102,9 +103,9 @@ void set_config(std::shared_ptr < api_registry_builder20 > rb, http_context& ctx
|
||||
});
|
||||
});
|
||||
|
||||
cs::find_config_id.set(r, [&cfg] (const_req r) {
|
||||
cs::find_config_id.set(r, [&ctx] (const_req r) {
|
||||
auto id = r.param["id"];
|
||||
for (auto&& cfg_ref : cfg.values()) {
|
||||
for (auto&& cfg_ref : ctx.db.local().get_config().values()) {
|
||||
auto&& cfg = cfg_ref.get();
|
||||
if (id == cfg.name()) {
|
||||
return cfg.value_as_json();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018-present ScyllaDB
|
||||
* Copyright (C) 2018 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -26,5 +26,5 @@
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_config(std::shared_ptr<api_registry_builder20> rb, http_context& ctx, routes& r, const db::config& cfg);
|
||||
void set_config(std::shared_ptr<api_registry_builder20> rb, http_context& ctx, routes& r);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020-present ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
* This file is part of Scylla.
|
||||
*
|
||||
* Scylla is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Scylla is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "api/api-doc/error_injection.json.hh"
|
||||
#include "api/api.hh"
|
||||
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "log.hh"
|
||||
#include "utils/error_injection.hh"
|
||||
#include "seastar/core/future-util.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace hf = httpd::error_injection_json;
|
||||
|
||||
void set_error_injection(http_context& ctx, routes& r) {
|
||||
|
||||
hf::enable_injection.set(r, [](std::unique_ptr<request> req) {
|
||||
sstring injection = req->param["injection"];
|
||||
bool one_shot = req->get_query_param("one_shot") == "True";
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.enable_on_all(injection, one_shot).then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
hf::get_enabled_injections_on_all.set(r, [](std::unique_ptr<request> req) {
|
||||
auto& errinj = utils::get_local_injector();
|
||||
auto ret = errinj.enabled_injections_on_all();
|
||||
return make_ready_future<json::json_return_type>(ret);
|
||||
});
|
||||
|
||||
hf::disable_injection.set(r, [](std::unique_ptr<request> req) {
|
||||
sstring injection = req->param["injection"];
|
||||
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.disable_on_all(injection).then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
hf::disable_on_all.set(r, [](std::unique_ptr<request> req) {
|
||||
auto& errinj = utils::get_local_injector();
|
||||
return errinj.disable_on_all().then([] {
|
||||
return make_ready_future<json::json_return_type>(json::json_void());
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019-present 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 "api.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_error_injection(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -28,10 +28,10 @@ namespace api {
|
||||
|
||||
namespace fd = httpd::failure_detector_json;
|
||||
|
||||
void set_failure_detector(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
fd::get_all_endpoint_states.set(r, [&g](std::unique_ptr<request> req) {
|
||||
void set_failure_detector(http_context& ctx, routes& r) {
|
||||
fd::get_all_endpoint_states.set(r, [](std::unique_ptr<request> req) {
|
||||
std::vector<fd::endpoint_state> res;
|
||||
for (auto i : g.endpoint_state_map) {
|
||||
for (auto i : gms::get_local_gossiper().endpoint_state_map) {
|
||||
fd::endpoint_state val;
|
||||
val.addrs = boost::lexical_cast<std::string>(i.first);
|
||||
val.is_alive = i.second.is_alive();
|
||||
@@ -52,14 +52,14 @@ void set_failure_detector(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
|
||||
fd::get_up_endpoint_count.set(r, [&g](std::unique_ptr<request> req) {
|
||||
return gms::get_up_endpoint_count(g).then([](int res) {
|
||||
fd::get_up_endpoint_count.set(r, [](std::unique_ptr<request> req) {
|
||||
return gms::get_up_endpoint_count().then([](int res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
fd::get_down_endpoint_count.set(r, [&g](std::unique_ptr<request> req) {
|
||||
return gms::get_down_endpoint_count(g).then([](int res) {
|
||||
fd::get_down_endpoint_count.set(r, [](std::unique_ptr<request> req) {
|
||||
return gms::get_down_endpoint_count().then([](int res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
@@ -70,8 +70,8 @@ void set_failure_detector(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
});
|
||||
});
|
||||
|
||||
fd::get_simple_states.set(r, [&g] (std::unique_ptr<request> req) {
|
||||
return gms::get_simple_states(g).then([](const std::map<sstring, sstring>& map) {
|
||||
fd::get_simple_states.set(r, [] (std::unique_ptr<request> req) {
|
||||
return gms::get_simple_states().then([](const std::map<sstring, sstring>& map) {
|
||||
return make_ready_future<json::json_return_type>(map_to_key_value<fd::mapper>(map));
|
||||
});
|
||||
});
|
||||
@@ -83,8 +83,8 @@ void set_failure_detector(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
});
|
||||
});
|
||||
|
||||
fd::get_endpoint_state.set(r, [&g] (std::unique_ptr<request> req) {
|
||||
return get_endpoint_state(g, req->param["addr"]).then([](const sstring& state) {
|
||||
fd::get_endpoint_state.set(r, [](std::unique_ptr<request> req) {
|
||||
return gms::get_endpoint_state(req->param["addr"]).then([](const sstring& state) {
|
||||
return make_ready_future<json::json_return_type>(state);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -23,14 +23,8 @@
|
||||
|
||||
#include "api.hh"
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_failure_detector(http_context& ctx, routes& r, gms::gossiper& g);
|
||||
void set_failure_detector(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -21,55 +21,48 @@
|
||||
|
||||
#include "gossiper.hh"
|
||||
#include "api/api-doc/gossiper.json.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include <gms/gossiper.hh>
|
||||
|
||||
namespace api {
|
||||
using namespace json;
|
||||
|
||||
void set_gossiper(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
httpd::gossiper_json::get_down_endpoint.set(r, [&g] (const_req req) {
|
||||
auto res = g.get_unreachable_members();
|
||||
void set_gossiper(http_context& ctx, routes& r) {
|
||||
httpd::gossiper_json::get_down_endpoint.set(r, [] (const_req req) {
|
||||
auto res = gms::get_local_gossiper().get_unreachable_members();
|
||||
return container_to_vec(res);
|
||||
});
|
||||
|
||||
httpd::gossiper_json::get_live_endpoint.set(r, [&g] (const_req req) {
|
||||
auto res = g.get_live_members();
|
||||
httpd::gossiper_json::get_live_endpoint.set(r, [] (const_req req) {
|
||||
auto res = gms::get_local_gossiper().get_live_members();
|
||||
return container_to_vec(res);
|
||||
});
|
||||
|
||||
httpd::gossiper_json::get_endpoint_downtime.set(r, [&g] (const_req req) {
|
||||
httpd::gossiper_json::get_endpoint_downtime.set(r, [] (const_req req) {
|
||||
gms::inet_address ep(req.param["addr"]);
|
||||
return g.get_endpoint_downtime(ep);
|
||||
return gms::get_local_gossiper().get_endpoint_downtime(ep);
|
||||
});
|
||||
|
||||
httpd::gossiper_json::get_current_generation_number.set(r, [&g] (std::unique_ptr<request> req) {
|
||||
httpd::gossiper_json::get_current_generation_number.set(r, [] (std::unique_ptr<request> req) {
|
||||
gms::inet_address ep(req->param["addr"]);
|
||||
return g.get_current_generation_number(ep).then([] (int res) {
|
||||
return gms::get_local_gossiper().get_current_generation_number(ep).then([] (int res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
httpd::gossiper_json::get_current_heart_beat_version.set(r, [&g] (std::unique_ptr<request> req) {
|
||||
httpd::gossiper_json::get_current_heart_beat_version.set(r, [] (std::unique_ptr<request> req) {
|
||||
gms::inet_address ep(req->param["addr"]);
|
||||
return g.get_current_heart_beat_version(ep).then([] (int res) {
|
||||
return gms::get_local_gossiper().get_current_heart_beat_version(ep).then([] (int res) {
|
||||
return make_ready_future<json::json_return_type>(res);
|
||||
});
|
||||
});
|
||||
|
||||
httpd::gossiper_json::assassinate_endpoint.set(r, [&g](std::unique_ptr<request> req) {
|
||||
httpd::gossiper_json::assassinate_endpoint.set(r, [](std::unique_ptr<request> req) {
|
||||
if (req->get_query_param("unsafe") != "True") {
|
||||
return g.assassinate_endpoint(req->param["addr"]).then([] {
|
||||
return gms::get_local_gossiper().assassinate_endpoint(req->param["addr"]).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
}
|
||||
return g.unsafe_assassinate_endpoint(req->param["addr"]).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
httpd::gossiper_json::force_remove_endpoint.set(r, [&g](std::unique_ptr<request> req) {
|
||||
gms::inet_address ep(req->param["addr"]);
|
||||
return g.force_remove_endpoint(ep).then([] {
|
||||
return gms::get_local_gossiper().unsafe_assassinate_endpoint(req->param["addr"]).then([] {
|
||||
return make_ready_future<json::json_return_type>(json_void());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -23,14 +23,8 @@
|
||||
|
||||
#include "api.hh"
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_gossiper(http_context& ctx, routes& r, gms::gossiper& g);
|
||||
void set_gossiper(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -19,93 +19,15 @@
|
||||
* along with Scylla. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "hinted_handoff.hh"
|
||||
#include "api/api-doc/hinted_handoff.json.hh"
|
||||
|
||||
#include "gms/inet_address.hh"
|
||||
#include "gms/gossiper.hh"
|
||||
#include "service/storage_proxy.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
using namespace json;
|
||||
namespace hh = httpd::hinted_handoff_json;
|
||||
|
||||
void set_hinted_handoff(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
hh::create_hints_sync_point.set(r, [&ctx, &g] (std::unique_ptr<request> req) -> future<json::json_return_type> {
|
||||
auto parse_hosts_list = [&g] (sstring arg) {
|
||||
std::vector<sstring> hosts_str = split(arg, ",");
|
||||
std::vector<gms::inet_address> hosts;
|
||||
hosts.reserve(hosts_str.size());
|
||||
|
||||
if (hosts_str.empty()) {
|
||||
// No target_hosts specified means that we should wait for hints for all nodes to be sent
|
||||
const auto members_set = g.get_live_members();
|
||||
std::copy(members_set.begin(), members_set.end(), std::back_inserter(hosts));
|
||||
} else {
|
||||
for (const auto& host_str : hosts_str) {
|
||||
try {
|
||||
gms::inet_address host;
|
||||
host = gms::inet_address(host_str);
|
||||
hosts.push_back(host);
|
||||
} catch (std::exception& e) {
|
||||
throw httpd::bad_param_exception(format("Failed to parse host address {}: {}", host_str, e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
};
|
||||
|
||||
std::vector<gms::inet_address> target_hosts = parse_hosts_list(req->get_query_param("target_hosts"));
|
||||
return ctx.sp.local().create_hint_sync_point(std::move(target_hosts)).then([] (db::hints::sync_point sync_point) {
|
||||
return json::json_return_type(sync_point.encode());
|
||||
});
|
||||
});
|
||||
|
||||
hh::get_hints_sync_point.set(r, [&ctx] (std::unique_ptr<request> req) -> future<json::json_return_type> {
|
||||
db::hints::sync_point sync_point;
|
||||
const sstring encoded = req->get_query_param("id");
|
||||
try {
|
||||
sync_point = db::hints::sync_point::decode(encoded);
|
||||
} catch (std::exception& e) {
|
||||
throw httpd::bad_param_exception(format("Failed to parse the sync point description {}: {}", encoded, e.what()));
|
||||
}
|
||||
|
||||
lowres_clock::time_point deadline;
|
||||
const sstring timeout_str = req->get_query_param("timeout");
|
||||
try {
|
||||
deadline = [&] {
|
||||
if (timeout_str.empty()) {
|
||||
// Empty string - don't wait at all, just check the status
|
||||
return lowres_clock::time_point::min();
|
||||
} else {
|
||||
const auto timeout = std::stoll(timeout_str);
|
||||
if (timeout >= 0) {
|
||||
// Wait until the point is reached, or until `timeout` seconds elapse
|
||||
return lowres_clock::now() + std::chrono::seconds(timeout);
|
||||
} else {
|
||||
// Negative value indicates infinite timeout
|
||||
return lowres_clock::time_point::max();
|
||||
}
|
||||
}
|
||||
} ();
|
||||
} catch (std::exception& e) {
|
||||
throw httpd::bad_param_exception(format("Failed to parse the timeout parameter {}: {}", timeout_str, e.what()));
|
||||
}
|
||||
|
||||
using return_type = hh::ns_get_hints_sync_point::get_hints_sync_point_return_type;
|
||||
using return_type_wrapper = hh::ns_get_hints_sync_point::return_type_wrapper;
|
||||
|
||||
return ctx.sp.local().wait_for_hint_sync_point(std::move(sync_point), deadline).then([] {
|
||||
return json::json_return_type(return_type_wrapper(return_type::DONE));
|
||||
}).handle_exception_type([] (const timed_out_error&) {
|
||||
return json::json_return_type(return_type_wrapper(return_type::IN_PROGRESS));
|
||||
});
|
||||
});
|
||||
|
||||
void set_hinted_handoff(http_context& ctx, routes& r) {
|
||||
hh::list_endpoints_pending_hints.set(r, [] (std::unique_ptr<request> req) {
|
||||
//TBD
|
||||
unimplemented();
|
||||
@@ -149,16 +71,5 @@ void set_hinted_handoff(http_context& ctx, routes& r, gms::gossiper& g) {
|
||||
});
|
||||
}
|
||||
|
||||
void unset_hinted_handoff(http_context& ctx, routes& r) {
|
||||
hh::create_hints_sync_point.unset(r);
|
||||
hh::get_hints_sync_point.unset(r);
|
||||
|
||||
hh::list_endpoints_pending_hints.unset(r);
|
||||
hh::truncate_all_hints.unset(r);
|
||||
hh::schedule_hint_delivery.unset(r);
|
||||
hh::pause_hints_delivery.unset(r);
|
||||
hh::get_create_hint_count.unset(r);
|
||||
hh::get_not_stored_hints_count.unset(r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -23,15 +23,8 @@
|
||||
|
||||
#include "api.hh"
|
||||
|
||||
namespace gms {
|
||||
|
||||
class gossiper;
|
||||
|
||||
}
|
||||
|
||||
namespace api {
|
||||
|
||||
void set_hinted_handoff(http_context& ctx, routes& r, gms::gossiper& g);
|
||||
void unset_hinted_handoff(http_context& ctx, routes& r);
|
||||
void set_hinted_handoff(http_context& ctx, routes& r);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2015-present ScyllaDB
|
||||
* Copyright (C) 2015 ScyllaDB
|
||||
*/
|
||||
|
||||
/*
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <seastar/http/exception.hh>
|
||||
#include "utils/logalloc.hh"
|
||||
#include "log.hh"
|
||||
#include "database.hh"
|
||||
|
||||
namespace api {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user