Compare commits

...

19 Commits

Author SHA1 Message Date
Ruihang Xia
a179481966 docs: refine contributing.md (#450)
Signed-off-by: Ruihang Xia <waynestxia@gmail.com>

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>
2022-11-10 19:29:38 +08:00
Yingwen
3ae7362f58 ci: Upgrade ci dependencies and switch to nextest (#446)
* ci: Upgrade rust-cache to v2.2.0

v2.0.0 uses API that is deprecated

* ci: Use --workspace in cargo llvm-cov

* ci: Replace actions-rs/toolchain by dtolnay/rust-toolchain

actions-rs/toolchain is under inactive maintenance, it uses node12 that
would soon becomes deprecated

* ci: Replace actions-rs/cargo by run

* ci: rust-cache and cleanup-disk-action try not to specific full version

* ci: Use nextest

Also sets timeout for nextest to avoid a test hanging too long

* ci: Upgrade actions/checkout to v3

To upgrade node from 12 to 16

* ci: Specific cleanup-disk-action version
2022-11-10 19:21:53 +08:00
Lei, Huang
2e9c9f2176 fix: sort system catalog entries to ensure catalog entry are firstlt processed (#449) 2022-11-10 19:21:27 +08:00
zyy17
89b942798c feat: add 'scripts/install.sh' to make the installation more easy (#443) 2022-11-10 19:10:49 +08:00
Lei, Huang
952e646e1d refactor: move insert-to-create logic to a separate crate (#447) 2022-11-10 17:16:40 +08:00
Jiachun Feng
23f0320ffb feat: route for insert&select (#425)
* feat: route for insert/select

* chore: remove redundant tests

* chore: add fouce quit loop count limit to sequence

* chore: by code review

* chore: use ref with TableRouteKey

* chore: minor refactor
2022-11-10 16:13:15 +08:00
xiaomin tang
49403012b5 docs: Add Apache 2.0 license (#434) 2022-11-10 14:21:24 +08:00
zyy17
b87d5334d1 ci: modify docker image tag and push the latest tag image (#419) 2022-11-10 13:25:45 +08:00
Yingwen
fa4a74a408 ci: Use cargo-llvm-cov to generate coverage data (#438)
* ci: Use cargo-llvm-cov to generate coverage data

* ci: Remove usage of lld in coverage.yml

* ci: Move codecov.yml to project root
2022-11-10 13:25:18 +08:00
dennis zhuang
e62b302fb2 feat: some improvements on python coprocessor (#423)
* feat: supports list array in arrow_array_get

* feat: supports string and list type conversions in python coprocessor

* test: add test cases for returning list in coprocessor
2022-11-10 11:53:27 +08:00
Lei, Huang
6288fdb6bc feat: frontend catalog (#437)
* feat: add frontend catalog
2022-11-10 11:52:57 +08:00
Yingwen
cefdffff09 fix: CURRENT_TIMESTAMP supports int64 type (#436)
* fix: Fix int64 type not considered in DEFAULT CURRENT_TIMESTAMP() constraint

Also avoid using `ConstantVector` in default constraint, as other user
may try to downcast it to a concrete type, and sometimes may forget to
check whether it is a constant vector.

* test: Add test for writing default value
2022-11-10 11:35:16 +08:00
Lei, Huang
c3776ddd18 fix: stop background task when LogFile is dropped (#440) 2022-11-10 11:33:33 +08:00
fys
056d7cb911 feat: support convert prometheus write request to InsertRequest (#433)
* support convert prometheus to InsertRequest.
2022-11-10 10:48:41 +08:00
Ruihang Xia
16d1132733 chore: github action manually trigger (#439)
Signed-off-by: Ruihang Xia <waynestxia@gmail.com>

Signed-off-by: Ruihang Xia <waynestxia@gmail.com>
2022-11-09 20:35:01 +08:00
LFC
37dc85a29e feat: add gRPC reflection service (#431)
Co-authored-by: luofucong <luofucong@greptime.com>
2022-11-09 19:13:01 +08:00
fys
d08f8b87a6 feat: convert different protocol to InsertRequest (#426)
* add line_writer and convert insert_stmt to InsertRequest

* support convert influxdb line protocol to InsertRequest

* support convert opentsdb to InsertRequest

* cr
2022-11-09 16:18:54 +08:00
Ning Sun
64a706d6f0 ci: disable lld in release workflow for now (#427) 2022-11-09 14:37:25 +08:00
Yingwen
cf4e876e51 feat: mito table supports RemoveColumns alter kind (#395)
* feat: Support removing columns from mito table

Implements drop column for mito table engine, and adjusts the execution
order of altering table, persists the table manifest first, then alter
the schema of the region.

* feat(storage): Remove duplicate table_info() impl

Table already provides a table_info() now, some downcast in tests are
also no longer needed.

* test: Add tests for add/remove columns

* style(table): Fix clippy

* fix: Find timestamp index by its column name

Previous implementation updates the timestamp index too early, which
would cause the index check that compare the index to remove with
timestamp index failed.

* chore: Remove generated comment in Cargo.toml

* chore: Rename alter to builder_with_alter_kind

* refactor: Alloc new column from TableMeta

* style: Fix clippy
2022-11-09 11:50:02 +08:00
93 changed files with 3527 additions and 918 deletions

2
.config/nextest.toml Normal file
View File

@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "60s", terminate-after = 3, grace-period = "30s" }

View File

@@ -5,6 +5,7 @@ on:
branches:
- "main"
- "develop"
workflow_dispatch:
name: Code coverage
@@ -12,51 +13,37 @@ env:
RUST_TOOLCHAIN: nightly-2022-07-14
jobs:
grcov:
coverage:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- name: Cache LLVM and Clang
id: cache-llvm
uses: actions/cache@v3
with:
path: ./llvm
key: llvm
- uses: actions/checkout@v3
- uses: arduino/setup-protoc@v1
- uses: KyleMayes/install-llvm-action@v1
with:
version: "14.0"
cached: ${{ steps.cache-llvm.outputs.cache-hit }}
- name: Install toolchain
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
components: llvm-tools-preview
- name: Rust Cache
uses: Swatinem/rust-cache@v2.0.0
uses: Swatinem/rust-cache@v2
- name: Cleanup disk
uses: curoky/cleanup-disk-action@v2.0
with:
retain: 'rust,llvm'
- name: Execute tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
retain: 'rust'
- name: Install latest nextest release
uses: taiki-e/install-action@nextest
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Collect coverage data
run: cargo llvm-cov nextest --workspace --lcov --output-path lcov.info
env:
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests -Clink-arg=-fuse-ld=lld"
GT_S3_BUCKET: ${{ secrets.S3_BUCKET }}
GT_S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
GT_S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
UNITTEST_LOG_DIR: "__unittest_logs"
- name: Gather coverage data
id: coverage
uses: actions-rs/grcov@v0.1
- name: Codecov upload
uses: codecov/codecov-action@v2
with:

View File

@@ -13,6 +13,7 @@ on:
- '**.yml'
- '.dockerignore'
- 'docker/**'
workflow_dispatch:
name: Continuous integration for developing
@@ -26,19 +27,15 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: arduino/setup-protoc@v1
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2.0.0
- uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --all-targets
uses: Swatinem/rust-cache@v2
- name: Run cargo check
run: cargo check --workspace --all-targets
test:
name: Test Suite
@@ -46,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Cache LLVM and Clang
id: cache-llvm
uses: actions/cache@v3
@@ -58,21 +55,19 @@ jobs:
with:
version: "14.0"
cached: ${{ steps.cache-llvm.outputs.cache-hit }}
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v2.0.0
uses: Swatinem/rust-cache@v2
- name: Cleanup disk
uses: curoky/cleanup-disk-action@v2.0
with:
retain: 'rust,llvm'
- uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
- name: Install latest nextest release
uses: taiki-e/install-action@nextest
- name: Run tests
run: cargo nextest run
env:
CARGO_BUILD_RUSTFLAGS: "-C link-arg=-fuse-ld=lld"
RUST_BACKTRACE: 1
@@ -87,20 +82,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: arduino/setup-protoc@v1
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt
- name: Rust Cache
uses: Swatinem/rust-cache@v2.0.0
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
run: cargo fmt --all -- --check
clippy:
name: Clippy
@@ -108,17 +99,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: arduino/setup-protoc@v1
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2.0.0
- run: rustup component add clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets -- -D warnings -D clippy::print_stdout -D clippy::print_stderr
uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings -D clippy::print_stdout -D clippy::print_stderr

View File

@@ -2,6 +2,7 @@ on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
name: Release
@@ -13,20 +14,20 @@ jobs:
name: Build binary
strategy:
matrix:
# The file format is greptime-<tag>.<os>-<arch>
# The file format is greptime-<os>-<arch>
include:
- arch: x86_64-unknown-linux-gnu
os: ubuntu-latest
file: greptime-${{ github.ref_name }}.linux-amd64
file: greptime-linux-amd64
- arch: aarch64-unknown-linux-gnu
os: ubuntu-latest
file: greptime-${{ github.ref_name }}.linux-arm64
file: greptime-linux-arm64
- arch: aarch64-apple-darwin
os: macos-latest
file: greptime-${{ github.ref_name }}.darwin-arm64
file: greptime-darwin-arm64
- arch: x86_64-apple-darwin
os: macos-latest
file: greptime-${{ github.ref_name }}.darwin-amd64
file: greptime-darwin-amd64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
@@ -37,7 +38,6 @@ jobs:
uses: actions/cache@v3
with:
path: |
./llvm
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
@@ -53,11 +53,6 @@ jobs:
sudo cp protoc/bin/protoc /usr/local/bin/
sudo cp -r protoc/include/google /usr/local/include/
- uses: KyleMayes/install-llvm-action@v1
with:
version: "14.0"
cached: ${{ steps.cache.outputs.cache-hit }}
- name: Install Protoc for macos
if: contains(matrix.arch, 'darwin')
run: |
@@ -69,37 +64,31 @@ jobs:
sudo apt-get -y update
sudo apt-get -y install libssl-dev pkg-config g++-aarch64-linux-gnu gcc-aarch64-linux-gnu
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
target: ${{ matrix.arch }}
targets: ${{ matrix.arch }}
- name: Output package versions
run: protoc --version ; cargo version ; rustc --version ; gcc --version ; g++ --version
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
args: ${{ matrix.opts }} --release --locked --target ${{ matrix.arch }}
env:
CARGO_BUILD_RUSTFLAGS: "-C link-arg=-fuse-ld=lld"
run: cargo build ${{ matrix.opts }} --release --locked --target ${{ matrix.arch }}
- name: Calculate checksum and rename binary
shell: bash
run: |
cd target/${{ matrix.arch }}/release
cp greptime ${{ matrix.file }}
echo $(shasum -a 256 greptime | cut -f1 -d' ') > ${{ matrix.file }}.sha256sum
chmod +x greptime
tar -zcvf ${{ matrix.file }}.tgz greptime
echo $(shasum -a 256 ${{ matrix.file }}.tgz | cut -f1 -d' ') > ${{ matrix.file }}.sha256sum
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.file }}
path: target/${{ matrix.arch }}/release/${{ matrix.file }}
path: target/${{ matrix.arch }}/release/${{ matrix.file }}.tgz
- name: Upload checksum of artifacts
uses: actions/upload-artifact@v3
@@ -122,7 +111,7 @@ jobs:
with:
name: "Release ${{ github.ref_name }}"
files: |
**/greptime-${{ github.ref_name }}.*
**/greptime-*
docker:
name: Build docker image
@@ -130,32 +119,31 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download amd64 binary
uses: actions/download-artifact@v3
with:
name: greptime-${{ github.ref_name }}.linux-amd64
name: greptime-linux-amd64
path: amd64
- name: Rename amd64 binary
- name: Unzip the amd64 artifacts
run: |
mv amd64/greptime-${{ github.ref_name }}.linux-amd64 amd64/greptime
cd amd64
tar xvf greptime-linux-amd64.tgz
rm greptime-linux-amd64.tgz
- name: Download arm64 binary
uses: actions/download-artifact@v3
with:
name: greptime-${{ github.ref_name }}.linux-arm64
name: greptime-linux-arm64
path: arm64
- name: Rename arm64 binary
- name: Unzip the arm64 artifacts
run: |
mv arm64/greptime-${{ github.ref_name }}.linux-arm64 arm64/greptime
- name: Set file permissions
shell: bash
run: |
chmod +x amd64/greptime arm64/greptime
cd arm64
tar xvf greptime-linux-arm64.tgz
rm greptime-linux-arm64.tgz
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
@@ -170,6 +158,12 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Configure tag # If the release tag is v0.1.0, then the image version tag will be 0.1.0.
shell: bash
run: |
VERSION=${{ github.ref_name }}
echo "VERSION=${VERSION:1}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
@@ -184,5 +178,6 @@ jobs:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/greptimeteam/greptimedb:${{ github.ref_name }}
greptime/greptimedb:${{ github.ref_name }}
greptime/greptimedb:latest
greptime/greptimedb:${{ env.VERSION }}
ghcr.io/greptimeteam/greptimedb:${{ env.VERSION }}

View File

@@ -10,7 +10,6 @@ To learn about the design of GreptimeDB, please refer to the [design docs](https
- Make sure all unit tests are passed.
- Make sure all clippy warnings are fixed (you can check it locally by running `cargo clippy --workspace --all-targets -- -D warnings -D clippy::print_stdout -D clippy::print_stderr`).
#### `pre-commit` Hooks
You could setup the [`pre-commit`](https://pre-commit.com/#plugins) hooks to run these checks on every commit automatically.
@@ -37,11 +36,10 @@ pre-commit installed at .git/hooks/pre-pus
now `pre-commit` will run automatically on `git commit`.
### Title
The titles of pull requests should be prefixed with category name listed in [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0)
like `feat`/`fix`/`doc`, with a concise summary of code change follows. DO NOT use last commit message as pull request title.
The titles of pull requests should be prefixed with one of the change types listed in [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0)
like `feat`/`fix`/`docs`, with a concise summary of code change follows. The following scope field is optional, you can fill it with the name of sub-crate if the pull request only changes one, or just leave it blank.
### Description
@@ -49,15 +47,10 @@ like `feat`/`fix`/`doc`, with a concise summary of code change follows. DO NOT u
- But if it contains large code change, make sure to state the motivation/design details of this PR so that reviewers can understand what you're trying to do.
- If the PR contains any breaking change or API change, make sure that is clearly listed in your description.
### Commit Messages
All commit messages SHOULD adhere to the [Conventional Commits specification](https://conventionalcommits.org/).
## Getting help
There are many ways to get help when you're stuck. It is recommended to ask for help by opening an issue, with a detailed description
of what you were trying to do and what went wrong. You can also reach for help in our Slack channel.
## Bug report
To report a bug or a security issue, you can [open a new GitHub issue](https://github.com/GrepTimeTeam/greptimedb/issues/new).

39
Cargo.lock generated
View File

@@ -1121,6 +1121,22 @@ dependencies = [
"tower",
]
[[package]]
name = "common-insert"
version = "0.1.0"
dependencies = [
"api",
"async-trait",
"common-base",
"common-error",
"common-query",
"common-telemetry",
"common-time",
"datatypes",
"snafu",
"table",
]
[[package]]
name = "common-query"
version = "0.1.0"
@@ -1635,6 +1651,7 @@ dependencies = [
"common-catalog",
"common-error",
"common-grpc",
"common-insert",
"common-query",
"common-recordbatch",
"common-runtime",
@@ -2000,6 +2017,7 @@ dependencies = [
"catalog",
"client",
"common-base",
"common-catalog",
"common-error",
"common-grpc",
"common-query",
@@ -2014,13 +2032,16 @@ dependencies = [
"datatypes",
"futures",
"itertools",
"meta-client",
"openmetrics-parser",
"prost 0.11.0",
"query",
"serde",
"serde_json",
"servers",
"snafu",
"sql",
"sqlparser",
"store-api",
"table",
"tempdir",
@@ -2940,6 +2961,7 @@ dependencies = [
"api",
"async-trait",
"common-base",
"common-catalog",
"common-error",
"common-grpc",
"common-runtime",
@@ -2951,6 +2973,7 @@ dependencies = [
"http-body",
"lazy_static",
"parking_lot",
"prost 0.11.0",
"regex",
"serde",
"serde_json",
@@ -5068,6 +5091,7 @@ dependencies = [
"tokio-stream",
"tokio-test",
"tonic",
"tonic-reflection",
"tower",
"tower-http",
]
@@ -5968,6 +5992,21 @@ dependencies = [
"syn",
]
[[package]]
name = "tonic-reflection"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0455f730d540a1484bffc3c55c94100b18a662597b982c2e9073f2c55c602616"
dependencies = [
"bytes",
"prost 0.11.0",
"prost-types 0.11.1",
"tokio",
"tokio-stream",
"tonic",
"tonic-build",
]
[[package]]
name = "tower"
version = "0.4.13"

View File

@@ -15,6 +15,7 @@ members = [
"src/common/recordbatch",
"src/common/runtime",
"src/common/substrait",
"src/common/insert",
"src/common/telemetry",
"src/common/time",
"src/datanode",

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Greptime Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -2,7 +2,6 @@
name = "benchmarks"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
arrow = "10"

63
scripts/install.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/sh
set -ue
OS_TYPE=
ARCH_TYPE=
VERSION=${1:-latest}
GITHUB_ORG=GreptimeTeam
GITHUB_REPO=greptimedb
BIN=greptimedb
get_os_type() {
os_type="$(uname -s)"
case "$os_type" in
Darwin)
OS_TYPE=darwin
;;
Linux)
OS_TYPE=linux
;;
*)
echo "Error: Unknown OS type: $os_type"
exit 1
esac
}
get_arch_type() {
arch_type="$(uname -m)"
case "$arch_type" in
arm64)
ARCH_TYPE=arm64
;;
aarch64)
ARCH_TYPE=arm64
;;
x86_64)
ARCH_TYPE=amd64
;;
amd64)
ARCH_TYPE=amd64
;;
*)
echo "Error: Unknown CPU type: $arch_type"
exit 1
esac
}
get_os_type
get_arch_type
if [ -n "${OS_TYPE}" ] && [ -n "${ARCH_TYPE}" ]; then
echo "Downloading ${BIN}, OS: ${OS_TYPE}, Arch: ${ARCH_TYPE}, Version: ${VERSION}"
if [ "${VERSION}" = "latest" ]; then
wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/latest/download/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz"
else
wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/download/${VERSION}/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz"
fi
tar xvf ${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz && rm ${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz && echo "Run '${BIN} --help' to get started"
fi

View File

@@ -1,5 +1,9 @@
use std::path::PathBuf;
fn main() {
let default_out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
tonic_build::configure()
.file_descriptor_set_path(default_out_dir.join("greptime_fd.bin"))
.compile(
&[
"greptime/v1/insert.proto",

View File

@@ -9,6 +9,6 @@ message InsertBatch {
uint32 row_count = 2;
}
message RegionId {
uint64 id = 1;
message RegionNumber {
uint32 id = 1;
}

View File

@@ -63,8 +63,9 @@ message RegionRoute {
}
message Table {
TableName table_name = 1;
bytes table_schema = 2;
uint64 id = 1;
TableName table_name = 2;
bytes table_schema = 3;
}
message Region {
@@ -80,3 +81,9 @@ message Partition {
repeated bytes column_list = 1;
repeated bytes value_list = 2;
}
// This message is only for saving into store.
message TableRouteValue {
repeated Peer peers = 1;
TableRoute table_route = 2;
}

View File

@@ -1,7 +1,11 @@
pub use prost::DecodeError;
use prost::Message;
use crate::v1::codec::{InsertBatch, PhysicalPlanNode, RegionId, SelectResult};
use crate::v1::codec::InsertBatch;
use crate::v1::codec::PhysicalPlanNode;
use crate::v1::codec::RegionNumber;
use crate::v1::codec::SelectResult;
use crate::v1::meta::TableRouteValue;
macro_rules! impl_convert_with_bytes {
($data_type: ty) => {
@@ -24,7 +28,8 @@ macro_rules! impl_convert_with_bytes {
impl_convert_with_bytes!(InsertBatch);
impl_convert_with_bytes!(SelectResult);
impl_convert_with_bytes!(PhysicalPlanNode);
impl_convert_with_bytes!(RegionId);
impl_convert_with_bytes!(RegionNumber);
impl_convert_with_bytes!(TableRouteValue);
#[cfg(test)]
mod tests {
@@ -130,10 +135,10 @@ mod tests {
#[test]
fn test_convert_region_id() {
let region_id = RegionId { id: 12 };
let region_id = RegionNumber { id: 12 };
let bytes: Vec<u8> = region_id.into();
let region_id: RegionId = bytes.deref().try_into().unwrap();
let region_id: RegionNumber = bytes.deref().try_into().unwrap();
assert_eq!(12, region_id.id);
}

View File

@@ -1,6 +1,8 @@
#![allow(clippy::derive_partial_eq_without_eq)]
tonic::include_proto!("greptime.v1");
pub const GREPTIME_FD_SET: &[u8] = tonic::include_file_descriptor_set!("greptime_fd");
pub mod codec {
tonic::include_proto!("greptime.v1.codec");
}

View File

@@ -1,7 +1,47 @@
tonic::include_proto!("greptime.v1.meta");
use std::collections::HashMap;
use std::hash::Hash;
use std::hash::Hasher;
pub const PROTOCOL_VERSION: u64 = 1;
#[derive(Default)]
pub struct PeerDict {
peers: HashMap<Peer, usize>,
index: usize,
}
impl PeerDict {
pub fn get_or_insert(&mut self, peer: Peer) -> usize {
let index = self.peers.entry(peer).or_insert_with(|| {
let v = self.index;
self.index += 1;
v
});
*index
}
pub fn into_peers(self) -> Vec<Peer> {
let mut array = vec![Peer::default(); self.index];
for (p, i) in self.peers {
array[i] = p;
}
array
}
}
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for Peer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.addr.hash(state);
}
}
impl Eq for Peer {}
impl RequestHeader {
#[inline]
pub fn new((cluster_id, member_id): (u64, u64)) -> Self {
@@ -33,6 +73,21 @@ impl ResponseHeader {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
NoActiveDatanodes = 1,
}
impl Error {
#[inline]
pub fn no_active_datanodes() -> Self {
Self {
code: ErrorCode::NoActiveDatanodes as i32,
err_msg: "No active datanodes".to_string(),
}
}
}
macro_rules! gen_set_header {
($req: ty) => {
impl $req {
@@ -52,3 +107,59 @@ gen_set_header!(PutRequest);
gen_set_header!(BatchPutRequest);
gen_set_header!(CompareAndPutRequest);
gen_set_header!(DeleteRangeRequest);
#[cfg(test)]
mod tests {
use std::vec;
use super::*;
#[test]
fn test_peer_dict() {
let mut dict = PeerDict::default();
dict.get_or_insert(Peer {
id: 1,
addr: "111".to_string(),
});
dict.get_or_insert(Peer {
id: 2,
addr: "222".to_string(),
});
dict.get_or_insert(Peer {
id: 1,
addr: "111".to_string(),
});
dict.get_or_insert(Peer {
id: 1,
addr: "111".to_string(),
});
dict.get_or_insert(Peer {
id: 1,
addr: "111".to_string(),
});
dict.get_or_insert(Peer {
id: 1,
addr: "111".to_string(),
});
dict.get_or_insert(Peer {
id: 2,
addr: "222".to_string(),
});
assert_eq!(2, dict.index);
assert_eq!(
vec![
Peer {
id: 1,
addr: "111".to_string(),
},
Peer {
id: 2,
addr: "222".to_string(),
}
],
dict.into_peers()
);
}
}

View File

@@ -48,13 +48,19 @@ pub enum Error {
},
#[snafu(display("Invalid system catalog entry type: {:?}", entry_type))]
InvalidEntryType { entry_type: Option<u8> },
InvalidEntryType {
entry_type: Option<u8>,
backtrace: Backtrace,
},
#[snafu(display("Invalid system catalog key: {:?}", key))]
InvalidKey { key: Option<String> },
InvalidKey {
key: Option<String>,
backtrace: Backtrace,
},
#[snafu(display("Catalog value is not present"))]
EmptyValue,
EmptyValue { backtrace: Backtrace },
#[snafu(display("Failed to deserialize value, source: {}", source))]
ValueDeserialize {
@@ -63,10 +69,16 @@ pub enum Error {
},
#[snafu(display("Cannot find catalog by name: {}", catalog_name))]
CatalogNotFound { catalog_name: String },
CatalogNotFound {
catalog_name: String,
backtrace: Backtrace,
},
#[snafu(display("Cannot find schema, schema info: {}", schema_info))]
SchemaNotFound { schema_info: String },
SchemaNotFound {
schema_info: String,
backtrace: Backtrace,
},
#[snafu(display("Table {} already exists", table))]
TableExists { table: String, backtrace: Backtrace },
@@ -85,7 +97,10 @@ pub enum Error {
},
#[snafu(display("Table not found while opening table, table info: {}", table_info))]
TableNotFound { table_info: String },
TableNotFound {
table_info: String,
backtrace: Backtrace,
},
#[snafu(display("Failed to read system catalog table records"))]
ReadSystemCatalog {
@@ -155,6 +170,25 @@ pub enum Error {
#[snafu(display("Failed to parse table id from metasrv, data: {:?}", data))]
ParseTableId { data: String, backtrace: Backtrace },
#[snafu(display("Failed to deserialize partition rule from string: {:?}", data))]
DeserializePartitionRule {
data: String,
source: serde_json::error::Error,
backtrace: Backtrace,
},
#[snafu(display("Invalid table schema in catalog, source: {:?}", source))]
InvalidSchemaInCatalog {
#[snafu(backtrace)]
source: datatypes::error::Error,
},
#[snafu(display("Catalog internal error: {}", source))]
Internal {
#[snafu(backtrace)]
source: BoxedError,
},
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -171,7 +205,7 @@ impl ErrorExt for Error {
| Error::CatalogStateInconsistent { .. } => StatusCode::Unexpected,
Error::SystemCatalog { .. }
| Error::EmptyValue
| Error::EmptyValue { .. }
| Error::ValueDeserialize { .. }
| Error::Io { .. } => StatusCode::StorageUnavailable,
@@ -194,6 +228,9 @@ impl ErrorExt for Error {
Error::BumpTableId { .. } | Error::ParseTableId { .. } => {
StatusCode::StorageUnavailable
}
Error::DeserializePartitionRule { .. } => StatusCode::Unexpected,
Error::InvalidSchemaInCatalog { .. } => StatusCode::Unexpected,
Error::Internal { source, .. } => source.status_code(),
}
}
@@ -233,7 +270,7 @@ mod tests {
assert_eq!(
StatusCode::Unexpected,
Error::InvalidKey { key: None }.status_code()
InvalidKeySnafu { key: None }.build().status_code()
);
assert_eq!(
@@ -274,7 +311,7 @@ mod tests {
);
assert_eq!(
StatusCode::StorageUnavailable,
Error::EmptyValue.status_code()
EmptyValueSnafu {}.build().status_code()
);
}

View File

@@ -3,5 +3,5 @@ pub mod memory;
pub use manager::LocalCatalogManager;
pub use memory::{
new_memory_catalog_list, MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider,
new_memory_catalog_list, MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider,
};

View File

@@ -6,12 +6,11 @@ use common_catalog::consts::{
DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME, INFORMATION_SCHEMA_NAME, MIN_USER_TABLE_ID,
SYSTEM_CATALOG_NAME, SYSTEM_CATALOG_TABLE_NAME,
};
use common_recordbatch::RecordBatch;
use common_recordbatch::{RecordBatch, SendableRecordBatchStream};
use common_telemetry::info;
use datatypes::prelude::ScalarVector;
use datatypes::vectors::{BinaryVector, UInt8Vector};
use futures_util::lock::Mutex;
use futures_util::StreamExt;
use snafu::{ensure, OptionExt, ResultExt};
use table::engine::{EngineContext, TableEngineRef};
use table::metadata::TableId;
@@ -19,13 +18,12 @@ use table::requests::OpenTableRequest;
use table::table::numbers::NumbersTable;
use table::TableRef;
use crate::error::Result;
use crate::error::{
CatalogNotFoundSnafu, IllegalManagerStateSnafu, OpenTableSnafu, ReadSystemCatalogSnafu,
SchemaNotFoundSnafu, SystemCatalogSnafu, SystemCatalogTypeMismatchSnafu, TableExistsSnafu,
TableNotFoundSnafu,
CatalogNotFoundSnafu, IllegalManagerStateSnafu, OpenTableSnafu, SchemaNotFoundSnafu,
SystemCatalogSnafu, SystemCatalogTypeMismatchSnafu, TableExistsSnafu, TableNotFoundSnafu,
};
use crate::local::memory::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use crate::error::{ReadSystemCatalogSnafu, Result};
use crate::local::memory::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use crate::system::{
decode_system_catalog, Entry, SystemCatalogTable, TableEntry, ENTRY_TYPE_INDEX, KEY_INDEX,
VALUE_INDEX,
@@ -40,7 +38,7 @@ use crate::{
/// A `CatalogManager` consists of a system catalog and a bunch of user catalogs.
pub struct LocalCatalogManager {
system: Arc<SystemCatalog>,
catalogs: Arc<MemoryCatalogList>,
catalogs: Arc<MemoryCatalogManager>,
engine: TableEngineRef,
next_table_id: AtomicU32,
init_lock: Mutex<bool>,
@@ -70,17 +68,10 @@ impl LocalCatalogManager {
/// Scan all entries from system catalog table
pub async fn init(&self) -> Result<()> {
self.init_system_catalog()?;
let mut system_records = self.system.information_schema.system.records().await?;
let mut max_table_id = 0;
while let Some(records) = system_records
.next()
.await
.transpose()
.context(ReadSystemCatalogSnafu)?
{
let table_id = self.handle_system_catalog_entries(records).await?;
max_table_id = max_table_id.max(table_id);
}
let system_records = self.system.information_schema.system.records().await?;
let entries = self.collect_system_catalog_entries(system_records).await?;
let max_table_id = self.handle_system_catalog_entries(entries).await?;
info!(
"All system catalog entries processed, max table id: {}",
max_table_id
@@ -110,7 +101,6 @@ impl LocalCatalogManager {
let default_schema = Arc::new(MemorySchemaProvider::new());
// Add numbers table for test
// TODO(hl): remove this registration
let table = Arc::new(NumbersTable::default());
default_schema.register_table("numbers".to_string(), table)?;
@@ -120,47 +110,65 @@ impl LocalCatalogManager {
Ok(())
}
/// Processes records from system catalog table and returns the max table id persisted
/// in system catalog table.
async fn handle_system_catalog_entries(&self, records: RecordBatch) -> Result<TableId> {
/// Collect stream of system catalog entries to `Vec<Entry>`
async fn collect_system_catalog_entries(
&self,
stream: SendableRecordBatchStream,
) -> Result<Vec<Entry>> {
let record_batch = common_recordbatch::util::collect(stream)
.await
.context(ReadSystemCatalogSnafu)?;
let rbs = record_batch
.into_iter()
.map(Self::record_batch_to_entry)
.collect::<Result<Vec<_>>>()?;
Ok(rbs.into_iter().flat_map(Vec::into_iter).collect::<_>())
}
/// Convert `RecordBatch` to a vector of `Entry`.
fn record_batch_to_entry(rb: RecordBatch) -> Result<Vec<Entry>> {
ensure!(
records.df_recordbatch.columns().len() >= 6,
rb.df_recordbatch.columns().len() >= 6,
SystemCatalogSnafu {
msg: format!(
"Length mismatch: {}",
records.df_recordbatch.columns().len()
)
msg: format!("Length mismatch: {}", rb.df_recordbatch.columns().len())
}
);
let entry_type = UInt8Vector::try_from_arrow_array(&records.df_recordbatch.columns()[0])
let entry_type = UInt8Vector::try_from_arrow_array(&rb.df_recordbatch.columns()[0])
.with_context(|_| SystemCatalogTypeMismatchSnafu {
data_type: records.df_recordbatch.columns()[ENTRY_TYPE_INDEX]
data_type: rb.df_recordbatch.columns()[ENTRY_TYPE_INDEX]
.data_type()
.clone(),
})?;
let key = BinaryVector::try_from_arrow_array(&records.df_recordbatch.columns()[1])
let key = BinaryVector::try_from_arrow_array(&rb.df_recordbatch.columns()[1])
.with_context(|_| SystemCatalogTypeMismatchSnafu {
data_type: records.df_recordbatch.columns()[KEY_INDEX]
.data_type()
.clone(),
data_type: rb.df_recordbatch.columns()[KEY_INDEX].data_type().clone(),
})?;
let value = BinaryVector::try_from_arrow_array(&records.df_recordbatch.columns()[3])
let value = BinaryVector::try_from_arrow_array(&rb.df_recordbatch.columns()[3])
.with_context(|_| SystemCatalogTypeMismatchSnafu {
data_type: records.df_recordbatch.columns()[VALUE_INDEX]
.data_type()
.clone(),
data_type: rb.df_recordbatch.columns()[VALUE_INDEX].data_type().clone(),
})?;
let mut max_table_id = 0;
let mut res = Vec::with_capacity(rb.num_rows());
for ((t, k), v) in entry_type
.iter_data()
.zip(key.iter_data())
.zip(value.iter_data())
{
let entry = decode_system_catalog(t, k, v)?;
res.push(entry);
}
Ok(res)
}
/// Processes records from system catalog table and returns the max table id persisted
/// in system catalog table.
async fn handle_system_catalog_entries(&self, entries: Vec<Entry>) -> Result<TableId> {
let entries = Self::sort_entries(entries);
let mut max_table_id = 0;
for entry in entries {
match entry {
Entry::Catalog(c) => {
self.catalogs.register_catalog_if_absent(
@@ -192,6 +200,13 @@ impl LocalCatalogManager {
Ok(max_table_id)
}
/// Sort catalog entries to ensure catalog entries comes first, then schema entries,
/// and table entries is the last.
fn sort_entries(mut entries: Vec<Entry>) -> Vec<Entry> {
entries.sort();
entries
}
async fn open_and_register_table(&self, t: &TableEntry) -> Result<()> {
let catalog = self
.catalogs
@@ -351,3 +366,50 @@ impl CatalogManager for LocalCatalogManager {
schema.table(table_name)
}
}
#[cfg(test)]
mod tests {
use std::assert_matches::assert_matches;
use super::*;
use crate::system::{CatalogEntry, SchemaEntry};
#[test]
fn test_sort_entry() {
let vec = vec![
Entry::Table(TableEntry {
catalog_name: "C1".to_string(),
schema_name: "S1".to_string(),
table_name: "T1".to_string(),
table_id: 1,
}),
Entry::Catalog(CatalogEntry {
catalog_name: "C2".to_string(),
}),
Entry::Schema(SchemaEntry {
catalog_name: "C1".to_string(),
schema_name: "S1".to_string(),
}),
Entry::Schema(SchemaEntry {
catalog_name: "C2".to_string(),
schema_name: "S2".to_string(),
}),
Entry::Catalog(CatalogEntry {
catalog_name: "".to_string(),
}),
Entry::Table(TableEntry {
catalog_name: "C1".to_string(),
schema_name: "S1".to_string(),
table_name: "T2".to_string(),
table_id: 2,
}),
];
let res = LocalCatalogManager::sort_entries(vec);
assert_matches!(res[0], Entry::Catalog(..));
assert_matches!(res[1], Entry::Catalog(..));
assert_matches!(res[2], Entry::Schema(..));
assert_matches!(res[3], Entry::Schema(..));
assert_matches!(res[4], Entry::Table(..));
assert_matches!(res[5], Entry::Table(..));
}
}

View File

@@ -1,23 +1,94 @@
use std::any::Any;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::sync::RwLock;
use common_catalog::consts::MIN_USER_TABLE_ID;
use snafu::OptionExt;
use table::metadata::TableId;
use table::TableRef;
use crate::error::{Result, TableExistsSnafu};
use crate::error::{CatalogNotFoundSnafu, Result, SchemaNotFoundSnafu, TableExistsSnafu};
use crate::schema::SchemaProvider;
use crate::{CatalogList, CatalogProvider, CatalogProviderRef, SchemaProviderRef};
use crate::{
CatalogList, CatalogManager, CatalogProvider, CatalogProviderRef, RegisterSystemTableRequest,
RegisterTableRequest, SchemaProviderRef,
};
/// Simple in-memory list of catalogs
#[derive(Default)]
pub struct MemoryCatalogList {
pub struct MemoryCatalogManager {
/// Collection of catalogs containing schemas and ultimately Tables
pub catalogs: RwLock<HashMap<String, CatalogProviderRef>>,
pub table_id: AtomicU32,
}
impl MemoryCatalogList {
impl Default for MemoryCatalogManager {
fn default() -> Self {
let manager = Self {
table_id: AtomicU32::new(MIN_USER_TABLE_ID),
catalogs: Default::default(),
};
let default_catalog = Arc::new(MemoryCatalogProvider::new());
manager
.register_catalog("greptime".to_string(), default_catalog.clone())
.unwrap();
default_catalog
.register_schema("public".to_string(), Arc::new(MemorySchemaProvider::new()))
.unwrap();
manager
}
}
#[async_trait::async_trait]
impl CatalogManager for MemoryCatalogManager {
async fn start(&self) -> Result<()> {
self.table_id.store(MIN_USER_TABLE_ID, Ordering::Relaxed);
Ok(())
}
async fn next_table_id(&self) -> Result<TableId> {
Ok(self.table_id.fetch_add(1, Ordering::Relaxed))
}
async fn register_table(&self, request: RegisterTableRequest) -> Result<usize> {
let catalogs = self.catalogs.write().unwrap();
let catalog = catalogs
.get(&request.catalog)
.context(CatalogNotFoundSnafu {
catalog_name: &request.catalog,
})?
.clone();
let schema = catalog
.schema(&request.schema)?
.with_context(|| SchemaNotFoundSnafu {
schema_info: format!("{}.{}", &request.catalog, &request.schema),
})?;
schema
.register_table(request.table_name, request.table)
.map(|v| if v.is_some() { 0 } else { 1 })
}
async fn register_system_table(&self, _request: RegisterSystemTableRequest) -> Result<()> {
unimplemented!()
}
fn table(&self, catalog: &str, schema: &str, table_name: &str) -> Result<Option<TableRef>> {
let c = self.catalogs.read().unwrap();
let catalog = if let Some(c) = c.get(catalog) {
c.clone()
} else {
return Ok(None);
};
match catalog.schema(schema)? {
None => Ok(None),
Some(s) => s.table(table_name),
}
}
}
impl MemoryCatalogManager {
/// Registers a catalog and return `None` if no catalog with the same name was already
/// registered, or `Some` with the previously registered catalog.
pub fn register_catalog_if_absent(
@@ -37,7 +108,7 @@ impl MemoryCatalogList {
}
}
impl CatalogList for MemoryCatalogList {
impl CatalogList for MemoryCatalogManager {
fn as_any(&self) -> &dyn Any {
self
}
@@ -162,8 +233,8 @@ impl SchemaProvider for MemorySchemaProvider {
}
/// Create a memory catalog list contains a numbers table for test
pub fn new_memory_catalog_list() -> Result<Arc<MemoryCatalogList>> {
Ok(Arc::new(MemoryCatalogList::default()))
pub fn new_memory_catalog_list() -> Result<Arc<MemoryCatalogManager>> {
Ok(Arc::new(MemoryCatalogManager::default()))
}
#[cfg(test)]
@@ -178,23 +249,11 @@ mod tests {
#[test]
fn test_new_memory_catalog_list() {
let catalog_list = new_memory_catalog_list().unwrap();
let default_catalog = catalog_list.catalog(DEFAULT_CATALOG_NAME).unwrap().unwrap();
assert!(catalog_list
.catalog(DEFAULT_CATALOG_NAME)
.unwrap()
.is_none());
let default_catalog = Arc::new(MemoryCatalogProvider::default());
catalog_list
.register_catalog(DEFAULT_CATALOG_NAME.to_string(), default_catalog.clone())
.unwrap();
assert!(default_catalog
let default_schema = default_catalog
.schema(DEFAULT_SCHEMA_NAME)
.unwrap()
.is_none());
let default_schema = Arc::new(MemorySchemaProvider::default());
default_catalog
.register_schema(DEFAULT_SCHEMA_NAME.to_string(), default_schema.clone())
.unwrap();
default_schema
@@ -203,7 +262,6 @@ mod tests {
let table = default_schema.table("numbers").unwrap();
assert!(table.is_some());
assert!(default_schema.table("not_exists").unwrap().is_none());
}
@@ -229,7 +287,7 @@ mod tests {
#[test]
pub fn test_register_if_absent() {
let list = MemoryCatalogList::default();
let list = MemoryCatalogManager::default();
assert!(list
.register_catalog_if_absent(
"test_catalog".to_string(),
@@ -241,6 +299,8 @@ mod tests {
Arc::new(MemoryCatalogProvider::new()),
)
.unwrap();
list.as_any().downcast_ref::<MemoryCatalogList>().unwrap();
list.as_any()
.downcast_ref::<MemoryCatalogManager>()
.unwrap();
}
}

View File

@@ -306,25 +306,25 @@ impl TryFrom<u8> for EntryType {
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum Entry {
Catalog(CatalogEntry),
Schema(SchemaEntry),
Table(TableEntry),
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct CatalogEntry {
pub catalog_name: String,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct SchemaEntry {
pub catalog_name: String,
pub schema_name: String,
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct TableEntry {
pub catalog_name: String,
pub schema_name: String,

View File

@@ -312,6 +312,7 @@ fn build_schema_for_tables() -> Schema {
#[cfg(test)]
mod tests {
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_query::physical_plan::RuntimeEnv;
use datatypes::arrow::array::Utf8Array;
use datatypes::arrow::datatypes::DataType;
@@ -319,32 +320,30 @@ mod tests {
use table::table::numbers::NumbersTable;
use super::*;
use crate::local::memory::{
new_memory_catalog_list, MemoryCatalogProvider, MemorySchemaProvider,
};
use crate::local::memory::new_memory_catalog_list;
use crate::CatalogList;
#[tokio::test]
async fn test_tables() {
let catalog_list = new_memory_catalog_list().unwrap();
let catalog_provider = Arc::new(MemoryCatalogProvider::default());
let schema = Arc::new(MemorySchemaProvider::new());
let schema = catalog_list
.catalog(DEFAULT_CATALOG_NAME)
.unwrap()
.unwrap()
.schema(DEFAULT_SCHEMA_NAME)
.unwrap()
.unwrap();
schema
.register_table("test_table".to_string(), Arc::new(NumbersTable::default()))
.unwrap();
catalog_provider
.register_schema("test_schema".to_string(), schema)
.unwrap();
catalog_list
.register_catalog("test_catalog".to_string(), catalog_provider)
.unwrap();
let tables = Tables::new(catalog_list, "test_engine".to_string());
let tables = Tables::new(catalog_list, "test_engine".to_string());
let tables_stream = tables.scan(&None, &[], None).await.unwrap();
let mut tables_stream = tables_stream
.execute(0, Arc::new(RuntimeEnv::default()))
.await
.unwrap();
if let Some(t) = tables_stream.next().await {
let batch = t.unwrap().df_recordbatch;
assert_eq!(1, batch.num_rows());
@@ -354,7 +353,7 @@ mod tests {
assert_eq!(&DataType::Utf8, batch.column(2).data_type());
assert_eq!(&DataType::Utf8, batch.column(3).data_type());
assert_eq!(
"test_catalog",
"greptime",
batch
.column(0)
.as_any()
@@ -364,7 +363,7 @@ mod tests {
);
assert_eq!(
"test_schema",
"public",
batch
.column(1)
.as_any()

View File

@@ -12,6 +12,7 @@ use crate::error::{Result, TypeMismatchSnafu};
type ColumnName = String;
// TODO(fys): will remove in the future.
#[derive(Default)]
pub struct LinesWriter {
column_name_index: HashMap<ColumnName, usize>,
@@ -208,7 +209,7 @@ impl LinesWriter {
}
}
fn to_ms_ts(p: Precision, ts: i64) -> i64 {
pub fn to_ms_ts(p: Precision, ts: i64) -> i64 {
match p {
Precision::NANOSECOND => ts / 1_000_000,
Precision::MICROSECOND => ts / 1000,

View File

@@ -0,0 +1,18 @@
[package]
name = "common-insert"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
api = { path = "../../api" }
async-trait = "0.1"
common-base = { path = "../base" }
common-error = { path = "../error" }
common-telemetry = { path = "../telemetry" }
common-time = { path = "../time" }
common-query = { path = "../query" }
datatypes = { path = "../../datatypes" }
snafu = { version = "0.7", features = ["backtraces"] }
table = { path = "../../table" }

View File

@@ -0,0 +1,72 @@
use std::any::Any;
use api::DecodeError;
use common_error::ext::ErrorExt;
use common_error::prelude::{Snafu, StatusCode};
use snafu::{Backtrace, ErrorCompat};
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum Error {
#[snafu(display("Column {} not found in table {}", column_name, table_name))]
ColumnNotFound {
column_name: String,
table_name: String,
},
#[snafu(display("Failed to convert bytes to insert batch, source: {}", source))]
DecodeInsert { source: DecodeError },
#[snafu(display("Illegal insert data"))]
IllegalInsertData,
#[snafu(display("Column datatype error, source: {}", source))]
ColumnDataType {
#[snafu(backtrace)]
source: api::error::Error,
},
#[snafu(display("Failed to create schema when creating table, source: {}", source))]
CreateSchema {
#[snafu(backtrace)]
source: datatypes::error::Error,
},
#[snafu(display(
"Duplicated timestamp column in gRPC requests, exists {}, duplicated: {}",
exists,
duplicated
))]
DuplicatedTimestampColumn {
exists: String,
duplicated: String,
backtrace: Backtrace,
},
#[snafu(display("Missing timestamp column in request"))]
MissingTimestampColumn { backtrace: Backtrace },
}
pub type Result<T> = std::result::Result<T, Error>;
impl ErrorExt for Error {
fn status_code(&self) -> StatusCode {
match self {
Error::ColumnNotFound { .. } => StatusCode::TableColumnNotFound,
Error::DecodeInsert { .. } | Error::IllegalInsertData { .. } => {
StatusCode::InvalidArguments
}
Error::ColumnDataType { .. } => StatusCode::Internal,
Error::CreateSchema { .. }
| Error::DuplicatedTimestampColumn { .. }
| Error::MissingTimestampColumn { .. } => StatusCode::InvalidArguments,
}
}
fn backtrace_opt(&self) -> Option<&Backtrace> {
ErrorCompat::backtrace(self)
}
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@@ -24,15 +24,17 @@ use table::{
Table,
};
use crate::error::{self, ColumnNotFoundSnafu, DecodeInsertSnafu, IllegalInsertDataSnafu, Result};
use crate::error::{
ColumnDataTypeSnafu, ColumnNotFoundSnafu, CreateSchemaSnafu, DecodeInsertSnafu,
DuplicatedTimestampColumnSnafu, IllegalInsertDataSnafu, MissingTimestampColumnSnafu, Result,
};
const TAG_SEMANTIC_TYPE: i32 = SemanticType::Tag as i32;
const TIMESTAMP_SEMANTIC_TYPE: i32 = SemanticType::Timestamp as i32;
#[inline]
fn build_column_schema(column_name: &str, datatype: i32, nullable: bool) -> Result<ColumnSchema> {
let datatype_wrapper =
ColumnDataTypeWrapper::try_new(datatype).context(error::ColumnDataTypeSnafu)?;
let datatype_wrapper = ColumnDataTypeWrapper::try_new(datatype).context(ColumnDataTypeSnafu)?;
Ok(ColumnSchema::new(
column_name,
@@ -127,7 +129,7 @@ pub fn build_create_table_request(
TIMESTAMP_SEMANTIC_TYPE => {
ensure!(
timestamp_index == usize::MAX,
error::DuplicatedTimestampColumnSnafu {
DuplicatedTimestampColumnSnafu {
exists: &columns[timestamp_index].column_name,
duplicated: column_name,
}
@@ -145,17 +147,14 @@ pub fn build_create_table_request(
}
}
ensure!(
timestamp_index != usize::MAX,
error::MissingTimestampColumnSnafu
);
ensure!(timestamp_index != usize::MAX, MissingTimestampColumnSnafu);
let schema = Arc::new(
SchemaBuilder::try_from(column_schemas)
.unwrap()
.timestamp_index(Some(timestamp_index))
.build()
.context(error::CreateSchemaSnafu)?,
.context(CreateSchemaSnafu)?,
);
return Ok(CreateTableRequest {
@@ -172,7 +171,7 @@ pub fn build_create_table_request(
});
}
error::IllegalInsertDataSnafu.fail()
IllegalInsertDataSnafu.fail()
}
pub fn insertion_expr_to_request(
@@ -357,7 +356,8 @@ fn is_null(null_mask: &BitVec, idx: usize) -> Option<bool> {
#[cfg(test)]
mod tests {
use std::{any::Any, sync::Arc};
use std::any::Any;
use std::sync::Arc;
use api::v1::{
codec::InsertBatch,

View File

@@ -0,0 +1,6 @@
pub mod error;
mod insert;
pub use insert::{
build_alter_table_request, build_create_table_request, find_new_columns, insert_batches,
insertion_expr_to_request,
};

View File

@@ -22,6 +22,7 @@ common-recordbatch = { path = "../common/recordbatch" }
common-runtime = { path = "../common/runtime" }
common-telemetry = { path = "../common/telemetry" }
common-time = { path = "../common/time" }
common-insert = { path = "../common/insert" }
datafusion = { git = "https://github.com/apache/arrow-datafusion.git", branch = "arrow2", features = [
"simd",
] }

View File

@@ -1,6 +1,5 @@
use std::any::Any;
use api::serde::DecodeError;
use common_error::prelude::*;
use datatypes::arrow::error::ArrowError;
use storage::error::Error as StorageError;
@@ -73,9 +72,6 @@ pub enum Error {
#[snafu(display("Missing required field in protobuf, field: {}", field))]
MissingField { field: String, backtrace: Backtrace },
#[snafu(display("Missing timestamp column in request"))]
MissingTimestampColumn { backtrace: Backtrace },
#[snafu(display(
"Columns and values number mismatch, columns: {}, values: {}",
columns,
@@ -92,15 +88,10 @@ pub enum Error {
#[snafu(display("Failed to insert value to table: {}, source: {}", table_name, source))]
Insert {
table_name: String,
#[snafu(backtrace)]
source: TableError,
},
#[snafu(display("Illegal insert data"))]
IllegalInsertData,
#[snafu(display("Failed to convert bytes to insert batch, source: {}", source))]
DecodeInsert { source: DecodeError },
#[snafu(display("Failed to start server, source: {}", source))]
StartServer {
#[snafu(backtrace)]
@@ -257,17 +248,6 @@ pub enum Error {
source: datatypes::error::Error,
},
#[snafu(display(
"Duplicated timestamp column in gRPC requests, exists {}, duplicated: {}",
exists,
duplicated
))]
DuplicatedTimestampColumn {
exists: String,
duplicated: String,
backtrace: Backtrace,
},
#[snafu(display("Failed to access catalog, source: {}", source))]
Catalog {
#[snafu(backtrace)]
@@ -285,6 +265,15 @@ pub enum Error {
#[snafu(backtrace)]
source: meta_client::error::Error,
},
#[snafu(display("Failed to insert data, source: {}", source))]
InsertData {
#[snafu(backtrace)]
source: common_insert::error::Error,
},
#[snafu(display("Insert batch is empty"))]
EmptyInsertBatch,
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -316,18 +305,14 @@ impl ErrorExt for Error {
| Error::ConvertSchema { source, .. } => source.status_code(),
Error::ColumnValuesNumberMismatch { .. }
| Error::IllegalInsertData { .. }
| Error::DecodeInsert { .. }
| Error::InvalidSql { .. }
| Error::KeyColumnNotFound { .. }
| Error::InvalidPrimaryKey { .. }
| Error::MissingField { .. }
| Error::MissingTimestampColumn { .. }
| Error::CatalogNotFound { .. }
| Error::SchemaNotFound { .. }
| Error::ConstraintNotSupported { .. }
| Error::ParseTimestamp { .. }
| Error::DuplicatedTimestampColumn { .. } => StatusCode::InvalidArguments,
| Error::ParseTimestamp { .. } => StatusCode::InvalidArguments,
// TODO(yingwen): Further categorize http error.
Error::StartServer { .. }
@@ -353,6 +338,8 @@ impl ErrorExt for Error {
Error::ArrowComputation { .. } => StatusCode::Unexpected,
Error::MetaClientInit { source, .. } => source.status_code(),
Error::InsertData { source, .. } => source.status_code(),
Error::EmptyInsertBatch => StatusCode::InvalidArguments,
}
}

View File

@@ -1,6 +1,6 @@
use std::ops::Deref;
use api::v1::codec::RegionId;
use api::v1::codec::RegionNumber;
use api::v1::{
admin_expr, codec::InsertBatch, insert_expr, object_expr, select_expr, AdminExpr, AdminResult,
ObjectExpr, ObjectResult, SelectExpr,
@@ -17,12 +17,11 @@ use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
use table::requests::AddColumnRequest;
use crate::error::{
self, CatalogSnafu, DecodeLogicalPlanSnafu, ExecuteSqlSnafu, InsertSnafu, Result,
TableNotFoundSnafu, UnsupportedExprSnafu,
CatalogSnafu, DecodeLogicalPlanSnafu, EmptyInsertBatchSnafu, ExecuteSqlSnafu, InsertDataSnafu,
InsertSnafu, Result, TableNotFoundSnafu, UnsupportedExprSnafu,
};
use crate::instance::Instance;
use crate::server::grpc::handler::{build_err_result, ObjectResultBuilder};
use crate::server::grpc::insert::{self, insertion_expr_to_request};
use crate::server::grpc::plan::PhysicalPlanner;
use crate::server::grpc::select::to_object_result;
use crate::sql::SqlRequest;
@@ -38,7 +37,7 @@ impl Instance {
.map(|req| req.column_schema.name.clone())
.collect::<Vec<_>>();
let alter_request = insert::build_alter_table_request(table_name, add_columns);
let alter_request = common_insert::build_alter_table_request(table_name, add_columns);
debug!(
"Adding new columns: {:?} to table: {}",
@@ -70,13 +69,14 @@ impl Instance {
.next_table_id()
.await
.context(CatalogSnafu)?;
let create_table_request = insert::build_create_table_request(
let create_table_request = common_insert::build_create_table_request(
catalog_name,
schema_name,
table_id,
table_name,
insert_batches,
)?;
)
.context(InsertDataSnafu)?;
info!(
"Try to create table: {} automatically with request: {:?}",
@@ -111,12 +111,16 @@ impl Instance {
.expect("default schema must exist")
.unwrap();
let insert_batches = insert::insert_batches(values.values)?;
ensure!(!insert_batches.is_empty(), error::IllegalInsertDataSnafu);
let insert_batches =
common_insert::insert_batches(values.values).context(InsertDataSnafu)?;
ensure!(!insert_batches.is_empty(), EmptyInsertBatchSnafu);
let table = if let Some(table) = schema_provider.table(table_name).context(CatalogSnafu)? {
let schema = table.schema();
if let Some(add_columns) = insert::find_new_columns(&schema, &insert_batches)? {
if let Some(add_columns) = common_insert::find_new_columns(&schema, &insert_batches)
.context(InsertDataSnafu)?
{
self.add_new_columns_to_table(table_name, add_columns)
.await?;
}
@@ -137,7 +141,9 @@ impl Instance {
.context(TableNotFoundSnafu { table_name })?
};
let insert = insertion_expr_to_request(table_name, insert_batches, table.clone())?;
let insert =
common_insert::insertion_expr_to_request(table_name, insert_batches, table.clone())
.context(InsertDataSnafu)?;
let affected_rows = table
.insert(insert)
@@ -209,13 +215,13 @@ impl GrpcQueryHandler for Instance {
})?;
// TODO(fys): _region_id is for later use.
let _region_id: Option<RegionId> = insert_expr
let _region_id: Option<RegionNumber> = insert_expr
.options
.get("region_id")
.map(|id| {
id.deref()
.try_into()
.context(servers::error::DecodeRegionIdSnafu)
.context(servers::error::DecodeRegionNumberSnafu)
})
.transpose()?;

View File

@@ -1,5 +1,4 @@
mod ddl;
pub(crate) mod handler;
pub(crate) mod insert;
pub(crate) mod plan;
pub mod select;

View File

@@ -1,6 +1,8 @@
use arrow::array::{Int64Array, UInt64Array};
use common_query::Output;
use common_recordbatch::util;
use datafusion::arrow_print;
use datafusion_common::record_batch::RecordBatch as DfRecordBatch;
use datatypes::arrow_array::StringArray;
use datatypes::prelude::ConcreteDataType;
@@ -240,12 +242,24 @@ pub async fn test_create_table_illegal_timestamp_type() {
}
}
async fn check_output_stream(output: Output, expected: Vec<&str>) {
match output {
Output::Stream(stream) => {
let recordbatches = util::collect(stream).await.unwrap();
let recordbatch = recordbatches
.into_iter()
.map(|r| r.df_recordbatch)
.collect::<Vec<DfRecordBatch>>();
let pretty_print = arrow_print::write(&recordbatch);
let pretty_print = pretty_print.lines().collect::<Vec<&str>>();
assert_eq!(pretty_print, expected);
}
_ => unreachable!(),
}
}
#[tokio::test]
async fn test_alter_table() {
use datafusion::arrow_print;
use datafusion_common::record_batch::RecordBatch as DfRecordBatch;
// TODO(LFC) Use real Mito engine when we can alter its region schema,
// and delete the `new_mock` method.
let instance = Instance::new_mock().await.unwrap();
instance.start().await.unwrap();
@@ -278,26 +292,69 @@ async fn test_alter_table() {
assert!(matches!(output, Output::AffectedRows(1)));
let output = instance.execute_sql("select * from demo").await.unwrap();
match output {
Output::Stream(stream) => {
let recordbatches = util::collect(stream).await.unwrap();
let recordbatch = recordbatches
.into_iter()
.map(|r| r.df_recordbatch)
.collect::<Vec<DfRecordBatch>>();
let pretty_print = arrow_print::write(&recordbatch);
let pretty_print = pretty_print.lines().collect::<Vec<&str>>();
let expected = vec![
"+-------+-----+--------+---------------------+--------+",
"| host | cpu | memory | ts | my_tag |",
"+-------+-----+--------+---------------------+--------+",
"| host1 | 1.1 | 100 | 1970-01-01 00:00:01 | |",
"| host2 | 2.2 | 200 | 1970-01-01 00:00:02 | hello |",
"| host3 | 3.3 | 300 | 1970-01-01 00:00:03 | |",
"+-------+-----+--------+---------------------+--------+",
];
assert_eq!(pretty_print, expected);
}
_ => unreachable!(),
}
let expected = vec![
"+-------+-----+--------+---------------------+--------+",
"| host | cpu | memory | ts | my_tag |",
"+-------+-----+--------+---------------------+--------+",
"| host1 | 1.1 | 100 | 1970-01-01 00:00:01 | |",
"| host2 | 2.2 | 200 | 1970-01-01 00:00:02 | hello |",
"| host3 | 3.3 | 300 | 1970-01-01 00:00:03 | |",
"+-------+-----+--------+---------------------+--------+",
];
check_output_stream(output, expected).await;
}
async fn test_insert_with_default_value_for_type(type_name: &str) {
let (opts, _guard) = test_util::create_tmp_dir_and_datanode_opts("execute_create");
let instance = Instance::with_mock_meta_client(&opts).await.unwrap();
instance.start().await.unwrap();
let create_sql = format!(
r#"create table test_table(
host string,
ts {} DEFAULT CURRENT_TIMESTAMP,
cpu double default 0,
TIME INDEX (ts),
PRIMARY KEY(host)
) engine=mito with(regions=1);"#,
type_name
);
let output = instance.execute_sql(&create_sql).await.unwrap();
assert!(matches!(output, Output::AffectedRows(1)));
// Insert with ts.
instance
.execute_sql("insert into test_table(host, cpu, ts) values ('host1', 1.1, 1000)")
.await
.unwrap();
assert!(matches!(output, Output::AffectedRows(1)));
// Insert without ts, so it should be filled by default value.
let output = instance
.execute_sql("insert into test_table(host, cpu) values ('host2', 2.2)")
.await
.unwrap();
assert!(matches!(output, Output::AffectedRows(1)));
let output = instance
.execute_sql("select host, cpu from test_table")
.await
.unwrap();
let expected = vec![
"+-------+-----+",
"| host | cpu |",
"+-------+-----+",
"| host1 | 1.1 |",
"| host2 | 2.2 |",
"+-------+-----+",
];
check_output_stream(output, expected).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_insert_with_default_value() {
common_telemetry::init_default_ut_logging();
test_insert_with_default_value_for_type("timestamp").await;
test_insert_with_default_value_for_type("bigint").await;
}

View File

@@ -1,6 +1,6 @@
use arrow::array::{
self, Array, BinaryArray as ArrowBinaryArray, MutableBinaryArray as ArrowMutableBinaryArray,
MutableUtf8Array, PrimitiveArray, Utf8Array,
self, Array, BinaryArray as ArrowBinaryArray, ListArray,
MutableBinaryArray as ArrowMutableBinaryArray, MutableUtf8Array, PrimitiveArray, Utf8Array,
};
use arrow::datatypes::DataType as ArrowDataType;
use common_time::timestamp::Timestamp;
@@ -8,7 +8,7 @@ use snafu::OptionExt;
use crate::error::{ConversionSnafu, Result};
use crate::prelude::ConcreteDataType;
use crate::value::Value;
use crate::value::{ListValue, Value};
pub type BinaryArray = ArrowBinaryArray<i64>;
pub type MutableBinaryArray = ArrowMutableBinaryArray<i64>;
@@ -69,7 +69,14 @@ pub fn arrow_array_get(array: &dyn Array, idx: usize) -> Result<Value> {
};
Value::Timestamp(Timestamp::new(value, unit))
}
// TODO(sunng87): List
ArrowDataType::List(_) => {
let array = cast_array!(array, ListArray::<i32>).value(idx);
let inner_datatype = ConcreteDataType::try_from(array.data_type())?;
let values = (0..array.len())
.map(|i| arrow_array_get(&*array, i))
.collect::<Result<Vec<Value>>>()?;
Value::List(ListValue::new(Some(Box::new(values)), inner_datatype))
}
_ => unimplemented!("Arrow array datatype: {:?}", array.data_type()),
};
@@ -80,6 +87,7 @@ pub fn arrow_array_get(array: &dyn Array, idx: usize) -> Result<Value> {
mod test {
use arrow::array::Int64Array as ArrowI64Array;
use arrow::array::*;
use arrow::array::{MutableListArray, MutablePrimitiveArray, TryExtend};
use arrow::buffer::Buffer;
use arrow::datatypes::{DataType, TimeUnit as ArrowTimeUnit};
use common_time::timestamp::{TimeUnit, Timestamp};
@@ -164,5 +172,40 @@ mod test {
Value::Timestamp(Timestamp::new(1, TimeUnit::Nanosecond)),
arrow_array_get(&array4, 0).unwrap()
);
// test list array
let data = vec![
Some(vec![Some(1i32), Some(2), Some(3)]),
None,
Some(vec![Some(4), None, Some(6)]),
];
let mut arrow_array = MutableListArray::<i32, MutablePrimitiveArray<i32>>::new();
arrow_array.try_extend(data).unwrap();
let arrow_array: ListArray<i32> = arrow_array.into();
let v0 = arrow_array_get(&arrow_array, 0).unwrap();
match v0 {
Value::List(list) => {
assert!(matches!(list.datatype(), ConcreteDataType::Int32(_)));
let items = list.items().as_ref().unwrap();
assert_eq!(
**items,
vec![Value::Int32(1), Value::Int32(2), Value::Int32(3)]
);
}
_ => unreachable!(),
}
assert_eq!(Value::Null, arrow_array_get(&arrow_array, 1).unwrap());
let v2 = arrow_array_get(&arrow_array, 2).unwrap();
match v2 {
Value::List(list) => {
assert!(matches!(list.datatype(), ConcreteDataType::Int32(_)));
let items = list.items().as_ref().unwrap();
assert_eq!(**items, vec![Value::Int32(4), Value::Null, Value::Int32(6)]);
}
_ => unreachable!(),
}
}
}

View File

@@ -1,14 +1,13 @@
use std::sync::Arc;
use common_time::{util, Timestamp};
use common_time::util;
use serde::{Deserialize, Serialize};
use snafu::{ensure, ResultExt};
use crate::data_type::{ConcreteDataType, DataType};
use crate::error::{self, Result};
use crate::scalars::ScalarVector;
use crate::value::Value;
use crate::vectors::{ConstantVector, TimestampVector, VectorRef};
use crate::vectors::{Int64Vector, TimestampVector, VectorRef};
const CURRENT_TIMESTAMP: &str = "current_timestamp()";
@@ -107,15 +106,7 @@ impl ColumnDefaultConstraint {
match &expr[..] {
// TODO(dennis): we only supports current_timestamp right now,
// it's better to use a expression framework in future.
CURRENT_TIMESTAMP => {
// TODO(yingwen): We should coerce the type to the physical type of
// input `data_type`.
let vector =
Arc::new(TimestampVector::from_slice(&[Timestamp::from_millis(
util::current_time_millis(),
)]));
Ok(Arc::new(ConstantVector::new(vector, num_rows)))
}
CURRENT_TIMESTAMP => create_current_timestamp_vector(data_type, num_rows),
_ => error::UnsupportedDefaultExprSnafu { expr }.fail(),
}
}
@@ -143,9 +134,31 @@ impl ColumnDefaultConstraint {
}
}
fn create_current_timestamp_vector(
data_type: &ConcreteDataType,
num_rows: usize,
) -> Result<VectorRef> {
match data_type {
ConcreteDataType::Timestamp(_) => Ok(Arc::new(TimestampVector::from_values(
std::iter::repeat(util::current_time_millis()).take(num_rows),
))),
ConcreteDataType::Int64(_) => Ok(Arc::new(Int64Vector::from_values(
std::iter::repeat(util::current_time_millis()).take(num_rows),
))),
_ => error::DefaultValueTypeSnafu {
reason: format!(
"Not support to assign current timestamp to {:?} type",
data_type
),
}
.fail(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::vectors::Int32Vector;
#[test]
@@ -224,6 +237,7 @@ mod tests {
#[test]
fn test_create_default_vector_by_func() {
let constraint = ColumnDefaultConstraint::Function(CURRENT_TIMESTAMP.to_string());
// Timestamp type.
let data_type = ConcreteDataType::timestamp_millis_datatype();
let v = constraint
.create_default_vector(&data_type, false, 4)
@@ -235,10 +249,32 @@ mod tests {
v.get(0)
);
// Int64 type.
let data_type = ConcreteDataType::int64_datatype();
let v = constraint
.create_default_vector(&data_type, false, 4)
.unwrap();
assert_eq!(4, v.len());
assert!(
matches!(v.get(0), Value::Int64(_)),
"v {:?} is not timestamp",
v.get(0)
);
let constraint = ColumnDefaultConstraint::Function("no".to_string());
let data_type = ConcreteDataType::timestamp_millis_datatype();
constraint
.create_default_vector(&data_type, false, 4)
.unwrap_err();
}
#[test]
fn test_create_by_func_and_invalid_type() {
let constraint = ColumnDefaultConstraint::Function(CURRENT_TIMESTAMP.to_string());
let data_type = ConcreteDataType::boolean_datatype();
let err = constraint
.create_default_vector(&data_type, false, 4)
.unwrap_err();
assert!(matches!(err, Error::DefaultValueType { .. }), "{:?}", err);
}
}

View File

@@ -14,6 +14,7 @@ common-error = { path = "../common/error" }
common-grpc = { path = "../common/grpc" }
common-query = { path = "../common/query" }
common-recordbatch = { path = "../common/recordbatch" }
common-catalog = { path = "../common/catalog" }
common-runtime = { path = "../common/runtime" }
common-telemetry = { path = "../common/telemetry" }
common-time = { path = "../common/time" }
@@ -21,17 +22,21 @@ datafusion = { git = "https://github.com/apache/arrow-datafusion.git", branch =
datafusion-common = { git = "https://github.com/apache/arrow-datafusion.git", branch = "arrow2" }
datafusion-expr = { git = "https://github.com/apache/arrow-datafusion.git", branch = "arrow2" }
datatypes = { path = "../datatypes" }
futures = "0.3"
itertools = "0.10"
openmetrics-parser = "0.4"
prost = "0.11"
query = { path = "../query" }
serde = "1.0"
serde_json = "1.0"
sqlparser = "0.15"
servers = { path = "../servers" }
snafu = { version = "0.7", features = ["backtraces"] }
sql = { path = "../sql" }
store-api = { path = "../store-api" }
table = { path = "../table" }
tokio = { version = "1.18", features = ["full"] }
meta-client = {path = "../meta-client"}
[dependencies.arrow]
package = "arrow2"

270
src/frontend/src/catalog.rs Normal file
View File

@@ -0,0 +1,270 @@
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use catalog::error::{
DeserializePartitionRuleSnafu, InvalidCatalogValueSnafu, InvalidSchemaInCatalogSnafu,
};
use catalog::remote::{Kv, KvBackendRef};
use catalog::{
CatalogList, CatalogProvider, CatalogProviderRef, SchemaProvider, SchemaProviderRef,
};
use common_catalog::{CatalogKey, SchemaKey, TableGlobalKey, TableGlobalValue};
use common_error::ext::BoxedError;
use futures::StreamExt;
use snafu::{OptionExt, ResultExt};
use table::TableRef;
use tokio::sync::RwLock;
use crate::error::DatanodeNotAvailableSnafu;
use crate::mock::{DatanodeId, DatanodeInstance};
use crate::partitioning::range::RangePartitionRule;
use crate::table::DistTable;
pub type DatanodeInstances = HashMap<DatanodeId, DatanodeInstance>;
pub struct FrontendCatalogManager {
backend: KvBackendRef,
datanode_instances: Arc<RwLock<DatanodeInstances>>,
}
impl FrontendCatalogManager {
#[allow(dead_code)]
pub fn new(backend: KvBackendRef, datanode_instances: Arc<RwLock<DatanodeInstances>>) -> Self {
Self {
backend,
datanode_instances,
}
}
}
impl CatalogList for FrontendCatalogManager {
fn as_any(&self) -> &dyn Any {
self
}
fn register_catalog(
&self,
_name: String,
_catalog: CatalogProviderRef,
) -> catalog::error::Result<Option<CatalogProviderRef>> {
unimplemented!("Frontend catalog list does not support register catalog")
}
fn catalog_names(&self) -> catalog::error::Result<Vec<String>> {
let backend = self.backend.clone();
let res = std::thread::spawn(|| {
common_runtime::block_on_read(async move {
let key = common_catalog::build_catalog_prefix();
let mut iter = backend.range(key.as_bytes());
let mut res = HashSet::new();
while let Some(r) = iter.next().await {
let Kv(k, _) = r?;
let key = CatalogKey::parse(String::from_utf8_lossy(&k))
.context(InvalidCatalogValueSnafu)?;
res.insert(key.catalog_name);
}
Ok(res.into_iter().collect())
})
})
.join()
.unwrap();
res
}
fn catalog(&self, name: &str) -> catalog::error::Result<Option<CatalogProviderRef>> {
let all_catalogs = self.catalog_names()?;
if all_catalogs.contains(&name.to_string()) {
Ok(Some(Arc::new(FrontendCatalogProvider {
catalog_name: name.to_string(),
backend: self.backend.clone(),
datanode_instances: self.datanode_instances.clone(),
})))
} else {
Ok(None)
}
}
}
pub struct FrontendCatalogProvider {
catalog_name: String,
backend: KvBackendRef,
datanode_instances: Arc<RwLock<DatanodeInstances>>,
}
impl CatalogProvider for FrontendCatalogProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn schema_names(&self) -> catalog::error::Result<Vec<String>> {
let backend = self.backend.clone();
let catalog_name = self.catalog_name.clone();
let res = std::thread::spawn(|| {
common_runtime::block_on_read(async move {
let key = common_catalog::build_schema_prefix(&catalog_name);
let mut iter = backend.range(key.as_bytes());
let mut res = HashSet::new();
while let Some(r) = iter.next().await {
let Kv(k, _) = r?;
let key = SchemaKey::parse(String::from_utf8_lossy(&k))
.context(InvalidCatalogValueSnafu)?;
res.insert(key.schema_name);
}
Ok(res.into_iter().collect())
})
})
.join()
.unwrap();
res
}
fn register_schema(
&self,
_name: String,
_schema: SchemaProviderRef,
) -> catalog::error::Result<Option<SchemaProviderRef>> {
unimplemented!("Frontend catalog provider does not support register schema")
}
fn schema(&self, name: &str) -> catalog::error::Result<Option<SchemaProviderRef>> {
let all_schemas = self.schema_names()?;
if all_schemas.contains(&name.to_string()) {
Ok(Some(Arc::new(FrontendSchemaProvider {
catalog_name: self.catalog_name.clone(),
schema_name: name.to_string(),
backend: self.backend.clone(),
datanode_instances: self.datanode_instances.clone(),
})))
} else {
Ok(None)
}
}
}
pub struct FrontendSchemaProvider {
catalog_name: String,
schema_name: String,
backend: KvBackendRef,
datanode_instances: Arc<RwLock<DatanodeInstances>>,
}
impl SchemaProvider for FrontendSchemaProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn table_names(&self) -> catalog::error::Result<Vec<String>> {
let backend = self.backend.clone();
let catalog_name = self.catalog_name.clone();
let schema_name = self.schema_name.clone();
std::thread::spawn(|| {
common_runtime::block_on_read(async move {
let key = common_catalog::build_table_global_prefix(catalog_name, schema_name);
let mut iter = backend.range(key.as_bytes());
let mut res = HashSet::new();
while let Some(r) = iter.next().await {
let Kv(k, _) = r?;
let key = TableGlobalKey::parse(String::from_utf8_lossy(&k))
.context(InvalidCatalogValueSnafu)?;
res.insert(key.table_name);
}
Ok(res.into_iter().collect())
})
})
.join()
.unwrap()
}
fn table(&self, name: &str) -> catalog::error::Result<Option<TableRef>> {
let table_global_key = TableGlobalKey {
catalog_name: self.catalog_name.clone(),
schema_name: self.schema_name.clone(),
table_name: name.to_string(),
};
let instances = self.datanode_instances.clone();
let backend = self.backend.clone();
let table_name = name.to_string();
let result: Result<Option<TableRef>, catalog::error::Error> = std::thread::spawn(|| {
common_runtime::block_on_read(async move {
let mut datanode_instances = HashMap::new();
let res = match backend.get(table_global_key.to_string().as_bytes()).await? {
None => {
return Ok(None);
}
Some(r) => r,
};
let mut region_to_datanode_map = HashMap::new();
let val = TableGlobalValue::parse(String::from_utf8_lossy(&res.1))
.context(InvalidCatalogValueSnafu)?;
let node_id: DatanodeId = val.node_id;
// TODO(hl): We need to deserialize string to PartitionRule trait object
let partition_rule: Arc<RangePartitionRule> =
Arc::new(serde_json::from_str(&val.partition_rules).context(
DeserializePartitionRuleSnafu {
data: &val.partition_rules,
},
)?);
for (node_id, region_ids) in val.regions_id_map {
for region_id in region_ids {
region_to_datanode_map.insert(region_id, node_id);
}
}
datanode_instances.insert(
node_id,
instances
.read()
.await
.get(&node_id)
.context(DatanodeNotAvailableSnafu { node_id })
.map_err(BoxedError::new)
.context(catalog::error::InternalSnafu)?
.clone(),
);
let table = Arc::new(DistTable {
table_name: table_name.clone(),
schema: Arc::new(
val.meta
.schema
.try_into()
.context(InvalidSchemaInCatalogSnafu)?,
),
partition_rule,
region_dist_map: region_to_datanode_map,
datanode_instances,
});
Ok(Some(table as _))
})
})
.join()
.unwrap();
result
}
fn register_table(
&self,
_name: String,
_table: TableRef,
) -> catalog::error::Result<Option<TableRef>> {
unimplemented!("Frontend schema provider does not support register table")
}
fn deregister_table(&self, _name: &str) -> catalog::error::Result<Option<TableRef>> {
unimplemented!("Frontend schema provider does not support deregister table")
}
fn table_exist(&self, name: &str) -> catalog::error::Result<bool> {
Ok(self.table_names()?.contains(&name.to_string()))
}
}

View File

@@ -142,11 +142,50 @@ pub enum Error {
backtrace: Backtrace,
},
#[snafu(display("Table not found: {}", table_name))]
TableNotFound {
table_name: String,
backtrace: Backtrace,
},
#[snafu(display("Column {} not found in table {}", column_name, table_name))]
ColumnNotFound {
column_name: String,
table_name: String,
backtrace: Backtrace,
},
#[snafu(display(
"Columns and values number mismatch, columns: {}, values: {}",
columns,
values,
))]
ColumnValuesNumberMismatch {
columns: usize,
values: usize,
backtrace: Backtrace,
},
#[snafu(display("Failed to join task, source: {}", source))]
JoinTask {
source: common_runtime::JoinError,
backtrace: Backtrace,
},
#[snafu(display("Failed access catalog: {}", source))]
Catalog {
#[snafu(backtrace)]
source: catalog::error::Error,
},
#[snafu(display("Failed to parse catalog entry: {}", source))]
ParseCatalogEntry {
#[snafu(backtrace)]
source: common_catalog::error::Error,
},
#[snafu(display("Cannot find datanode by id: {}", node_id))]
DatanodeNotAvailable { node_id: u64, backtrace: Backtrace },
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -161,6 +200,7 @@ impl ErrorExt for Error {
| Error::FindRegions { .. }
| Error::InvalidInsertRequest { .. }
| Error::FindPartitionColumn { .. }
| Error::ColumnValuesNumberMismatch { .. }
| Error::RegionKeysSize { .. } => StatusCode::InvalidArguments,
Error::RuntimeResource { source, .. } => source.status_code(),
@@ -182,7 +222,14 @@ impl ErrorExt for Error {
StatusCode::Unexpected
}
Error::ExecOpentsdbPut { .. } => StatusCode::Internal,
Error::TableNotFound { .. } => StatusCode::TableNotFound,
Error::ColumnNotFound { .. } => StatusCode::TableColumnNotFound,
Error::JoinTask { .. } => StatusCode::Unexpected,
Error::Catalog { source, .. } => source.status_code(),
Error::ParseCatalogEntry { source, .. } => source.status_code(),
Error::DatanodeNotAvailable { .. } => StatusCode::StorageUnavailable,
}
}

View File

@@ -1,5 +1,6 @@
#![feature(assert_matches)]
mod catalog;
pub mod error;
pub mod frontend;
pub mod grpc;
@@ -13,6 +14,7 @@ pub mod postgres;
pub mod prometheus;
mod server;
pub mod spliter;
mod sql;
mod table;
#[cfg(test)]
mod tests;

View File

@@ -21,7 +21,7 @@ use table::table::adapter::DfTableProviderAdapter;
pub(crate) type DatanodeId = u64;
#[derive(Clone)]
pub(crate) struct DatanodeInstance {
pub struct DatanodeInstance {
pub(crate) datanode_id: DatanodeId,
catalog_manager: CatalogManagerRef,
db: Database,
@@ -37,12 +37,12 @@ impl DatanodeInstance {
#[allow(dead_code)]
pub(crate) fn new(
datanode_id: DatanodeId,
catalog_manager: CatalogManagerRef,
catalog_list: CatalogManagerRef,
db: Database,
) -> Self {
Self {
datanode_id,
catalog_manager,
catalog_manager: catalog_list,
db,
}
}

View File

@@ -6,7 +6,7 @@ use std::sync::Arc;
pub use datafusion_expr::Operator;
use datatypes::prelude::Value;
use store_api::storage::RegionId;
use store_api::storage::RegionNumber;
pub(crate) type PartitionRuleRef<E> = Arc<dyn PartitionRule<Error = E>>;
@@ -17,9 +17,9 @@ pub trait PartitionRule: Sync + Send {
// TODO(LFC): Unify `find_region` and `find_regions` methods when distributed read and write features are both merged into develop.
// Or find better names since one is mainly for writes and the other is for reads.
fn find_region(&self, values: &[Value]) -> Result<RegionId, Self::Error>;
fn find_region(&self, values: &[Value]) -> Result<RegionNumber, Self::Error>;
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionId>, Self::Error>;
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionNumber>, Self::Error>;
}
/// The right bound(exclusive) of partition range.

View File

@@ -1,9 +1,10 @@
use datafusion_expr::Operator;
use datatypes::value::Value;
use snafu::ensure;
use store_api::storage::RegionNumber;
use crate::error::{self, Error};
use crate::partitioning::{PartitionBound, PartitionExpr, PartitionRule, RegionId};
use crate::partitioning::{PartitionBound, PartitionExpr, PartitionRule};
/// A [RangeColumnsPartitionRule] is very similar to [RangePartitionRule] except that it allows
/// partitioning by multiple columns.
@@ -32,7 +33,7 @@ use crate::partitioning::{PartitionBound, PartitionExpr, PartitionRule, RegionId
struct RangeColumnsPartitionRule {
column_list: Vec<String>,
value_lists: Vec<Vec<PartitionBound>>,
regions: Vec<RegionId>,
regions: Vec<RegionNumber>,
// TODO(LFC): Implement finding regions by all partitioning columns, not by the first one only.
// Singled out the first partitioning column's bounds for finding regions by range.
@@ -54,7 +55,7 @@ struct RangeColumnsPartitionRule {
//
// The following two fields are acted as caches, so we don't need to recalculate them every time.
first_column_bounds: Vec<PartitionBound>,
first_column_regions: Vec<Vec<RegionId>>,
first_column_regions: Vec<Vec<RegionNumber>>,
}
impl RangeColumnsPartitionRule {
@@ -65,7 +66,7 @@ impl RangeColumnsPartitionRule {
fn new(
column_list: Vec<String>,
value_lists: Vec<Vec<PartitionBound>>,
regions: Vec<RegionId>,
regions: Vec<RegionNumber>,
) -> Self {
// An example range columns partition rule to calculate the first column bounds and regions:
// SQL:
@@ -87,7 +88,7 @@ impl RangeColumnsPartitionRule {
let mut distinct_bounds = Vec::<PartitionBound>::new();
distinct_bounds.push(first_column_bounds[0].clone());
let mut first_column_regions = Vec::<Vec<RegionId>>::new();
let mut first_column_regions = Vec::<Vec<RegionNumber>>::new();
first_column_regions.push(vec![regions[0]]);
for i in 1..first_column_bounds.len() {
@@ -116,7 +117,7 @@ impl PartitionRule for RangeColumnsPartitionRule {
self.column_list.clone()
}
fn find_region(&self, values: &[Value]) -> Result<RegionId, Self::Error> {
fn find_region(&self, values: &[Value]) -> Result<RegionNumber, Self::Error> {
ensure!(
values.len() == self.column_list.len(),
error::RegionKeysSizeSnafu {
@@ -137,7 +138,7 @@ impl PartitionRule for RangeColumnsPartitionRule {
})
}
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionId>, Self::Error> {
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionNumber>, Self::Error> {
let regions = if exprs.iter().all(|x| self.column_list.contains(&x.column)) {
let PartitionExpr {
column: _,
@@ -173,7 +174,7 @@ impl PartitionRule for RangeColumnsPartitionRule {
.iter()
.flatten()
.cloned()
.collect::<Vec<RegionId>>()
.collect::<Vec<RegionNumber>>()
} else {
self.regions.clone()
};
@@ -224,7 +225,7 @@ mod tests {
vec![1, 2, 3, 4, 5, 6],
);
let test = |op: Operator, value: &str, expected_regions: Vec<u64>| {
let test = |op: Operator, value: &str, expected_regions: Vec<RegionNumber>| {
let exprs = vec![
// Intentionally fix column b's partition expr to "b < 1". If we support finding
// regions by both columns("a" and "b") in the future, some test cases should fail.
@@ -242,7 +243,7 @@ mod tests {
let regions = rule.find_regions(&exprs).unwrap();
assert_eq!(
regions,
expected_regions.into_iter().collect::<Vec<RegionId>>()
expected_regions.into_iter().collect::<Vec<RegionNumber>>()
);
};

View File

@@ -1,8 +1,10 @@
use datatypes::prelude::*;
use serde::{Deserialize, Serialize};
use snafu::OptionExt;
use store_api::storage::RegionNumber;
use crate::error::{self, Error};
use crate::partitioning::{Operator, PartitionExpr, PartitionRule, RegionId};
use crate::partitioning::{Operator, PartitionExpr, PartitionRule};
/// [RangePartitionRule] manages the distribution of partitions partitioning by some column's value
/// range. It's generated from create table request, using MySQL's syntax:
@@ -41,13 +43,14 @@ use crate::partitioning::{Operator, PartitionExpr, PartitionRule, RegionId};
///
// TODO(LFC): Further clarify "partition" and "region".
// Could be creating an extra layer between partition and region.
pub(crate) struct RangePartitionRule {
#[derive(Debug, Serialize, Deserialize)]
pub struct RangePartitionRule {
column_name: String,
// Does not store the last "MAXVALUE" bound; because in this way our binary search in finding
// partitions are easier (besides, it's hard to represent "MAXVALUE" in our `Value`).
// Then the length of `bounds` is one less than `regions`.
bounds: Vec<Value>,
regions: Vec<RegionId>,
regions: Vec<RegionNumber>,
}
impl RangePartitionRule {
@@ -56,7 +59,7 @@ impl RangePartitionRule {
pub(crate) fn new(
column_name: impl Into<String>,
bounds: Vec<Value>,
regions: Vec<RegionId>,
regions: Vec<RegionNumber>,
) -> Self {
Self {
column_name: column_name.into(),
@@ -69,7 +72,7 @@ impl RangePartitionRule {
&self.column_name
}
fn all_regions(&self) -> &Vec<RegionId> {
fn all_regions(&self) -> &Vec<RegionNumber> {
&self.regions
}
}
@@ -81,11 +84,11 @@ impl PartitionRule for RangePartitionRule {
vec![self.column_name().to_string()]
}
fn find_region(&self, _values: &[Value]) -> Result<RegionId, Self::Error> {
fn find_region(&self, _values: &[Value]) -> Result<RegionNumber, Self::Error> {
unimplemented!()
}
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionId>, Self::Error> {
fn find_regions(&self, exprs: &[PartitionExpr]) -> Result<Vec<RegionNumber>, Self::Error> {
if exprs.is_empty() {
return Ok(self.regions.clone());
}
@@ -152,18 +155,19 @@ mod test {
regions: vec![1, 2, 3, 4],
};
let test = |column: &str, op: Operator, value: &str, expected_regions: Vec<u64>| {
let expr = PartitionExpr {
column: column.to_string(),
op,
value: value.into(),
let test =
|column: &str, op: Operator, value: &str, expected_regions: Vec<RegionNumber>| {
let expr = PartitionExpr {
column: column.to_string(),
op,
value: value.into(),
};
let regions = rule.find_regions(&[expr]).unwrap();
assert_eq!(
regions,
expected_regions.into_iter().collect::<Vec<RegionNumber>>()
);
};
let regions = rule.find_regions(&[expr]).unwrap();
assert_eq!(
regions,
expected_regions.into_iter().collect::<Vec<RegionId>>()
);
};
test("a", Operator::NotEq, "hz", vec![1, 2, 3, 4]);
test("a", Operator::NotEq, "what", vec![1, 2, 3, 4]);

View File

@@ -5,7 +5,7 @@ use datatypes::vectors::VectorBuilder;
use datatypes::vectors::VectorRef;
use snafu::ensure;
use snafu::OptionExt;
use store_api::storage::RegionId;
use store_api::storage::RegionNumber;
use table::requests::InsertRequest;
use crate::error::Error;
@@ -15,7 +15,7 @@ use crate::error::InvalidInsertRequestSnafu;
use crate::error::Result;
use crate::partitioning::PartitionRuleRef;
pub type DistInsertRequest = HashMap<RegionId, InsertRequest>;
pub type DistInsertRequest = HashMap<RegionNumber, InsertRequest>;
pub struct WriteSpliter {
partition_rule: PartitionRuleRef<Error>,
@@ -41,11 +41,11 @@ impl WriteSpliter {
fn split_partitioning_values(
&self,
values: &[VectorRef],
) -> Result<HashMap<RegionId, Vec<usize>>> {
) -> Result<HashMap<RegionNumber, Vec<usize>>> {
if values.is_empty() {
return Ok(HashMap::default());
}
let mut region_map: HashMap<RegionId, Vec<usize>> = HashMap::new();
let mut region_map: HashMap<RegionNumber, Vec<usize>> = HashMap::new();
let row_count = values[0].len();
for idx in 0..row_count {
let region_id = match self
@@ -113,9 +113,9 @@ fn partition_values(partition_columns: &[VectorRef], idx: usize) -> Vec<Value> {
fn partition_insert_request(
insert: &InsertRequest,
region_map: HashMap<RegionId, Vec<usize>>,
region_map: HashMap<RegionNumber, Vec<usize>>,
) -> DistInsertRequest {
let mut dist_insert: HashMap<RegionId, HashMap<&str, VectorBuilder>> =
let mut dist_insert: HashMap<RegionNumber, HashMap<&str, VectorBuilder>> =
HashMap::with_capacity(region_map.len());
let column_count = insert.columns_values.len();
@@ -162,10 +162,12 @@ mod tests {
value::Value,
vectors::VectorBuilder,
};
use serde::{Deserialize, Serialize};
use store_api::storage::RegionNumber;
use table::requests::InsertRequest;
use super::{
check_req, find_partitioning_values, partition_insert_request, partition_values, RegionId,
check_req, find_partitioning_values, partition_insert_request, partition_values,
WriteSpliter,
};
use crate::{
@@ -242,13 +244,13 @@ mod tests {
#[test]
fn test_partition_insert_request() {
let insert = mock_insert_request();
let mut region_map: HashMap<RegionId, Vec<usize>> = HashMap::with_capacity(2);
let mut region_map: HashMap<RegionNumber, Vec<usize>> = HashMap::with_capacity(2);
region_map.insert(1, vec![2, 0]);
region_map.insert(2, vec![1]);
let dist_insert = partition_insert_request(&insert, region_map);
let r1_insert = dist_insert.get(&1_u64).unwrap();
let r1_insert = dist_insert.get(&1_u32).unwrap();
assert_eq!("demo", r1_insert.table_name);
let expected: Value = 3_i16.into();
assert_eq!(expected, r1_insert.columns_values.get("id").unwrap().get(0));
@@ -283,7 +285,7 @@ mod tests {
.get(1)
);
let r2_insert = dist_insert.get(&2_u64).unwrap();
let r2_insert = dist_insert.get(&2_u32).unwrap();
assert_eq!("demo", r2_insert.table_name);
let expected: Value = 2_i16.into();
assert_eq!(expected, r2_insert.columns_values.get("id").unwrap().get(0));
@@ -401,6 +403,7 @@ mod tests {
}
}
#[derive(Debug, Serialize, Deserialize)]
struct MockPartitionRule;
// PARTITION BY LIST COLUMNS(id) (
@@ -414,7 +417,7 @@ mod tests {
vec!["id".to_string()]
}
fn find_region(&self, values: &[Value]) -> Result<RegionId, Self::Error> {
fn find_region(&self, values: &[Value]) -> Result<RegionNumber, Self::Error> {
let val = values.get(0).unwrap().to_owned();
let id_1: Value = 1_i16.into();
let id_2: Value = 2_i16.into();
@@ -428,7 +431,7 @@ mod tests {
unreachable!()
}
fn find_regions(&self, _: &[PartitionExpr]) -> Result<Vec<RegionId>, Self::Error> {
fn find_regions(&self, _: &[PartitionExpr]) -> Result<Vec<RegionNumber>, Error> {
unimplemented!()
}
}

102
src/frontend/src/sql.rs Normal file
View File

@@ -0,0 +1,102 @@
use catalog::SchemaProviderRef;
use common_error::snafu::ensure;
use datatypes::prelude::ConcreteDataType;
use datatypes::vectors::VectorBuilder;
use snafu::{OptionExt, ResultExt};
use sql::ast::Value as SqlValue;
use sql::statements;
use sql::statements::insert::Insert;
use table::requests::InsertRequest;
use crate::error::{self, Result};
// TODO(fys): Extract the common logic in datanode and frontend in the future.
#[allow(dead_code)]
pub(crate) fn insert_to_request(
schema_provider: &SchemaProviderRef,
stmt: Insert,
) -> Result<InsertRequest> {
let columns = stmt.columns();
let values = stmt.values().context(error::ParseSqlSnafu)?;
let table_name = stmt.table_name();
let table = schema_provider
.table(&table_name)
.context(error::CatalogSnafu)?
.context(error::TableNotFoundSnafu {
table_name: &table_name,
})?;
let schema = table.schema();
let columns_num = if columns.is_empty() {
schema.column_schemas().len()
} else {
columns.len()
};
let rows_num = values.len();
let mut columns_builders: Vec<(&String, &ConcreteDataType, VectorBuilder)> =
Vec::with_capacity(columns_num);
if columns.is_empty() {
for column_schema in schema.column_schemas() {
let data_type = &column_schema.data_type;
columns_builders.push((
&column_schema.name,
data_type,
VectorBuilder::with_capacity(data_type.clone(), rows_num),
));
}
} else {
for column_name in columns {
let column_schema = schema.column_schema_by_name(column_name).with_context(|| {
error::ColumnNotFoundSnafu {
table_name: &table_name,
column_name: column_name.to_string(),
}
})?;
let data_type = &column_schema.data_type;
columns_builders.push((
column_name,
data_type,
VectorBuilder::with_capacity(data_type.clone(), rows_num),
));
}
}
for row in values {
ensure!(
row.len() == columns_num,
error::ColumnValuesNumberMismatchSnafu {
columns: columns_num,
values: row.len(),
}
);
for (sql_val, (column_name, data_type, builder)) in
row.iter().zip(columns_builders.iter_mut())
{
add_row_to_vector(column_name, data_type, sql_val, builder)?;
}
}
Ok(InsertRequest {
table_name,
columns_values: columns_builders
.into_iter()
.map(|(c, _, mut b)| (c.to_owned(), b.finish()))
.collect(),
})
}
fn add_row_to_vector(
column_name: &str,
data_type: &ConcreteDataType,
sql_val: &SqlValue,
builder: &mut VectorBuilder,
) -> Result<()> {
let value = statements::sql_value_to_value(column_name, data_type, sql_val)
.context(error::ParseSqlSnafu)?;
builder.push(&value);
Ok(())
}

View File

@@ -14,7 +14,7 @@ use datafusion::logical_plan::Expr as DfExpr;
use datafusion::physical_plan::Partitioning;
use datatypes::schema::{ColumnSchema, Schema, SchemaRef};
use snafu::prelude::*;
use store_api::storage::RegionId;
use store_api::storage::RegionNumber;
use table::error::Error as TableError;
use table::metadata::{FilterPushDownType, TableInfoRef};
use table::requests::InsertRequest;
@@ -26,12 +26,12 @@ use crate::mock::{DatanodeId, DatanodeInstance, TableScanPlan};
use crate::partitioning::{Operator, PartitionExpr, PartitionRuleRef};
use crate::spliter::WriteSpliter;
struct DistTable {
table_name: String,
schema: SchemaRef,
partition_rule: PartitionRuleRef<Error>,
region_dist_map: HashMap<RegionId, DatanodeId>,
datanode_instances: HashMap<DatanodeId, DatanodeInstance>,
pub struct DistTable {
pub table_name: String,
pub schema: SchemaRef,
pub partition_rule: PartitionRuleRef<Error>,
pub region_dist_map: HashMap<RegionNumber, DatanodeId>,
pub datanode_instances: HashMap<DatanodeId, DatanodeInstance>,
}
#[async_trait]
@@ -104,7 +104,7 @@ impl Table for DistTable {
impl DistTable {
// TODO(LFC): Finding regions now seems less efficient, should be further looked into.
fn find_regions(&self, filters: &[Expr]) -> Result<Vec<RegionId>> {
fn find_regions(&self, filters: &[Expr]) -> Result<Vec<RegionNumber>> {
let regions = if let Some((first, rest)) = filters.split_first() {
let mut target = self.find_regions0(first)?;
for filter in rest {
@@ -119,7 +119,7 @@ impl DistTable {
break;
}
}
target.into_iter().collect::<Vec<RegionId>>()
target.into_iter().collect::<Vec<_>>()
} else {
self.partition_rule.find_regions(&[])?
};
@@ -136,7 +136,7 @@ impl DistTable {
// - BETWEEN and IN (maybe more)
// - expr with arithmetic like "a + 1 < 10" (should have been optimized in logic plan?)
// - not comparison or neither "AND" nor "OR" operations, for example, "a LIKE x"
fn find_regions0(&self, filter: &Expr) -> Result<HashSet<RegionId>> {
fn find_regions0(&self, filter: &Expr) -> Result<HashSet<RegionNumber>> {
let expr = filter.df_expr();
match expr {
DfExpr::BinaryExpr { left, op, right } if is_compare_op(op) => {
@@ -156,7 +156,7 @@ impl DistTable {
.partition_rule
.find_regions(&[PartitionExpr::new(column, op, value)])?
.into_iter()
.collect::<HashSet<RegionId>>());
.collect::<HashSet<RegionNumber>>());
}
}
DfExpr::BinaryExpr { left, op, right }
@@ -168,11 +168,11 @@ impl DistTable {
Operator::And => left_regions
.intersection(&right_regions)
.cloned()
.collect::<HashSet<RegionId>>(),
.collect::<HashSet<RegionNumber>>(),
Operator::Or => left_regions
.union(&right_regions)
.cloned()
.collect::<HashSet<RegionId>>(),
.collect::<HashSet<RegionNumber>>(),
_ => unreachable!(),
};
return Ok(regions);
@@ -185,10 +185,13 @@ impl DistTable {
.partition_rule
.find_regions(&[])?
.into_iter()
.collect::<HashSet<RegionId>>())
.collect::<HashSet<RegionNumber>>())
}
fn find_datanodes(&self, regions: Vec<RegionId>) -> Result<HashMap<DatanodeId, Vec<RegionId>>> {
fn find_datanodes(
&self,
regions: Vec<RegionNumber>,
) -> Result<HashMap<DatanodeId, Vec<RegionNumber>>> {
let mut datanodes = HashMap::new();
for region in regions.iter() {
let datanode = *self
@@ -344,6 +347,7 @@ mod test {
#[tokio::test(flavor = "multi_thread")]
async fn test_dist_table_scan() {
common_telemetry::init_default_ut_logging();
let table = Arc::new(new_dist_table().await);
// should scan all regions
@@ -434,7 +438,7 @@ mod test {
let partition_rule = RangePartitionRule::new(
"a",
vec![10_i32.into(), 20_i32.into(), 50_i32.into()],
vec![1_u64, 2, 3, 4],
vec![1_u32, 2, 3, 4],
);
let table1 = new_memtable(schema.clone(), (0..5).collect::<Vec<i32>>());
@@ -458,7 +462,7 @@ mod test {
table_name: "dist_numbers".to_string(),
schema,
partition_rule: Arc::new(partition_rule),
region_dist_map: HashMap::from([(1_u64, 1), (2_u64, 2), (3_u64, 3), (4_u64, 4)]),
region_dist_map: HashMap::from([(1_u32, 1), (2_u32, 2), (3_u32, 3), (4_u32, 4)]),
datanode_instances,
}
}
@@ -491,20 +495,21 @@ mod test {
let instance = Arc::new(Instance::with_mock_meta_client(&opts).await.unwrap());
instance.start().await.unwrap();
let catalog_manager = instance.catalog_manager().clone();
let client = crate::tests::create_datanode_client(instance).await;
let table_name = table.table_name().to_string();
catalog_manager
.register_table(RegisterTableRequest {
catalog: "greptime".to_string(),
schema: "public".to_string(),
table_name: table.table_name().to_string(),
table_name: table_name.clone(),
table_id: 1234,
table: Arc::new(table),
})
.await
.unwrap();
let client = crate::tests::create_datanode_client(instance).await;
DatanodeInstance::new(
datanode_id,
catalog_manager,
@@ -516,7 +521,7 @@ mod test {
async fn test_find_regions() {
let table = new_dist_table().await;
let test = |filters: Vec<Expr>, expect_regions: Vec<u64>| {
let test = |filters: Vec<Expr>, expect_regions: Vec<RegionNumber>| {
let mut regions = table.find_regions(filters.as_slice()).unwrap();
regions.sort();

View File

@@ -11,7 +11,7 @@ use client::ObjectResult;
use snafu::ensure;
use snafu::OptionExt;
use snafu::ResultExt;
use store_api::storage::RegionId;
use store_api::storage::RegionNumber;
use table::requests::InsertRequest;
use super::DistTable;
@@ -21,7 +21,7 @@ use crate::error::Result;
impl DistTable {
pub async fn dist_insert(
&self,
inserts: HashMap<RegionId, InsertRequest>,
inserts: HashMap<RegionNumber, InsertRequest>,
) -> Result<ObjectResult> {
let mut joins = Vec::with_capacity(inserts.len());
@@ -66,7 +66,7 @@ impl DistTable {
}
}
fn to_insert_expr(region_id: RegionId, insert: InsertRequest) -> Result<InsertExpr> {
fn to_insert_expr(region_id: RegionNumber, insert: InsertRequest) -> Result<InsertExpr> {
let mut row_count = None;
let columns = insert
@@ -109,7 +109,7 @@ fn to_insert_expr(region_id: RegionId, insert: InsertRequest) -> Result<InsertEx
options.insert(
// TODO(fys): Temporarily hard code here
"region_id".to_string(),
codec::RegionId { id: region_id }.into(),
codec::RegionNumber { id: region_id }.into(),
);
Ok(InsertExpr {
@@ -196,7 +196,7 @@ mod tests {
}
let bytes = insert_expr.options.get("region_id").unwrap();
let region_id: codec::RegionId = bytes.deref().try_into().unwrap();
let region_id: codec::RegionNumber = bytes.deref().try_into().unwrap();
assert_eq!(12, region_id.id);
}
}

View File

@@ -113,6 +113,13 @@ pub struct LogFile {
append_buffer_size: usize,
}
impl Drop for LogFile {
fn drop(&mut self) {
self.state.stopped.store(true, Ordering::Relaxed);
info!("Stopping log file {}", self.name);
}
}
impl LogFile {
/// Opens a file in path with given log config.
pub async fn open(path: impl Into<String>, config: &LogConfig) -> Result<Self> {
@@ -826,4 +833,24 @@ mod tests {
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn test_shutdown() {
logging::init_default_ut_logging();
let config = LogConfig::default();
let dir = TempDir::new("greptimedb-store-test").unwrap();
let path_buf = dir.path().join("0010.log");
let path = path_buf.to_str().unwrap().to_string();
File::create(path.as_str()).unwrap();
let mut file = LogFile::open(path.clone(), &config)
.await
.unwrap_or_else(|_| panic!("Failed to open file: {}", path));
let state = file.state.clone();
file.start().await.unwrap();
drop(file);
assert!(state.stopped.load(Ordering::Relaxed));
}
}

View File

@@ -372,14 +372,14 @@ mod tests {
#[tokio::test]
async fn test_ask_leader() {
let client = mocks::mock_client_with_noopstore().await;
let client = mocks::mock_client_with_memstore().await;
let res = client.ask_leader().await;
assert!(res.is_ok());
}
#[tokio::test]
async fn test_heartbeat() {
let client = mocks::mock_client_with_noopstore().await;
let client = mocks::mock_client_with_memstore().await;
let (sender, mut receiver) = client.heartbeat().await.unwrap();
// send heartbeats
tokio::spawn(async move {
@@ -428,9 +428,10 @@ mod tests {
}
#[tokio::test]
async fn test_create_route() {
async fn test_route() {
let selector = Arc::new(MockSelector {});
let client = mocks::mock_client_with_selector(selector).await;
let client = mocks::mock_client_with_memorystore_and_selector(selector).await;
let p1 = Partition {
column_list: vec![b"col_1".to_vec(), b"col_2".to_vec()],
value_list: vec![b"k1".to_vec(), b"k2".to_vec()],
@@ -440,29 +441,17 @@ mod tests {
value_list: vec![b"Max1".to_vec(), b"Max2".to_vec()],
};
let table_name = TableName::new("test_catalog", "test_schema", "test_table");
let req = CreateRequest::new(table_name)
let req = CreateRequest::new(table_name.clone())
.add_partition(p1)
.add_partition(p2);
let res = client.create_route(req).await.unwrap();
assert_eq!(1, res.table_routes.len());
let table_routes = res.table_routes;
assert_eq!(1, table_routes.len());
let table_route = table_routes.get(0).unwrap();
let table = &table_route.table;
let table_name = &table.table_name;
assert_eq!("test_catalog", table_name.catalog_name);
assert_eq!("test_schema", table_name.schema_name);
assert_eq!("test_table", table_name.table_name);
let region_routes = &table_route.region_routes;
assert_eq!(2, region_routes.len());
let r0 = region_routes.get(0).unwrap();
let r1 = region_routes.get(1).unwrap();
assert_eq!(0, r0.region.as_ref().unwrap().id);
assert_eq!(1, r1.region.as_ref().unwrap().id);
assert_eq!("peer0", r0.leader_peer.as_ref().unwrap().addr);
assert_eq!("peer1", r1.leader_peer.as_ref().unwrap().addr);
let req = RouteRequest::new().add_table_name(table_name);
let res = client.route(req).await.unwrap();
// empty table_routes since no TableGlobalValue is stored by datanode
assert!(res.table_routes.is_empty());
}
async fn gen_data(client: &MetaClient) {

View File

@@ -5,11 +5,6 @@ use meta_srv::mocks::MockInfo;
use crate::client::MetaClient;
use crate::client::MetaClientBuilder;
pub async fn mock_client_with_noopstore() -> MetaClient {
let mock_info = server_mock::mock_with_noopstore().await;
mock_client_by(mock_info).await
}
pub async fn mock_client_with_memstore() -> MetaClient {
let mock_info = server_mock::mock_with_memstore().await;
mock_client_by(mock_info).await
@@ -21,8 +16,8 @@ pub async fn mock_client_with_etcdstore(addr: &str) -> MetaClient {
mock_client_by(mock_info).await
}
pub async fn mock_client_with_selector(selector: SelectorRef) -> MetaClient {
let mock_info = server_mock::mock_with_selector(selector).await;
pub async fn mock_client_with_memorystore_and_selector(selector: SelectorRef) -> MetaClient {
let mock_info = server_mock::mock_with_memstore_and_selector(selector).await;
mock_client_by(mock_info).await
}

View File

@@ -134,6 +134,7 @@ pub struct TableRoute {
#[derive(Debug, Clone)]
pub struct Table {
pub id: u64,
pub table_name: TableName,
pub table_schema: Vec<u8>,
}
@@ -149,6 +150,7 @@ impl TryFrom<PbTable> for Table {
})?
.into();
Ok(Self {
id: t.id,
table_name,
table_schema: t.table_schema,
})
@@ -296,6 +298,7 @@ mod tests {
],
table_routes: vec![PbTableRoute {
table: Some(PbTable {
id: 1,
table_name: Some(PbTableName {
catalog_name: "c1".to_string(),
schema_name: "s1".to_string(),
@@ -324,6 +327,7 @@ mod tests {
assert_eq!(1, table_routes.len());
let table_route = table_routes.remove(0);
let table = table_route.table;
assert_eq!(1, table.id);
assert_eq!("c1", table.table_name.catalog_name);
assert_eq!("s1", table.table_name.schema_name);
assert_eq!("t1", table.table_name.table_name);

View File

@@ -10,6 +10,7 @@ mock = []
api = { path = "../api" }
async-trait = "0.1"
common-base = { path = "../common/base" }
common-catalog = { path = "../common/catalog" }
common-error = { path = "../common/error" }
common-grpc = { path = "../common/grpc" }
common-runtime = { path = "../common/runtime" }
@@ -21,6 +22,7 @@ h2 = "0.3"
http-body = "0.4"
lazy_static = "1.4"
parking_lot = "0.12"
prost = "0.11"
regex = "1.6"
serde = "1.0"
serde_json = "1.0"

View File

@@ -79,6 +79,33 @@ pub enum Error {
err_msg: String,
backtrace: Backtrace,
},
#[snafu(display("Cannot parse catalog value, source: {}", source))]
InvalidCatalogValue {
#[snafu(backtrace)]
source: common_catalog::error::Error,
},
#[snafu(display("Unexcepted sequence value: {}", err_msg))]
UnexceptedSequenceValue {
err_msg: String,
backtrace: Backtrace,
},
#[snafu(display("Failed to decode table route, source: {}", source))]
DecodeTableRoute {
source: prost::DecodeError,
backtrace: Backtrace,
},
#[snafu(display("Table route not found: {}", key))]
TableRouteNotFound { key: String, backtrace: Backtrace },
#[snafu(display("Failed to get sequence: {}", err_msg))]
NextSequence {
err_msg: String,
backtrace: Backtrace,
},
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -106,15 +133,19 @@ impl ErrorExt for Error {
| Error::TcpBind { .. }
| Error::SerializeToJson { .. }
| Error::DeserializeFromJson { .. }
| Error::DecodeTableRoute { .. }
| Error::StartGrpc { .. } => StatusCode::Internal,
Error::EmptyKey { .. }
| Error::EmptyTableName { .. }
| Error::InvalidLeaseKey { .. }
| Error::ParseNum { .. }
| Error::InvalidArguments { .. } => StatusCode::InvalidArguments,
Error::LeaseKeyFromUtf8 { .. } | Error::InvalidTxnResult { .. } => {
StatusCode::Unexpected
}
Error::LeaseKeyFromUtf8 { .. }
| Error::UnexceptedSequenceValue { .. }
| Error::TableRouteNotFound { .. }
| Error::NextSequence { .. }
| Error::InvalidTxnResult { .. } => StatusCode::Unexpected,
Error::InvalidCatalogValue { source, .. } => source.status_code(),
}
}
}

View File

@@ -31,7 +31,7 @@ impl HeartbeatHandler for DatanodeLeaseHandler {
node_addr: peer.addr.clone(),
};
info!("Receive a heartbeat from datanode: {:?}, {:?}", key, value);
info!("Receive a heartbeat: {:?}, {:?}", key, value);
let key = key.try_into()?;
let value = value.try_into()?;
@@ -41,8 +41,7 @@ impl HeartbeatHandler for DatanodeLeaseHandler {
..Default::default()
};
let kv_store = ctx.kv_store();
let _ = kv_store.put(put).await?;
let _ = ctx.kv_store().put(put).await?;
}
Ok(())

View File

@@ -35,11 +35,12 @@ mod tests {
use api::v1::meta::{HeartbeatResponse, RequestHeader};
use super::*;
use crate::{handler::Context, service::store::noop::NoopKvStore};
use crate::handler::Context;
use crate::service::store::memory::MemStore;
#[tokio::test]
async fn test_handle_heartbeat_resp_header() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let ctx = Context {
server_addr: "0.0.0.0:0000".to_string(),
kv_store,

View File

@@ -1,5 +1,7 @@
use std::str::FromStr;
use api::v1::meta::TableName;
use common_catalog::TableGlobalKey;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
@@ -12,6 +14,8 @@ use crate::error;
use crate::error::Result;
pub(crate) const DN_LEASE_PREFIX: &str = "__meta_dnlease";
pub(crate) const SEQ_PREFIX: &str = "__meta_seq";
pub(crate) const TABLE_ROUTE_PREFIX: &str = "__meta_table_route";
lazy_static! {
static ref DATANODE_KEY_PATTERN: Regex =
@@ -108,6 +112,44 @@ impl TryFrom<LeaseValue> for Vec<u8> {
}
}
pub struct TableRouteKey<'a> {
pub table_id: u64,
pub catalog_name: &'a str,
pub schema_name: &'a str,
pub table_name: &'a str,
}
impl<'a> TableRouteKey<'a> {
pub fn with_table_name(table_id: u64, t: &'a TableName) -> Self {
Self {
table_id,
catalog_name: &t.catalog_name,
schema_name: &t.schema_name,
table_name: &t.table_name,
}
}
pub fn with_table_global_key(table_id: u64, t: &'a TableGlobalKey) -> Self {
Self {
table_id,
catalog_name: &t.catalog_name,
schema_name: &t.schema_name,
table_name: &t.table_name,
}
}
pub fn prefix(&self) -> String {
format!(
"{}-{}-{}-{}",
TABLE_ROUTE_PREFIX, self.catalog_name, self.schema_name, self.table_name
)
}
pub fn key(&self) -> String {
format!("{}-{}", self.prefix(), self.table_id)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,5 +1,4 @@
use api::v1::meta::RangeRequest;
use api::v1::meta::RangeResponse;
use crate::error::Result;
use crate::keys::LeaseKey;
@@ -10,7 +9,7 @@ use crate::util;
pub async fn alive_datanodes<P>(
cluster_id: u64,
kv_store: KvStoreRef,
kv_store: &KvStoreRef,
predicate: P,
) -> Result<Vec<(LeaseKey, LeaseValue)>>
where
@@ -26,7 +25,7 @@ where
let res = kv_store.range(req).await?;
let RangeResponse { kvs, .. } = res;
let kvs = res.kvs;
let mut lease_kvs = vec![];
for kv in kvs {
let lease_key: LeaseKey = kv.key.try_into()?;

View File

@@ -3,11 +3,12 @@ pub mod bootstrap;
pub mod error;
pub mod handler;
mod keys;
pub mod lease;
mod lease;
pub mod metasrv;
#[cfg(feature = "mock")]
pub mod mocks;
pub mod selector;
mod sequence;
pub mod service;
mod util;

View File

@@ -9,8 +9,12 @@ use crate::handler::response_header::ResponseHeaderHandler;
use crate::handler::HeartbeatHandlerGroup;
use crate::selector::lease_based::LeaseBasedSelector;
use crate::selector::Selector;
use crate::sequence::Sequence;
use crate::sequence::SequenceRef;
use crate::service::store::kv::KvStoreRef;
pub const TABLE_ID_SEQ: &str = "table_id";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetaSrvOptions {
pub bind_addr: String,
@@ -24,7 +28,7 @@ impl Default for MetaSrvOptions {
Self {
bind_addr: "0.0.0.0:3002".to_string(),
server_addr: "0.0.0.0:3002".to_string(),
store_addr: "0.0.0.0:2380".to_string(),
store_addr: "0.0.0.0:2379".to_string(),
datanode_lease_secs: 15,
}
}
@@ -42,6 +46,7 @@ pub type SelectorRef = Arc<dyn Selector<Context = Context, Output = Vec<Peer>>>;
pub struct MetaSrv {
options: MetaSrvOptions,
kv_store: KvStoreRef,
table_id_sequence: SequenceRef,
selector: SelectorRef,
handler_group: HeartbeatHandlerGroup,
}
@@ -52,6 +57,7 @@ impl MetaSrv {
kv_store: KvStoreRef,
selector: Option<SelectorRef>,
) -> Self {
let table_id_sequence = Arc::new(Sequence::new(TABLE_ID_SEQ, 10, kv_store.clone()));
let selector = selector.unwrap_or_else(|| Arc::new(LeaseBasedSelector {}));
let handler_group = HeartbeatHandlerGroup::default();
handler_group.add_handler(ResponseHeaderHandler).await;
@@ -60,6 +66,7 @@ impl MetaSrv {
Self {
options,
kv_store,
table_id_sequence,
selector,
handler_group,
}
@@ -75,6 +82,11 @@ impl MetaSrv {
self.kv_store.clone()
}
#[inline]
pub fn table_id_sequence(&self) -> SequenceRef {
self.table_id_sequence.clone()
}
#[inline]
pub fn selector(&self) -> SelectorRef {
self.selector.clone()

View File

@@ -13,18 +13,12 @@ use crate::metasrv::SelectorRef;
use crate::service::store::etcd::EtcdStore;
use crate::service::store::kv::KvStoreRef;
use crate::service::store::memory::MemStore;
use crate::service::store::noop::NoopKvStore;
pub struct MockInfo {
pub server_addr: String,
pub channel_manager: ChannelManager,
}
pub async fn mock_with_noopstore() -> MockInfo {
let kv_store = Arc::new(NoopKvStore {});
mock(Default::default(), kv_store, None).await
}
pub async fn mock_with_memstore() -> MockInfo {
let kv_store = Arc::new(MemStore::default());
mock(Default::default(), kv_store, None).await
@@ -35,8 +29,8 @@ pub async fn mock_with_etcdstore(addr: &str) -> MockInfo {
mock(Default::default(), kv_store, None).await
}
pub async fn mock_with_selector(selector: SelectorRef) -> MockInfo {
let kv_store = Arc::new(NoopKvStore {});
pub async fn mock_with_memstore_and_selector(selector: SelectorRef) -> MockInfo {
let kv_store = Arc::new(MemStore::default());
mock(Default::default(), kv_store, Some(selector)).await
}

View File

@@ -21,7 +21,7 @@ impl Selector for LeaseBasedSelector {
let lease_filter = |_: &LeaseKey, v: &LeaseValue| {
time_util::current_time_millis() - v.timestamp_millis < ctx.datanode_lease_secs
};
let mut lease_kvs = lease::alive_datanodes(ns, ctx.kv_store.clone(), lease_filter).await?;
let mut lease_kvs = lease::alive_datanodes(ns, &ctx.kv_store, lease_filter).await?;
// TODO(jiachun): At the moment we are just pushing the latest to the forefront,
// and it is better to use load-based strategies in the future.
lease_kvs.sort_by(|a, b| b.1.timestamp_millis.cmp(&a.1.timestamp_millis));

View File

@@ -0,0 +1,197 @@
use std::ops::Range;
use std::sync::Arc;
use api::v1::meta::CompareAndPutRequest;
use snafu::ensure;
use tokio::sync::Mutex;
use crate::error::{self, Result};
use crate::keys;
use crate::service::store::kv::KvStoreRef;
pub type SequenceRef = Arc<Sequence>;
pub struct Sequence {
inner: Mutex<Inner>,
}
impl Sequence {
pub fn new(name: impl AsRef<str>, step: u64, generator: KvStoreRef) -> Self {
let name = format!("{}-{}", keys::SEQ_PREFIX, name.as_ref());
let step = step.max(1);
Self {
inner: Mutex::new(Inner {
name,
generator,
next: 0,
step,
range: None,
force_quit: 1024,
}),
}
}
pub async fn next(&self) -> Result<u64> {
let mut inner = self.inner.lock().await;
inner.next().await
}
}
struct Inner {
name: String,
generator: KvStoreRef,
// The next available sequences(if it is in the range,
// otherwise it need to fetch from generator again).
next: u64,
// Fetch several sequences at once: [start, start + step).
step: u64,
// The range of available sequences for the local cache.
range: Option<Range<u64>>,
// Used to avoid dead loops.
force_quit: usize,
}
impl Inner {
/// 1. returns the `next` value directly if it is in the `range` (local cache)
/// 2. fetch(CAS) next `range` from the `generator`
/// 3. jump to step 1
pub async fn next(&mut self) -> Result<u64> {
for _ in 0..self.force_quit {
match &self.range {
Some(range) => {
if range.contains(&self.next) {
let res = Ok(self.next);
self.next += 1;
return res;
}
self.range = None;
}
None => {
let range = self.next_range().await?;
self.next = range.start;
self.range = Some(range);
}
}
}
error::NextSequenceSnafu {
err_msg: format!("{}.next()", &self.name),
}
.fail()
}
pub async fn next_range(&self) -> Result<Range<u64>> {
let key = self.name.as_bytes();
let mut start = self.next;
for _ in 0..self.force_quit {
let expect = if start == 0 {
vec![]
} else {
u64::to_le_bytes(start).to_vec()
};
let value = u64::to_le_bytes(start + self.step);
let req = CompareAndPutRequest {
key: key.to_vec(),
expect,
value: value.to_vec(),
..Default::default()
};
let res = self.generator.compare_and_put(req).await?;
if !res.success {
if let Some(kv) = res.prev_kv {
let value = kv.value;
ensure!(
value.len() == std::mem::size_of::<u64>(),
error::UnexceptedSequenceValueSnafu {
err_msg: format!("key={}, unexpected value={:?}", self.name, value)
}
);
start = u64::from_le_bytes(value.try_into().unwrap());
} else {
start = 0;
}
continue;
}
return Ok(Range {
start,
end: start + self.step,
});
}
error::NextSequenceSnafu {
err_msg: format!("{}.next_range()", &self.name),
}
.fail()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::service::store::{kv::KvStore, memory::MemStore};
#[tokio::test]
async fn test_sequence() {
let kv_store = Arc::new(MemStore::new());
let seq = Sequence::new("test_seq", 10, kv_store);
for i in 0..100 {
assert_eq!(i, seq.next().await.unwrap());
}
}
#[tokio::test]
async fn test_sequence_fouce_quit() {
struct Noop;
#[async_trait::async_trait]
impl KvStore for Noop {
async fn range(
&self,
_: api::v1::meta::RangeRequest,
) -> Result<api::v1::meta::RangeResponse> {
unreachable!()
}
async fn put(
&self,
_: api::v1::meta::PutRequest,
) -> Result<api::v1::meta::PutResponse> {
unreachable!()
}
async fn batch_put(
&self,
_: api::v1::meta::BatchPutRequest,
) -> Result<api::v1::meta::BatchPutResponse> {
unreachable!()
}
async fn compare_and_put(
&self,
_: CompareAndPutRequest,
) -> Result<api::v1::meta::CompareAndPutResponse> {
Ok(api::v1::meta::CompareAndPutResponse::default())
}
async fn delete_range(
&self,
_: api::v1::meta::DeleteRangeRequest,
) -> Result<api::v1::meta::DeleteRangeResponse> {
unreachable!()
}
}
let kv_store = Arc::new(Noop {});
let seq = Sequence::new("test_seq", 10, kv_store);
let next = seq.next().await;
assert!(next.is_err());
}
}

View File

@@ -142,11 +142,11 @@ mod tests {
use super::*;
use crate::metasrv::MetaSrvOptions;
use crate::service::store::noop::NoopKvStore;
use crate::service::store::memory::MemStore;
#[tokio::test]
async fn test_ask_leader() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = AskLeaderRequest {

View File

@@ -1,5 +1,9 @@
use api::v1::meta::router_server;
use api::v1::meta::CreateRequest;
use api::v1::meta::Error;
use api::v1::meta::PeerDict;
use api::v1::meta::PutRequest;
use api::v1::meta::RangeRequest;
use api::v1::meta::Region;
use api::v1::meta::RegionRoute;
use api::v1::meta::ResponseHeader;
@@ -7,22 +11,31 @@ use api::v1::meta::RouteRequest;
use api::v1::meta::RouteResponse;
use api::v1::meta::Table;
use api::v1::meta::TableRoute;
use api::v1::meta::TableRouteValue;
use common_catalog::TableGlobalKey;
use common_catalog::TableGlobalValue;
use common_telemetry::warn;
use snafu::OptionExt;
use snafu::ResultExt;
use tonic::Request;
use tonic::Response;
use super::store::kv::KvStoreRef;
use super::GrpcResult;
use crate::error;
use crate::error::Result;
use crate::keys::TableRouteKey;
use crate::metasrv::Context;
use crate::metasrv::MetaSrv;
use crate::metasrv::SelectorRef;
use crate::sequence::SequenceRef;
#[async_trait::async_trait]
impl router_server::Router for MetaSrv {
async fn route(&self, req: Request<RouteRequest>) -> GrpcResult<RouteResponse> {
let req = req.into_inner();
let res = handle_route(req).await?;
let ctx = self.new_ctx();
let res = handle_route(req, ctx).await?;
Ok(Response::new(res))
}
@@ -31,20 +44,68 @@ impl router_server::Router for MetaSrv {
let req = req.into_inner();
let ctx = self.new_ctx();
let selector = self.selector();
let res = handle_create(req, ctx, selector).await?;
let table_id_sequence = self.table_id_sequence();
let res = handle_create(req, ctx, selector, table_id_sequence).await?;
Ok(Response::new(res))
}
}
async fn handle_route(_req: RouteRequest) -> Result<RouteResponse> {
todo!()
async fn handle_route(req: RouteRequest, ctx: Context) -> Result<RouteResponse> {
let RouteRequest {
header,
table_names,
} = req;
let cluster_id = header.as_ref().map_or(0, |h| h.cluster_id);
let table_global_keys = table_names.into_iter().map(|t| TableGlobalKey {
catalog_name: t.catalog_name,
schema_name: t.schema_name,
table_name: t.table_name,
});
let tables = fetch_tables(&ctx.kv_store, table_global_keys).await?;
let mut peer_dict = PeerDict::default();
let mut table_routes = vec![];
for (tg, tr) in tables {
let TableRouteValue {
peers,
mut table_route,
} = tr;
if let Some(ref mut table_route) = table_route {
for rr in &mut table_route.region_routes {
if let Some(peer) = peers.get(rr.leader_peer_index as usize) {
rr.leader_peer_index = peer_dict.get_or_insert(peer.clone()) as u64;
}
for index in &mut rr.follower_peer_indexes {
if let Some(peer) = peers.get(*index as usize) {
*index = peer_dict.get_or_insert(peer.clone()) as u64;
}
}
}
if let Some(ref mut table) = table_route.table {
table.table_schema = tg.as_bytes().context(error::InvalidCatalogValueSnafu)?;
}
}
if let Some(table_route) = table_route {
table_routes.push(table_route)
}
}
let peers = peer_dict.into_peers();
let header = Some(ResponseHeader::success(cluster_id));
Ok(RouteResponse {
header,
peers,
table_routes,
})
}
async fn handle_create(
req: CreateRequest,
ctx: Context,
selector: SelectorRef,
table_id_sequence: SequenceRef,
) -> Result<RouteResponse> {
let CreateRequest {
header,
@@ -55,22 +116,37 @@ async fn handle_create(
let cluster_id = header.as_ref().map_or(0, |h| h.cluster_id);
let peers = selector.select(cluster_id, &ctx).await?;
if peers.is_empty() {
let header = Some(ResponseHeader::failed(
cluster_id,
Error::no_active_datanodes(),
));
return Ok(RouteResponse {
header,
..Default::default()
});
}
let id = table_id_sequence.next().await?;
let table_route_key = TableRouteKey::with_table_name(id, &table_name)
.key()
.into_bytes();
let table = Table {
id,
table_name: Some(table_name),
..Default::default()
};
let region_num = partitions.len();
let mut region_routes = Vec::with_capacity(region_num);
for i in 0..region_num {
let mut region_routes = Vec::with_capacity(partitions.len());
for (i, partition) in partitions.into_iter().enumerate() {
let region = Region {
id: i as u64,
partition: Some(partition),
..Default::default()
};
let region_route = RegionRoute {
region: Some(region),
leader_peer_index: (i % peers.len()) as u64,
follower_peer_indexes: vec![(i % peers.len()) as u64],
follower_peer_indexes: vec![], // follower_peers is not supported at the moment
};
region_routes.push(region_route);
}
@@ -79,6 +155,13 @@ async fn handle_create(
region_routes,
};
// save table route data into meta store
let table_route_value = TableRouteValue {
peers: peers.clone(),
table_route: Some(table_route.clone()),
};
put_into_store(&ctx.kv_store, table_route_key, table_route_value).await?;
let header = Some(ResponseHeader::success(cluster_id));
Ok(RouteResponse {
header,
@@ -87,112 +170,89 @@ async fn handle_create(
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use api::v1::meta::router_server::Router;
use api::v1::meta::*;
use tonic::IntoRequest;
use super::*;
use crate::metasrv::MetaSrvOptions;
use crate::selector::{Namespace, Selector};
use crate::service::store::noop::NoopKvStore;
#[should_panic]
#[tokio::test]
async fn test_handle_route() {
let kv_store = Arc::new(NoopKvStore {});
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = RouteRequest {
header: Some(RequestHeader::new((1, 1))),
table_names: vec![
TableName {
catalog_name: "catalog1".to_string(),
schema_name: "schema1".to_string(),
table_name: "table1".to_string(),
},
TableName {
catalog_name: "catalog1".to_string(),
schema_name: "schema1".to_string(),
table_name: "table2".to_string(),
},
TableName {
catalog_name: "catalog1".to_string(),
schema_name: "schema1".to_string(),
table_name: "table3".to_string(),
},
],
};
let _res = meta_srv.route(req.into_request()).await.unwrap();
}
struct MockSelector;
#[async_trait::async_trait]
impl Selector for MockSelector {
type Context = Context;
type Output = Vec<Peer>;
async fn select(&self, _ns: Namespace, _ctx: &Self::Context) -> Result<Self::Output> {
Ok(vec![
Peer {
id: 0,
addr: "127.0.0.1:3000".to_string(),
},
Peer {
id: 1,
addr: "127.0.0.1:3001".to_string(),
},
])
async fn fetch_tables(
kv_store: &KvStoreRef,
keys: impl Iterator<Item = TableGlobalKey>,
) -> Result<Vec<(TableGlobalValue, TableRouteValue)>> {
let mut tables = vec![];
// Maybe we can optimize the for loop in the future, but in general,
// there won't be many keys, in fact, there is usually just one.
for tk in keys {
let tv = get_table_global_value(kv_store, &tk).await?;
if tv.is_none() {
warn!("Table global value is absent: {}", tk);
continue;
}
let tv = tv.unwrap();
let table_id = tv.id as u64;
let tr_key = TableRouteKey::with_table_global_key(table_id, &tk);
let tr = get_table_route_value(kv_store, &tr_key).await?;
tables.push((tv, tr));
}
#[tokio::test]
async fn test_handle_create() {
let kv_store = Arc::new(NoopKvStore {});
let table_name = TableName {
catalog_name: "test_catalog".to_string(),
schema_name: "test_db".to_string(),
table_name: "table1".to_string(),
};
let p0 = Partition {
column_list: vec![b"col1".to_vec(), b"col2".to_vec()],
value_list: vec![b"v1".to_vec(), b"v2".to_vec()],
};
let p1 = Partition {
column_list: vec![b"col1".to_vec(), b"col2".to_vec()],
value_list: vec![b"v11".to_vec(), b"v22".to_vec()],
};
let req = CreateRequest {
header: Some(RequestHeader::new((1, 1))),
table_name: Some(table_name),
partitions: vec![p0, p1],
};
let ctx = Context {
datanode_lease_secs: 10,
kv_store,
};
let selector = Arc::new(MockSelector {});
let res = handle_create(req, ctx, selector).await.unwrap();
Ok(tables)
}
assert_eq!(
vec![
Peer {
id: 0,
addr: "127.0.0.1:3000".to_string(),
},
Peer {
id: 1,
addr: "127.0.0.1:3001".to_string(),
},
],
res.peers
);
assert_eq!(1, res.table_routes.len());
assert_eq!(2, res.table_routes.get(0).unwrap().region_routes.len());
async fn get_table_route_value(
kv_store: &KvStoreRef,
key: &TableRouteKey<'_>,
) -> Result<TableRouteValue> {
let tr = get_from_store(kv_store, key.key().into_bytes())
.await?
.context(error::TableRouteNotFoundSnafu { key: key.key() })?;
let tr: TableRouteValue = tr
.as_slice()
.try_into()
.context(error::DecodeTableRouteSnafu)?;
Ok(tr)
}
async fn get_table_global_value(
kv_store: &KvStoreRef,
key: &TableGlobalKey,
) -> Result<Option<TableGlobalValue>> {
let tg_key = format!("{}", key).into_bytes();
let tv = get_from_store(kv_store, tg_key).await?;
match tv {
Some(tv) => {
let tv = TableGlobalValue::parse(&String::from_utf8_lossy(&tv))
.context(error::InvalidCatalogValueSnafu)?;
Ok(Some(tv))
}
None => Ok(None),
}
}
async fn put_into_store(
kv_store: &KvStoreRef,
key: impl Into<Vec<u8>>,
value: impl Into<Vec<u8>>,
) -> Result<()> {
let key = key.into();
let value = value.into();
let put_req = PutRequest {
key,
value,
..Default::default()
};
let _ = kv_store.put(put_req).await?;
Ok(())
}
async fn get_from_store(kv_store: &KvStoreRef, key: Vec<u8>) -> Result<Option<Vec<u8>>> {
let req = RangeRequest {
key,
..Default::default()
};
let res = kv_store.range(req).await?;
let mut kvs = res.kvs;
if kvs.is_empty() {
Ok(None)
} else {
Ok(Some(kvs.pop().unwrap().value))
}
}

View File

@@ -1,7 +1,6 @@
pub mod etcd;
pub mod kv;
pub mod memory;
pub mod noop;
use api::v1::meta::store_server;
use api::v1::meta::BatchPutRequest;
@@ -74,11 +73,11 @@ mod tests {
use super::*;
use crate::metasrv::MetaSrvOptions;
use crate::service::store::noop::NoopKvStore;
use crate::service::store::memory::MemStore;
#[tokio::test]
async fn test_range() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = RangeRequest::default();
let res = meta_srv.range(req.into_request()).await;
@@ -88,7 +87,7 @@ mod tests {
#[tokio::test]
async fn test_put() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = PutRequest::default();
let res = meta_srv.put(req.into_request()).await;
@@ -98,7 +97,7 @@ mod tests {
#[tokio::test]
async fn test_batch_put() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = BatchPutRequest::default();
let res = meta_srv.batch_put(req.into_request()).await;
@@ -108,7 +107,7 @@ mod tests {
#[tokio::test]
async fn test_compare_and_put() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = CompareAndPutRequest::default();
let res = meta_srv.compare_and_put(req.into_request()).await;
@@ -118,7 +117,7 @@ mod tests {
#[tokio::test]
async fn test_delete_range() {
let kv_store = Arc::new(NoopKvStore {});
let kv_store = Arc::new(MemStore::new());
let meta_srv = MetaSrv::new(MetaSrvOptions::default(), kv_store, None).await;
let req = DeleteRangeRequest::default();
let res = meta_srv.delete_range(req.into_request()).await;

View File

@@ -1,4 +1,4 @@
use std::cmp::Ordering;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::ops::Range;
use std::sync::Arc;
@@ -146,19 +146,26 @@ impl KvStore for MemStore {
let mut memory = self.inner.write();
let prev_val = memory.get(&key);
let (success, prev_kv) = match memory.entry(key) {
Entry::Vacant(e) => {
let success = expect.is_empty();
if success {
e.insert(value);
}
(success, None)
}
Entry::Occupied(mut e) => {
let key = e.key().clone();
let prev_val = e.get().clone();
let success = prev_val == expect;
if success {
e.insert(value);
}
(success, Some((key, prev_val)))
}
};
let success = prev_val
.map(|v| expect.cmp(v) == Ordering::Equal)
.unwrap_or(false | expect.is_empty());
let prev_kv = prev_val.map(|v| KeyValue {
key: key.clone(),
value: v.clone(),
});
if success {
memory.insert(key, value);
}
let prev_kv = prev_kv.map(|(key, value)| KeyValue { key, value });
let cluster_id = header.map_or(0, |h| h.cluster_id);
let header = Some(ResponseHeader::success(cluster_id));

View File

@@ -1,41 +0,0 @@
use api::v1::meta::BatchPutRequest;
use api::v1::meta::BatchPutResponse;
use api::v1::meta::CompareAndPutRequest;
use api::v1::meta::CompareAndPutResponse;
use api::v1::meta::DeleteRangeRequest;
use api::v1::meta::DeleteRangeResponse;
use api::v1::meta::PutRequest;
use api::v1::meta::PutResponse;
use api::v1::meta::RangeRequest;
use api::v1::meta::RangeResponse;
use super::kv::KvStore;
use crate::error::Result;
/// A noop kv_store which only for test
// TODO(jiachun): Add a test feature
#[derive(Clone)]
pub struct NoopKvStore;
#[async_trait::async_trait]
impl KvStore for NoopKvStore {
async fn range(&self, _req: RangeRequest) -> Result<RangeResponse> {
Ok(RangeResponse::default())
}
async fn put(&self, _req: PutRequest) -> Result<PutResponse> {
Ok(PutResponse::default())
}
async fn batch_put(&self, _req: BatchPutRequest) -> Result<BatchPutResponse> {
Ok(BatchPutResponse::default())
}
async fn compare_and_put(&self, _req: CompareAndPutRequest) -> Result<CompareAndPutResponse> {
Ok(CompareAndPutResponse::default())
}
async fn delete_range(&self, _req: DeleteRangeRequest) -> Result<DeleteRangeResponse> {
Ok(DeleteRangeResponse::default())
}
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use catalog::local::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::local::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::{CatalogList, CatalogProvider, SchemaProvider};
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_query::Output;
@@ -18,7 +18,7 @@ use table::test_util::MemTable;
pub fn create_query_engine() -> Arc<dyn QueryEngine> {
let schema_provider = Arc::new(MemorySchemaProvider::new());
let catalog_provider = Arc::new(MemoryCatalogProvider::new());
let catalog_list = Arc::new(MemoryCatalogList::default());
let catalog_list = Arc::new(MemoryCatalogManager::default());
let mut column_schemas = vec![];
let mut columns = vec![];

View File

@@ -2,7 +2,7 @@ use std::fmt::Debug;
use std::marker::PhantomData;
use std::sync::Arc;
use catalog::local::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::local::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::{CatalogList, CatalogProvider, SchemaProvider};
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_function::scalars::aggregate::AggregateFunctionMeta;
@@ -242,7 +242,7 @@ fn new_query_engine_factory(table: MemTable) -> QueryEngineFactory {
let schema_provider = Arc::new(MemorySchemaProvider::new());
let catalog_provider = Arc::new(MemoryCatalogProvider::new());
let catalog_list = Arc::new(MemoryCatalogList::default());
let catalog_list = Arc::new(MemoryCatalogManager::default());
schema_provider.register_table(table_name, table).unwrap();
catalog_provider

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
mod function;
use catalog::local::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::local::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::{CatalogList, CatalogProvider, SchemaProvider};
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_query::Output;
@@ -114,7 +114,7 @@ fn create_correctness_engine() -> Arc<dyn QueryEngine> {
// create engine
let schema_provider = Arc::new(MemorySchemaProvider::new());
let catalog_provider = Arc::new(MemoryCatalogProvider::new());
let catalog_list = Arc::new(MemoryCatalogList::default());
let catalog_list = Arc::new(MemoryCatalogManager::default());
let mut column_schemas = vec![];
let mut columns = vec![];

View File

@@ -3,7 +3,7 @@ mod pow;
use std::sync::Arc;
use arrow::array::UInt32Array;
use catalog::local::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::local::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::{CatalogList, CatalogProvider, SchemaProvider};
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_query::prelude::{create_udf, make_scalar_function, Volatility};
@@ -150,7 +150,7 @@ async fn test_udf() -> Result<()> {
fn create_query_engine() -> Arc<dyn QueryEngine> {
let schema_provider = Arc::new(MemorySchemaProvider::new());
let catalog_provider = Arc::new(MemoryCatalogProvider::new());
let catalog_list = Arc::new(MemoryCatalogList::default());
let catalog_list = Arc::new(MemoryCatalogManager::default());
// create table with primitives, and all columns' length are even
let mut column_schemas = vec![];

View File

@@ -15,7 +15,7 @@ use datatypes::vectors::Helper as HelperVec;
use rustpython_vm::builtins::PyList;
use rustpython_vm::pymodule;
use rustpython_vm::{
builtins::{PyBaseExceptionRef, PyBool, PyFloat, PyInt},
builtins::{PyBaseExceptionRef, PyBool, PyFloat, PyInt, PyStr},
AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
};
@@ -50,14 +50,15 @@ fn collect_diff_types_string(values: &[ScalarValue], ty: &DataType) -> String {
///
/// supported scalar are(leftside is python data type, right side is rust type):
///
/// | Python | Rust |
/// | ------ | ---- |
/// | integer| i64 |
/// | float | f64 |
/// | bool | bool |
/// | vector | array|
/// | Python | Rust |
/// | ------ | ------ |
/// | integer| i64 |
/// | float | f64 |
/// | str | String |
/// | bool | bool |
/// | vector | array |
/// | list | `ScalarValue::List` |
fn try_into_columnar_value(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<DFColValue> {
pub fn try_into_columnar_value(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<DFColValue> {
if is_instance::<PyVector>(&obj, vm) {
let ret = obj
.payload::<PyVector>()
@@ -67,6 +68,9 @@ fn try_into_columnar_value(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<DF
// Note that a `PyBool` is also a `PyInt`, so check if it is a bool first to get a more precise type
let ret = obj.try_into_value::<bool>(vm)?;
Ok(DFColValue::Scalar(ScalarValue::Boolean(Some(ret))))
} else if is_instance::<PyStr>(&obj, vm) {
let ret = obj.try_into_value::<String>(vm)?;
Ok(DFColValue::Scalar(ScalarValue::Utf8(Some(ret))))
} else if is_instance::<PyInt>(&obj, vm) {
let ret = obj.try_into_value::<i64>(vm)?;
Ok(DFColValue::Scalar(ScalarValue::Int64(Some(ret))))
@@ -92,10 +96,10 @@ fn try_into_columnar_value(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<DF
.collect::<Result<_, _>>()?;
if ret.is_empty() {
//TODO(dennis): empty list, we set type as f64.
// TODO(dennis): empty list, we set type as null.
return Ok(DFColValue::Scalar(ScalarValue::List(
None,
Box::new(DataType::Float64),
Box::new(DataType::Null),
)));
}

View File

@@ -1,4 +1,4 @@
// This is the file for UDF&UDAF binding from datafusion,
// This is the file for UDF&UDAF binding from datafusion,
// including most test for those function(except ApproxMedian which datafusion didn't implement)
// check src/script/builtins/test.rs::run_builtin_fn_testcases() for more information
[
@@ -632,7 +632,7 @@ variance_pop(values)"#,
ty: Float64
))
),
// GrepTime's own UDF
TestCase(
@@ -782,7 +782,7 @@ sin(num)"#,
script: r#"
from greptime import *
sin(num)"#,
expect: Err("Can't cast object of type str into vector or scalar")
expect: Err("TypeError: Can't cast type Utf8 to Float64")
),
TestCase(
input: {},

View File

@@ -13,7 +13,7 @@ use datatypes::arrow::compute::cast::CastOptions;
use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema};
use datatypes::schema::Schema;
use datatypes::vectors::Helper;
use datatypes::vectors::{BooleanVector, Vector, VectorRef};
use datatypes::vectors::{BooleanVector, StringVector, Vector, VectorRef};
use rustpython_bytecode::CodeObject;
use rustpython_vm as vm;
use rustpython_vm::{class::PyClassImpl, AsObject};
@@ -186,10 +186,11 @@ fn try_into_py_vector(fetch_args: Vec<ArrayRef>) -> Result<Vec<PyVector>> {
DataType::Int16 => try_into_vector::<i16>(arg)?,
DataType::Int32 => try_into_vector::<i32>(arg)?,
DataType::Int64 => try_into_vector::<i64>(arg)?,
DataType::Utf8 => {
Arc::new(StringVector::try_from_arrow_array(arg).context(TypeCastSnafu)?) as _
}
DataType::Boolean => {
let v: VectorRef =
Arc::new(BooleanVector::try_from_arrow_array(arg).context(TypeCastSnafu)?);
v
Arc::new(BooleanVector::try_from_arrow_array(arg).context(TypeCastSnafu)?) as _
}
_ => {
return ret_other_error_with(format!(

View File

@@ -1,5 +1,6 @@
use common_error::prelude::{ErrorCompat, ErrorExt, StatusCode};
use console::{style, Style};
use datafusion::error::DataFusionError;
use datatypes::arrow::error::ArrowError;
use datatypes::error::Error as DataTypeError;
use query::error::Error as QueryError;
@@ -50,6 +51,12 @@ pub enum Error {
source: ArrowError,
},
#[snafu(display("DataFusion error: {}", source))]
DataFusion {
backtrace: Backtrace,
source: DataFusionError,
},
/// errors in coprocessors' parse check for types and etc.
#[snafu(display("Coprocessor error: {} {}.", reason,
if let Some(loc) = loc{
@@ -93,9 +100,10 @@ impl From<QueryError> for Error {
impl ErrorExt for Error {
fn status_code(&self) -> StatusCode {
match self {
Error::Arrow { .. } | Error::PyRuntime { .. } | Error::Other { .. } => {
StatusCode::Internal
}
Error::DataFusion { .. }
| Error::Arrow { .. }
| Error::PyRuntime { .. }
| Error::Other { .. } => StatusCode::Internal,
Error::RecordBatch { source } => source.status_code(),
Error::DatabaseQuery { source } => source.status_code(),

View File

@@ -362,6 +362,39 @@ def a(cpu: vector[f32], mem: vector[f64])->(vector[f64|None],
]
)
),
(
name: "constant_list",
code: r#"
@copr(args=["cpu", "mem"], returns=["what"])
def a(cpu: vector[f32], mem: vector[f64]):
return ["apple" ,"banana", "cherry"]
"#,
predicate: ExecIsOk(
fields: [
(
datatype: Some(Utf8),
is_nullable: false,
),
],
columns: [
(
ty: Utf8,
len: 3
),
]
)
),
(
name: "constant_list_different_type",
code: r#"
@copr(args=["cpu", "mem"], returns=["what"])
def a(cpu: vector[f32], mem: vector[f64]):
return ["apple" ,3, "cherry"]
"#,
predicate: ExecIsErr(
reason: "All elements in a list should be same type to cast to Datafusion list!",
)
),
(
// expect 4 vector ,found 5
name: "ret_nums_wrong",

View File

@@ -1,11 +1,16 @@
use std::sync::Arc;
use datafusion::arrow::array::{ArrayRef, BooleanArray, PrimitiveArray};
use rustpython_vm::builtins::{PyBool, PyFloat, PyInt};
use datafusion::arrow::array::{ArrayRef, BooleanArray, NullArray, PrimitiveArray, Utf8Array};
use datafusion_common::ScalarValue;
use datafusion_expr::ColumnarValue as DFColValue;
use datatypes::arrow::datatypes::DataType;
use rustpython_vm::builtins::{PyBool, PyFloat, PyInt, PyList, PyStr};
use rustpython_vm::{builtins::PyBaseExceptionRef, PyObjectRef, PyPayload, PyRef, VirtualMachine};
use snafu::OptionExt;
use snafu::ResultExt;
use snafu::{Backtrace, GenerateImplicitData};
use crate::python::builtins::try_into_columnar_value;
use crate::python::error;
use crate::python::error::ret_other_error_with;
use crate::python::PyVector;
@@ -39,6 +44,7 @@ pub fn py_vec_obj_to_array(
vm: &VirtualMachine,
col_len: usize,
) -> Result<ArrayRef, error::Error> {
// It's ugly, but we can't find a better way right now.
if is_instance::<PyVector>(obj, vm) {
let pyv = obj.payload::<PyVector>().with_context(|| {
ret_other_error_with(format!("can't cast obj {:?} to PyVector", obj))
@@ -66,6 +72,30 @@ pub fn py_vec_obj_to_array(
let ret = BooleanArray::from_iter(std::iter::repeat(Some(val)).take(col_len));
Ok(Arc::new(ret) as _)
} else if is_instance::<PyStr>(obj, vm) {
let val = obj
.to_owned()
.try_into_value::<String>(vm)
.map_err(|e| format_py_error(e, vm))?;
let ret = Utf8Array::<i32>::from_iter(std::iter::repeat(Some(val)).take(col_len));
Ok(Arc::new(ret) as _)
} else if is_instance::<PyList>(obj, vm) {
let columnar_value =
try_into_columnar_value(obj.clone(), vm).map_err(|e| format_py_error(e, vm))?;
match columnar_value {
DFColValue::Scalar(ScalarValue::List(scalars, _datatype)) => match scalars {
Some(scalars) => {
let array = ScalarValue::iter_to_array(scalars.into_iter())
.context(error::DataFusionSnafu)?;
Ok(array)
}
None => Ok(Arc::new(NullArray::new(DataType::Null, 0))),
},
_ => unreachable!(),
}
} else {
ret_other_error_with(format!("Expect a vector or a constant, found {:?}", obj)).fail()
}

View File

@@ -37,9 +37,11 @@ serde = "1.0"
serde_json = "1.0"
snafu = { version = "0.7", features = ["backtraces"] }
snap = "1"
table = { path = "../table" }
tokio = { version = "1.20", features = ["full"] }
tokio-stream = { version = "0.1", features = ["net"] }
tonic = "0.8"
tonic-reflection = "0.5"
tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.3", features = ["full"] }

View File

@@ -154,7 +154,13 @@ pub enum Error {
InvalidPromRemoteReadQueryResult { msg: String, backtrace: Backtrace },
#[snafu(display("Failed to decode region id, source: {}", source))]
DecodeRegionId { source: api::DecodeError },
DecodeRegionNumber { source: api::DecodeError },
#[snafu(display("Failed to build gRPC reflection service, source: {}", source))]
GrpcReflectionService {
source: tonic_reflection::server::Error,
backtrace: Backtrace,
},
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -172,7 +178,8 @@ impl ErrorExt for Error {
| StartGrpc { .. }
| AlreadyStarted { .. }
| InvalidPromRemoteReadQueryResult { .. }
| TcpBind { .. } => StatusCode::Internal,
| TcpBind { .. }
| GrpcReflectionService { .. } => StatusCode::Internal,
InsertScript { source, .. }
| ExecuteScript { source, .. }
@@ -189,7 +196,7 @@ impl ErrorExt for Error {
| DecodePromRemoteRequest { .. }
| DecompressPromRemoteRequest { .. }
| InvalidPromRemoteRequest { .. }
| DecodeRegionId { .. }
| DecodeRegionNumber { .. }
| TimePrecision { .. } => StatusCode::InvalidArguments,
InfluxdbLinesWrite { source, .. } => source.status_code(),

View File

@@ -16,7 +16,7 @@ use tokio::sync::Mutex;
use tokio_stream::wrappers::TcpListenerStream;
use tonic::{Request, Response, Status};
use crate::error::{AlreadyStartedSnafu, Result, StartGrpcSnafu, TcpBindSnafu};
use crate::error::{self, AlreadyStartedSnafu, Result, StartGrpcSnafu, TcpBindSnafu};
use crate::grpc::handler::BatchHandler;
use crate::query_handler::{GrpcAdminHandlerRef, GrpcQueryHandlerRef};
use crate::server::Server;
@@ -104,9 +104,16 @@ impl Server for GrpcServer {
(listener, addr)
};
let reflection_service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(api::v1::GREPTIME_FD_SET)
.with_service_name("greptime.v1.Greptime")
.build()
.context(error::GrpcReflectionServiceSnafu)?;
// Would block to serve requests.
tonic::transport::Server::builder()
.add_service(self.create_service())
.add_service(reflection_service)
.serve_with_incoming_shutdown(TcpListenerStream::new(listener), rx.map(drop))
.await
.context(StartGrpcSnafu)?;

View File

@@ -7,8 +7,10 @@ use api::v1::{
use common_grpc::writer::{LinesWriter, Precision};
use influxdb_line_protocol::{parse_lines, FieldValue};
use snafu::ResultExt;
use table::requests::InsertRequest;
use crate::error::{Error, InfluxdbLineProtocolSnafu, InfluxdbLinesWriteSnafu};
use crate::line_writer::LineWriter;
pub const INFLUXDB_TIMESTAMP_COLUMN_NAME: &str = "ts";
pub const DEFAULT_TIME_PRECISION: Precision = Precision::NANOSECOND;
@@ -20,6 +22,60 @@ pub struct InfluxdbRequest {
type TableName = String;
impl TryFrom<&InfluxdbRequest> for Vec<InsertRequest> {
type Error = Error;
fn try_from(value: &InfluxdbRequest) -> Result<Self, Self::Error> {
let lines = parse_lines(&value.lines)
.collect::<influxdb_line_protocol::Result<Vec<_>>>()
.context(InfluxdbLineProtocolSnafu)?;
let line_len = lines.len();
let mut writers: HashMap<TableName, LineWriter> = HashMap::new();
for line in lines {
let table_name = line.series.measurement;
let writer = writers
.entry(table_name.to_string())
.or_insert_with(|| LineWriter::with_lines(table_name, line_len));
let tags = line.series.tag_set;
if let Some(tags) = tags {
for (k, v) in tags {
writer.write_tag(k.as_str(), v.as_str());
}
}
let fields = line.field_set;
for (k, v) in fields {
let column_name = k.as_str();
match v {
FieldValue::I64(value) => writer.write_i64(column_name, value),
FieldValue::U64(value) => writer.write_u64(column_name, value),
FieldValue::F64(value) => writer.write_f64(column_name, value),
FieldValue::String(value) => writer.write_string(column_name, value.as_str()),
FieldValue::Boolean(value) => writer.write_bool(column_name, value),
}
}
if let Some(timestamp) = line.timestamp {
let precision = if let Some(val) = &value.precision {
*val
} else {
DEFAULT_TIME_PRECISION
};
writer.write_ts(INFLUXDB_TIMESTAMP_COLUMN_NAME, (timestamp, precision));
}
writer.commit();
}
Ok(writers
.into_iter()
.map(|(_, writer)| writer.finish())
.collect())
}
}
// TODO(fys): will remove in the future.
impl TryFrom<&InfluxdbRequest> for Vec<InsertExpr> {
type Error = Error;
@@ -106,7 +162,7 @@ impl TryFrom<&InfluxdbRequest> for Vec<InsertExpr> {
#[cfg(test)]
mod tests {
use std::ops::Deref;
use std::{ops::Deref, sync::Arc};
use api::v1::{
codec::InsertBatch,
@@ -115,6 +171,9 @@ mod tests {
Column, ColumnDataType, InsertExpr,
};
use common_base::BitVec;
use common_time::{timestamp::TimeUnit, Timestamp};
use datatypes::{value::Value, vectors::Vector};
use table::requests::InsertRequest;
use crate::influxdb::InfluxdbRequest;
@@ -124,6 +183,30 @@ mod tests {
monitor1,host=host1 cpu=66.6,memory=1024 1663840496100023100
monitor1,host=host2 memory=1027 1663840496400340001
monitor2,host=host3 cpu=66.5 1663840496100023102
monitor2,host=host4 cpu=66.3,memory=1029 1663840496400340003";
let influxdb_req = &InfluxdbRequest {
precision: None,
lines: lines.to_string(),
};
let insert_reqs: Vec<InsertRequest> = influxdb_req.try_into().unwrap();
for insert_req in insert_reqs {
match &insert_req.table_name[..] {
"monitor1" => assert_table_1(&insert_req),
"monitor2" => assert_table_2(&insert_req),
_ => panic!(),
}
}
}
// TODO(fys): will remove in the future.
#[test]
fn test_convert_influxdb_lines_1() {
let lines = r"
monitor1,host=host1 cpu=66.6,memory=1024 1663840496100023100
monitor1,host=host2 memory=1027 1663840496400340001
monitor2,host=host3 cpu=66.5 1663840496100023102
monitor2,host=host4 cpu=66.3,memory=1029 1663840496400340003";
let influxdb_req = &InfluxdbRequest {
@@ -150,6 +233,77 @@ monitor2,host=host4 cpu=66.3,memory=1029 1663840496400340003";
}
}
fn assert_table_1(insert_req: &InsertRequest) {
let table_name = &insert_req.table_name;
assert_eq!("monitor1", table_name);
let columns = &insert_req.columns_values;
let host = columns.get("host").unwrap();
let expected: Vec<Value> = vec!["host1".into(), "host2".into()];
assert_vector(&expected, host);
let cpu = columns.get("cpu").unwrap();
let expected: Vec<Value> = vec![66.6.into(), Value::Null];
assert_vector(&expected, cpu);
let memory = columns.get("memory").unwrap();
let expected: Vec<Value> = vec![1024.0.into(), 1027.0.into()];
assert_vector(&expected, memory);
let ts = columns.get("ts").unwrap();
let expected: Vec<Value> = vec![
datatypes::prelude::Value::Timestamp(Timestamp::new(
1663840496100,
TimeUnit::Millisecond,
)),
datatypes::prelude::Value::Timestamp(Timestamp::new(
1663840496400,
TimeUnit::Millisecond,
)),
];
assert_vector(&expected, ts);
}
fn assert_table_2(insert_req: &InsertRequest) {
let table_name = &insert_req.table_name;
assert_eq!("monitor2", table_name);
let columns = &insert_req.columns_values;
let host = columns.get("host").unwrap();
let expected: Vec<Value> = vec!["host3".into(), "host4".into()];
assert_vector(&expected, host);
let cpu = columns.get("cpu").unwrap();
let expected: Vec<Value> = vec![66.5.into(), 66.3.into()];
assert_vector(&expected, cpu);
let memory = columns.get("memory").unwrap();
let expected: Vec<Value> = vec![Value::Null, 1029.0.into()];
assert_vector(&expected, memory);
let ts = columns.get("ts").unwrap();
let expected: Vec<Value> = vec![
datatypes::prelude::Value::Timestamp(Timestamp::new(
1663840496100,
TimeUnit::Millisecond,
)),
datatypes::prelude::Value::Timestamp(Timestamp::new(
1663840496400,
TimeUnit::Millisecond,
)),
];
assert_vector(&expected, ts);
}
fn assert_vector(expected: &[Value], vector: &Arc<dyn Vector>) {
for (idx, expected) in expected.iter().enumerate() {
let val = vector.get(idx);
assert_eq!(*expected, val);
}
}
fn assert_monitor_1(insert_batch: &InsertBatch) {
let columns = &insert_batch.columns;
assert_eq!(4, columns.len());

View File

@@ -5,6 +5,7 @@ pub mod error;
pub mod grpc;
pub mod http;
pub mod influxdb;
pub mod line_writer;
pub mod mysql;
pub mod opentsdb;
pub mod postgres;

View File

@@ -0,0 +1,202 @@
use std::collections::HashMap;
use common_grpc::writer::{to_ms_ts, Precision};
use common_time::{timestamp::TimeUnit::Millisecond, Timestamp};
use datatypes::{
prelude::ConcreteDataType,
types::TimestampType,
value::Value,
vectors::{VectorBuilder, VectorRef},
};
use table::requests::InsertRequest;
type ColumnLen = usize;
type ColumnName = String;
pub struct LineWriter {
table_name: String,
expected_rows: usize,
current_rows: usize,
columns_builders: HashMap<ColumnName, (VectorBuilder, ColumnLen)>,
}
impl LineWriter {
pub fn with_lines(table_name: impl Into<String>, lines: usize) -> Self {
Self {
table_name: table_name.into(),
expected_rows: lines,
current_rows: 0,
columns_builders: Default::default(),
}
}
pub fn write_ts(&mut self, column_name: &str, value: (i64, Precision)) {
let (val, precision) = value;
let datatype = ConcreteDataType::Timestamp(TimestampType { unit: Millisecond });
let ts_val = Value::Timestamp(Timestamp::new(to_ms_ts(precision, val), Millisecond));
self.write(column_name, datatype, ts_val);
}
pub fn write_tag(&mut self, column_name: &str, value: &str) {
self.write(
column_name,
ConcreteDataType::string_datatype(),
Value::String(value.into()),
);
}
pub fn write_u64(&mut self, column_name: &str, value: u64) {
self.write(
column_name,
ConcreteDataType::uint64_datatype(),
Value::UInt64(value),
);
}
pub fn write_i64(&mut self, column_name: &str, value: i64) {
self.write(
column_name,
ConcreteDataType::int64_datatype(),
Value::Int64(value),
);
}
pub fn write_f64(&mut self, column_name: &str, value: f64) {
self.write(
column_name,
ConcreteDataType::float64_datatype(),
Value::Float64(value.into()),
);
}
pub fn write_string(&mut self, column_name: &str, value: &str) {
self.write(
column_name,
ConcreteDataType::string_datatype(),
Value::String(value.into()),
);
}
pub fn write_bool(&mut self, column_name: &str, value: bool) {
self.write(
column_name,
ConcreteDataType::boolean_datatype(),
Value::Boolean(value),
);
}
fn write(&mut self, column_name: &str, datatype: ConcreteDataType, value: Value) {
let or_insert = || {
let rows = self.current_rows;
let mut builder = VectorBuilder::with_capacity(datatype, self.expected_rows);
(0..rows).into_iter().for_each(|_| builder.push_null());
(builder, rows)
};
let (builder, column_len) = self
.columns_builders
.entry(column_name.to_string())
.or_insert_with(or_insert);
builder.push(&value);
*column_len += 1;
}
pub fn commit(&mut self) {
self.current_rows += 1;
self.columns_builders
.values_mut()
.into_iter()
.for_each(|(builder, len)| {
if self.current_rows > *len {
builder.push(&Value::Null)
}
});
}
pub fn finish(self) -> InsertRequest {
let columns_values: HashMap<ColumnName, VectorRef> = self
.columns_builders
.into_iter()
.map(|(column_name, (mut builder, _))| (column_name, builder.finish()))
.collect();
InsertRequest {
table_name: self.table_name,
columns_values,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use common_time::Timestamp;
use datatypes::{value::Value, vectors::Vector};
use crate::line_writer::{LineWriter, Precision};
#[test]
fn test_writer() {
let mut writer = LineWriter::with_lines("demo".to_string(), 4);
writer.write_ts("ts", (1665893727685, Precision::MILLISECOND));
writer.write_tag("host", "host-1");
writer.write_i64("memory", 10_i64);
writer.commit();
writer.write_ts("ts", (1665893727686, Precision::MILLISECOND));
writer.write_tag("host", "host-2");
writer.write_tag("region", "region-2");
writer.write_i64("memory", 9_i64);
writer.commit();
writer.write_ts("ts", (1665893727689, Precision::MILLISECOND));
writer.write_tag("host", "host-3");
writer.write_tag("region", "region-3");
writer.write_i64("cpu", 19_i64);
writer.commit();
let insert_request = writer.finish();
assert_eq!("demo", insert_request.table_name);
let columns = insert_request.columns_values;
assert_eq!(5, columns.len());
assert!(columns.contains_key("ts"));
assert!(columns.contains_key("host"));
assert!(columns.contains_key("memory"));
assert!(columns.contains_key("region"));
assert!(columns.contains_key("cpu"));
let ts = columns.get("ts").unwrap();
let host = columns.get("host").unwrap();
let memory = columns.get("memory").unwrap();
let region = columns.get("region").unwrap();
let cpu = columns.get("cpu").unwrap();
let expected: Vec<Value> = vec![
Value::Timestamp(Timestamp::from_millis(1665893727685_i64)),
Value::Timestamp(Timestamp::from_millis(1665893727686_i64)),
Value::Timestamp(Timestamp::from_millis(1665893727689_i64)),
];
assert_vector(&expected, ts);
let expected: Vec<Value> = vec!["host-1".into(), "host-2".into(), "host-3".into()];
assert_vector(&expected, host);
let expected: Vec<Value> = vec![10_i64.into(), 9_i64.into(), Value::Null];
assert_vector(&expected, memory);
let expected: Vec<Value> = vec![Value::Null, "region-2".into(), "region-3".into()];
assert_vector(&expected, region);
let expected: Vec<Value> = vec![Value::Null, Value::Null, 19_i64.into()];
assert_vector(&expected, cpu);
}
fn assert_vector(expected: &[Value], vector: &Arc<dyn Vector>) {
for (idx, expected) in expected.iter().enumerate() {
let val = vector.get(idx);
assert_eq!(*expected, val);
}
}
}

View File

@@ -2,8 +2,11 @@ use std::collections::HashMap;
use api::v1::codec::InsertBatch;
use api::v1::{column, column::SemanticType, insert_expr, Column, ColumnDataType, InsertExpr};
use common_grpc::writer::Precision;
use table::requests::InsertRequest;
use crate::error::{self, Result};
use crate::line_writer::LineWriter;
pub const OPENTSDB_TIMESTAMP_COLUMN_NAME: &str = "greptime_timestamp";
pub const OPENTSDB_VALUE_COLUMN_NAME: &str = "greptime_value";
@@ -112,6 +115,23 @@ impl DataPoint {
self.value
}
pub fn as_insert_request(&self) -> InsertRequest {
let mut line_writer = LineWriter::with_lines(self.metric.clone(), 1);
line_writer.write_ts(
OPENTSDB_TIMESTAMP_COLUMN_NAME,
(self.ts_millis(), Precision::MILLISECOND),
);
line_writer.write_f64(OPENTSDB_VALUE_COLUMN_NAME, self.value);
for (tagk, tagv) in self.tags.iter() {
line_writer.write_tag(tagk, tagv);
}
line_writer.commit();
line_writer.finish()
}
// TODO(fys): will remove in the future.
pub fn as_grpc_insert(&self) -> InsertExpr {
let mut columns = Vec::with_capacity(2 + self.tags.len());
@@ -180,6 +200,12 @@ impl DataPoint {
#[cfg(test)]
mod test {
use std::sync::Arc;
use common_time::{timestamp::TimeUnit, Timestamp};
use datatypes::value::Value;
use datatypes::vectors::Vector;
use super::*;
#[test]
@@ -239,6 +265,43 @@ mod test {
);
}
#[test]
fn test_as_insert_request() {
let data_point = DataPoint {
metric: "my_metric_1".to_string(),
ts_millis: 1000,
value: 1.0,
tags: vec![
("tagk1".to_string(), "tagv1".to_string()),
("tagk2".to_string(), "tagv2".to_string()),
],
};
let insert_request = data_point.as_insert_request();
assert_eq!("my_metric_1", insert_request.table_name);
let columns = insert_request.columns_values;
assert_eq!(4, columns.len());
let ts = columns.get(OPENTSDB_TIMESTAMP_COLUMN_NAME).unwrap();
let expected = vec![datatypes::prelude::Value::Timestamp(Timestamp::new(
1000,
TimeUnit::Millisecond,
))];
assert_vector(&expected, ts);
let val = columns.get(OPENTSDB_VALUE_COLUMN_NAME).unwrap();
assert_vector(&[1.0.into()], val);
let tagk1 = columns.get("tagk1").unwrap();
assert_vector(&["tagv1".into()], tagk1);
let tagk2 = columns.get("tagk2").unwrap();
assert_vector(&["tagv2".into()], tagk2);
}
fn assert_vector(expected: &[Value], vector: &Arc<dyn Vector>) {
for (idx, expected) in expected.iter().enumerate() {
let val = vector.get(idx);
assert_eq!(*expected, val);
}
}
// TODO(fys): will remove in the future.
#[test]
fn test_as_grpc_insert() {
let data_point = DataPoint {

View File

@@ -11,11 +11,14 @@ use api::v1::{
codec::SelectResult, column, column::SemanticType, insert_expr, Column, ColumnDataType,
InsertExpr,
};
use common_grpc::writer::Precision::MILLISECOND;
use openmetrics_parser::{MetricsExposition, PrometheusType, PrometheusValue};
use snafu::{OptionExt, ResultExt};
use snap::raw::{Decoder, Encoder};
use table::requests::InsertRequest;
use crate::error::{self, Result};
use crate::line_writer::LineWriter;
const TIMESTAMP_COLUMN_NAME: &str = "greptime_timestamp";
const VALUE_COLUMN_NAME: &str = "greptime_value";
@@ -275,6 +278,55 @@ pub fn select_result_to_timeseries(
Ok(timeseries_map.into_values().collect())
}
/// Cast a remote write request into InsertRequest
pub fn write_request_to_insert_reqs(mut request: WriteRequest) -> Result<Vec<InsertRequest>> {
let timeseries = std::mem::take(&mut request.timeseries);
timeseries
.into_iter()
.map(timeseries_to_insert_request)
.collect()
}
fn timeseries_to_insert_request(mut timeseries: TimeSeries) -> Result<InsertRequest> {
// TODO(dennis): save exemplars into a column
let labels = std::mem::take(&mut timeseries.labels);
let samples = std::mem::take(&mut timeseries.samples);
let mut table_name = None;
for label in &labels {
// The metric name is a special label
if label.name == METRIC_NAME_LABEL {
table_name = Some(&label.value);
}
}
let table_name = table_name.context(error::InvalidPromRemoteRequestSnafu {
msg: "missing '__name__' label in timeseries",
})?;
let row_count = samples.len();
let mut line_writer = LineWriter::with_lines(table_name, row_count);
for sample in samples {
let ts_millis = sample.timestamp;
let val = sample.value;
line_writer.write_ts(TIMESTAMP_COLUMN_NAME, (ts_millis, MILLISECOND));
line_writer.write_f64(VALUE_COLUMN_NAME, val);
labels
.iter()
.filter(|label| label.name != METRIC_NAME_LABEL)
.for_each(|label| {
line_writer.write_tag(&label.name, &label.value);
});
line_writer.commit();
}
Ok(line_writer.finish())
}
// TODO(fys): it will remove in the future.
/// Cast a remote write request into gRPC's InsertExpr.
pub fn write_request_to_insert_exprs(mut request: WriteRequest) -> Result<Vec<InsertExpr>> {
let timeseries = std::mem::take(&mut request.timeseries);
@@ -285,6 +337,7 @@ pub fn write_request_to_insert_exprs(mut request: WriteRequest) -> Result<Vec<In
.collect()
}
// TODO(fys): it will remove in the future.
fn timeseries_to_insert_expr(mut timeseries: TimeSeries) -> Result<InsertExpr> {
// TODO(dennis): save exemplars into a column
let labels = std::mem::take(&mut timeseries.labels);
@@ -439,7 +492,12 @@ pub fn mock_timeseries() -> Vec<TimeSeries> {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use api::prometheus::remote::LabelMatcher;
use common_time::timestamp::TimeUnit;
use common_time::Timestamp;
use datatypes::{value::Value, vectors::Vector};
use super::*;
@@ -499,6 +557,91 @@ mod tests {
assert_eq!("select * from test where greptime_timestamp>=1000 AND greptime_timestamp<=2000 AND job~'*prom*' AND instance!='localhost' order by greptime_timestamp", sql);
}
#[test]
fn test_write_request_to_insert_reqs() {
let write_request = WriteRequest {
timeseries: mock_timeseries(),
..Default::default()
};
let reqs = write_request_to_insert_reqs(write_request).unwrap();
assert_eq!(3, reqs.len());
let req1 = reqs.get(0).unwrap();
assert_eq!("metric1", req1.table_name);
let columns = &req1.columns_values;
let job = columns.get("job").unwrap();
let expected: Vec<Value> = vec!["spark".into(), "spark".into()];
assert_vector(&expected, job);
let ts = columns.get(TIMESTAMP_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![
datatypes::prelude::Value::Timestamp(Timestamp::new(1000, TimeUnit::Millisecond)),
datatypes::prelude::Value::Timestamp(Timestamp::new(2000, TimeUnit::Millisecond)),
];
assert_vector(&expected, ts);
let val = columns.get(VALUE_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![1.0_f64.into(), 2.0_f64.into()];
assert_vector(&expected, val);
let req2 = reqs.get(1).unwrap();
assert_eq!("metric2", req2.table_name);
let columns = &req2.columns_values;
let instance = columns.get("instance").unwrap();
let expected: Vec<Value> = vec!["test_host1".into(), "test_host1".into()];
assert_vector(&expected, instance);
let idc = columns.get("idc").unwrap();
let expected: Vec<Value> = vec!["z001".into(), "z001".into()];
assert_vector(&expected, idc);
let ts = columns.get(TIMESTAMP_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![
datatypes::prelude::Value::Timestamp(Timestamp::new(1000, TimeUnit::Millisecond)),
datatypes::prelude::Value::Timestamp(Timestamp::new(2000, TimeUnit::Millisecond)),
];
assert_vector(&expected, ts);
let val = columns.get(VALUE_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![3.0_f64.into(), 4.0_f64.into()];
assert_vector(&expected, val);
let req3 = reqs.get(2).unwrap();
assert_eq!("metric3", req3.table_name);
let columns = &req3.columns_values;
let idc = columns.get("idc").unwrap();
let expected: Vec<Value> = vec!["z002".into(), "z002".into(), "z002".into()];
assert_vector(&expected, idc);
let app = columns.get("app").unwrap();
let expected: Vec<Value> = vec!["biz".into(), "biz".into(), "biz".into()];
assert_vector(&expected, app);
let ts = columns.get(TIMESTAMP_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![
datatypes::prelude::Value::Timestamp(Timestamp::new(1000, TimeUnit::Millisecond)),
datatypes::prelude::Value::Timestamp(Timestamp::new(2000, TimeUnit::Millisecond)),
datatypes::prelude::Value::Timestamp(Timestamp::new(3000, TimeUnit::Millisecond)),
];
assert_vector(&expected, ts);
let val = columns.get(VALUE_COLUMN_NAME).unwrap();
let expected: Vec<Value> = vec![5.0_f64.into(), 6.0_f64.into(), 7.0_f64.into()];
assert_vector(&expected, val);
}
fn assert_vector(expected: &[Value], vector: &Arc<dyn Vector>) {
for (idx, expected) in expected.iter().enumerate() {
let val = vector.get(idx);
assert_eq!(*expected, val);
}
}
#[test]
fn test_write_request_to_insert_exprs() {
let write_request = WriteRequest {

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use catalog::local::{MemoryCatalogList, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::local::{MemoryCatalogManager, MemoryCatalogProvider, MemorySchemaProvider};
use catalog::{CatalogList, CatalogProvider, SchemaProvider};
use common_catalog::consts::{DEFAULT_CATALOG_NAME, DEFAULT_SCHEMA_NAME};
use common_query::Output;
@@ -70,7 +70,7 @@ fn create_testing_sql_query_handler(table: MemTable) -> SqlQueryHandlerRef {
let schema_provider = Arc::new(MemorySchemaProvider::new());
let catalog_provider = Arc::new(MemoryCatalogProvider::new());
let catalog_list = Arc::new(MemoryCatalogList::default());
let catalog_list = Arc::new(MemoryCatalogManager::default());
schema_provider.register_table(table_name, table).unwrap();
catalog_provider
.register_schema(DEFAULT_SCHEMA_NAME.to_string(), schema_provider)

View File

@@ -10,6 +10,8 @@ pub type ColumnFamilyId = u32;
/// Id of the region.
pub type RegionId = u64;
pub type RegionNumber = u32;
/// A [ColumnDescriptor] contains information to create a column.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Builder)]
#[builder(pattern = "owned", build_fn(validate = "Self::validate"))]

View File

@@ -19,6 +19,7 @@ use table::Result as TableResult;
use table::{
metadata::{TableId, TableInfoBuilder, TableMetaBuilder, TableType, TableVersion},
table::TableRef,
Table,
};
use tokio::sync::Mutex;
@@ -740,10 +741,6 @@ mod tests {
let (_engine, table_engine, table, _object_store, _dir) =
test_util::setup_mock_engine_and_table().await;
let table = table
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
.unwrap();
let table_info = table.table_info();
let request = CreateTableRequest {
@@ -760,14 +757,7 @@ mod tests {
};
let created_table = table_engine.create_table(&ctx, request).await.unwrap();
assert_eq!(
table_info,
created_table
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
.unwrap()
.table_info()
);
assert_eq!(table_info, created_table.table_info());
// test create_if_not_exists=false
let request = CreateTableRequest {
@@ -826,10 +816,6 @@ mod tests {
.unwrap();
assert_eq!(table.schema(), reopened.schema());
let table = table
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
.unwrap();
let reopened = reopened
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
@@ -850,30 +836,92 @@ mod tests {
assert_eq!(18446744069414584330, region_id(u32::MAX, 10));
}
fn new_add_columns_req(new_tag: &ColumnSchema, new_field: &ColumnSchema) -> AlterTableRequest {
AlterTableRequest {
catalog_name: None,
schema_name: None,
table_name: TABLE_NAME.to_string(),
alter_kind: AlterKind::AddColumns {
columns: vec![
AddColumnRequest {
column_schema: new_tag.clone(),
is_key: true,
},
AddColumnRequest {
column_schema: new_field.clone(),
is_key: false,
},
],
},
}
}
#[tokio::test]
async fn test_alter_table_add_column() {
let (_engine, table_engine, table, _object_store, _dir) =
test_util::setup_mock_engine_and_table().await;
let table = table
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
.unwrap();
let table_info = table.table_info();
let old_info = (*table_info).clone();
let old_info = table.table_info();
let old_meta = &old_info.meta;
let old_schema = &old_meta.schema;
let new_column = ColumnSchema::new("my_tag", ConcreteDataType::string_datatype(), true);
let new_tag = ColumnSchema::new("my_tag", ConcreteDataType::string_datatype(), true);
let new_field = ColumnSchema::new("my_field", ConcreteDataType::string_datatype(), true);
let req = new_add_columns_req(&new_tag, &new_field);
let table = table_engine
.alter_table(&EngineContext::default(), req)
.await
.unwrap();
let new_info = table.table_info();
let new_meta = &new_info.meta;
let new_schema = &new_meta.schema;
assert_eq!(&[0, 4], &new_meta.primary_key_indices[..]);
assert_eq!(&[1, 2, 3, 5], &new_meta.value_indices[..]);
assert_eq!(new_schema.num_columns(), old_schema.num_columns() + 2);
assert_eq!(
&new_schema.column_schemas()[..old_schema.num_columns()],
old_schema.column_schemas()
);
assert_eq!(
&new_schema.column_schemas()[old_schema.num_columns()],
&new_tag
);
assert_eq!(
&new_schema.column_schemas()[old_schema.num_columns() + 1],
&new_field
);
assert_eq!(new_schema.timestamp_column(), old_schema.timestamp_column());
assert_eq!(new_schema.version(), old_schema.version() + 1);
assert_eq!(new_meta.next_column_id, old_meta.next_column_id + 2);
}
#[tokio::test]
async fn test_alter_table_remove_column() {
let (_engine, table_engine, _table, _object_store, _dir) =
test_util::setup_mock_engine_and_table().await;
// Add two columns to the table first.
let new_tag = ColumnSchema::new("my_tag", ConcreteDataType::string_datatype(), true);
let new_field = ColumnSchema::new("my_field", ConcreteDataType::string_datatype(), true);
let req = new_add_columns_req(&new_tag, &new_field);
let table = table_engine
.alter_table(&EngineContext::default(), req)
.await
.unwrap();
let old_info = table.table_info();
let old_meta = &old_info.meta;
let old_schema = &old_meta.schema;
// Then remove memory and my_field from the table.
let req = AlterTableRequest {
catalog_name: None,
schema_name: None,
table_name: TABLE_NAME.to_string(),
alter_kind: AlterKind::AddColumns {
columns: vec![AddColumnRequest {
column_schema: new_column.clone(),
is_key: false,
}],
alter_kind: AlterKind::RemoveColumns {
names: vec![String::from("memory"), String::from("my_field")],
},
};
let table = table_engine
@@ -881,21 +929,20 @@ mod tests {
.await
.unwrap();
let table = table
.as_any()
.downcast_ref::<MitoTable<MockRegion>>()
.unwrap();
let new_info = table.table_info();
let new_meta = &new_info.meta;
let new_schema = &new_meta.schema;
assert_eq!(new_schema.num_columns(), old_schema.num_columns() + 1);
assert_eq!(
new_schema.column_schemas().split_last().unwrap(),
(&new_column, old_schema.column_schemas())
);
assert_eq!(new_schema.num_columns(), old_schema.num_columns() - 2);
let remaining_names: Vec<String> = new_schema
.column_schemas()
.iter()
.map(|column_schema| column_schema.name.clone())
.collect();
assert_eq!(&["host", "cpu", "ts", "my_tag"], &remaining_names[..]);
assert_eq!(&[0, 3], &new_meta.primary_key_indices[..]);
assert_eq!(&[1, 2], &new_meta.value_indices[..]);
assert_eq!(new_schema.timestamp_column(), old_schema.timestamp_column());
assert_eq!(new_schema.version(), old_schema.version() + 1);
assert_eq!(new_meta.next_column_id, old_meta.next_column_id + 1);
}
}

View File

@@ -136,13 +136,6 @@ pub enum Error {
table_name: String,
},
#[snafu(display("Column {} already exists in table {}", column_name, table_name))]
ColumnExists {
backtrace: Backtrace,
column_name: String,
table_name: String,
},
#[snafu(display("Columns {} not exist in table {}", column_names.join(","), table_name))]
ColumnsNotExist {
backtrace: Backtrace,
@@ -150,13 +143,6 @@ pub enum Error {
table_name: String,
},
#[snafu(display("Failed to build schema, msg: {}, source: {}", msg, source))]
SchemaBuild {
#[snafu(backtrace)]
source: datatypes::error::Error,
msg: String,
},
#[snafu(display("Failed to alter table {}, source: {}", table_name, source))]
AlterTable {
table_name: String,
@@ -206,8 +192,6 @@ impl ErrorExt for Error {
AlterTable { source, .. } => source.status_code(),
SchemaBuild { source, .. } => source.status_code(),
BuildRowKeyDescriptor { .. }
| BuildColumnDescriptor { .. }
| BuildColumnFamilyDescriptor { .. }
@@ -220,8 +204,6 @@ impl ErrorExt for Error {
| UnsupportedDefaultConstraint { .. }
| TableNotFound { .. } => StatusCode::InvalidArguments,
ColumnExists { .. } => StatusCode::TableColumnExists,
ColumnsNotExist { .. } => StatusCode::TableColumnNotFound,
TableInfoNotFound { .. } | ConvertRaw { .. } => StatusCode::Unexpected,

View File

@@ -12,7 +12,7 @@ use common_query::physical_plan::PhysicalPlanRef;
use common_recordbatch::error::{Error as RecordBatchError, Result as RecordBatchResult};
use common_recordbatch::{RecordBatch, RecordBatchStream};
use common_telemetry::logging;
use datatypes::schema::{ColumnSchema, SchemaBuilder};
use datatypes::schema::ColumnSchema;
use datatypes::vectors::VectorRef;
use futures::task::{Context, Poll};
use futures::Stream;
@@ -20,23 +20,22 @@ use object_store::ObjectStore;
use snafu::{ensure, OptionExt, ResultExt};
use store_api::manifest::{self, Manifest, ManifestVersion, MetaActionIterator};
use store_api::storage::{
AddColumn, AlterOperation, AlterRequest, ChunkReader, ColumnDescriptorBuilder, PutOperation,
ReadContext, Region, RegionMeta, ScanRequest, SchemaRef, Snapshot, WriteContext, WriteRequest,
AddColumn, AlterOperation, AlterRequest, ChunkReader, PutOperation, ReadContext, Region,
RegionMeta, ScanRequest, SchemaRef, Snapshot, WriteContext, WriteRequest,
};
use table::error::{Error as TableError, MissingColumnSnafu, Result as TableResult};
use table::metadata::{FilterPushDownType, TableInfoRef, TableMetaBuilder};
use table::metadata::{FilterPushDownType, TableInfoRef};
use table::requests::{AddColumnRequest, AlterKind, AlterTableRequest, InsertRequest};
use table::table::scan::SimpleTableScan;
use table::{
metadata::{RawTableInfo, TableInfo, TableType},
metadata::{RawTableInfo, TableInfo, TableMeta, TableType},
table::Table,
};
use tokio::sync::Mutex;
use crate::error::{
self, ColumnsNotExistSnafu, ProjectedColumnNotFoundSnafu, Result, ScanTableManifestSnafu,
SchemaBuildSnafu, TableInfoNotFoundSnafu, UnsupportedDefaultConstraintSnafu,
UpdateTableManifestSnafu,
TableInfoNotFoundSnafu, UnsupportedDefaultConstraintSnafu, UpdateTableManifestSnafu,
};
use crate::manifest::action::*;
use crate::manifest::TableManifest;
@@ -186,100 +185,26 @@ impl<R: Region> Table for MitoTable<R> {
Ok(Arc::new(SimpleTableScan::new(stream)))
}
// Alter table changes the schemas of the table. The altering happens as cloning a new schema,
// change the new one, and swap the old. Though we can change the schema in place, considering
// the complex interwinding of inner data representation of schema, I think it's safer to
// change it like this to avoid partial inconsistent during the altering. For example, schema's
// `name_to_index` field must changed with `column_schemas` synchronously. If we add or remove
// columns from `column_schemas` *and then* update the `name_to_index`, there's a slightly time
// window of an inconsistency of the two field, which might bring some hard to trace down
// concurrency related bugs or failures. (Of course we could introduce some guards like readwrite
// lock to protect the consistency of schema altering, but that would hurt the performance of
// schema reads, and the reads are the dominant operation of schema. At last, altering is
// performed far lesser frequent.)
/// Alter table changes the schemas of the table.
async fn alter(&self, req: AlterTableRequest) -> TableResult<()> {
let _lock = self.alter_lock.lock().await;
let table_info = self.table_info();
let table_name = &table_info.name;
let table_meta = &table_info.meta;
let (alter_op, table_schema, new_columns_num) = match &req.alter_kind {
AlterKind::AddColumns {
columns: new_columns,
} => {
let columns = new_columns
.iter()
.enumerate()
.map(|(i, add_column)| {
let new_column = &add_column.column_schema;
let desc = ColumnDescriptorBuilder::new(
table_meta.next_column_id + i as u32,
&new_column.name,
new_column.data_type.clone(),
)
.is_nullable(new_column.is_nullable())
.default_constraint(new_column.default_constraint().cloned())
.build()
.context(error::BuildColumnDescriptorSnafu {
table_name,
column_name: &new_column.name,
})?;
Ok(AddColumn {
desc,
is_key: add_column.is_key,
})
})
.collect::<Result<Vec<_>>>()?;
let alter_op = AlterOperation::AddColumns { columns };
// TODO(yingwen): [alter] Better way to alter the schema struct. In fact the column order
// in table schema could differ from the region schema, so we could just push this column
// to the back of the schema (as last column).
let table_schema = build_table_schema_with_new_columns(
table_name,
&table_meta.schema,
new_columns,
)?;
(alter_op, table_schema, new_columns.len() as u32)
}
// TODO(dennis): supports removing columns etc.
_ => unimplemented!(),
};
let new_meta = TableMetaBuilder::default()
.schema(table_schema.clone())
.engine(&table_meta.engine)
.next_column_id(table_meta.next_column_id + new_columns_num) // Bump next column id.
.primary_key_indices(table_meta.primary_key_indices.clone())
let mut new_meta = table_meta
.builder_with_alter_kind(table_name, &req.alter_kind)?
.build()
.context(error::BuildTableMetaSnafu { table_name })?;
let alter_op = create_alter_operation(table_name, &req.alter_kind, &mut new_meta)?;
let mut new_info = TableInfo::clone(&*table_info);
// Increase version of the table.
new_info.ident.version = table_info.ident.version + 1;
new_info.meta = new_meta;
// FIXME(yingwen): [alter] Alter the region, now this is a temporary implementation.
let region = self.region();
let region_meta = region.in_memory_metadata();
let alter_req = AlterRequest {
operation: alter_op,
version: region_meta.version(),
};
logging::debug!(
"start altering region {} of table {}, with request {:?}",
region.name(),
table_name,
alter_req,
);
region.alter(alter_req).await.map_err(TableError::new)?;
// then alter table info
// Persist the alteration to the manifest.
logging::debug!(
"start updating the manifest of table {} with new table info {:?}",
table_name,
@@ -295,13 +220,27 @@ impl<R: Region> Table for MitoTable<R> {
.context(UpdateTableManifestSnafu {
table_name: &self.table_info().name,
})?;
// TODO(yingwen): Error handling. Maybe the region need to provide a method to
// validate the request first.
let region = self.region();
let region_meta = region.in_memory_metadata();
let alter_req = AlterRequest {
operation: alter_op,
version: region_meta.version(),
};
// Alter the region.
logging::debug!(
"start altering region {} of table {}, with request {:?}",
region.name(),
table_name,
alter_req,
);
region.alter(alter_req).await.map_err(TableError::new)?;
// Update in memory metadata of the table.
self.set_table_info(new_info);
// TODO(LFC): Think of a way to properly handle the metadata integrity between region and table.
// Currently there are no "transactions" to alter the metadata of region and table together,
// they are altered in sequence. That means there might be cases where the metadata of region
// is altered while the table's is not. Then the metadata integrity between region and
// table cannot be hold.
Ok(())
}
@@ -310,47 +249,6 @@ impl<R: Region> Table for MitoTable<R> {
}
}
fn build_table_schema_with_new_columns(
table_name: &str,
table_schema: &SchemaRef,
new_columns: &[AddColumnRequest],
) -> Result<SchemaRef> {
let mut columns = table_schema.column_schemas().to_vec();
for add_column in new_columns {
let new_column = &add_column.column_schema;
if table_schema
.column_schema_by_name(&new_column.name)
.is_some()
{
return error::ColumnExistsSnafu {
column_name: &new_column.name,
table_name,
}
.fail()?;
}
columns.push(new_column.clone());
}
// Right now we are not support adding the column
// before or after some column, so just clone a new schema like this.
// TODO(LFC): support adding column before or after some column
let mut builder = SchemaBuilder::try_from_columns(columns)
.context(SchemaBuildSnafu {
msg: "Failed to convert column schemas into table schema",
})?
.timestamp_index(table_schema.timestamp_index())
.version(table_schema.version() + 1);
for (k, v) in table_schema.arrow_schema().metadata.iter() {
builder = builder.add_metadata(k, v);
}
let new_schema = Arc::new(builder.build().with_context(|_| error::SchemaBuildSnafu {
msg: format!("cannot add new columns {:?}", new_columns),
})?);
Ok(new_schema)
}
struct ChunkStream {
schema: SchemaRef,
stream: Pin<Box<dyn Stream<Item = RecordBatchResult<RecordBatch>> + Send>>,
@@ -449,7 +347,7 @@ impl<R: Region> MitoTable<R> {
) -> Result<MitoTable<R>> {
let manifest = TableManifest::new(&table_manifest_dir(table_name), object_store);
// TODO(dennis): save manifest version into catalog?
// TODO(dennis): save manifest version into catalog?
let _manifest_version = manifest
.update(TableMetaActionList::with_action(TableMetaAction::Change(
Box::new(TableChange {
@@ -543,11 +441,6 @@ impl<R: Region> MitoTable<R> {
&self.region
}
#[inline]
pub fn table_info(&self) -> TableInfoRef {
Arc::clone(&self.table_info.load())
}
pub fn set_table_info(&self, table_info: TableInfo) {
self.table_info.swap(Arc::new(table_info));
}
@@ -563,57 +456,50 @@ impl<R: Region> MitoTable<R> {
}
}
/// Create [`AlterOperation`] according to given `alter_kind`.
fn create_alter_operation(
table_name: &str,
alter_kind: &AlterKind,
table_meta: &mut TableMeta,
) -> TableResult<AlterOperation> {
match alter_kind {
AlterKind::AddColumns { columns } => {
create_add_columns_operation(table_name, columns, table_meta)
}
AlterKind::RemoveColumns { names } => Ok(AlterOperation::DropColumns {
names: names.to_vec(),
}),
}
}
fn create_add_columns_operation(
table_name: &str,
requests: &[AddColumnRequest],
table_meta: &mut TableMeta,
) -> TableResult<AlterOperation> {
let columns = requests
.iter()
.map(|request| {
let new_column = &request.column_schema;
let desc = table_meta.alloc_new_column(table_name, new_column)?;
Ok(AddColumn {
desc,
is_key: request.is_key,
})
})
.collect::<TableResult<Vec<_>>>()?;
Ok(AlterOperation::AddColumns { columns })
}
#[cfg(test)]
mod tests {
use datatypes::prelude::ConcreteDataType;
use super::*;
use crate::table::test_util;
#[test]
fn test_table_manifest_dir() {
assert_eq!("demo/manifest/", table_manifest_dir("demo"));
assert_eq!("numbers/manifest/", table_manifest_dir("numbers"));
}
#[test]
fn test_build_table_schema_with_new_column() {
let table_info = test_util::build_test_table_info();
let table_name = &table_info.name;
let table_meta = &table_info.meta;
let table_schema = &table_meta.schema;
let new_column = ColumnSchema::new("host", ConcreteDataType::string_datatype(), true);
let new_columns = vec![AddColumnRequest {
column_schema: new_column,
is_key: false,
}];
let result = build_table_schema_with_new_columns(table_name, table_schema, &new_columns);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Column host already exists in table demo"));
let new_column = ColumnSchema::new("my_tag", ConcreteDataType::string_datatype(), true);
let new_columns = vec![AddColumnRequest {
column_schema: new_column.clone(),
is_key: false,
}];
let new_schema =
build_table_schema_with_new_columns(table_name, table_schema, &new_columns).unwrap();
assert_eq!(new_schema.num_columns(), table_schema.num_columns() + 1);
assert_eq!(
new_schema.column_schemas().split_last().unwrap(),
(&new_column, table_schema.column_schemas())
);
assert_eq!(
new_schema.timestamp_column(),
table_schema.timestamp_column()
);
assert_eq!(new_schema.version(), table_schema.version() + 1);
}
}

View File

@@ -50,7 +50,8 @@ pub fn build_test_table_info() -> TableInfo {
.schema(Arc::new(schema_for_test()))
.engine(MITO_ENGINE)
.next_column_id(1)
.primary_key_indices(vec![0, 1])
// host is primary key column.
.primary_key_indices(vec![0])
.build()
.unwrap();
@@ -71,6 +72,21 @@ pub async fn new_test_object_store(prefix: &str) -> (TempDir, ObjectStore) {
(dir, ObjectStore::new(accessor))
}
fn new_create_request(schema: SchemaRef) -> CreateTableRequest {
CreateTableRequest {
id: 1,
catalog_name: "greptime".to_string(),
schema_name: "public".to_string(),
table_name: TABLE_NAME.to_string(),
desc: Some("a test table".to_string()),
schema,
region_numbers: vec![0],
create_if_not_exists: true,
primary_key_indices: vec![0],
table_options: HashMap::new(),
}
}
pub async fn setup_test_engine_and_table() -> (
MitoEngine<EngineImpl<NoopLogStore>>,
TableRef,
@@ -93,18 +109,7 @@ pub async fn setup_test_engine_and_table() -> (
let table = table_engine
.create_table(
&EngineContext::default(),
CreateTableRequest {
id: 1,
catalog_name: "greptime".to_string(),
schema_name: "public".to_string(),
table_name: TABLE_NAME.to_string(),
desc: Some("a test table".to_string()),
schema: schema.clone(),
create_if_not_exists: true,
primary_key_indices: Vec::default(),
table_options: HashMap::new(),
region_numbers: vec![0],
},
new_create_request(schema.clone()),
)
.await
.unwrap();
@@ -124,21 +129,7 @@ pub async fn setup_mock_engine_and_table(
let schema = Arc::new(schema_for_test());
let table = table_engine
.create_table(
&EngineContext::default(),
CreateTableRequest {
id: 1,
catalog_name: "greptime".to_string(),
schema_name: "public".to_string(),
table_name: TABLE_NAME.to_string(),
desc: None,
schema: schema.clone(),
create_if_not_exists: true,
primary_key_indices: Vec::default(),
table_options: HashMap::new(),
region_numbers: vec![0],
},
)
.create_table(&EngineContext::default(), new_create_request(schema))
.await
.unwrap();

View File

@@ -51,6 +51,51 @@ pub enum InnerError {
#[snafu(backtrace)]
source: BoxedError,
},
#[snafu(display("Column {} already exists in table {}", column_name, table_name))]
ColumnExists {
column_name: String,
table_name: String,
backtrace: Backtrace,
},
#[snafu(display("Failed to build schema, msg: {}, source: {}", msg, source))]
SchemaBuild {
#[snafu(backtrace)]
source: datatypes::error::Error,
msg: String,
},
#[snafu(display("Column {} not exists in table {}", column_name, table_name))]
ColumnNotExists {
column_name: String,
table_name: String,
backtrace: Backtrace,
},
#[snafu(display(
"Not allowed to remove index column {} from table {}",
column_name,
table_name
))]
RemoveColumnInIndex {
column_name: String,
table_name: String,
backtrace: Backtrace,
},
#[snafu(display(
"Failed to build column descriptor for table: {}, column: {}, source: {}",
table_name,
column_name,
source,
))]
BuildColumnDescriptor {
source: store_api::storage::ColumnDescriptorBuilderError,
table_name: String,
column_name: String,
backtrace: Backtrace,
},
}
impl ErrorExt for InnerError {
@@ -60,8 +105,13 @@ impl ErrorExt for InnerError {
| InnerError::PollStream { .. }
| InnerError::SchemaConversion { .. }
| InnerError::TableProjection { .. } => StatusCode::EngineExecuteQuery,
InnerError::MissingColumn { .. } => StatusCode::InvalidArguments,
InnerError::MissingColumn { .. }
| InnerError::RemoveColumnInIndex { .. }
| InnerError::BuildColumnDescriptor { .. } => StatusCode::InvalidArguments,
InnerError::TablesRecordBatch { .. } => StatusCode::Unexpected,
InnerError::ColumnExists { .. } => StatusCode::TableColumnExists,
InnerError::SchemaBuild { source, .. } => source.status_code(),
InnerError::ColumnNotExists { .. } => StatusCode::TableColumnNotFound,
}
}

View File

@@ -1,12 +1,16 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use chrono::{DateTime, Utc};
pub use datatypes::error::{Error as ConvertError, Result as ConvertResult};
use datatypes::schema::{RawSchema, Schema, SchemaRef};
use datatypes::schema::{ColumnSchema, RawSchema, Schema, SchemaBuilder, SchemaRef};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use store_api::storage::ColumnId;
use snafu::{ensure, ResultExt};
use store_api::storage::{ColumnDescriptor, ColumnDescriptorBuilder, ColumnId};
use crate::error::{self, Result};
use crate::requests::{AddColumnRequest, AlterKind};
pub type TableId = u32;
pub type TableVersion = u64;
@@ -42,7 +46,10 @@ pub enum TableType {
/// Identifier of the table.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Default)]
pub struct TableIdent {
/// Unique id of this table.
pub table_id: TableId,
/// Version of the table, bumped when metadata (such as schema) of the table
/// being changed.
pub version: TableVersion,
}
@@ -50,6 +57,8 @@ pub struct TableIdent {
#[builder(pattern = "mutable")]
pub struct TableMeta {
pub schema: SchemaRef,
/// The indices of columns in primary key. Note that the index of timestamp column
/// is not included in these indices.
pub primary_key_indices: Vec<usize>,
#[builder(default = "self.default_value_indices()?")]
pub value_indices: Vec<usize>,
@@ -69,7 +78,7 @@ pub struct TableMeta {
}
impl TableMetaBuilder {
fn default_value_indices(&self) -> Result<Vec<usize>, String> {
fn default_value_indices(&self) -> std::result::Result<Vec<usize>, String> {
match (&self.primary_key_indices, &self.schema) {
(Some(v), Some(schema)) => {
let column_schemas = schema.column_schemas();
@@ -96,13 +105,230 @@ impl TableMeta {
.iter()
.map(|idx| &columns_schemas[*idx].name)
}
/// Returns the new [TableMetaBuilder] after applying given `alter_kind`.
///
/// The returned builder would derive the next column id of this meta.
pub fn builder_with_alter_kind(
&self,
table_name: &str,
alter_kind: &AlterKind,
) -> Result<TableMetaBuilder> {
match alter_kind {
AlterKind::AddColumns { columns } => self.add_columns(table_name, columns),
AlterKind::RemoveColumns { names } => self.remove_columns(table_name, names),
}
}
/// Allocate a new column for the table.
///
/// This method would bump the `next_column_id` of the meta.
pub fn alloc_new_column(
&mut self,
table_name: &str,
new_column: &ColumnSchema,
) -> Result<ColumnDescriptor> {
let desc = ColumnDescriptorBuilder::new(
self.next_column_id as ColumnId,
&new_column.name,
new_column.data_type.clone(),
)
.is_nullable(new_column.is_nullable())
.default_constraint(new_column.default_constraint().cloned())
.build()
.context(error::BuildColumnDescriptorSnafu {
table_name,
column_name: &new_column.name,
})?;
// Bump next column id.
self.next_column_id += 1;
Ok(desc)
}
fn new_meta_builder(&self) -> TableMetaBuilder {
let mut builder = TableMetaBuilder::default();
builder
.engine(&self.engine)
.engine_options(self.engine_options.clone())
.options(self.options.clone())
.created_on(self.created_on)
.next_column_id(self.next_column_id);
builder
}
fn add_columns(
&self,
table_name: &str,
requests: &[AddColumnRequest],
) -> Result<TableMetaBuilder> {
let table_schema = &self.schema;
let mut meta_builder = self.new_meta_builder();
// Check whether columns to add are already existing.
for request in requests {
let column_name = &request.column_schema.name;
ensure!(
table_schema.column_schema_by_name(column_name).is_none(),
error::ColumnExistsSnafu {
column_name,
table_name,
}
);
}
// Collect names of columns to add for error message.
let mut column_names = Vec::with_capacity(requests.len());
let mut primary_key_indices = self.primary_key_indices.clone();
let mut columns = Vec::with_capacity(table_schema.num_columns() + requests.len());
columns.extend_from_slice(table_schema.column_schemas());
// Append new columns to the end of column list.
for request in requests {
column_names.push(request.column_schema.name.clone());
if request.is_key {
// If a key column is added, we also need to store its index in primary_key_indices.
primary_key_indices.push(columns.len());
}
columns.push(request.column_schema.clone());
}
let mut builder = SchemaBuilder::try_from(columns)
.with_context(|_| error::SchemaBuildSnafu {
msg: format!(
"Failed to convert column schemas into schema for table {}",
table_name
),
})?
.timestamp_index(table_schema.timestamp_index())
// Also bump the schema version.
.version(table_schema.version() + 1);
for (k, v) in table_schema.metadata().iter() {
builder = builder.add_metadata(k, v);
}
let new_schema = builder.build().with_context(|_| error::SchemaBuildSnafu {
msg: format!(
"Table {} cannot add new columns {:?}",
table_name, column_names
),
})?;
// value_indices would be generated automatically.
meta_builder
.schema(Arc::new(new_schema))
.primary_key_indices(primary_key_indices);
Ok(meta_builder)
}
fn remove_columns(
&self,
table_name: &str,
column_names: &[String],
) -> Result<TableMetaBuilder> {
let table_schema = &self.schema;
let column_names: HashSet<_> = column_names.iter().collect();
let mut meta_builder = self.new_meta_builder();
let timestamp_index = table_schema.timestamp_index();
// Check whether columns are existing and not in primary key index.
for column_name in &column_names {
if let Some(index) = table_schema.column_index_by_name(column_name) {
// This is a linear search, but since there won't be too much columns, the performance should
// be acceptable.
ensure!(
!self.primary_key_indices.contains(&index),
error::RemoveColumnInIndexSnafu {
column_name: *column_name,
table_name,
}
);
if let Some(ts_index) = timestamp_index {
// Not allowed to remove column in timestamp index.
ensure!(
index != ts_index,
error::RemoveColumnInIndexSnafu {
column_name: table_schema.column_name_by_index(ts_index),
table_name,
}
);
}
} else {
return error::ColumnNotExistsSnafu {
column_name: *column_name,
table_name,
}
.fail()?;
}
}
// Collect columns after removal.
let columns: Vec<_> = table_schema
.column_schemas()
.iter()
.filter(|column_schema| !column_names.contains(&column_schema.name))
.cloned()
.collect();
// Find the index of the timestamp column.
let timestamp_column = table_schema.timestamp_column();
let timestamp_index = columns.iter().enumerate().find_map(|(idx, column_schema)| {
let is_timestamp = timestamp_column
.map(|c| c.name == column_schema.name)
.unwrap_or(false);
if is_timestamp {
Some(idx)
} else {
None
}
});
let mut builder = SchemaBuilder::try_from_columns(columns)
.with_context(|_| error::SchemaBuildSnafu {
msg: format!(
"Failed to convert column schemas into schema for table {}",
table_name
),
})?
// Need to use the newly computed timestamp index.
.timestamp_index(timestamp_index)
// Also bump the schema version.
.version(table_schema.version() + 1);
for (k, v) in table_schema.metadata().iter() {
builder = builder.add_metadata(k, v);
}
let new_schema = builder.build().with_context(|_| error::SchemaBuildSnafu {
msg: format!(
"Table {} cannot add remove columns {:?}",
table_name, column_names
),
})?;
// Rebuild the indices of primary key columns.
let primary_key_indices = self
.primary_key_indices
.iter()
.map(|idx| table_schema.column_name_by_index(*idx))
// This unwrap is safe since we don't allow removing a primary key column.
.map(|name| new_schema.column_index_by_name(name).unwrap())
.collect();
meta_builder
.schema(Arc::new(new_schema))
.primary_key_indices(primary_key_indices);
Ok(meta_builder)
}
}
#[derive(Clone, Debug, PartialEq, Builder)]
#[builder(pattern = "owned")]
pub struct TableInfo {
/// Id and version of the table.
#[builder(default, setter(into))]
pub ident: TableIdent,
/// Name of the table.
#[builder(setter(into))]
pub name: String,
/// Comment of the table.
@@ -248,6 +474,7 @@ impl TryFrom<RawTableInfo> for TableInfo {
#[cfg(test)]
mod tests {
use common_error::prelude::*;
use datatypes::data_type::ConcreteDataType;
use datatypes::schema::{ColumnSchema, Schema, SchemaBuilder};
@@ -257,6 +484,7 @@ mod tests {
let column_schemas = vec![
ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true),
ColumnSchema::new("ts", ConcreteDataType::timestamp_millis_datatype(), false),
ColumnSchema::new("col2", ConcreteDataType::int32_datatype(), true),
];
SchemaBuilder::try_from(column_schemas)
.unwrap()
@@ -271,10 +499,9 @@ mod tests {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![1])
.value_indices(vec![0])
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(2)
.next_column_id(3)
.build()
.unwrap();
let info = TableInfoBuilder::default()
@@ -290,4 +517,236 @@ mod tests {
assert_eq!(info, info_new);
}
fn add_columns_to_meta(meta: &TableMeta) -> TableMeta {
let new_tag = ColumnSchema::new("my_tag", ConcreteDataType::string_datatype(), true);
let new_field = ColumnSchema::new("my_field", ConcreteDataType::string_datatype(), true);
let alter_kind = AlterKind::AddColumns {
columns: vec![
AddColumnRequest {
column_schema: new_tag,
is_key: true,
},
AddColumnRequest {
column_schema: new_field,
is_key: false,
},
],
};
let builder = meta
.builder_with_alter_kind("my_table", &alter_kind)
.unwrap();
builder.build().unwrap()
}
#[test]
fn test_add_columns() {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
let new_meta = add_columns_to_meta(&meta);
let names: Vec<String> = new_meta
.schema
.column_schemas()
.iter()
.map(|column_schema| column_schema.name.clone())
.collect();
assert_eq!(&["col1", "ts", "col2", "my_tag", "my_field"], &names[..]);
assert_eq!(&[0, 3], &new_meta.primary_key_indices[..]);
assert_eq!(&[1, 2, 4], &new_meta.value_indices[..]);
}
#[test]
fn test_remove_columns() {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema.clone())
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
// Add more columns so we have enough candidate columns to remove.
let meta = add_columns_to_meta(&meta);
let alter_kind = AlterKind::RemoveColumns {
names: vec![String::from("col2"), String::from("my_field")],
};
let new_meta = meta
.builder_with_alter_kind("my_table", &alter_kind)
.unwrap()
.build()
.unwrap();
let names: Vec<String> = new_meta
.schema
.column_schemas()
.iter()
.map(|column_schema| column_schema.name.clone())
.collect();
assert_eq!(&["col1", "ts", "my_tag"], &names[..]);
assert_eq!(&[0, 2], &new_meta.primary_key_indices[..]);
assert_eq!(&[1], &new_meta.value_indices[..]);
assert_eq!(
schema.timestamp_column(),
new_meta.schema.timestamp_column()
);
}
#[test]
fn test_remove_multiple_columns_before_timestamp() {
let column_schemas = vec![
ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true),
ColumnSchema::new("col2", ConcreteDataType::int32_datatype(), true),
ColumnSchema::new("col3", ConcreteDataType::int32_datatype(), true),
ColumnSchema::new("ts", ConcreteDataType::timestamp_millis_datatype(), false),
];
let schema = Arc::new(
SchemaBuilder::try_from(column_schemas)
.unwrap()
.timestamp_index(Some(3))
.version(123)
.build()
.unwrap(),
);
let meta = TableMetaBuilder::default()
.schema(schema.clone())
.primary_key_indices(vec![1])
.engine("engine")
.next_column_id(4)
.build()
.unwrap();
// Remove columns in reverse order to test whether timestamp index is valid.
let alter_kind = AlterKind::RemoveColumns {
names: vec![String::from("col3"), String::from("col1")],
};
let new_meta = meta
.builder_with_alter_kind("my_table", &alter_kind)
.unwrap()
.build()
.unwrap();
let names: Vec<String> = new_meta
.schema
.column_schemas()
.iter()
.map(|column_schema| column_schema.name.clone())
.collect();
assert_eq!(&["col2", "ts"], &names[..]);
assert_eq!(&[0], &new_meta.primary_key_indices[..]);
assert_eq!(&[1], &new_meta.value_indices[..]);
assert_eq!(
schema.timestamp_column(),
new_meta.schema.timestamp_column()
);
}
#[test]
fn test_add_existing_column() {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
let alter_kind = AlterKind::AddColumns {
columns: vec![AddColumnRequest {
column_schema: ColumnSchema::new("col1", ConcreteDataType::string_datatype(), true),
is_key: false,
}],
};
let err = meta
.builder_with_alter_kind("my_table", &alter_kind)
.err()
.unwrap();
assert_eq!(StatusCode::TableColumnExists, err.status_code());
}
#[test]
fn test_remove_unknown_column() {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
let alter_kind = AlterKind::RemoveColumns {
names: vec![String::from("unknown")],
};
let err = meta
.builder_with_alter_kind("my_table", &alter_kind)
.err()
.unwrap();
assert_eq!(StatusCode::TableColumnNotFound, err.status_code());
}
#[test]
fn test_remove_key_column() {
let schema = Arc::new(new_test_schema());
let meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
// Remove column in primary key.
let alter_kind = AlterKind::RemoveColumns {
names: vec![String::from("col1")],
};
let err = meta
.builder_with_alter_kind("my_table", &alter_kind)
.err()
.unwrap();
assert_eq!(StatusCode::InvalidArguments, err.status_code());
// Remove timestamp column.
let alter_kind = AlterKind::RemoveColumns {
names: vec![String::from("ts")],
};
let err = meta
.builder_with_alter_kind("my_table", &alter_kind)
.err()
.unwrap();
assert_eq!(StatusCode::InvalidArguments, err.status_code());
}
#[test]
fn test_alloc_new_column() {
let schema = Arc::new(new_test_schema());
let mut meta = TableMetaBuilder::default()
.schema(schema)
.primary_key_indices(vec![0])
.engine("engine")
.next_column_id(3)
.build()
.unwrap();
assert_eq!(3, meta.next_column_id);
let column_schema = ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true);
let desc = meta.alloc_new_column("test_table", &column_schema).unwrap();
assert_eq!(4, meta.next_column_id);
assert_eq!(column_schema.name, desc.name);
}
}