Merge branch 'engine/v2' into next

next
Sayan Nandan 10 months ago
commit 2b9a3ca6c1
No known key found for this signature in database
GPG Key ID: 42EEDF4AE9D96B54

@ -1,3 +1,2 @@
[env]
ROOT_DIR = { value = "", relative = true }
TEST_ORIGIN_KEY = "4527387f92a381cbe804593f33991d327d456a97"

@ -4,4 +4,5 @@
# Only include the skyd binary
!target/release/skyd
!target/release/skysh
!examples/config-files/docker.toml
!examples/config-files/dpkg/config.yaml
!pkg/docker/start-server.sh

@ -1,29 +0,0 @@
name: Update benches (release)
on:
push:
tags:
- "v*"
jobs:
request-bench:
name: Add request for updating bench
runs-on: ubuntu-latest
steps:
- name: Clone perf repo
run: git clone https://github.com/skytable/perf.git
- name: Configure git
run: |
git config --global user.name "Glydr"
git config --global user.email "${{ secrets.GLYDR_MAIL }}"
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Add request and push
env:
REL: ${{ env.RELEASE_VERSION }}
run: |
cd perf
printf "\ntarget/release/skyreport update release $REL" >> requests.txt
git add .
git commit -m "Add update request for release ``$REL``"
git push https://glydr:${{ secrets.GLYDR }}@github.com/skytable/perf.git --all

@ -1,34 +0,0 @@
name: Rebuild docs
on:
push:
branches:
- next
env:
IS_ACTIONS_DOC: "false"
jobs:
rebuild-docs:
name: Build new actiondoc
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Setup environment
run: |
chmod +x ci/setvars.sh
ci/setvars.sh
- name: Publish docs
env:
BOT_USER: ${{ secrets.BOT_INIT_USER }}
BOT_MAIL: ${{ secrets.BOT_INIT_MAIL }}
BOT_API: ${{ secrets.BOT_API_CALL }}
GIT_SHA: ${{ env.GITHUB_SHA }}
run: |
chmod +x ci/doc.sh
ci/doc.sh
if: env.IS_ACTIONS_DOC == 'true'

@ -2,16 +2,13 @@ name: Docker image
on:
push:
# Publish `next` as Docker `latest` image.
branches:
- next
# Publish `v1.2.3` tags as releases.
tags:
- v*
env:
IMAGE_NAME: sdb
IMAGE_NAME: skytable
BUILD: "false"
jobs:
@ -34,14 +31,16 @@ jobs:
command: build
args: --release
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
if: env.BUILD == 'true' || github.event_name == 'create' && startsWith(github.ref, 'refs/tags/v')
run: docker build . --file Dockerfile --tag $IMAGE_NAME:${{ github.ref == 'refs/heads/next' && 'next' || github.ref_name }}
if: github.ref == 'refs/heads/next' || startsWith(github.ref, 'refs/tags/v')
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: skytable/sdb
tags: latest
tag_with_ref: true
if: env.BUILD == 'true' || github.event_name == 'create' && startsWith(github.ref, 'refs/tags/v')
repository: skytable/skytable
tags: |
${{ github.ref == 'refs/heads/next' && 'next' || '' }}
${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }}
${{ startsWith(github.ref, 'refs/tags/v') && 'latest' || '' }}
if: github.ref == 'refs/heads/next' || startsWith(github.ref, 'refs/tags/v')

@ -36,12 +36,9 @@ jobs:
- name: Install Rust
run: |
rustup update ${{ matrix.rust }} --no-self-update
rustup update
rustup default ${{ matrix.rust }}
if: env.BUILD == 'true'
- name: Install perl modules
uses: perl-actions/install-with-cpanm@v1
with:
install: "HTML::Entities"
- name: Run Tests
run: make test
env:

@ -43,10 +43,6 @@ jobs:
brew install gnu-tar
echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH
if: runner.os == 'macOS'
- name: Install perl modules
uses: perl-actions/install-with-cpanm@v1
with:
install: "HTML::Entities"
- name: Setup environment
run: |
chmod +x ci/setvars.sh
@ -72,29 +68,6 @@ jobs:
RUST_BACKTRACE: 1
TARGET: ${{ matrix.rust }}
- name: Archive artifacts
run: |
mkdir dist
cargo build --target ${{ matrix.rust }}
mv target/${{ matrix.rust }}/debug/skyd target/${{ matrix.rust }}/debug/skysh target/${{ matrix.rust }}/debug/sky-bench dist
if: runner.os != 'Windows'
- name: Archive artifacts
shell: cmd
run: |
cargo build --target ${{ matrix.rust }}
rm -rf dist
mkdir dist
move target\${{ matrix.rust }}\debug\*.exe dist
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
if: runner.os == 'Windows'
- name: Save artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ github.sha }}-${{ matrix.rust }}-builds.zip
path: dist
test_32bit:
name: Test (32-bit)
runs-on: ${{ matrix.os }}
@ -116,10 +89,6 @@ jobs:
run: |
chmod +x ci/setvars.sh
ci/setvars.sh
- name: Install perl modules
uses: perl-actions/install-with-cpanm@v1
with:
install: "HTML::Entities"
- name: Restore cache
uses: actions/cache@v3
@ -132,6 +101,7 @@ jobs:
- name: Install Rust
run: |
rustup self update
rustup default stable
rustup target add ${{ matrix.rust }}
@ -141,29 +111,6 @@ jobs:
RUST_BACKTRACE: 1
TARGET: ${{ matrix.rust }}
- name: Archive artifacts
run: |
mkdir dist
cargo build --target ${{ matrix.rust }}
mv target/${{ matrix.rust }}/debug/skyd target/${{ matrix.rust }}/debug/skysh target/${{ matrix.rust }}/debug/sky-bench dist
if: runner.os == 'Linux'
- name: Archive artifacts
shell: cmd
run: |
cargo build --target ${{ matrix.rust }}
rm -rf dist
mkdir dist
move target\${{ matrix.rust }}\debug\*.exe dist
env:
RUSTFLAGS: -Ctarget-feature=+crt-static
if: runner.os == 'Windows'
- name: Save artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ github.sha }}-${{ matrix.rust }}-builds.zip
path: dist
test_musl64:
name: Test MUSL x86_64 (Tier 2)
runs-on: ${{ matrix.os }}
@ -178,10 +125,6 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Install perl modules
uses: perl-actions/install-with-cpanm@v1
with:
install: "HTML::Entities"
- name: Setup environment
run: |
@ -199,6 +142,7 @@ jobs:
- name: Install Rust
run: |
rustup self update
rustup default stable
rustup target add ${{ matrix.rust }}
@ -207,15 +151,3 @@ jobs:
env:
RUST_BACKTRACE: 1
TARGET: ${{ matrix.rust }}
- name: Archive artifacts
run: |
mkdir dist
cargo build --target ${{ matrix.rust }}
mv target/${{ matrix.rust }}/debug/skyd target/${{ matrix.rust }}/debug/skysh target/${{ matrix.rust }}/debug/sky-bench dist
- name: Save artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ github.sha }}-${{ matrix.rust }}-builds.zip
path: dist

6
.gitignore vendored

@ -1,7 +1,8 @@
/target
/.vscode
*.bin
data
/data/
/server/data
/server/snapshots
snapstore.bin
snapstore.partmap
@ -14,3 +15,6 @@ snapstore.partmap
*.deb
.skytest_*
*.pem
passphrase.txt
*.db-tlog
*.db

@ -0,0 +1,18 @@
----
Notes for v0.8.0
----
I'd like to thank a lot of the people that have very indirectly had some influence on Skytable's design. Here's a little list (in no
particular order):
- First of all I'd like to thank Raymond F. Boyce and Donald D. Chamberlin for their work on SQL. While I'm not fortunate enough to have
any connection to them, a lot of their work have laid out guiding principles for my work.
- I'd also like to thank several people from the Rust community (listed in no particular order):
- Aaron Turon: Aaron's work on concurrency libraries have greatly helped in parts of "design thinking"
- Carl Lerche (@carllerche): For the immense amount of work Carl done on Tokio and related systems.
- Michael Vaner (@vorner): For their work on designing concurrency primitives. I'm a great admirer of Michael's work but
unfortunately haven't had the opportunity to directly talk.
- Amanieu d'Antras (@Amanieu): Amanieu's work on parking_lot and hashbrown have been eye openers for several things that I've designed
and implemented, both in and out of Skytable
-- Sayan N. (Dec, 2023 <ohsayan@outlook.com>)

@ -2,23 +2,109 @@
All changes in this project will be noted in this file.
## Unreleased
## Version 0.8.0
> This is the first release of Skytable Octave, and it changes the query API entirely making all previous versions incompatible
> excluding the data files which are automatically upgraded per our backwards compatibility guarantees
### Additions
#### BlueQL query language
- DDL:
- `space`s are the equivalent of the `keyspace` from previous versions
- `model`s are the equivalent of `table`s from previous version
- The following queries were added:
- `CREATE SPACE [IF NOT EXISTS] ...`
- `CREATE MODEL [IF NOT EXISTS] ...`
- Nested lists are now supported
- Type definitions are now supported
- Multiple fields are now supported
- `ALTER SPACE ...`
- `ALTER MODEL ...`
- `DROP SPACE [IF EXISTS] ...`
- `DROP MODEL [IF EXISTS] ...`
- `USE <space>`:
- works just like SQL
- **does not work with DDL queries**: the reason it works in this way is to prevent accidental deletes
- `INSPECT ...`:
- `INSPECT global`: can be used to inspect the global state, seeing all spaces currently present on the system, users and other information. Some information is limited to the root account only (as JSON)
- `INSPECT space <space>`: can be used to inspect a single space, returning a list of models and relevant information (as JSON)
- `INSPECT model <model>`: can be used to inspect a single model, returning declaration and relevant information (as JSON)
- DML:
- **All actions removed**: All the prior `SET`, `GET` and other actions have been removed in favor of the new query language
- The following queries were added:
- `INSERT INTO <space>.<model>(col, col2, col3, ...)`
- Insert queries can use several ways of insertion including:
- a standard order of declared columns like this:
```sql
INSERT INTO myspace.mymodel(col1, col2, col3, ...)
```
- using a map:
```sql
INSERT INTO myspace.mymodel { col1: val1, col2: val2, col4: val4, col3: val3 }
```
- Inserts can also make use of function calls during inserts:
- It can be called like this: `INSERT INTO myspace.mymodel(@uuidstr, ...)`
- The following functions are available:
- `@uuidstr`: returns a string with a randomly generated v4 UUID
- `@uuidbin`: same as `@uuidstr` but returns it as a blob
- `@timesec`: returns a 64-bit integer with the current time in seconds
- `SELECT [ALL] field1, field2, ... FROM <space>.<model> WHERE <primary_key_column> = <value> [LIMIT n]`
- New data manipulation via `UPDATE` allows arithmetic operations, string manipulation and more! Examples:
- `UPDATE <space>.<model> SET col_num += 1 WHERE <primary_key_column> = <value>`
- `UPDATE <space>.<model> SET mystring += " last part of string" WHERE ...`
- `DELETE FROM <space>.<model> WHERE <primary_key_column> = <value>`
- DCL:
- `SYSCTL CREATE USER <name> WITH { password: <password> }`
- `SYSCTL ALTER USER <name> WITH { password: <new password> }`
- `SYSCTL DROP USER <name>`
#### Fractal engine
- The fractal engine is the start of the development of advanced internal state management in Skytable
- Effectively balances performance and reliability tasks
#### Skyhash 2 protocol
- The `Skyhash-2` protocol now uses a multi-stage connection sequence for improved security and performance
- Seamless auth
- More types
- Fewer retransmissions
#### Storage engines
- **New `deltax` storage engine for data**:
- The `deltax` storage engine monitors the database for changes and only records the changes in an append only file
- The interval can be adjusted per requirements of reliability
- Faster and hugely more reliable than the previous engine
- **New DDL ACID transactions with with the `logx` engine**:
- DDL queries are now fully transactional which means that if they are executed, you can be sure that they were complete and synced to disk
- This largely improves reliability
#### New shell
- The new client shell easily authenticates based on the Skyhash-2 protocol
- More reliable
#### Benchmark tool
- New benchmark engines to enable more efficient load testing
- New benchmark engines use far lesser memory
- New engines can handle midway benchmark crashes
### Breaking changes
- `skyd`:
- New protocol: Skyhash 2.0
- Reduced bandwidth usage (as much as 50%)
- Even simpler client implementations
- Backward compatibility with Skyhash 1.0:
- Simply set the protocol version you want to use in the config file, env vars or pass it as a CLI
argument
- Even faster implementation, even for Skyhash 1.0
- New query language: BlueQL
- `create keyspace` is now `create space`
- `create table` is now `create model`
- Similary, all `inspect` queries have been changed
- Entities are now of the form `space.model` instead of `ks:tbl`
- **The entire query API has changed as actions have been removed**
- **Spaces and models**: replace keyspaces and models respectively
- **Configuration**:
- The configuration system now uses YAML instead of TOML for better readability
- The configuration options have changed across CLI, ENV and the config file
- Authentication **must be enabled** irrespective of `dev`/`prod` mode
- `sky-bench`:
- New benchmark engines are completely different from the previous engines (see above)
- Configuration options have changed because of how the new Skytable engine works
- `skysh`:
- Configuration options have changed because of how the new Skytable engine works
- `sky-migrate`: **This tool is deprecated and has been removed**. The Skytable engine will now automatically manage data upgrades. (please see [this issue](https://github.com/skytable/skytable/issues/320) for discussion on the same)
## Version 0.7.6

@ -50,13 +50,6 @@ The main parts (ignorning CI scripts, stress test suite, test harness and custom
- `cli`: REPL shell
- `server`: database server
- `sky-bench`: benchmark tool
- `sky-migrate`: migration tool
### Jargon
Each project has its own jargon — and so do we!
- _actiondoc_ and _actions docs_ : This refers to the `actiondoc.yml` file, which is used by the Skytable documentation website for automatically building documentation for the actions
### Branches
@ -116,4 +109,4 @@ Testing is simple: just run this:
make test
```
> **NOTE**: Make sure ports 2003 and 2004 are not used by any applications. Also, make sure your _own instance_ isn't running on any of these ports; if that is the case, you might end up losing data due to conflicting entity names! The test suite creates a `testsuite` keyspace and some tables within it to run all the tests.
> **NOTE**: Make sure port 2003 and 2004 are not used by any applications. Also, make sure your _own instance_ isn't running on any of these ports; if that is the case, you might end up losing data due to conflicting entity names! The test suite creates multiple spaces and some models within it to run all the tests.

1403
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,15 +1,6 @@
[workspace]
members = [
"cli",
"server",
"libsky",
"sky-bench",
"sky-macros",
"libstress",
"stress-test",
"sky-migrate",
"harness",
]
resolver = "1"
members = ["cli", "server", "libsky", "sky-bench", "sky-macros", "harness"]
[profile.release]
opt-level = 3

@ -1,14 +1,16 @@
#
# The Dockerfile for the Skytable server sdb
#
FROM debian:stable
# Copy the necessary binaries
COPY target/release/skyd /usr/local/bin
COPY target/release/skysh /usr/local/bin
RUN mkdir /etc/skytable
# Create necessary directories
RUN mkdir /var/lib/skytable
COPY examples/config-files/docker.toml /etc/skytable/skyd.toml
COPY examples/config-files/dpkg/config.yaml /var/lib/skytable/config.yaml
COPY pkg/docker/start-server.sh /usr/local/bin/start-server.sh
WORKDIR /var/lib/skytable
CMD ["skyd", "-c", "/etc/skytable/skyd.toml"]
# Install uuidgen for generating a random password
RUN apt-get update && apt-get install -y uuid-runtime
RUN chmod +x /usr/local/bin/start-server.sh
ENTRYPOINT ["/usr/local/bin/start-server.sh"]
EXPOSE 2003/tcp

@ -1,82 +1,125 @@
<html>
<div align="center">
<img src="assets/logo.jpg" height=64 width=64>
<h1>Skytable</h1><h3>Your next NoSQL database</h3>
<br/><p align="center">
<img width="150" src="assets/logo.jpg">
</p>
<h2 align = "center">
Skytable — A modern database for building powerful experiences
</h2>
<h3 align="center">
Performance, scalability and flexibility. Choose three.
</h3>
</p>
<p align="center">
<a href="https://github.com/skytable/skytable/releases"><img src="https://img.shields.io/github/v/release/skytable/skytable?style=flat" alt="GitHub release (with filter)"></a> <a href="https://github.com/skytable/skytable/actions"><img src="https://img.shields.io/github/actions/workflow/status/skytable/skytable/test-push.yml?style=flat" alt="GitHub Workflow Status (with event)"></a> <a href="https://discord.gg/QptWFdx"><img src="https://img.shields.io/discord/729378001023926282?logo=discord&style=flat" alt="Discord"></a> <a href="https://docs.skytable.io"><img src="https://img.shields.io/badge/read%20the%20docs-here-blue?style=flat" alt="Docs"></a> <a href="https://github.com/skytable/skytable/discussions?style=flat"><img src="https://img.shields.io/badge/discuss-here-8A3324?style=flat&logo=github&labelColor=C34723" alt="Static Badge"></a>
</p>
![GitHub Workflow Status](<https://img.shields.io/github/actions/workflow/status/skytable/skytable/test-push.yml?branch=next>) ![Development](https://img.shields.io/badge/development-regular-32CD32?style=flat-square) ![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/skytable/skytable?include_prereleases&sort=semver&style=flat-square)
[![Docs](https://img.shields.io/badge/readthedocs-here-blueviolet?style=flat-square)](https://docs.skytable.io) [![Contribute Now](https://img.shields.io/badge/%F0%9F%8C%9Fcontribute-now-a94064)](https://ohsayan.github.io/skythanks) [![Discord](https://img.shields.io/badge/talk-on%20discord-7289DA?logo=discord&style=flat-square")](https://discord.gg/QptWFdx)
## 💡 What is Skytable?
</div>
</html>
Skytable is a **modern NoSQL database** that focuses on **performance, flexibility and scalability**. Our goal is to deliver **a rock-solid database** that you can use as the foundation for your next application — **minus the gimmicks**.
## What is Skytable?
Skytable makes every attempt to **remove the gotchas from SQL-systems**. For example, nonempty `model`s and `space`s cannot be dropped and **BlueQL is designed to greatly deter SQL injection vulnerabilities** with a new **mandatory parameterization** design and several other **secure query language design principles**.
Skytable is a free and open-source NoSQL database that aims to provide flexible data modeling at
scale. For us simplicity, performance and flexibility are our guiding design principles.
We were previously known as TerrabaseDB or Skybase and are nicknamed Sky, SDB or STable by the community.
Every component in Skytable has been **engineered from the ground up** to meet our design goals. **Skytable uses BlueQL<sup>TM</sup>** which is our own **new in-house query language** designed from the ground up for a **clean, powerful, modern and secure querying experience** and is **generally more secure than SQL**.
Features like keyspaces, tables, data types, authn+authz, snapshots and more are ready for you to use while we're working on [several new data models and features](https://github.com/skytable/skytable/issues/203). Skytable's key/value store is performant, secure and ready for you to deploy.
Skytable works with **structured and semi-structured data**. We're currently working on supporting unstructured data.
## Getting started 🚀
> **You can read more about Skytable's architecture, including information on the clustering and HA implementation that we're currently working on, and limitations [on this page](https://docs.skytable.io/architecture).**
1. Download a bundle for your platform from [here ⬇️ ](https://github.com/skytable/skytable/releases)
2. Unzip the bundle
3. Make the files executable (run `chmod +x skyd skysh` on \*nix systems)
4. First run `skyd` to start the database server and then run `skysh` to start the interactive shell
5. Run commands like: `SET foo bar` , `GET bar` , `UPDATE cat mitten` or `DEL proprietary` on `skysh`!
## 🎨 Features
You can learn more about installation [here](https://docs.skytable.io/getting-started)
- **Spaces, models and more**: For flexible data definition
- **Powerful querying with BlueQL**: A modern query language, designed for the 21<sup>st</sup> century
- **Rich data modeling**: Use `model`s to define data with complex types, collections and more
- **Performant, in and out of the box**: Heavily multithreaded and optimized
- **Secure, query in and response out**: BlueQL is designed to strongly deter query injection pathways
- **SQL minus the gotchas**: Ever done a `DROP TABLE users` and lost all data? **That won't happen in Skytable**.
- **Designed to scale by enforcing best practices**: If you're building with Skytable today, the practices you'll learn here will let you easily take on the job of building large scale systems
## Features
> Learn more about [Skytable's features here](https://docs.skytable.io).
- **Insanely fast**: Scale to millions of queries per second per node. See [benchmarks here](https://github.com/ohsayan/sky-benches).
- **Multiple keyspaces/tables**: Seamlessly integrates with actions to provide a SQL-like experience
- **Key/value store**: `GET` , `SET` , `UPDATE` and [all that stuff](https://docs.skytable.io/all-actions). With the `str` and `binstr` types.
- **Authn/Authz**: Simple and secure authentication/authorization
- **Volatile tables**: For all the caching you need
- **Snapshots**: Automated (and tunable) snapshots for stress-free backups
- **Secure**: Secure connections are built into Skytable with SSL/TLS
- **Multithreaded**: Designed to exploit all CPU cores
- **Resource friendly**: The database server doesn't need more than 1MB to run
- **Convenient**: Without the setup hassle and system-specific dependencies
## 🚀 Getting started
**🛣️ There's a lot more coming! View our [roadmap](https://github.com/skytable/skytable/issues/203)**
1. **Set up Skytable on your machine**: You'll need to download a bundled release file [from the releases page](https://github.com/skytable/skytable/releases). Unzip the files and you're ready to go.
2. Start the database server: `./skyd --auth-root-password <password>` with your choice of a password for the `root` account. The `root` account is just like a `root` account on Unix based systems that has control over everything.
3. Start the interactive client REPL: `./skysh` and then enter your password.
4. You're ready to run queries!
## Clients 🔌
> **For a more detailed guide on installation and deployment, [follow the guide here.](https://docs.skytable.io/installation)**
The project currently maintains an official [Rust driver](https://github.com/skytable/client-rust) and we have plans
to support more languages along the way!
We also maintain a list of [community supported drivers here](https://github.com/skytable/skytable/wiki/Drivers).
If you want to use a different language, for now you'll just need to implement the simple and performant [Skyhash Protocol](https://docs.skytable.io/protocol/skyhash).
## ⚡ Using Skytable
## Community 👐
Skytable has `SPACE`s instead of `DATABASE`s due to signficant operational differences (and because `SPACE`s store a lot more than tabular data).
A project which is powered by the community believes in the power of community! If you get stuck anywhere - here are your options!
**With the REPL started, follow this guide**:
<html>
<a href="https://gitter.im/skytable/community"><img src="https://img.shields.io/badge/chat%20on-gitter-ed1965?logo=gitter&style=flat-square"></img>
</a><a href="https://discord.gg/QptWFdx"><img src="https://img.shields.io/badge/talk-on%20discord-7289DA?logo=discord&style=flat-square"></img></a>
</html>
1. Create a `space` and switch to it:
```sql
CREATE SPACE myspace
USE myspace
```
2. Create a `model`:
```sql
CREATE MODEL myspace.mymodel(username: string, password: string, notes: list { type: string })
```
The rough representation for this in Rust would be:
```rust
pub struct MyModel {
username: String,
password: Strin,
notes: Vec<String>,
}
```
3. `INSERT` some data:
```sql
INSERT INTO mymodel('sayan', 'pass123', [])
```
4. `UPDATE` some data:
```sql
UPDATE mymodel SET notes += "my first note" WHERE username = 'sayan'
```
5. `SELECT` some data
```sql
SELECT * FROM mymodel WHERE username = 'sayan'
```
6. Poke around! **And then make sure you [read the documentation learn BlueQL](https://docs.skytable.io/blueql/overview).**
## Platforms 💻
> **For a complete guide on Skytable, it's architecture, BlueQL, queries and more we strongly recommend you to [read the documentation here.](https://docs.skytable.io)**
>
> While you're seeing strings and other values being used here, this is so because the REPL client smartly parameterizes queries behind the scenes. **BlueQL has mandatory parameterization**. (See below to see how the Rust client handles this)
![Linux supported](https://img.shields.io/badge/Linux%2032--bit%2F64--bit-Supported%20✓-%23228B22?logo=linux) ![macOS supported](https://img.shields.io/badge/macOS%20x86__64%2Farm64-supported%20✓-228B22?style=flat-square&logo=apple) ![Windows supported](https://img.shields.io/badge/Windows%2032--bit%2F64--bit-supported%20✓-228B22?style=flat-square&logo=windows)
## 🧩 Find a client driver
## Versioning
You need a client driver to use Skytable in your programs. Officially, we maintain a regularly updated [Rust client driver](https://github.com/skytable/client-rust) which is liberally license under the Apache-2.0 license so that you can use it anywhere.
This project strictly follows semver, however, since this project is currently in the development phase (0.x.y), the API may change unpredictably
Using the Rust client driver, it's very straightforward to run queries thanks to Rust's powerful type system and macros:
## Contributing
```rust
use skytable::{Config, query};
fn main() {
let mut db = Config::new_default("username", "password").connect().unwrap();
let query = query!("select username, password from myspace.mymodel where username = ?", "sayan");
let (username, password): (String, Vec<u8>) = db.query_parse(&query).unwrap();
// do something with it!
}
```
> **You can find more information on client drivers on [this page](https://docs.skytable.io/libraries). If you want to help write a client driver for your language of choice, *we're here to support your work*. Please reach out to: hey@skytable.io or leave a message on our Discord server!**
## 🙋 Getting help
[![Contribute Now](https://img.shields.io/badge/%F0%9F%8C%9Fcontribute-now-a94064?style=for-the-badge)](https://ohsayan.github.io/skythanks)
We exclusively use [Discord](https://discord.gg/QptWFdx) for most real-time communications — you can chat with developers, maintainers, and our amazing users! Outside that, we recommend that you use our [GitHub Discussions page](https://github.com/skytable/skytable/discussions) for any questions or open a new issue if you think you've found a bug.
*We're here to help!*
## Contributing
You are welcome to contribute to Skytable! Beginner friendly issues are marked with the [<img src=https://img.shields.io/badge/L--easy-C71585>](https://github.com/skytable/skytable/labels/L-easy) label. Read the guide [here](./CONTRIBUTING.md).
Please read the [contributing guide here](./CONTRIBUTING.md).
## Contributors
## Acknowledgements
You can see a full list of contributors [here](https://ohsayan.github.io/skythanks)
Please read the [acknowledgements](./ACKNOWLEDGEMENTS.txt) document.
## License
This project is licensed under the [AGPL-3.0 License](./LICENSE).
Skytable is distributed under the [AGPL-3.0 License](./LICENSE). **You may not use Skytable's logo for other projects.**

@ -1,380 +0,0 @@
#
# Created on Thu Aug 27 2020
#
# This file is a part of Skytable
# Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
# NoSQL database written by Sayan Nandan ("the Author") with the
# vision to provide flexibility in data modelling without compromising
# on performance, queryability or scalability.
#
# Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
#
#
# This file is used by Skytable's documentation website for automatically
# generating documentation for the actions. It will also be used by the Skytable
# server in the future
# the docbuilder expects:
# 'name': str, 'complexity': str, 'accept': [str]
# 'return': [str], 'syntax': [str], 'desc': str
global:
- name: HEYA
complexity: O(1)
accept: [AnyArray]
syntax: [HEYA, HEYA <message>]
desc: |
Either returns a "HEY!" or returns the provided argument as an `str`
return: [String]
- name: DBSIZE
complexity: O(1)
accept: [AnyArray]
syntax: [DBSIZE, DBSIZE <entity>]
desc: Check the number of entries stored in the current table or in the provided entity
return: [Integer]
- name: MKSNAP
complexity: O(n)
accept: [AnyArray]
syntax: [MKSNAP, MKSNAP <SNAPNAME>]
desc: |
This action can be used to create a snapshot. Do note that this action **requires
snapshotting to be enabled on the server side**, before it can create snapshots.
If you want to create snapshots **without** snapshots being enabled on the server-side,
pass a second argument `<SNAPNAME>` to specify a snapshot name and a snapshot will
be create in a folder called `rsnap` under your data directory. For more
information on snapshots, read [this document](/snapshots)
return: [Rcode 0, err-snapshot-disabled, err-snapshot-busy]
- name: FLUSHDB
complexity: O(n)
accept: [AnyArray]
syntax: [FLUSHDB, FLUSHDB <entity>]
desc: Removes all entries stored in the current table or in the provided entity
return: [Rcode 0, Rcode 5]
- name: WHEREAMI
complexity: O(1)
accept: [AnyArray]
syntax: [WHEREAMI]
desc: |
Returns an array with either the name of the current keyspace as the first element or if a default table
is set, then it returns the keyspace name as the first element and the table name as the second element
return: [Non-null array]
- name: AUTH
desc: Change global authn/authz settings
subactions:
- name: LOGIN
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH LOGIN <username> <token>]
desc: Attempts to log in using the provided credentials
return: [Rcode 0, Rcode 10]
- name: CLAIM
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH CLAIM <origin key>]
desc: Attempts to claim the root account using the origin key
return: [String, Rcode 10]
- name: LOGOUT
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH LOGOUT]
desc: Attempts to log out the currently logged in user
return: [Rcode 0, Rcode 10]
- name: ADDUSER
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH ADDUSER <username>]
desc: Attempts to create a new user with the provided username, returning the token
return: [String, Rcode 11]
- name: DELUSER
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH DELUSER <username>]
desc: Attempts to delete the user with the provided username
return: [Rcode 0, Rcode 10, Rcode 11]
- name: RESTORE
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH RESTORE <username>, AUTH RESTORE <origin-key> <username>]
desc: |
Attempts to restore the password for the provided user. This will regenerate the token
and return the newly issued token. However, if you aren't a root account, that is, you
lost your root password, then you'll need to run `AUTH RESTORE <origin-key> root`.
return: [String, Rcode 10, Rcode 11]
- name: LISTUSER
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH LISTUSER]
desc: |
Attempts to return a list of users for the current database instance
return: [Non-null array]
- name: WHOAMI
complexity: O(1)
accept: [AnyArray]
syntax: [AUTH WHOAMI]
desc: |
Returns a string with the AuthID of the currently logged in user or errors if the user
is not logged in
return: [String]
- name: SYS
desc: |
Get system information and metrics
subactions:
- name: INFO
complexity: O(1)
accept: [AnyArray]
syntax: [sys info <property>]
return: [String, Float]
desc: |
Returns static properties of the system, i.e properties that do not change during runtime.
The following properties are available:
- `version`: Returns the server version (String)
- `protocol`: Returns the protocol version string (String)
- `protover`: Returns the protocol version (float)
- name: METRIC
complexity: O(1)
accept: [AnyArray]
syntax: [sys metric <metric>]
return: [String, Float]
desc: |
Returns dynamic properties of the system, i.e metrics are properties that can change during
runtime. The following metrics are available:
- `health`: Returns "good" or "critical" depending on the system state (String)
- `storage`: Returns bytes used for on-disk storage (uint64)
keyvalue:
generic:
- name: DEL
complexity: O(n)
accept: [AnyArray]
syntax: [DEL <key1> <key2> ...]
desc: |
Delete 'n' keys from the current table. This will return the number of keys that were deleted
as an unsigned integer
return: [Integer, Rcode 5]
- name: EXISTS
complexity: O(n)
accept: [AnyArray]
syntax: [EXISTS <key1> <key2> ...]
desc: |
Check if 'n' keys exist in the current table. This will return the number of keys that exist
as an unsigned integer.
return: [Integer]
- name: LSKEYS
complexity: O(n)
accept: [AnyArray]
syntax: [LSKEYS <limit>, LSKEYS <entity>, LSKEYS <entity> <limit>]
desc: |
Returns a flat string array of keys present in the current table or in the provided entity.
If no `<limit>` is given, then a maximum of 10 keys are returned. If a limit is specified,
then a maximum of `<limit>` keys are returned. The order of keys is meaningless.
return: [Typed Array]
string:
- name: GET
complexity: O(1)
accept: [AnyArray]
syntax: [GET <key>]
desc: Get the value of a key from the current table, if it exists
return: [Rcode 1, String, Binstr]
- name: MGET
complexity: O(n)
accept: [AnyArray]
syntax: [MGET <key1> <key2> ...]
desc: Get the value of 'n' keys from the current table, if they exist
return: [Typed Array]
- name: SET
complexity: O(1)
accept: [AnyArray]
syntax: [SET <key> <value>]
desc: Set the value of a key in the current table, if it doesn't already exist
return: [Rcode 0, Rcode 2, Rcode 5]
- name: MSET
complexity: O(n)
accept: [AnyArray]
syntax: [MSET <key1> <value1> <key2> <value2> ...]
desc: |
Set the value of 'n' keys in the current table, if they don't already exist. This will
return the number of keys that were set as an unsigned integer.
return: [Integer, Rcode 5]
- name: UPDATE
complexity: O(1)
accept: [AnyArray]
syntax: [UPDATE <key> <value>]
desc: Update the value of an existing key in the current table
return: [Rcode 0, Rcode 1, Rcode 5]
- name: MUPDATE
complexity: O(n)
accept: [AnyArray]
syntax: [MUPDATE <key1> <value1> <key2> <value2> ...]
desc: |
Update the value of 'n' keys in the current table, if they already exist. This will return
the number of keys that were updated as an unsigned integer.
return: [Integer, Rcode 5]
- name: SSET
complexity: O(n)
accept: [AnyArray]
syntax: [SSET <key1> <value1> <key2> <value2> ...]
desc: Set all keys to the given values only if all of them don't exist in the current table
return: [Rcode 0, Rcode 2, Rcode 5]
- name: SDEL
complexity: O(n)
accept: [AnyArray]
syntax: [SDEL <key1> <key2> ...]
desc: |
Delete all keys if all of the keys exist in the current table. Do note that if a single key doesn't
exist, then a `Nil` code is returned.
return: [Rcode 0, Rcode 1, Rcode 5]
- name: SUPDATE
complexity: O(n)
accept: [AnyArray]
syntax: [SUPDATE <key1> <value1> <key2> <value2> ...]
desc: |
Update all keys if all of the keys exist in the current table. Do note that if a single key doesn't
exist, then a `Nil` code is returned.
return: [Rcode 0, Rcode 1, Rcode 5]
- name: USET
complexity: O(n)
accept: [AnyArray]
syntax: [USET <key1> <value1> <key2> <value2> ...]
desc: SET all keys if they don't exist, or UPDATE them if they do exist. This operation performs `USET`s in the current table
return: [Integer, Rcode 5]
- name: KEYLEN
complexity: O(1)
accept: [AnyArray]
syntax: [KEYLEN <key>]
desc: Returns the length of the UTF-8 string, if it exists in the current table
return: [Integer, Rcode 1]
- name: POP
complexity: O(1)
accept: [AnyArray]
syntax: [POP <key>]
desc: |
Deletes and return the value of the provided key from the current table.
If the database is poisoned, this will return a server error.
return: [String, Binstr, Rcode 5]
- name: MPOP
complexity: O(n)
accept: [AnyArray]
syntax: [MPOP <key1> <key2> ...]
desc: |
Deletes and returns the values of the provided 'n' keys from the current table.
If the database is poisoned, this will return a server error
return: [Typed Array, Rcode 5]
lists:
- name: LGET
desc: |
`LGET` can be used to access the items in a list. Through the sub-actions provided by `lget`,
you can access multiple or individual elements in lists.
subactions:
- name: LGET
complexity: O(n)
accept: [AnyArray]
syntax: [LGET <list>]
desc: |
Returns all the values contained in a the provided list, if it exists in the current
table.
return: [Typed Array, Rcode 1]
- name: limit
complexity: O(n)
accept: [AnyArray]
syntax: [LGET <list> limit <limit>]
desc: Returns a maximum of `limit` values from the provided list, if it exists in the current table
return: [Typed Array, Rcode 1]
- name: len
complexity: O(1)
accept: [AnyArray]
syntax: [LGET <list> len]
desc: Returns the length of the list
return: [Integer, Rcode 1]
- name: valueat
complexity: O(1)
accept: [AnyArray]
syntax: [LGET <list> valueat <index>]
desc: Returns the element present at the provided `index`, if it exists in the given list.
return: [String, binstr, Rcode 1, bad-list-index]
- name: first
complexity: O(1)
accept: [AnyArray]
syntax: [LGET <list> first]
desc: Returns the first element present in the list, if it exists.
return: [String, binstr, Rcode 1, list-is-empty]
- name: last
complexity: O(1)
accept: [AnyArray]
syntax: [LGET <list> last]
desc: Returns the last element present in the list, if it exists.
return: [String, binstr, Rcode 1, list-is-empty]
- name: range
complexity: O(n)
accept: [AnyArray]
syntax: [LGET <list> range <start>, LGET <list> range <start> <stop>]
desc: |
Returns items in the given range. If no value for `stop` is provided, all the elements from that
index are returned. If a value for `stop` is provided, then a subarray is returned
return: [Typed Array, Rcode 1, bad-list-index]
- name: LMOD
desc: |
`LMOD` can be used to mutate the elements in a list
subactions:
- name: push
complexity: O(1)
accept: [AnyArray]
syntax: [LMOD <list> push <v1> <v2> ...]
desc: Appends the elements to the end of the provided list, if it exists.
return: [Rcode 0, Rcode 1, Rcode 5]
- name: insert
complexity: O(1)
accept: [AnyArray]
syntax: [LMOD <list> insert <index> <value>]
desc: |
Inserts the element to the provided index, if it is valid while shifting elements
to the right if required
return: [Rcode 0, Rcode 1, Rcode 5, bad-list-index]
- name: pop
complexity: O(1)
accept: [AnyArray]
syntax: [LMOD <list> pop, LMOD <list> pop <index>]
desc: |
Removes the element from the end of the list if no index is provided or from the provided
index while shifting elements to the right if required.
return: [String, Binstr, Rcode 1, Rcode 5, bad-list-index]
- name: remove
complexity: O(1)
accept: [AnyArray]
syntax: [LMOD <list> remove <index>]
desc: |
Removes the element at the provided index from the list, shifting elements to the right
if required.
return: [Rcode 0, Rcode 1, Rcode 5, bad-list-index]
- name: clear
complexity: O(n)
accept: [AnyArray]
syntax: [LMOD <list> clear]
desc: |
Removes all the elements present in the list
return: [Rcode 0, Rcode 1, Rcode 5]
- name: LSET
desc: |
`LSET` can be used to create empty lists or lists with the provided values.
subactions:
- name: LSET
complexity: O(n)
accept: [AnyArray]
syntax: [LSET <list>, LSET <list> <value1> <value2> ...]
desc: |
Creates a list with the provided values, or simply creates an empty list if it doesn't
already exist in the table.
return: [Rcode 0, Rcode 2, Rcode 5]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 180 KiB

@ -1,9 +0,0 @@
[server]
host = "127.0.0.1"
port = 2003
noart = true
[ssl]
key="../key.pem"
chain="../cert.pem"
port = 2004

@ -0,0 +1,17 @@
system:
mode: prod
auth:
plugin: pwd
root_pass: mypassword12345678
endpoints:
insecure:
host: 127.0.0.1
port: 2003
secure:
host: 127.0.0.1
port: 2004
cert: ../cert.pem
private_key: ../key.pem
pkey_passphrase: ../passphrase.txt

@ -1,12 +0,0 @@
[server]
host = "127.0.0.1"
port = 2005
noart = true
[auth]
origin_key = "4527387f92a381cbe804593f33991d327d456a97"
[ssl]
key = "../key.pem"
chain = "../cert.pem"
port = 2006

@ -1,14 +0,0 @@
[server]
host = "127.0.0.1"
port = 2007
noart = true
[snapshot]
every = 3600
atmost = 4
failsafe = true
[ssl]
key = "../key.pem"
chain = "../cert.pem"
port = 2008

@ -10,13 +10,7 @@ description = "The Skytable Shell (skysh)"
[dependencies]
# internal deps
libsky = { path = "../libsky" }
skytable = { git = "https://github.com/skytable/client-rust", branch = "next", features = [
"aio",
"aio-sslv",
], default-features = false }
skytable = { git = "https://github.com/skytable/client-rust.git", branch = "octave" }
# external deps
tokio = { version = "1.24.1", features = ["full"] }
clap = { version = "4.0.32", features = ["derive"] }
rustyline = "10.0.0"
crossterm = "0.25.0"
lazy_static = "1.4.0"
crossterm = "0.27.0"
rustyline = "13.0.0"

@ -0,0 +1,30 @@
skysh 0.8.0
Sayan N. <ohsayan@outlook.com>
The Skytable interactive shell (skysh)
USAGE:
skysh [OPTIONS]
FLAGS:
--help Diplays this help message
--version Displays the shell version
OPTIONS:
--endpoint Set the endpoint for the connection
--user Set the user for this client session
--password Set the password for this client session
--tls-cert Set the TLS certificate to use (for TLS endpoints)
NOTES:
- When no endpoint is specified, skysh will attempt to connect to the default
TCP endpoint `tcp@127.0.0.1:2003`
- When no user is specified, skysh will attempt to authenticate as root
- All connections need an username and password. If this is not provided
via arguments, it will be asked for interactively
- Endpoints are specified using the Skytable endpoint syntax. For example,
the default TCP endpoint is `tcp@127.0.0.1:2003` while the default TLS
endpoint is `tls@127.0.0.1:2004`
- If you choose to use a TLS endpoint, you must provide a certificate.
Failing to do so will throw an error, as expected
- All history is stored in the `.sky_history` file. If you wish to delete
it, simply remove the file

@ -0,0 +1,16 @@
Welcome to the Skytable Interactive shell (REPL environment).
Here are a few tips to help you get started:
- Skytable uses its own query language called BlueQL. It is mostly like SQL
but with some important changes for security
- Since BlueQL doesn't need a query terminator ';' you do not need to use it
here for running queries
- You might be surprised to see that you can use literals in this REPL while
Skytable does not allow the use of literals for security concerns. This is
because whenever you run a query, the REPL turns it into a parameterized query.
- You can also run some `skysh` specific commands:
- `!help` displays this help message
- `clear` clears the terminal screen
- `exit` exits the REPL session
Now, it's time to get querying!

@ -1,271 +0,0 @@
/*
* Created on Wed Jul 01 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{cli::Cli, runner::Runner, tokenizer},
clap::Parser,
crossterm::{
cursor, execute,
terminal::{Clear, ClearType},
},
libsky::{URL, VERSION},
rustyline::{config::Configurer, error::ReadlineError, Editor},
skytable::{Pipeline, Query},
std::{io::stdout, process},
};
const SKYSH_HISTORY_FILE: &str = ".sky_history";
const HELP_TEXT: &str = r#"
Welcome to Skytable's interactive shell (REPL) environment. Using the Skytable
shell, you can create, read, update or delete data on your remote Skytable
instance. When you connect to your database instance, you'll be connected to
the `default` table in the `default` keyspace. This table has binary keys and
binary values as the default data type. Here's a brief guide on doing some
everyday tasks:
(1) Running actions
================================================================================
An action is like a shell command: it starts with a name and contains arguments!
To run actions, simply type them out, like "set x 100" or "inspect table mytbl"
and hit enter.
(2) Running shell commands
================================================================================
Shell commands are those which are provided by `skysh` and are not supported by
the server. These enable you to do convenient things like:
- "exit": exits the shell
- "clear": clears the terminal screen
Apart from these, you can use the following shell commands:
- "!pipe": Lets you create a pipeline. Terminate with a semicolon (`;`)
- "!help": Brings up this help menu
- "?<command name>": Describes what the built-in shell command is for
With Skytable in your hands, the sky is the only limit on what you can create!"#;
const SKY_WELCOME: &str = "
Welcome to Skytable's interactive shell (REPL) environment. For usage and help
within the shell, you can run `!help` anytime. Now that you have Skytable in
your hands, the sky is the only limit on what you can create!
";
/// This creates a REPL on the command line and also parses command-line arguments
///
/// Anything that is entered following a return, is parsed into a query and is
/// written to the socket (which is either `localhost:2003` or it is determined by
/// command line parameters)
pub async fn start_repl() {
let mut skysh_blank: String = " > ".to_owned();
let mut skysh_prompt: String = "skysh@default:default> ".to_owned();
let mut did_swap = false;
macro_rules! readln {
($editor:expr) => {
match $editor.readline(&skysh_blank) {
Ok(l) => l,
Err(ReadlineError::Interrupted | ReadlineError::Eof) => return,
Err(err) => fatal!("ERROR: Failed to read line with error: {}", err),
}
};
}
let cli = Cli::parse();
let mut editor = match Editor::<()>::new() {
Ok(e) => e,
Err(e) => fatal!("Editor init error: {}", e),
};
editor.set_auto_add_history(true);
editor.set_history_ignore_dups(true);
editor.bind_sequence(
rustyline::KeyEvent(
rustyline::KeyCode::BracketedPasteStart,
rustyline::Modifiers::NONE,
),
rustyline::Cmd::Noop,
);
let con = match cli.ssl_cert {
Some(cert) => Runner::new_secure(&cli.host, cli.port, &cert).await,
None => Runner::new_insecure(&cli.host, cli.port).await,
};
let mut runner = match con {
Ok(c) => c,
Err(e) => fatal!("Failed to connect to server with error: {}", e),
};
macro_rules! checkswap {
() => {
if did_swap {
// noice, we need to poll for the location of the new entity
runner
.check_entity(&mut skysh_blank, &mut skysh_prompt)
.await;
}
};
}
if let Some(expressions) = cli.expressions {
for eval_expr in expressions {
if !eval_expr.is_empty() {
runner.run_query(&eval_expr).await;
}
}
process::exit(0x00);
}
println!("Skytable v{} | {}", VERSION, URL);
match editor.load_history(SKYSH_HISTORY_FILE) {
Ok(_) => {}
Err(e) => match e {
ReadlineError::Io(e) if e.kind() == std::io::ErrorKind::NotFound => {
println!("{}", SKY_WELCOME)
}
_ => fatal!("Failed to read history file with error: {}", e),
},
}
loop {
match editor.readline(&skysh_prompt) {
Ok(mut line) => {
macro_rules! tokenize {
($inp:expr) => {
match tokenizer::get_query($inp) {
Ok(q) => q,
Err(e) => {
eskysh!(e);
continue;
}
}
};
() => {
tokenize!(line.as_bytes())
};
}
match line.to_lowercase().as_str() {
"exit" => break,
"clear" => {
clear_screen();
continue;
}
"help" => {
println!("To get help, run `!help`");
continue;
}
_ => {
if line.is_empty() {
continue;
}
match line.as_bytes()[0] {
b'#' => continue,
b'!' => {
match &line.as_bytes()[1..] {
b"" => eskysh!("Bad shell command"),
b"help" => println!("{}", HELP_TEXT),
b"pipe" => {
// so we need to handle a pipeline
let mut pipeline = Pipeline::new();
line = readln!(editor);
loop {
did_swap = line
.get(..3)
.map(|v| v.eq_ignore_ascii_case("use"))
.unwrap_or(did_swap);
if !line.is_empty() {
if *(line.as_bytes().last().unwrap()) == b';' {
break;
} else {
let q: Query = tokenize!();
pipeline.push(q);
}
}
line = readln!(editor);
}
if line.len() > 1 {
line.drain(line.len() - 1..);
let q: Query = tokenize!();
pipeline.push(q);
}
runner.run_pipeline(pipeline).await;
checkswap!();
}
_ => eskysh!("Unknown shell command"),
}
continue;
}
b'?' => {
// handle explanation for a shell command
print_help(&line);
continue;
}
_ => {}
}
while line.len() >= 2 && line[line.len() - 2..].as_bytes().eq(br#" \"#) {
// continuation on next line
let cl = readln!(editor);
line.drain(line.len() - 2..);
line.push_str(&cl);
}
did_swap = line
.get(..3)
.map(|v| v.eq_ignore_ascii_case("use"))
.unwrap_or(did_swap);
runner.run_query(&line).await;
checkswap!();
}
}
}
Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
Err(err) => fatal!("ERROR: Failed to read line with error: {}", err),
}
}
editor
.save_history(SKYSH_HISTORY_FILE)
.map_err(|e| {
fatal!("ERROR: Failed to save history with error: '{}'", e);
})
.unwrap();
}
fn print_help(line: &str) {
match &line.as_bytes()[1..] {
b"" => eskysh!("Bad shell command"),
b"help" => println!("`!help` shows the help menu"),
b"exit" => println!("`exit` ends the shell session"),
b"clear" => println!("`clear` clears the terminal screen"),
b"pipe" | b"!pipe" => println!("`!pipe` lets you run pipelines using the shell"),
_ => eskysh!("Unknown shell command"),
}
}
fn clear_screen() {
let mut stdout = stdout();
execute!(stdout, Clear(ClearType::All)).expect("Failed to clear screen");
execute!(stdout, cursor::MoveTo(0, 0)).expect("Failed to move cursor to origin");
}

@ -0,0 +1,192 @@
/*
* Created on Wed Nov 15 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::error::{CliError, CliResult},
crossterm::{
event::{self, Event, KeyCode, KeyEvent},
terminal,
},
libsky::CliAction,
std::{
collections::HashMap,
fs,
io::{self, Write},
process::exit,
},
};
const TXT_HELP: &str = include_str!("../help_text/help");
#[derive(Debug)]
pub struct ClientConfig {
pub kind: ClientConfigKind,
pub username: String,
pub password: String,
}
impl ClientConfig {
pub fn new(kind: ClientConfigKind, username: String, password: String) -> Self {
Self {
kind,
username,
password,
}
}
}
#[derive(Debug)]
pub enum ClientConfigKind {
Tcp(String, u16),
Tls(String, u16, String),
}
#[derive(Debug)]
pub enum Task {
HelpMessage(String),
OpenShell(ClientConfig),
}
enum TaskInner {
HelpMsg(String),
OpenShell(HashMap<String, String>),
}
fn load_env() -> CliResult<TaskInner> {
let action = libsky::parse_cli_args_disallow_duplicate()?;
match action {
CliAction::Help => Ok(TaskInner::HelpMsg(TXT_HELP.into())),
CliAction::Version => Ok(TaskInner::HelpMsg(libsky::version_msg("skysh"))),
CliAction::Action(a) => Ok(TaskInner::OpenShell(a)),
}
}
pub fn parse() -> CliResult<Task> {
let mut args = match load_env()? {
TaskInner::HelpMsg(msg) => return Ok(Task::HelpMessage(msg)),
TaskInner::OpenShell(args) => args,
};
let endpoint = match args.remove("--endpoint") {
None => ClientConfigKind::Tcp("127.0.0.1".into(), 2003),
Some(ep) => {
// should be in the format protocol@host:port
let proto_host_port: Vec<&str> = ep.split("@").collect();
if proto_host_port.len() != 2 {
return Err(CliError::ArgsErr("invalid value for --endpoint".into()));
}
let (protocol, host_port) = (proto_host_port[0], proto_host_port[1]);
let host_port: Vec<&str> = host_port.split(":").collect();
if host_port.len() != 2 {
return Err(CliError::ArgsErr("invalid value for --endpoint".into()));
}
let (host, port) = (host_port[0], host_port[1]);
let port = match port.parse::<u16>() {
Ok(port) => port,
Err(e) => {
return Err(CliError::ArgsErr(format!(
"invalid value for endpoint port. {e}"
)))
}
};
let tls_cert = args.remove("--tls-cert");
match protocol {
"tcp" => {
// TODO(@ohsayan): warn!
ClientConfigKind::Tcp(host.into(), port)
}
"tls" => {
// we need a TLS cert
match tls_cert {
Some(path) => {
let cert = fs::read_to_string(path)?;
ClientConfigKind::Tls(host.into(), port, cert)
}
None => {
return Err(CliError::ArgsErr(format!(
"must provide TLS cert when using TLS endpoint"
)))
}
}
}
_ => {
return Err(CliError::ArgsErr(format!(
"unknown protocol scheme `{protocol}`"
)))
}
}
}
};
let username = match args.remove("--user") {
Some(u) => u,
None => {
// default
"root".into()
}
};
let password = match args.remove("--password") {
Some(p) => p,
None => read_password("Enter password: ")?,
};
if args.is_empty() {
Ok(Task::OpenShell(ClientConfig::new(
endpoint, username, password,
)))
} else {
Err(CliError::ArgsErr(format!("found unknown arguments")))
}
}
fn read_password(prompt: &str) -> Result<String, std::io::Error> {
terminal::enable_raw_mode()?;
print!("{prompt}");
io::stdout().flush()?;
let mut password = String::new();
loop {
match event::read()? {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: event::KeyModifiers::CONTROL,
..
}) => {
terminal::disable_raw_mode()?;
println!();
exit(0x00)
}
Event::Key(KeyEvent { code, .. }) => match code {
KeyCode::Backspace => {
let _ = password.pop();
}
KeyCode::Char(c) => password.push(c),
KeyCode::Enter => break,
_ => {}
},
_ => {}
}
}
terminal::disable_raw_mode()?;
println!();
Ok(password)
}

@ -0,0 +1,73 @@
/*
* Created on Wed Nov 15 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use core::fmt;
pub type CliResult<T> = Result<T, CliError>;
#[derive(Debug)]
pub enum CliError {
QueryError(String),
ArgsErr(String),
ClientError(skytable::error::Error),
IoError(std::io::Error),
}
impl From<libsky::ArgParseError> for CliError {
fn from(e: libsky::ArgParseError) -> Self {
match e {
libsky::ArgParseError::Duplicate(d) => {
Self::ArgsErr(format!("duplicate value for `{d}`"))
}
libsky::ArgParseError::MissingValue(m) => {
Self::ArgsErr(format!("missing value for `{m}`"))
}
}
}
}
impl From<skytable::error::Error> for CliError {
fn from(cle: skytable::error::Error) -> Self {
Self::ClientError(cle)
}
}
impl From<std::io::Error> for CliError {
fn from(ioe: std::io::Error) -> Self {
Self::IoError(ioe)
}
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ArgsErr(e) => write!(f, "incorrect arguments. {e}"),
Self::ClientError(e) => write!(f, "client error. {e}"),
Self::IoError(e) => write!(f, "i/o error. {e}"),
Self::QueryError(e) => write!(f, "invalid query. {e}"),
}
}
}

@ -1,122 +0,0 @@
/*
* Created on Wed Nov 03 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
macro_rules! write_str {
($st:ident) => {
println!("\"{}\"", $st)
};
($idx:ident, $st:ident) => {
println!("({}) \"{}\"", $idx, $st)
};
}
macro_rules! write_binstr {
($st:ident) => {
println!("{}", BinaryData($st))
};
($idx:ident, $st:ident) => {
println!("({}) {}", $idx, BinaryData($st))
};
}
macro_rules! write_int {
($int:ident) => {
println!("{}", $int)
};
($idx:ident, $st:ident) => {
println!("({}) \"{}\"", $idx, $st)
};
}
macro_rules! write_err {
($idx:expr, $err:ident) => {
err!(if let Some(idx) = $idx {
format!("({}) ({})\n", idx, $err)
} else {
format!("({})\n", $err)
})
};
($idx:ident, $err:literal) => {
err!(
(if let Some(idx) = $idx {
format!("({}) ({})\n", idx, $err)
} else {
format!("({})\n", $err)
})
)
};
}
macro_rules! write_okay {
() => {
crossterm::execute!(
std::io::stdout(),
SetForegroundColor(Color::Cyan),
Print("(Okay)\n".to_string()),
ResetColor
)
.expect("Failed to write to stdout")
};
($idx:ident) => {
crossterm::execute!(
std::io::stdout(),
SetForegroundColor(Color::Cyan),
Print(format!("({}) (Okay)\n", $idx)),
ResetColor
)
.expect("Failed to write to stdout")
};
}
macro_rules! err {
($input:expr) => {
crossterm::execute!(
std::io::stdout(),
::crossterm::style::SetForegroundColor(::crossterm::style::Color::Red),
::crossterm::style::Print($input),
::crossterm::style::ResetColor
)
.expect("Failed to write to stdout")
};
}
macro_rules! eskysh {
($e:expr) => {
err!(format!("[SKYSH ERROR] {}\n", $e))
};
}
macro_rules! fatal {
($e:expr) => {{
err!($e);
::std::process::exit(0x01);
}};
($e:expr, $desc:expr) => {{
err!(format!($e, $desc));
println!();
::std::process::exit(0x01)
}};
}

@ -1,5 +1,5 @@
/*
* Created on Wed Jul 01 2020
* Created on Wed Nov 15 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
@ -7,7 +7,7 @@
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -24,22 +24,32 @@
*
*/
#![deny(unused_crate_dependencies)]
#![deny(unused_imports)]
macro_rules! fatal {
($($arg:tt)*) => {{
eprintln!($($arg)*);
std::process::exit(0x01);
}}
}
mod args;
mod error;
mod query;
mod repl;
mod resp;
#[macro_use]
mod macros;
mod argparse;
mod cli;
mod runner;
mod tokenizer;
use args::Task;
// tests
#[cfg(test)]
mod tests;
fn main() {
match run() {
Ok(()) => {}
Err(e) => fatal!("cli error: {e}"),
}
}
#[tokio::main]
async fn main() {
argparse::start_repl().await;
println!("Goodbye!");
fn run() -> error::CliResult<()> {
match args::parse()? {
Task::HelpMessage(msg) => println!("{msg}"),
Task::OpenShell(cfg) => repl::start(cfg)?,
}
Ok(())
}

@ -0,0 +1,295 @@
/*
* Created on Thu Nov 16 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::error::{CliError, CliResult},
skytable::{
error::ClientResult, query::SQParam, response::Response, Connection, ConnectionTls, Query,
},
};
pub trait IsConnection {
fn execute_query(&mut self, q: Query) -> ClientResult<Response>;
}
impl IsConnection for Connection {
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
self.query(&q)
}
}
impl IsConnection for ConnectionTls {
fn execute_query(&mut self, q: Query) -> ClientResult<Response> {
self.query(&q)
}
}
#[derive(Debug, PartialEq)]
enum Item {
UInt(u64),
SInt(i64),
Float(f64),
String(String),
Bin(Vec<u8>),
}
impl SQParam for Item {
fn append_param(self, buf: &mut Vec<u8>) {
match self {
Item::UInt(u) => u.append_param(buf),
Item::SInt(s) => s.append_param(buf),
Item::Float(f) => f.append_param(buf),
Item::String(s) => s.append_param(buf),
Item::Bin(b) => SQParam::append_param(&*b, buf),
}
}
}
pub struct Parameterizer {
buf: Vec<u8>,
i: usize,
params: Vec<Item>,
query: Vec<u8>,
}
#[derive(Debug, PartialEq)]
pub enum ExecKind {
Standard(Query),
UseSpace(Query, String),
UseNull(Query),
PrintSpecial(Query),
}
impl Parameterizer {
pub fn new(q: String) -> Self {
Self {
buf: q.into_bytes(),
i: 0,
params: vec![],
query: vec![],
}
}
pub fn parameterize(mut self) -> CliResult<ExecKind> {
while self.not_eof() {
match self.buf[self.i] {
b if b.is_ascii_alphabetic() || b == b'_' => self.read_ident(),
b if b.is_ascii_digit() => self.read_unsigned_integer(),
b'-' => self.read_signed_integer(),
quote_style @ (b'"' | b'\'') => {
self.i += 1;
self.read_string(quote_style)
}
b'`' => {
self.i += 1;
self.read_binary()
}
sym => {
self.i += 1;
Vec::push(&mut self.query, sym);
Ok(())
}
}?
}
match String::from_utf8(self.query) {
Ok(qstr) => {
let mut q = Query::new(&qstr);
self.params.into_iter().for_each(|p| {
q.push_param(p);
});
Ok(if qstr.eq_ignore_ascii_case("use null") {
ExecKind::UseNull(q)
} else {
if qstr.len() > 8 {
let qstr = &qstr[..8];
if qstr.eq_ignore_ascii_case("inspect ") {
return Ok(ExecKind::PrintSpecial(q));
}
}
let mut splits = qstr.split_ascii_whitespace();
let tok_use = splits.next();
let tok_name = splits.next();
match (tok_use, tok_name) {
(Some(tok_use), Some(tok_name))
if tok_use.eq_ignore_ascii_case("use")
&& !tok_name.eq_ignore_ascii_case("$current") =>
{
ExecKind::UseSpace(q, tok_name.into())
}
_ => ExecKind::Standard(q),
}
})
}
Err(_) => Err(CliError::QueryError("query is not valid UTF-8".into())),
}
}
fn read_string(&mut self, quote_style: u8) -> CliResult<()> {
self.query.push(b'?');
let mut string = Vec::new();
let mut terminated = false;
while self.not_eof() && !terminated {
let b = self.buf[self.i];
if b == b'\\' {
self.i += 1;
// escape sequence
if self.i == self.buf.len() {
// string was not terminated
return Err(CliError::QueryError("string not terminated".into()));
}
match self.buf[self.i] {
b'\\' => {
// escaped \
string.push(b'\\');
}
b if b == quote_style => {
// escape quote
string.push(quote_style);
}
_ => return Err(CliError::QueryError("unknown escape sequence".into())),
}
}
if b == quote_style {
terminated = true;
} else {
string.push(b);
}
self.i += 1;
}
if terminated {
match String::from_utf8(string) {
Ok(s) => self.params.push(Item::String(s)),
Err(_) => return Err(CliError::QueryError("invalid UTF-8 string".into())),
}
Ok(())
} else {
return Err(CliError::QueryError("string not terminated".into()));
}
}
fn read_ident(&mut self) -> CliResult<()> {
// we're looking at an ident
let start = self.i;
self.i += 1;
while self.not_eof() {
if self.buf[self.i].is_ascii_alphanumeric() || self.buf[self.i] == b'_' {
self.i += 1;
} else {
break;
}
}
let stop = self.i;
self.query.extend(&self.buf[start..stop]);
Ok(())
}
fn read_float(&mut self, start: usize) -> CliResult<()> {
self.read_until_number_escape();
let stop = self.i;
match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) {
Ok(Ok(num)) => self.params.push(Item::Float(num)),
_ => {
return Err(CliError::QueryError(
"invalid floating point literal".into(),
))
}
}
Ok(())
}
fn read_signed_integer(&mut self) -> CliResult<()> {
self.query.push(b'?');
// we must have encountered a `-`
let start = self.i;
self.read_until_number_escape();
let stop = self.i;
match core::str::from_utf8(&self.buf[start..stop]).map(|v| v.parse()) {
Ok(Ok(s)) => self.params.push(Item::SInt(s)),
_ => {
return Err(CliError::QueryError(
"invalid signed integer literal".into(),
))
}
}
Ok(())
}
fn read_unsigned_integer(&mut self) -> CliResult<()> {
self.query.push(b'?');
let start = self.i;
let mut ret = 0u64;
while self.not_eof() {
match self.buf[self.i] {
b if b.is_ascii_digit() => {
self.i += 1;
ret = match ret
.checked_mul(10)
.map(|v| v.checked_add((b & 0x0f) as u64))
{
Some(Some(r)) => r,
_ => return Err(CliError::QueryError("bad value for integer".into())),
};
}
b'.' => {
self.i += 1;
// uh oh, that's a float
return self.read_float(start);
}
b if b == b' ' || b == b'\t' || b.is_ascii_punctuation() => {
break;
}
_ => {
// nothing else is valid here
return Err(CliError::QueryError(
"invalid unsigned integer literal".into(),
));
}
}
}
self.params.push(Item::UInt(ret));
Ok(())
}
fn read_until_number_escape(&mut self) {
while self.not_eof() {
let b = self.buf[self.i];
if b == b'\n' || b == b'\t' || b.is_ascii_punctuation() {
break;
}
self.i += 1;
}
}
fn read_binary(&mut self) -> CliResult<()> {
self.query.push(b'?');
let start = self.i;
while self.not_eof() {
let b = self.buf[self.i];
self.i += 1;
if b == b'`' {
self.params
.push(Item::Bin(self.buf[start..self.i].to_vec()));
return Ok(());
}
}
Err(CliError::QueryError("binary literal not terminated".into()))
}
fn not_eof(&self) -> bool {
self.i < self.buf.len()
}
}

@ -0,0 +1,163 @@
/*
* Created on Thu Nov 16 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::query::ExecKind;
use {
crate::{
args::{ClientConfig, ClientConfigKind},
error::{CliError, CliResult},
query::{self, IsConnection},
resp,
},
crossterm::{cursor, execute, terminal},
rustyline::{config::Configurer, error::ReadlineError, DefaultEditor},
skytable::Config,
std::io::{stdout, ErrorKind},
};
const SKYSH_HISTORY_FILE: &str = ".sky_history";
const TXT_WELCOME: &str = include_str!("../help_text/welcome");
pub fn start(cfg: ClientConfig) -> CliResult<()> {
match cfg.kind {
ClientConfigKind::Tcp(host, port) => {
let c = Config::new(&host, port, &cfg.username, &cfg.password).connect()?;
println!(
"Authenticated as '{}' on {}:{} over Skyhash/TCP\n---",
&cfg.username, &host, &port
);
repl(c)
}
ClientConfigKind::Tls(host, port, cert) => {
let c = Config::new(&host, port, &cfg.username, &cfg.password).connect_tls(&cert)?;
println!(
"Authenticated as '{}' on {}:{} over Skyhash/TLS\n---",
&cfg.username, &host, &port
);
repl(c)
}
}
}
fn repl<C: IsConnection>(mut con: C) -> CliResult<()> {
let init_editor = || {
let mut editor = DefaultEditor::new()?;
editor.set_auto_add_history(true);
editor.set_history_ignore_dups(true)?;
editor.bind_sequence(
rustyline::KeyEvent(
rustyline::KeyCode::BracketedPasteStart,
rustyline::Modifiers::NONE,
),
rustyline::Cmd::Noop,
);
match editor.load_history(SKYSH_HISTORY_FILE) {
Ok(()) => {}
Err(e) => match e {
ReadlineError::Io(ref ioe) => match ioe.kind() {
ErrorKind::NotFound => {
println!("{TXT_WELCOME}");
}
_ => return Err(e),
},
e => return Err(e),
},
}
rustyline::Result::Ok(editor)
};
let mut editor = match init_editor() {
Ok(e) => e,
Err(e) => fatal!("error: failed to init REPL. {e}"),
};
let mut prompt = "> ".to_owned();
loop {
match editor.readline(&prompt) {
Ok(line) => match line.as_str() {
"!help" => println!("{TXT_WELCOME}"),
"exit" => break,
"clear" => clear_screen()?,
_ => {
if line.is_empty() {
continue;
}
match query::Parameterizer::new(line).parameterize() {
Ok(q) => {
let mut new_prompt = None;
let mut special = false;
let q = match q {
ExecKind::Standard(q) => q,
ExecKind::UseNull(q) => {
new_prompt = Some("> ".into());
q
}
ExecKind::UseSpace(q, space) => {
new_prompt = Some(format!("{space}> "));
q
}
ExecKind::PrintSpecial(q) => {
special = true;
q
}
};
if resp::format_response(con.execute_query(q)?, special) {
if let Some(pr) = new_prompt {
prompt = pr;
}
}
}
Err(e) => match e {
CliError::QueryError(e) => {
eprintln!("[skysh error]: bad query. {e}");
continue;
}
_ => return Err(e),
},
};
}
},
Err(e) => match e {
ReadlineError::Interrupted | ReadlineError::Eof => {
// done
break;
}
ReadlineError::WindowResized => {}
e => fatal!("error: failed to read line REPL. {e}"),
},
}
}
editor
.save_history(SKYSH_HISTORY_FILE)
.expect("failed to save history");
println!("Goodbye!");
Ok(())
}
fn clear_screen() -> std::io::Result<()> {
let mut stdout = stdout();
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
execute!(stdout, cursor::MoveTo(0, 0))
}

@ -0,0 +1,134 @@
/*
* Created on Thu Nov 16 2023
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2023, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crossterm::style::Stylize,
skytable::response::{Response, Row, Value},
};
pub fn format_response(resp: Response, print_special: bool) -> bool {
match resp {
Response::Empty => println!("{}", "(Okay)".cyan()),
Response::Error(e) => {
println!("{}", format!("(server error code: {e})").red());
return false;
}
Response::Value(v) => {
print_value(v, print_special);
println!();
}
Response::Row(r) => {
print_row(r);
println!();
}
Response::Rows(rows) => {
if rows.is_empty() {
println!("{}", "[0 rows returned]".grey().italic());
} else {
for (i, row) in rows.into_iter().enumerate().map(|(i, r)| (i + 1, r)) {
print!("{} ", format!("({i})").grey().bold());
print_row(row);
println!();
}
}
}
};
true
}
fn print_row(r: Row) {
print!("(");
let mut columns = r.into_values().into_iter().peekable();
while let Some(cell) = columns.next() {
print_value(cell, false);
if columns.peek().is_some() {
print!(", ");
}
}
print!(")");
}
fn print_value(v: Value, print_special: bool) {
match v {
Value::Null => print!("{}", "null".grey().italic()),
Value::String(s) => print_string(&s, print_special),
Value::Binary(b) => print_binary(&b),
Value::Bool(b) => print!("{b}"),
Value::UInt8(i) => print!("{i}"),
Value::UInt16(i) => print!("{i}"),
Value::UInt32(i) => print!("{i}"),
Value::UInt64(i) => print!("{i}"),
Value::SInt8(i) => print!("{i}"),
Value::SInt16(i) => print!("{i}"),
Value::SInt32(i) => print!("{i}"),
Value::SInt64(i) => print!("{i}"),
Value::Float32(f) => print!("{f}"),
Value::Float64(f) => print!("{f}"),
Value::List(items) => {
print!("[");
let mut items = items.into_iter().peekable();
while let Some(item) = items.next() {
print_value(item, print_special);
if items.peek().is_some() {
print!(", ");
}
}
print!("]");
}
}
}
fn print_binary(b: &[u8]) {
let mut it = b.into_iter().peekable();
print!("[");
while let Some(byte) = it.next() {
print!("{byte}");
if it.peek().is_some() {
print!(", ");
}
}
print!("]");
}
fn print_string(s: &str, print_special: bool) {
if print_special {
print!("{}", s.italic().grey());
} else {
print!("\"");
for ch in s.chars() {
if ch == '"' {
print!("\\{ch}");
} else if ch == '\t' {
print!("\\t");
} else if ch == '\n' {
print!("\\n");
} else {
print!("{ch}");
}
}
print!("\"");
}
}

@ -1,262 +0,0 @@
/*
* Created on Wed May 12 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::tokenizer,
core::fmt,
crossterm::style::{Color, Print, ResetColor, SetForegroundColor},
skytable::{
aio, error::Error, types::Array, types::FlatElement, Element, Pipeline, Query, RespCode,
},
};
type SkyResult<T> = Result<T, Error>;
pub enum Runner {
Insecure(aio::Connection),
Secure(aio::TlsConnection),
}
impl Runner {
pub async fn new_insecure(host: &str, port: u16) -> SkyResult<Self> {
let con = aio::Connection::new(host, port).await?;
Ok(Self::Insecure(con))
}
pub async fn new_secure(host: &str, port: u16, cert: &str) -> SkyResult<Self> {
let con = aio::TlsConnection::new(host, port, cert).await?;
Ok(Self::Secure(con))
}
pub async fn run_pipeline(&mut self, pipeline: Pipeline) {
let ret = match self {
Self::Insecure(con) => con.run_pipeline(pipeline).await,
Self::Secure(con) => con.run_pipeline(pipeline).await,
};
let retok = match ret {
Ok(r) => r,
Err(e) => fatal!("An I/O error occurred while querying: {}", e),
};
for (idx, resp) in retok
.into_iter()
.enumerate()
.map(|(idx, resp)| (idx + 1, resp))
{
println!("[Response {}]", idx);
print_element(resp);
}
}
pub async fn run_query(&mut self, unescaped: &str) {
let query: Query = match tokenizer::get_query(unescaped.as_bytes()) {
Ok(q) => q,
Err(e) => {
err!(format!("[Syntax Error: {}]\n", e));
return;
}
};
let ret = match self {
Self::Insecure(con) => con.run_query_raw(&query).await,
Self::Secure(con) => con.run_query_raw(&query).await,
};
match ret {
Ok(resp) => print_element(resp),
Err(e) => fatal!("An I/O error occurred while querying: {}", e),
}
}
pub async fn check_entity(&mut self, blank: &mut String, prompt: &mut String) {
let query: Query = tokenizer::get_query(b"whereami").unwrap();
let ret = match self {
Self::Insecure(con) => con.run_query_raw(&query).await,
Self::Secure(con) => con.run_query_raw(&query).await,
};
let ret = match ret {
Ok(resp) => resp,
Err(e) => fatal!("An I/O error occurred while querying: {}", e),
};
match ret {
Element::Array(Array::NonNullStr(srr)) => match srr.len() {
1 => {
*blank = format!(" {blank}> ", blank = " ".repeat(srr[0].len()));
*prompt = format!("skysh@{ks}> ", ks = srr[0]);
}
2 => {
let ks = &srr[0];
let tbl = &srr[1];
*blank = format!(
" {blank}> ",
blank = " ".repeat(ks.len() + tbl.len() + 1)
);
*prompt = format!("skysh@{ks}:{tbl}> ", ks = ks, tbl = tbl);
}
count => fatal!(
"The server returned {} IDs while checking entity state",
count
),
},
_ => fatal!("The server returned the wrong data type for entity state check"),
}
}
}
fn print_float(float: f32, idx: Option<usize>) {
if let Some(idx) = idx {
println!("({idx}) {float}")
} else {
println!("{float}");
}
}
fn print_element(el: Element) {
match el {
Element::String(st) => write_str!(st),
Element::Binstr(st) => write_binstr!(st),
Element::Array(Array::Bin(brr)) => print_bin_array(brr),
Element::Array(Array::Str(srr)) => print_str_array(srr),
Element::RespCode(r) => print_rcode(r, None),
Element::UnsignedInt(int) => write_int!(int),
Element::Array(Array::Flat(frr)) => write_flat_array(frr),
Element::Array(Array::Recursive(a)) => print_array(a),
Element::Array(Array::NonNullBin(nbrr)) => print_array_nonnull_bin(nbrr),
Element::Array(Array::NonNullStr(nsrr)) => print_array_nonnull_str(nsrr),
Element::Float(float) => print_float(float, None),
_ => eskysh!("The server possibly sent a newer data type that we can't parse"),
}
}
fn print_rcode(rcode: RespCode, idx: Option<usize>) {
match rcode {
RespCode::Okay => write_okay!(),
RespCode::ActionError => write_err!(idx, "Action Error"),
RespCode::ErrorString(st) => write_err!(idx, st),
RespCode::OtherError => write_err!(idx, "Other Error"),
RespCode::NotFound => write_err!(idx, "Not Found"),
RespCode::OverwriteError => write_err!(idx, "Overwrite Error"),
RespCode::PacketError => write_err!(idx, "Packet Error"),
RespCode::ServerError => write_err!(idx, "Server Error"),
RespCode::UnknownDataType => write_err!(idx, "Unknown data type"),
RespCode::EncodingError => write_err!(idx, "Encoding error"),
RespCode::AuthBadCredentials => write_err!(idx, "auth bad credentials"),
RespCode::AuthPermissionError => write_err!(idx, "auth permission error"),
_ => write_err!(idx, "Unknown error"),
}
}
fn print_bin_array(bin_array: Vec<Option<Vec<u8>>>) {
bin_array.into_iter().enumerate().for_each(|(idx, elem)| {
let idx = idx + 1;
match elem {
Some(ele) => {
write_binstr!(idx, ele);
}
None => print_rcode(RespCode::NotFound, Some(idx)),
}
})
}
fn print_str_array(str_array: Vec<Option<String>>) {
str_array.into_iter().enumerate().for_each(|(idx, elem)| {
let idx = idx + 1;
match elem {
Some(ele) => {
write_str!(idx, ele);
}
None => print_rcode(RespCode::NotFound, Some(idx)),
}
})
}
fn print_array_nonnull_str(str_array: Vec<String>) {
str_array.into_iter().enumerate().for_each(|(idx, elem)| {
let idx = idx + 1;
write_str!(idx, elem)
})
}
fn print_array_nonnull_bin(str_array: Vec<Vec<u8>>) {
str_array.into_iter().enumerate().for_each(|(idx, elem)| {
let idx = idx + 1;
write_binstr!(idx, elem)
})
}
fn write_flat_array(flat_array: Vec<FlatElement>) {
for (idx, item) in flat_array.into_iter().enumerate() {
let idx = idx + 1;
match item {
FlatElement::String(st) => write_str!(idx, st),
FlatElement::Binstr(st) => {
write_binstr!(idx, st)
}
FlatElement::RespCode(rc) => print_rcode(rc, Some(idx)),
FlatElement::UnsignedInt(int) => write_int!(int, idx),
_ => eskysh!("Element typed cannot yet be parsed"),
}
}
}
fn print_array(array: Vec<Element>) {
for (idx, item) in array.into_iter().enumerate() {
let idx = idx + 1;
match item {
Element::String(st) => write_str!(idx, st),
Element::RespCode(rc) => print_rcode(rc, Some(idx)),
Element::UnsignedInt(int) => write_int!(idx, int),
Element::Array(Array::Bin(brr)) => print_bin_array(brr),
Element::Array(Array::Str(srr)) => print_str_array(srr),
_ => eskysh!("Nested arrays cannot be printed just yet"),
}
}
}
pub struct BinaryData(Vec<u8>);
impl fmt::Display for BinaryData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "b\"")?;
for b in self.0.iter() {
let b = *b;
// See this: https://doc.rust-lang.org/reference/tokens.html#byte-escapes
// this idea was borrowed from the Bytes crate
#[allow(clippy::manual_range_contains)]
if b == b'\n' {
write!(f, "\\n")?;
} else if b == b'\r' {
write!(f, "\\r")?;
} else if b == b'\t' {
write!(f, "\\t")?;
} else if b == b'\\' || b == b'"' {
write!(f, "\\{}", b as char)?;
} else if b == b'\0' {
write!(f, "\\0")?;
// ASCII printable
} else if b >= 0x20 && b < 0x7f {
write!(f, "{}", b as char)?;
} else {
write!(f, "\\x{:02x}", b)?;
}
}
write!(f, "\"")?;
Ok(())
}
}

@ -1,196 +0,0 @@
/*
* Created on Sun Oct 10 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::tokenizer::{get_query, TokenizerError};
fn query_from(input: &[u8]) -> Result<Vec<String>, TokenizerError> {
get_query(input)
}
#[test]
fn test_basic_tokenization() {
let input = "set x 100".as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec!["set".to_owned(), "x".to_owned(), "100".to_owned()]
);
}
#[test]
fn test_single_quote_tokens() {
let input = "set 'x with a whitespace' 100".as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec![
"set".to_owned(),
"x with a whitespace".to_owned(),
"100".to_owned()
]
);
}
#[test]
fn test_double_quote_tokens() {
let input = r#"set "x with a whitespace" 100"#.as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec![
"set".to_owned(),
"x with a whitespace".to_owned(),
"100".to_owned()
]
);
}
#[test]
fn test_single_and_double_quote_tokens() {
let input = r#"set "x with a whitespace" 'y with a whitespace'"#.as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec![
"set".to_owned(),
"x with a whitespace".to_owned(),
"y with a whitespace".to_owned()
]
);
}
#[test]
fn test_multiple_single_quote_tokens() {
let input = r#"'set' 'x with a whitespace' 'y with a whitespace'"#.as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec![
"set".to_owned(),
"x with a whitespace".to_owned(),
"y with a whitespace".to_owned()
]
);
}
#[test]
fn test_multiple_double_quote_tokens() {
let input = r#""set" "x with a whitespace" "y with a whitespace""#.as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec![
"set".to_owned(),
"x with a whitespace".to_owned(),
"y with a whitespace".to_owned()
]
);
}
#[test]
fn test_missing_single_quote() {
let input = r#"'get' 'x with a whitespace"#.as_bytes();
let ret = format!("{}", query_from(input).unwrap_err());
assert_eq!(ret, "mismatched quotes near end of: `x with a whitespace`");
}
#[test]
fn test_missing_double_quote() {
let input = r#"'get' "x with a whitespace"#.as_bytes();
let ret = format!("{}", query_from(input).unwrap_err());
assert_eq!(ret, "mismatched quotes near end of: `x with a whitespace`");
}
#[test]
fn test_extra_whitespace() {
let input = "set x '100'".as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec!["set".to_owned(), "x".to_owned(), "100".to_owned()]
);
}
#[test]
fn test_singly_quoted() {
let input = "set tables' wth".as_bytes();
let ret = query_from(input).unwrap_err();
assert_eq!(ret, TokenizerError::ExpectedWhitespace("tables".to_owned()));
}
#[test]
fn test_text_after_quote_nospace() {
let input = "get 'rust'ferris".as_bytes();
let ret = query_from(input).unwrap_err();
assert_eq!(ret, TokenizerError::ExpectedWhitespace("rust'".to_owned()));
}
#[test]
fn test_text_after_double_quote_nospace() {
let input = r#"get "rust"ferris"#.as_bytes();
let ret = query_from(input).unwrap_err();
assert_eq!(ret, TokenizerError::ExpectedWhitespace("rust\"".to_owned()));
}
#[test]
fn test_inline_comment() {
let input = "set x 100 # sets x to 100".as_bytes();
let ret = query_from(input).unwrap();
assert_eq!(
ret,
vec!["set".to_owned(), "x".to_owned(), "100".to_owned()]
)
}
#[test]
fn test_full_comment() {
let input = "# what is going on?".as_bytes();
let ret = query_from(input).unwrap();
assert!(ret.is_empty());
}
#[test]
fn test_ignore_comment() {
let input = "set x \"# ooh la la\"".as_bytes();
assert_eq!(
query_from(input).unwrap(),
vec!["set".to_owned(), "x".to_owned(), "# ooh la la".to_owned()]
);
let input = "set x \"#\"".as_bytes();
assert_eq!(
query_from(input).unwrap(),
vec!["set".to_owned(), "x".to_owned(), "#".to_owned()]
);
}
#[test]
fn test_blueql_query() {
let input = b"create model mymodel(string, binary)";
assert_eq!(
query_from(input).unwrap(),
vec!["create model mymodel(string, binary)"]
);
}

@ -1,226 +0,0 @@
/*
* Created on Sat Oct 09 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! This module provides a simple way to avoid "the funk" with "funky input queries". It simply
//! tokenizes char-by-char analyzing quotes et al as required
//!
use {
core::fmt,
skytable::{types::RawString, Query},
std::collections::HashSet,
};
lazy_static::lazy_static! {
static ref BLUEQL_KW: HashSet<&'static [u8]> = {
let mut hs = HashSet::new();
hs.insert("create".as_bytes());
hs.insert(b"inspect");
hs.insert(b"drop");
hs.insert(b"use");
hs
};
}
#[derive(Debug, PartialEq, Eq)]
pub enum TokenizerError {
QuoteMismatch(String),
BacktickMismatch(String),
ExpectedWhitespace(String),
}
impl fmt::Display for TokenizerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::QuoteMismatch(expr) => write!(f, "mismatched quotes near end of: `{}`", expr),
Self::ExpectedWhitespace(expr) => {
write!(f, "expected whitespace near end of: `{}`", expr)
}
Self::BacktickMismatch(expr) => {
write!(f, "mismatched backticks near end of: `{}`", expr)
}
}
}
}
pub trait SequentialQuery {
fn push(&mut self, input: &[u8]);
fn new() -> Self;
fn is_empty(&self) -> bool;
}
// #[cfg(test)]
impl SequentialQuery for Vec<String> {
fn push(&mut self, input: &[u8]) {
Vec::push(self, String::from_utf8_lossy(input).to_string())
}
fn is_empty(&self) -> bool {
Vec::len(self) == 0
}
fn new() -> Self {
Vec::new()
}
}
impl SequentialQuery for Query {
fn push(&mut self, input: &[u8]) {
Query::push(self, RawString::from(input.to_owned()))
}
fn is_empty(&self) -> bool {
Query::len(self) == 0
}
fn new() -> Self {
Query::new()
}
}
// FIXME(@ohsayan): Fix this entire impl. At this point, it's almost like legacy code
pub fn get_query<T: SequentialQuery>(inp: &[u8]) -> Result<T, TokenizerError> {
assert!(!inp.is_empty(), "Input is empty");
let mut query = T::new();
let mut it = inp.iter().peekable();
macro_rules! pos {
() => {
inp.len() - it.len()
};
}
macro_rules! expect_whitespace {
($start:expr) => {
match it.peek() {
Some(b) => match **b {
b' ' => {}
_ => {
return Err(TokenizerError::ExpectedWhitespace(
String::from_utf8_lossy(&inp[$start..pos!()]).to_string(),
))
}
},
None => {}
}
};
}
// skip useless starting whitespace
while let Some(b' ') = it.next() {}
let end_of_first = match it.position(|x| *x == b' ') {
Some(e) => e + 1,
None if it.len() == 0 => inp.len(),
None => {
return Err(TokenizerError::ExpectedWhitespace(
String::from_utf8_lossy(inp).to_string(),
))
}
};
if BLUEQL_KW.contains(inp[..end_of_first].to_ascii_lowercase().as_slice()) {
query.push(inp);
return Ok(query);
} else {
it = inp.iter().peekable();
}
'outer: while let Some(tok) = it.next() {
match tok {
b'\'' => {
// hmm, quotes; let's see where it ends
let pos = pos!();
let qidx = it.position(|x| *x == b'\'');
match qidx {
Some(idx) => query.push(&inp[pos..idx + pos]),
None => {
let end = pos!();
return Err(TokenizerError::QuoteMismatch(
String::from_utf8_lossy(&inp[pos..end]).to_string(),
));
}
}
expect_whitespace!(pos);
}
b'"' => {
// hmm, quotes; let's see where it ends
let pos = pos!();
let qidx = it.position(|x| *x == b'"');
match qidx {
Some(idx) => query.push(&inp[pos..idx + pos]),
None => {
let end = pos!();
return Err(TokenizerError::QuoteMismatch(
String::from_utf8_lossy(&inp[pos..end]).to_string(),
));
}
}
expect_whitespace!(pos);
}
b'`' => {
// hmm, backtick? let's look for the end
let pos = pos!();
let qidx = it.position(|x| *x == b'`');
match qidx {
Some(idx) => query.push(&inp[pos..idx + pos]),
None => {
let end = pos!();
return Err(TokenizerError::BacktickMismatch(
String::from_utf8_lossy(&inp[pos..end]).to_string(),
));
}
}
expect_whitespace!(pos);
}
b' ' => {
// this just prevents control from being handed to the wildcard
continue;
}
b'#' => {
// so this is an inline comment; skip until newline
let _ = it.position(|x| *x == b'\n');
}
_ => {
let start = pos!() - 1;
let mut end = start;
// alpha? cool, go on
'inner: while let Some(tok) = it.peek() {
match **tok {
b' ' => {
it.next();
break 'inner;
}
b'\'' | b'"' => {
return Err(TokenizerError::ExpectedWhitespace(
String::from_utf8_lossy(&inp[start..pos!()]).to_string(),
))
}
b'#' => continue 'outer,
_ => {
end += 1;
it.next();
continue 'inner;
}
}
}
end += 1;
query.push(&inp[start..end]);
}
}
}
Ok(query)
}

@ -1,3 +0,0 @@
# This is a 'bad' configuration file since it contains an invalid port
[server]
port = 20033002

@ -1,8 +0,0 @@
# This is a bad toml file since it contains an invalid value of `every` in BGSAVE
[server]
host = "127.0.0.1"
port = 2003
[bgsave]
every = 0.5 # Not possible!
enabled = true

@ -1,6 +0,0 @@
[server]
host = "127.0.0.1"
port = 2003
[bgsave]
enabled = true

@ -1,6 +0,0 @@
[server]
host = "127.0.0.1"
port = 2003
[bgsave]
every = 600

@ -1,5 +0,0 @@
# this is the default configuration file for Docker images. Modify it as required
[server]
host = "0.0.0.0"
port = 2003
noart = true

@ -0,0 +1,14 @@
# this is the default configuration for the Debian package. Modify to use your own settings.
system:
mode: prod
rs_window: 300
auth:
plugin: pwd
# replace with your root password of choice
root_pass: rootpass
endpoints:
insecure:
host: 127.0.0.1
port: 2003

@ -1,4 +0,0 @@
# This makes use of an IPv6 address
[server]
host = "::1"
port = 2003

@ -1,4 +0,0 @@
[server]
host = '127.0.0.1' # i.e localhost
port = 2003 # The port to bind to
noart = true # No terminal artwork

@ -1,5 +0,0 @@
# This is a 'good' configuration file since it contains a valid port
# and appropriate keys
[server]
host = '127.0.0.1'
port = 2003 # Set the server's port to 2003

@ -1,17 +0,0 @@
[server]
host = "127.0.0.1" # The IP address to which you want sdb to bind to
port = 2003 # The port to which you want sdb to bind to
# Set `noart` to true if you want to disable terminal artwork
noart = false
[bgsave]
# Run `BGSAVE` `every` seconds. For example, setting this to 60 will cause BGSAVE to run
# after every 2 minutes
enabled = true
every = 120
[snapshot]
# Create a snapshot every hour (1 hour = 60 minutes = 60 * 60 seconds = 3600 seconds)
every = 3600
# How many of the snapshots to keep
atmost = 4 # keep the four most recent snapshots

@ -1,17 +0,0 @@
[server]
host = "127.0.0.1"
port = 2003
noart = false
[ssl]
key = "/path/to/keyfile.pem"
chain = "/path/to/chain.pem"
port = 2004
[bgsave]
enabled = true
every = 120
[snapshot]
every = 3600
atmost = 4

@ -1,40 +0,0 @@
# This is a complete sdb configuration template which is always kept updated
# to include all the configuration options. I encourage you to always use this
# when you use a configuration file
# Instead of deleting entire sections from this file, comment them out, so that you
# now what you've kept enabled and what you've kept disabled. This helps avoid
# configuration problems during production
# This is a *REQUIRED* key
[server]
host = "127.0.0.1" # The IP address to which you want sdb to bind to
port = 2003 # The port to which you want sdb to bind to
noart = false # Set `noart` to true if you want to disable terminal artwork
maxcon = 50000 # set the maximum number of clients that the server can accept
mode = "dev" # Set this to `prod` when you're running in production and `dev` when in development
# This is an optional key
[auth]
# the origin key to be used to claim the root account
origin_key = "4527387f92a381cbe804593f33991d327d456a97"
# This key is *OPTIONAL*
[bgsave]
# Run `BGSAVE` `every` seconds. For example, setting this to 60 will cause BGSAVE to run
# after every 2 minutes
enabled = true
every = 120
# This key is *OPTIONAL*
[snapshot]
every = 3600 # Make a snapshot after every 1 hour (60min * 60sec= 3600secs)
atmost = 4 # Keep the 4 most recent snapshots
failsafe = true # stops accepting writes if snapshotting fails
# This key is *OPTIONAL*, used for TLS/SSL config
[ssl]
key = "/path/to/keyfile.pem"
chain = "/path/to/chain.pem"
port = 2004
only = true # optional to enable SSL-only requests
passin = "/path/to/cert/passphrase.txt" # optional to programmatically verify the TLS cert

@ -0,0 +1,22 @@
system:
mode: prod
rs_window: 600
auth:
plugin: pwd
# replace with your root password of choice
root_pass: password
endpoints:
secure:
host: 127.0.0.1
port: 2004
# replace `cert` with the path to your self-signed certificate
cert: cert.pem
# replace `private_key` with the path to your private key
private_key: private.key
# replace `passphrase.txt` with the path to your private key passphrase
pkey_passphrase: passphrase.txt
insecure:
host: 127.0.0.1
port: 2003

@ -1,7 +0,0 @@
[server]
host = "127.0.0.1"
port = 2003
[bgsave]
enabled = true
every = 600 # Every 10 minutes

@ -7,13 +7,10 @@ edition = "2021"
[dependencies]
# internal deps
skytable = { git = "https://github.com/skytable/client-rust.git", features = [
"sync",
], default-features = false }
libsky = { path = "../libsky" }
# external deps
env_logger = "0.10.0"
log = "0.4.17"
zip = { version = "0.6.3", features = ["deflate"] }
powershell_script = "1.0.4"
openssl = { version = "0.10.45", features = ["vendored"] }
env_logger = "0.10.1"
log = "0.4.20"
zip = { version = "0.6.6", features = ["deflate"] }
powershell_script = "1.1.0"
openssl = { version = "0.10.61", features = ["vendored"] }

@ -34,7 +34,7 @@ use {
};
/// The binaries that will be present in a bundle
pub const BINARIES: [&str; 4] = ["skyd", "sky-bench", "skysh", "sky-migrate"];
pub const BINARIES: [&str; 3] = ["skyd", "sky-bench", "skysh"];
/// The build mode
#[derive(Copy, Clone, PartialEq, Eq)]

@ -76,6 +76,18 @@ pub fn create_linuxpkg(package_type: LinuxPackageType) -> HarnessResult<()> {
LinuxPackageType::Deb => {
// install cargo-deb
util::handle_child("install cargo-deb", cmd!("cargo", "install", "cargo-deb"))?;
// make files executable
util::handle_child(
"make maintainer scripts executable",
cmd!(
"chmod",
"+x",
"pkg/debian/postinst",
"pkg/debian/preinst",
"pkg/debian/postrm",
"pkg/debian/prerm"
),
)?;
// assemble the command
let mut build_args = vec!["cargo".into(), "deb".to_owned()];
if let Some(t) = util::get_var(util::VAR_TARGET) {

@ -52,10 +52,13 @@ fn main() {
Builder::new()
.parse_filters(&env::var("SKYHARNESS_LOG").unwrap_or_else(|_| "info".to_owned()))
.init();
// avoid verbose logging
env::set_var("SKY_LOG", "error");
env::set_var("SKY_LOG", "trace");
if let Err(e) = runner() {
error!("harness failed with: {}", e);
error!("fetching logs from server processes");
for ret in test::get_children() {
ret.print_logs();
}
process::exit(0x01);
}
}

@ -35,8 +35,9 @@ use {
bn::{BigNum, MsbOption},
error::ErrorStack,
hash::MessageDigest,
pkey::{PKey, Private},
pkey::PKey,
rsa::Rsa,
symm::Cipher,
x509::{
extension::{BasicConstraints, KeyUsage, SubjectKeyIdentifier},
X509NameBuilder, X509,
@ -45,6 +46,7 @@ use {
std::{fs, io::Write},
};
mod svc;
pub use svc::get_children;
/// Run the test suite
pub fn run_test() -> HarnessResult<()> {
@ -89,16 +91,16 @@ fn append_target(args: &mut Vec<String>) {
/// - The standard test suite
/// - The persistence test suite
fn run_test_inner() -> HarnessResult<()> {
const TEST_PASSWORD: &str = "xCqe4yuVM7l2MnHZOFZDDieqjqmmL3qvO5LOEOhpXPE=";
// first create the TLS keys
info!("Creating TLS key+cert");
let (cert, pkey) = mk_ca_cert().expect("Failed to create cert");
let (cert, pkey) = mk_ca_cert(TEST_PASSWORD.as_bytes()).expect("Failed to create cert");
let mut passfile = fs::File::create("passphrase.txt").unwrap();
passfile.write_all(TEST_PASSWORD.as_bytes()).unwrap();
let mut certfile = fs::File::create("cert.pem").expect("failed to create cert.pem");
certfile.write_all(&cert.to_pem().unwrap()).unwrap();
certfile.write_all(&cert).unwrap();
let mut pkeyfile = fs::File::create("key.pem").expect("failed to create key.pem");
pkeyfile
.write_all(&pkey.private_key_to_pem_pkcs8().unwrap())
.unwrap();
pkeyfile.write_all(&pkey).unwrap();
// assemble commands
let target_folder = util::get_target_folder(BuildMode::Debug);
let mut standard_test_suite_args = vec!["cargo".to_owned(), "test".into()];
@ -110,15 +112,9 @@ fn run_test_inner() -> HarnessResult<()> {
];
append_target(&mut build_cmd_args);
append_target(&mut standard_test_suite_args);
let persist_test_suite_args = [
standard_test_suite_args.as_slice(),
&["--features".to_owned(), "persist-suite".into()],
]
.concat();
// get cmd
let build_cmd = util::assemble_command_from_slice(build_cmd_args);
let standard_test_suite = util::assemble_command_from_slice(standard_test_suite_args);
let persist_test_suite = util::assemble_command_from_slice(persist_test_suite_args);
// build skyd
info!("Building server binary ...");
@ -130,20 +126,11 @@ fn run_test_inner() -> HarnessResult<()> {
util::handle_child("standard test suite", standard_test_suite)?;
Ok(())
})?;
// run persistence tests; don't kill the servers because if either of the tests fail
// then we can ensure that they will be killed by `run_test`
svc::run_with_servers(&target_folder, false, move || {
info!("Running persistence test suite ...");
util::handle_child("standard test suite", persist_test_suite)?;
Ok(())
})?;
Ok(())
}
/// Generate certificates
fn mk_ca_cert() -> Result<(X509, PKey<Private>), ErrorStack> {
fn mk_ca_cert(password: &[u8]) -> Result<(Vec<u8>, Vec<u8>), ErrorStack> {
let rsa = Rsa::generate(2048)?;
let key_pair = PKey::from_rsa(rsa)?;
@ -182,9 +169,8 @@ fn mk_ca_cert() -> Result<(X509, PKey<Private>), ErrorStack> {
let subject_key_identifier =
SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?;
cert_builder.append_extension(subject_key_identifier)?;
cert_builder.sign(&key_pair, MessageDigest::sha256())?;
let cert = cert_builder.build();
let cert = cert_builder.build().to_pem().unwrap();
let key_pair = key_pair.private_key_to_pem_pkcs8_passphrase(Cipher::aes_256_cbc(), password)?;
Ok((cert, key_pair))
}

@ -26,35 +26,97 @@
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use {
crate::{
util::{self},
HarnessError, HarnessResult, ROOT_DIR,
},
skytable::{error::Error, Connection, SkyResult},
std::{
cell::RefCell,
io::ErrorKind,
path::Path,
process::{Child, Command},
process::{Child, Command, Output, Stdio},
},
};
thread_local! {
static CHILDREN: RefCell<Vec<(&'static str, Child)>> = RefCell::default();
}
pub struct ChildStatus {
id: &'static str,
stdout: String,
stderr: String,
exit_code: i32,
}
impl ChildStatus {
pub fn new(id: &'static str, stdout: String, stderr: String, exit_code: i32) -> Self {
Self {
id,
stdout,
stderr,
exit_code,
}
}
pub fn print_logs(&self) {
println!(
"######################### LOGS FROM {} #########################",
self.id
);
println!("-> exit code: `{}`", self.exit_code);
if !self.stdout.is_empty() {
println!("+++++++++++++++++++++ STDOUT +++++++++++++++++++++");
println!("{}", self.stdout);
println!("++++++++++++++++++++++++++++++++++++++++++++++++++");
}
if !self.stderr.is_empty() {
println!("+++++++++++++++++++++ STDERR +++++++++++++++++++++");
println!("{}", self.stderr);
println!("++++++++++++++++++++++++++++++++++++++++++++++++++");
}
println!("######################### ############ #########################");
}
}
pub fn get_children() -> Vec<ChildStatus> {
CHILDREN.with(|c| {
let mut ret = vec![];
for (name, child) in c.borrow_mut().drain(..) {
let Output {
status,
stdout,
stderr,
} = child.wait_with_output().unwrap();
ret.push(ChildStatus::new(
name,
String::from_utf8(stdout).unwrap(),
String::from_utf8(stderr).unwrap(),
status.code().unwrap(),
))
}
ret
})
}
#[cfg(windows)]
/// The powershell script hack to send CTRL+C using kernel32
const POWERSHELL_SCRIPT: &str = include_str!("../../../ci/windows/stop.ps1");
#[cfg(windows)]
/// Flag for new console Window
const CREATE_NEW_CONSOLE: u32 = 0x00000010;
pub(super) const SERVERS: [(&str, [u16; 2]); 3] = [
("server1", [2003, 2004]),
("server2", [2005, 2006]),
("server3", [2007, 2008]),
];
pub(super) const SERVERS: [(&str, [u16; 2]); 1] = [("server1", [2003, 2004])];
/// The test suite server host
const TESTSUITE_SERVER_HOST: &str = "127.0.0.1";
/// The workspace root
const WORKSPACE_ROOT: &str = env!("ROOT_DIR");
fn connect_db(host: &str, port: u16) -> std::io::Result<std::net::TcpStream> {
let tcp_stream = std::net::TcpStream::connect((host, port))?;
Ok(tcp_stream)
}
/// Get the command to start the provided server1
pub fn get_run_server_cmd(server_id: &'static str, target_folder: impl AsRef<Path>) -> Command {
let args = vec![
@ -63,11 +125,13 @@ pub fn get_run_server_cmd(server_id: &'static str, target_folder: impl AsRef<Pat
.to_string_lossy()
.to_string(),
// config
"--withconfig".to_owned(),
format!("{WORKSPACE_ROOT}ci/{server_id}.toml"),
"--config".to_owned(),
format!("{WORKSPACE_ROOT}ci/{server_id}.yaml"),
];
let mut cmd = util::assemble_command_from_slice(&args);
cmd.current_dir(server_id);
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::piped());
#[cfg(windows)]
cmd.creation_flags(CREATE_NEW_CONSOLE);
cmd
@ -94,10 +158,10 @@ pub(super) fn wait_for_server_exit() -> HarnessResult<()> {
Ok(())
}
fn connection_refused<T>(input: SkyResult<T>) -> HarnessResult<bool> {
fn connection_refused<T>(input: std::io::Result<T>) -> HarnessResult<bool> {
match input {
Ok(_) => Ok(false),
Err(Error::IoError(e))
Err(e)
if matches!(
e.kind(),
ErrorKind::ConnectionRefused | ErrorKind::ConnectionReset
@ -118,7 +182,7 @@ fn wait_for_startup() -> HarnessResult<()> {
for port in ports {
let connection_string = format!("{TESTSUITE_SERVER_HOST}:{port}");
let mut backoff = 1;
let mut con = Connection::new(TESTSUITE_SERVER_HOST, port);
let mut con = connect_db(TESTSUITE_SERVER_HOST, port);
while connection_refused(con)? {
if backoff > 64 {
// enough sleeping, return an error
@ -131,7 +195,7 @@ fn wait_for_startup() -> HarnessResult<()> {
"Server at {connection_string} not started. Sleeping for {backoff} second(s) ..."
);
util::sleep_sec(backoff);
con = Connection::new(TESTSUITE_SERVER_HOST, port);
con = connect_db(TESTSUITE_SERVER_HOST, port);
backoff *= 2;
}
info!("Server at {connection_string} has started");
@ -148,7 +212,7 @@ fn wait_for_shutdown() -> HarnessResult<()> {
for port in ports {
let connection_string = format!("{TESTSUITE_SERVER_HOST}:{port}");
let mut backoff = 1;
let mut con = Connection::new(TESTSUITE_SERVER_HOST, port);
let mut con = connect_db(TESTSUITE_SERVER_HOST, port);
while !connection_refused(con)? {
if backoff > 64 {
// enough sleeping, return an error
@ -161,7 +225,7 @@ fn wait_for_shutdown() -> HarnessResult<()> {
"Server at {connection_string} still active. Sleeping for {backoff} second(s) ..."
);
util::sleep_sec(backoff);
con = Connection::new(TESTSUITE_SERVER_HOST, port);
con = connect_db(TESTSUITE_SERVER_HOST, port);
backoff *= 2;
}
info!("Server at {connection_string} has stopped accepting connections");
@ -173,15 +237,15 @@ fn wait_for_shutdown() -> HarnessResult<()> {
}
/// Start the servers returning handles to the child processes
fn start_servers(target_folder: impl AsRef<Path>) -> HarnessResult<Vec<Child>> {
let mut ret = Vec::with_capacity(SERVERS.len());
fn start_servers(target_folder: impl AsRef<Path>) -> HarnessResult<()> {
for (server_id, _ports) in SERVERS {
let cmd = get_run_server_cmd(server_id, target_folder.as_ref());
info!("Starting {server_id} ...");
ret.push(util::get_child(format!("start {server_id}"), cmd)?);
let child = util::get_child(format!("start {server_id}"), cmd)?;
CHILDREN.with(|c| c.borrow_mut().push((server_id, child)));
}
wait_for_startup()?;
Ok(ret)
Ok(())
}
pub(super) fn run_with_servers(
@ -190,14 +254,12 @@ pub(super) fn run_with_servers(
run_what: impl FnOnce() -> HarnessResult<()>,
) -> HarnessResult<()> {
info!("Starting servers ...");
let children = start_servers(target_folder.as_ref())?;
start_servers(target_folder.as_ref())?;
run_what()?;
if kill_servers_when_done {
kill_servers()?;
wait_for_shutdown()?;
}
// just use this to avoid ignoring the children vector
assert_eq!(children.len(), SERVERS.len());
Ok(())
}

@ -31,25 +31,142 @@
//!
//! This contains modules which are shared by both the `cli` and the `server` modules
use std::error::Error;
/// A generic result
pub type TResult<T> = Result<T, Box<dyn Error>>;
/// The size of the read buffer in bytes
pub const BUF_CAP: usize = 8 * 1024; // 8 KB per-connection
/// The current version
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// The URL
pub const URL: &str = "https://github.com/skytable/skytable";
#[macro_export]
/// Don't use unwrap_or but use this macro as the optimizer fails to optimize away usages
/// of unwrap_or and creates a lot of LLVM IR bloat. use
// FIXME(@ohsayan): Fix this when https://github.com/rust-lang/rust/issues/68667 is addressed
macro_rules! option_unwrap_or {
($try:expr, $fallback:expr) => {
match $try {
Some(t) => t,
None => $fallback,
pub mod test_utils {
pub const DEFAULT_USER_NAME: &str = "root";
pub const DEFAULT_USER_PASS: &str = "mypassword12345678";
pub const DEFAULT_HOST: &str = "127.0.0.1";
pub const DEFAULT_PORT: u16 = 2003;
}
use std::{
collections::{hash_map::Entry, HashMap},
env,
};
/// Returns a formatted version message `{binary} vx.y.z`
pub fn version_msg(binary: &str) -> String {
format!("{binary} v{VERSION}")
}
#[derive(Debug, PartialEq)]
/// The CLI action that is expected to be performed
pub enum CliAction<A> {
/// Display the `--help` message
Help,
/// Dipslay the `--version`
Version,
/// Perform an action using the given args
Action(A),
}
pub type CliActionMulti = CliAction<HashMap<String, Vec<String>>>;
pub type CliActionSingle = CliAction<HashMap<String, String>>;
/*
generic cli arg parser
*/
#[derive(Debug, PartialEq)]
/// Argument parse error
pub enum AnyArgsParseError {
/// The value for the given argument was either incorrectly formatted or missing
MissingValue(String),
}
/// Parse CLI args, allowing duplicates (bucketing them)
pub fn parse_cli_args_allow_duplicate() -> Result<CliActionMulti, AnyArgsParseError> {
parse_args(env::args())
}
/// Parse args allowing and bucketing any duplicates
pub fn parse_args(
args: impl IntoIterator<Item = String>,
) -> Result<CliActionMulti, AnyArgsParseError> {
let mut ret: HashMap<String, Vec<String>> = HashMap::new();
let mut args = args.into_iter().skip(1).peekable();
while let Some(arg) = args.next() {
if arg == "--help" {
return Ok(CliAction::Help);
}
if arg == "--version" {
return Ok(CliAction::Version);
}
let (arg, value) = extract_arg(arg, &mut args).map_err(AnyArgsParseError::MissingValue)?;
match ret.get_mut(&arg) {
Some(values) => {
values.push(value);
}
None => {
ret.insert(arg, vec![value]);
}
}
}
Ok(CliAction::Action(ret))
}
/*
no duplicate arg parser
*/
#[derive(Debug, PartialEq)]
/// Argument parse error
pub enum ArgParseError {
/// The given argument had a duplicate value
Duplicate(String),
/// The given argument did not have an appropriate value
MissingValue(String),
}
/// Parse all non-repeating CLI arguments
pub fn parse_cli_args_disallow_duplicate() -> Result<CliActionSingle, ArgParseError> {
parse_args_deny_duplicate(env::args())
}
/// Parse all arguments but deny any duplicates
pub fn parse_args_deny_duplicate(
args: impl IntoIterator<Item = String>,
) -> Result<CliActionSingle, ArgParseError> {
let mut ret: HashMap<String, String> = HashMap::new();
let mut args = args.into_iter().skip(1).peekable();
while let Some(arg) = args.next() {
if arg == "--help" {
return Ok(CliAction::Help);
}
if arg == "--version" {
return Ok(CliAction::Version);
}
let (arg, value) = extract_arg(arg, &mut args).map_err(ArgParseError::MissingValue)?;
match ret.entry(arg) {
Entry::Vacant(v) => {
v.insert(value);
}
Entry::Occupied(oe) => return Err(ArgParseError::Duplicate(oe.key().into())),
}
}
Ok(CliAction::Action(ret))
}
/// Extract an argument:
/// - `--arg=value`
/// - `--arg value`
fn extract_arg(
arg: String,
args: &mut impl Iterator<Item = String>,
) -> Result<(String, String), String> {
let this_args: Vec<&str> = arg.split("=").collect();
let (arg, value) = if this_args.len() == 2 {
// self contained arg
(this_args[0].to_owned(), this_args[1].to_owned())
} else {
if this_args.len() == 1 {
match args.next() {
None => return Err(arg),
Some(val) => (arg, val),
}
} else {
return Err(arg);
}
};
Ok((arg, value))
}

@ -1,14 +0,0 @@
[package]
name = "libstress"
version = "0.8.0"
authors = ["Sayan Nandan <nandansayan@outlook.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# external deps
crossbeam-channel = "0.5.6"
rayon = "1.6.1"
log = "0.4.17"
rand = "0.8.5"

@ -1,469 +0,0 @@
/*
* Created on Wed Jun 16 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # libstress
//!
//! Tools for emulating concurrent query behavior to _stress test_ the database server.
//! As of now, this crate provides a [`Workpool`] which is a generic synchronous threadpool
//! for doing multiple operations. But Workpool is a little different from standard threadpool
//! implementations in it categorizing a job to be made up of three parts, namely:
//!
//! - The init_pre_loop_var (the pre-loop stage)
//! - The on_loop (the in-loop stage)
//! - The on_exit (the post-loop stage)
//!
//! These stages form a part of the event loop.
//!
//! ## The event loop
//!
//! A task runs in a loop with the `on_loop` routine to which the a reference of the result of
//! the `init_pre_loop_var` is sent that is initialized. The loop proceeds whenever a worker
//! receives a task or else it blocks the current thread, waiting for a task. Hence the loop
//! cannot be terminated by an execute call. Instead, the _event loop_ is terminated when the
//! Workpool is dropped, either by scoping out, or by using the provided finish-like methods
//! (that call the destructor).
//!
//! ## Worker lifetime
//!
//! If a runtime panic occurs in the pre-loop stage, then the entire worker just terminates. Hence
//! this worker is no longer able to perform any tasks. Similarly, if a runtime panic occurs in
//! the in-loop stage, the worker terminates and is no longer available to do any work. This will
//! be reflected when the workpool attempts to terminate in entirety, i.e when the threads are joined
//! to the parent thread
//!
#![deny(unused_crate_dependencies)]
#![deny(unused_imports)]
pub mod traits;
pub use rayon;
use {
core::marker::PhantomData,
crossbeam_channel::{bounded, unbounded, Receiver as CReceiver, Sender as CSender},
rayon::prelude::{IntoParallelIterator, ParallelIterator},
std::{fmt::Display, thread},
};
#[derive(Debug)]
pub enum WorkpoolError {
ThreadStartFailure(usize, usize),
}
impl Display for WorkpoolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorkpoolError::ThreadStartFailure(expected, started) => {
write!(
f,
"couldn't start all threads. expected {expected} but started {started}"
)
}
}
}
}
pub type WorkpoolResult<T> = Result<T, WorkpoolError>;
/// A Job. The UIn type parameter is the type that will be used to execute the action
/// Nothing is a variant used by the drop implementation to terminate all the workers
/// and call the exit_loop function
enum JobType<UIn> {
Task(UIn),
Nothing,
}
/// A worker
///
/// The only reason we use option is to reduce the effort needed to implement [`Drop`] for the
/// [`Workpool`]
struct Worker {
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
/// Initialize a new worker
fn new<Inp: 'static, UIn, Lv, Lp, Ex>(
id: usize,
job_receiver: CReceiver<JobType<UIn>>,
init_pre_loop_var: Lv,
on_exit: Ex,
on_loop: Lp,
wgtx: CSender<()>,
) -> Self
where
UIn: Send + Sync + 'static,
Lv: Fn() -> Inp + 'static + Send,
Lp: Fn(&mut Inp, UIn) + Send + Sync + 'static,
Ex: Fn(&mut Inp) + Send + 'static,
{
let thread = thread::Builder::new()
.name(format!("worker-{id}"))
.spawn(move || {
let on_loop = on_loop;
let mut pre_loop_var = init_pre_loop_var();
wgtx.send(()).unwrap();
drop(wgtx);
loop {
let action = job_receiver.recv().unwrap();
match action {
JobType::Task(tsk) => on_loop(&mut pre_loop_var, tsk),
JobType::Nothing => {
on_exit(&mut pre_loop_var);
break;
}
}
}
})
.unwrap();
Self {
thread: Some(thread),
}
}
}
/// A pool configuration setting to easily generate [`Workpool`]s without
/// having to clone an entire pool and its threads upfront
pub struct PoolConfig<Inp, UIn, Lv, Lp, Ex> {
/// the pool size
count: usize,
/// the function that sets the pre-loop variable
init_pre_loop_var: Lv,
/// the function to be executed on worker termination
on_exit: Ex,
/// the function to be executed on loop
on_loop: Lp,
/// a marker for `Inp` since no parameters use it directly
_marker: PhantomData<(Inp, UIn)>,
/// check if self needs a pool for parallel iterators
needs_iterator_pool: bool,
/// expected maximum number of jobs
expected_max_sends: Option<usize>,
}
impl<Inp: 'static, UIn, Lv, Lp, Ex> PoolConfig<Inp, UIn, Lv, Lp, Ex>
where
UIn: Send + Sync + 'static,
Inp: Sync,
Ex: Fn(&mut Inp) + Send + Sync + 'static + Clone,
Lv: Fn() -> Inp + Send + Sync + 'static + Clone,
Lp: Fn(&mut Inp, UIn) + Clone + Send + Sync + 'static,
{
/// Create a new pool config
pub fn new(
count: usize,
init_pre_loop_var: Lv,
on_loop: Lp,
on_exit: Ex,
needs_iterator_pool: bool,
expected_max_sends: Option<usize>,
) -> Self {
Self {
count,
init_pre_loop_var,
on_loop,
on_exit,
needs_iterator_pool,
_marker: PhantomData,
expected_max_sends,
}
}
/// Get a new [`Workpool`] from the current config
pub fn get_pool(&self) -> WorkpoolResult<Workpool<Inp, UIn, Lv, Lp, Ex>> {
self.get_pool_with_workers(self.count)
}
/// Get a [`Workpool`] with the base config but with a different number of workers
pub fn get_pool_with_workers(
&self,
count: usize,
) -> WorkpoolResult<Workpool<Inp, UIn, Lv, Lp, Ex>> {
Workpool::new(
count,
self.init_pre_loop_var.clone(),
self.on_loop.clone(),
self.on_exit.clone(),
self.needs_iterator_pool,
self.expected_max_sends,
)
}
/// Get a [`Workpool`] with the base config but with a custom loop-stage closure
pub fn with_loop_closure<Dlp>(&self, lp: Dlp) -> WorkpoolResult<Workpool<Inp, UIn, Lv, Dlp, Ex>>
where
Dlp: Fn(&mut Inp, UIn) + Clone + Send + Sync + 'static,
{
Workpool::new(
self.count,
self.init_pre_loop_var.clone(),
lp,
self.on_exit.clone(),
self.needs_iterator_pool,
self.expected_max_sends,
)
}
}
/// # Workpool
///
/// A Workpool is a generic synchronous thread pool that can be used to perform, well, anything.
/// A workpool has to be initialized with the number of workers, the pre_loop_variable (set this
/// to None if there isn't any). what to do on loop and what to do on exit of each worker. The
/// closures are kept as `Clone`able types just to reduce complexity with copy (we were lazy).
///
/// ## Clones
///
/// Workpool clones simply create a new workpool with the same on_exit, on_loop and init_pre_loop_var
/// configurations. This provides a very convenient interface if one desires to use multiple workpools
/// to do the _same kind of thing_
///
/// ## Actual thread count
///
/// The actual thread count will depend on whether the caller requests the initialization of an
/// iterator pool or not. If the caller does request for an iterator pool, then the number of threads
/// spawned will be twice the number of the set workers. Else, the number of spawned threads is equal
/// to the number of workers.
pub struct Workpool<Inp, UIn, Lv, Lp, Ex> {
/// the workers
workers: Vec<Worker>,
/// the sender that sends jobs
job_distributor: CSender<JobType<UIn>>,
/// the function that sets the pre-loop variable
init_pre_loop_var: Lv,
/// the function to be executed on worker termination
on_exit: Ex,
/// the function to be executed on loop
on_loop: Lp,
/// a marker for `Inp` since no parameters use it directly
_marker: PhantomData<Inp>,
/// check if self needs a pool for parallel iterators
needs_iterator_pool: bool,
/// expected maximum number of sends
expected_max_sends: Option<usize>,
}
impl<Inp: 'static, UIn, Lv, Ex, Lp> Workpool<Inp, UIn, Lv, Lp, Ex>
where
UIn: Send + Sync + 'static,
Ex: Fn(&mut Inp) + Send + Sync + 'static + Clone,
Lv: Fn() -> Inp + Send + Sync + 'static + Clone,
Lp: Fn(&mut Inp, UIn) + Send + Sync + 'static + Clone,
Inp: Sync,
{
/// Create a new workpool
pub fn new(
count: usize,
init_pre_loop_var: Lv,
on_loop: Lp,
on_exit: Ex,
needs_iterator_pool: bool,
expected_max_sends: Option<usize>,
) -> WorkpoolResult<Self> {
// init threadpool for iterator
if needs_iterator_pool {
// initialize a global threadpool for parallel iterators
let _ = rayon::ThreadPoolBuilder::new()
.num_threads(count)
.build_global();
}
assert!(count != 0, "Runtime panic: Bad value `0` for thread count");
let (sender, receiver) = match expected_max_sends {
Some(limit) => bounded(limit),
None => unbounded(),
};
let (wgtx, wgrx) = bounded::<()>(count);
let mut workers = Vec::with_capacity(count);
for i in 0..count {
workers.push(Worker::new(
i,
receiver.clone(),
init_pre_loop_var.clone(),
on_exit.clone(),
on_loop.clone(),
wgtx.clone(),
));
}
drop(wgtx);
let sum: usize = wgrx.iter().map(|_| 1usize).sum();
if sum == count {
Ok(Self {
workers,
job_distributor: sender,
init_pre_loop_var,
on_exit,
on_loop,
_marker: PhantomData,
needs_iterator_pool,
expected_max_sends,
})
} else {
Err(WorkpoolError::ThreadStartFailure(count, sum))
}
}
pub fn clone_pool(&self) -> WorkpoolResult<Self> {
Self::new(
self.workers.len(),
self.init_pre_loop_var.clone(),
self.on_loop.clone(),
self.on_exit.clone(),
self.needs_iterator_pool,
self.expected_max_sends,
)
}
/// Execute something
pub fn execute(&self, inp: UIn) {
self.job_distributor
.send(JobType::Task(inp))
.expect("Worker thread crashed")
}
/// Execute something that can be executed as a parallel iterator
/// For the best performance, it is recommended that you pass true for `needs_iterator_pool`
/// on initialization of the [`Workpool`]
pub fn execute_iter(&self, iter: impl IntoParallelIterator<Item = UIn>) {
iter.into_par_iter().for_each(|inp| self.execute(inp))
}
/// Does the same thing as [`execute_iter`] but drops self ensuring that all the
/// workers actually finish their tasks
pub fn execute_and_finish_iter(self, iter: impl IntoParallelIterator<Item = UIn>) {
self.execute_iter(iter);
drop(self);
}
/// Initialize a new [`Workpool`] with the default count of threads. This is equal
/// to 2 * the number of logical cores.
pub fn new_default_threads(
init_pre_loop_var: Lv,
on_loop: Lp,
on_exit: Ex,
needs_iterator_pool: bool,
expected_max_sends: Option<usize>,
) -> WorkpoolResult<Self> {
// we'll naively use the number of CPUs present on the system times 2 to determine
// the number of workers (sure the scheduler does tricks all the time)
let worker_count = thread::available_parallelism().map_or(1, usize::from) * 2;
Self::new(
worker_count,
init_pre_loop_var,
on_loop,
on_exit,
needs_iterator_pool,
expected_max_sends,
)
}
}
impl<Inp, UIn, Lv, Lp, Ex> Drop for Workpool<Inp, UIn, Lp, Lv, Ex> {
fn drop(&mut self) {
for _ in &self.workers {
self.job_distributor.send(JobType::Nothing).unwrap();
}
for worker in &mut self.workers {
if let Some(thread) = worker.thread.take() {
thread.join().unwrap()
}
}
}
}
pub mod utils {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
use rand::distributions::{Alphanumeric, Standard};
use std::collections::HashSet;
use std::collections::TryReserveError;
/// Generate a random UTF-8 string
pub fn ran_string(len: usize, rand: impl rand::Rng) -> String {
let rand_string: String = rand
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect();
rand_string
}
/// Generate a vector of random bytes
pub fn ran_bytes(len: usize, rand: impl rand::Rng) -> Vec<u8> {
rand.sample_iter(&Standard).take(len).collect()
}
/// Generate multiple vectors of random bytes
pub fn generate_random_byte_vector(
count: usize,
size: usize,
mut rng: impl rand::Rng,
unique: bool,
) -> Result<Vec<Vec<u8>>, TryReserveError> {
if unique {
let mut keys = HashSet::new();
keys.try_reserve(size)?;
(0..count).into_iter().for_each(|_| {
let mut ran = ran_bytes(size, &mut rng);
while keys.contains(&ran) {
ran = ran_bytes(size, &mut rng);
}
keys.insert(ran);
});
Ok(keys.into_iter().collect())
} else {
let mut keys = Vec::new();
keys.try_reserve_exact(size)?;
let ran_byte_key = ran_bytes(size, &mut rng);
(0..count).for_each(|_| keys.push(ran_byte_key.clone()));
Ok(keys)
}
}
/// Generate a vector of random UTF-8 valid strings
pub fn generate_random_string_vector(
count: usize,
size: usize,
mut rng: impl rand::Rng,
unique: bool,
) -> Result<Vec<String>, TryReserveError> {
if unique {
let mut keys = HashSet::new();
keys.try_reserve(size)?;
(0..count).into_iter().for_each(|_| {
let mut ran = ran_string(size, &mut rng);
while keys.contains(&ran) {
ran = ran_string(size, &mut rng);
}
keys.insert(ran);
});
Ok(keys.into_iter().collect())
} else {
let mut keys = Vec::new();
keys.try_reserve_exact(size)?;
(0..count)
.into_iter()
.map(|_| ran_string(size, &mut rng))
.for_each(|bytes| keys.push(bytes));
Ok(keys)
}
}
pub fn rand_alphastring(len: usize, rng: &mut impl rand::Rng) -> String {
(0..len)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
}

@ -1,69 +0,0 @@
/*
* Created on Fri Jun 18 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use std::fmt;
/// A trait for aggresive erroring
pub trait ExitError<T> {
/// Abort the process if the type errors with an error code or
/// return the type
fn exit_error<Ms>(self, msg: Ms) -> T
where
Ms: ToString;
}
impl<T, E> ExitError<T> for Result<T, E>
where
E: fmt::Display,
{
fn exit_error<Ms>(self, msg: Ms) -> T
where
Ms: ToString,
{
match self {
Self::Err(e) => {
log::error!("{} : '{}'", msg.to_string(), e);
std::process::exit(0x01);
}
Self::Ok(v) => v,
}
}
}
impl<T> ExitError<T> for Option<T> {
fn exit_error<Ms>(self, msg: Ms) -> T
where
Ms: ToString,
{
match self {
Self::None => {
log::error!("{}", msg.to_string());
std::process::exit(0x01);
}
Self::Some(v) => v,
}
}
}

@ -8,7 +8,7 @@ Type=simple
Restart=always
RestartSec=1
User=skytable
ExecStart=/usr/bin/skyd --noart
ExecStart=/usr/bin/skyd --config=/var/lib/skytable/config.yaml
WorkingDirectory=/var/lib/skytable
[Install]

@ -1,5 +1,5 @@
Skytable is a free and open-source NoSQL database that aims
to provide flexibility in data modeling at scale.
The `skytable` package contains the database server (`skyd`),
an interactive command-line client (`skysh`), a benchmarking
tool (`sky-bench`) and a migration tool (`sky-migrate`).
an interactive command-line client (`skysh`) and a benchmarking
tool (`sky-bench`).

@ -1,15 +1,35 @@
#!/bin/sh -e
SKY_DIR=/var/lib/skytable
systemctl daemon-reload
if [ $1 = "install" ]; then
echo "Doing '$1'"
if [ "$1" = "configure" ]; then
# Enable and start skyd on fresh install
systemctl enable skyd
fi
systemctl start skyd
echo "Generating password and configuration"
if [ $1 = "upgrade" ]; then
if [ -f /var/lib/skytable/config.yaml ]; then
echo "Configuration already exists. Not updating configuration."
else
mv /var/lib/skytable/config.yaml.tmp /var/lib/skytable/config.yaml
# Generate and set password
if [ ! -f "$SKY_DIR/config.yaml" ]; then
echo "Error: The file $SKY_DIR/config.yaml does not exist."
exit 1 # Exit with an error code
fi
PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 16 ; echo '')
sed -i "s/rootpass/$PASSWORD/g" "$SKY_DIR/config.yaml"
echo "Your root password is: '$PASSWORD'. You can change this using the config file in $SKY_DIR/config.yaml"
fi
elif [ "$1" = "upgrade" ]; then
# On upgrade, just restart skyd
echo "Not changing configuration. This is an upgrade."
systemctl stop skyd
systemctl start skyd
fi
systemctl start skyd
echo "Done executing post install scripts."
#DEBHELPER#

@ -0,0 +1,27 @@
#!/bin/sh -e
SKY_DIR=/var/lib/skytable
SERVICE_NAME=skyd
# Only perform cleanup on package removal, not on upgrade
case "$1" in
remove)
echo "Removing the skytable user..."
# Remove the user and group, if they exist
# This will not remove the /var/lib/skytable directory
if getent passwd skytable > /dev/null; then
deluser --system skytable
fi
if getent group skytable > /dev/null; then
delgroup skytable
fi
echo "Removing the configuration file ..."
rm /var/lib/skytable/config.yaml
echo "Cleanup complete."
;;
*)
# No action required for other cases (upgrade, failed-upgrade, etc.)
;;
esac
#DEBHELPER#

@ -2,19 +2,22 @@
SKY_DIR=/var/lib/skytable
# create the data directory
if [ ! -e $SKY_DIR ]; then
mkdir $SKY_DIR
elif [ ! -d $SKY_DIR ]; then
echo "ERROR: /var/lib/skytable exists but it is not a directory" 1>&2
return 1
# Create the data directory if it doesn't exist
if [ ! -e "$SKY_DIR" ]; then
mkdir -p "$SKY_DIR"
echo "Created directory $SKY_DIR"
elif [ ! -d "$SKY_DIR" ]; then
echo "ERROR: $SKY_DIR exists but it is not a directory" 1>&2
exit 1
fi
if [ $1 = "install" ]; then
# add the `skytable` user
adduser --system --group skytable
# change ownership
chown skytable:skytable /var/lib/skytable
# On initial install, add the `skytable` user
if [ "$1" = "install" ]; then
echo "Creating user 'skytable'"
if ! getent passwd skytable > /dev/null; then
adduser --system --group --no-create-home skytable
fi
chown -R skytable:skytable "$SKY_DIR"
echo "Created user 'skytable'"
fi
#DEBHELPER#

@ -0,0 +1,6 @@
#!/bin/sh -e
echo "Stopping processes"
systemctl stop skyd
systemctl disable skyd
echo "Stopped processes"

@ -0,0 +1,22 @@
#!/bin/bash
CONFIG_FILE="/var/lib/skytable/config.yaml"
PASSWORD_MARKER="rootpass"
IP_MARKER="127.0.0.1"
generate_password() {
uuidgen | cut -c -16
}
sed -i "s/$IP_MARKER/0.0.0.0/g" "$CONFIG_FILE"
if grep -q "$PASSWORD_MARKER" "$CONFIG_FILE"; then
# Password not set, generate a new one
PASSWORD=$(generate_password)
sed -i "s/$PASSWORD_MARKER/$PASSWORD/g" "$CONFIG_FILE"
echo "Generated Password: $PASSWORD"
else
echo "Using existing password in config file"
fi
exec skyd --config "$CONFIG_FILE"

@ -1,227 +0,0 @@
#!/usr/bin/perl -w
=pod
All credits for the random unicode string generation logic go to Paul Sarena who released
the original version here: https://github.com/bits/UTF-8-Unicode-Test-Documents and released
it under the BSD 3-Clause "New" or "Revised" License
=cut
use strict;
use warnings qw( FATAL utf8 );
use utf8; # tell Perl parser there are non-ASCII characters in this lexical scope
use open qw( :encoding(UTF-8) :std ); # Declare that anything that opens a filehandles within this lexical scope is to assume that that stream is encoded in UTF-8 unless you tell it otherwise
use Encode;
use HTML::Entities;
my $html_pre = q|<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>UTF-8 Codepoint Sequence</title>
</head>
<body>|;
my $html_post = q|</body>
</html>|;
my $output_directory = './utf8/';
my $utf8_seq;
# 0000FFFF Plane 0: Basic Multilingual Plane
# 100001FFFF Plane 1: Supplementary Multilingual Plane
# 200002FFFF Plane 2: Supplementary Ideographic Plane
# 30000DFFFF Planes 313: Unassigned
# E0000EFFFF Plane 14: Supplement­ary Special-purpose Plane
# F000010FFFF Planes 1516: Supplement­ary Private Use Area
foreach my $separator ('', ' ') {
foreach my $end (0xFF, 0xFFF, 0xFFFF, 0x1FFFF, 0x2FFFF, 0x10FFFF) {
# UTF-8 codepoint sequence of assigned, printable codepoints
$utf8_seq = gen_seq({
start => 0x00,
end => $end,
separator => $separator,
skip_unprintable => 1,
replace_unprintable => 1,
skip_unassigned => 1,
writefiles => ($separator ? 'txt,html' : 'txt')
});
# UTF-8 codepoint sequence of assigned, printable and unprintable codepoints as-is
$utf8_seq = gen_seq({
start => 0x00,
end => $end,
separator => $separator,
skip_unprintable => 0,
replace_unprintable => 0,
skip_unassigned => 1,
writefiles => ($separator ? 'txt,html' : 'txt')
});
# UTF-8 codepoint sequence of assigned, printable and unprintable codepoints replaced
$utf8_seq = gen_seq({
start => 0x00,
end => $end,
separator => $separator,
skip_unprintable => 0,
replace_unprintable => 1,
skip_unassigned => 1,
writefiles => ($separator ? 'txt,html' : 'txt')
});
# UTF-8 codepoint sequence of assinged and unassigned, printable and unprintable codepoints as-is
$utf8_seq = gen_seq({
start => 0x00,
end => $end,
separator => $separator,
skip_unprintable => 0,
replace_unprintable => 0,
skip_unassigned => 0,
writefiles => ($separator ? 'txt,html' : 'txt')
});
# UTF-8 codepoint sequence of assinged and unassigned, printable and unprintable codepoints replaced
$utf8_seq = gen_seq({
start => 0x00,
end => $end,
separator => $separator,
skip_unprintable => 0,
replace_unprintable => 1,
skip_unassigned => 0,
writefiles => ($separator ? 'txt,html' : 'txt')
});
}
}
# print Encode::encode('UTF-8', $utf8_seq), "\n";
sub gen_seq{
my $config = shift;
$config->{start} = 0x00 unless defined $config->{start};
$config->{end} = 0x10FFFF unless defined $config->{end};
$config->{skip_unassigned} = 1 unless defined $config->{skip_unassigned};
$config->{skip_unprintable} = 1 unless defined $config->{skip_unprintable};
$config->{replace_unprintable} = 1 unless defined $config->{replace_unprintable};
$config->{separator} = ' ' unless defined $config->{separator};
$config->{newlines_every} = 50 unless defined $config->{newlines_every};
$config->{writefiles} = 'text,html' unless defined $config->{writefiles};
my $utf8_seq;
my $codepoints_this_line = 0;
my $codepoints_printed = 0;
for my $i ($config->{start} .. $config->{end}) {
next if ($i >= 0xD800 && $i <= 0xDFFF); # high and low surrogate halves used by UTF-16 (U+D800 through U+DFFF) are not legal Unicode values, and the UTF-8 encoding of them is an invalid byte sequence
next if ($i >= 0xFDD0 && $i <= 0xFDEF); # Non-characters
next if ( # Non-characters
$i == 0xFFFE || $i == 0xFFFF ||
$i == 0x1FFFE || $i == 0x1FFFF ||
$i == 0x2FFFE || $i == 0x2FFFF ||
$i == 0x3FFFE || $i == 0x3FFFF ||
$i == 0x4FFFE || $i == 0x4FFFF ||
$i == 0x5FFFE || $i == 0x5FFFF ||
$i == 0x6FFFE || $i == 0x6FFFF ||
$i == 0x7FFFE || $i == 0x7FFFF ||
$i == 0x8FFFE || $i == 0x8FFFF ||
$i == 0x9FFFE || $i == 0x9FFFF ||
$i == 0xaFFFE || $i == 0xAFFFF ||
$i == 0xbFFFE || $i == 0xBFFFF ||
$i == 0xcFFFE || $i == 0xCFFFF ||
$i == 0xdFFFE || $i == 0xDFFFF ||
$i == 0xeFFFE || $i == 0xEFFFF ||
$i == 0xfFFFE || $i == 0xFFFFF ||
$i == 0x10FFFE || $i == 0x10FFFF
);
my $codepoint = chr($i);
# skip unassiggned codepoints
next if $config->{skip_unassigned} && $codepoint !~ /^\p{Assigned}/o;
if ( $codepoint =~ /^\p{IsPrint}/o ) {
$utf8_seq .= $codepoint;
} else { # not printable
next if $config->{skip_unprintable};
# include unprintable or replace it
$utf8_seq .= $config->{replace_unprintable} ? '<27>' : $codepoint;
}
$codepoints_printed++;
if ($config->{separator}) {
if ($config->{newlines_every} && $codepoints_this_line++ == $config->{newlines_every}) {
$utf8_seq .= "\n";
$codepoints_this_line = 0;
} else {
$utf8_seq .= $config->{separator};
}
}
}
utf8::upgrade($utf8_seq);
if ($config->{writefiles}) {
my $filebasename = 'utf8_sequence_' .
(sprintf '%#x', $config->{start}) .
'-' .
(sprintf '%#x', $config->{end}) .
($config->{skip_unassigned} ? '_assigned' : '_including-unassigned') .
($config->{skip_unprintable} ? '_printable' : '_including-unprintable') .
(!$config->{skip_unprintable} ?
($config->{replace_unprintable} ? '-replaced' : '-asis') :
''
) .
($config->{separator} ?
($config->{newlines_every} ? '' : '_without-newlines') :
'_unseparated'
);
my $title = 'UTF-8 codepoint sequence' .
($config->{skip_unassigned} ? ' of assigned' : ' of assinged and unassigned') .
($config->{skip_unprintable} ? ', printable' : ', with unprintable') .
(!$config->{skip_unprintable} ?
($config->{replace_unprintable} ? ' codepoints replaced' : ' codepoints as-is') :
' codepoints'
) .
' in the range ' .
(sprintf '%#x', $config->{start}) .
'-' .
(sprintf '%#x', $config->{end}) .
($config->{newlines_every} ? '' : ', as a long string without newlines');
my $html_pre_custom = $html_pre;
$html_pre_custom =~ s|UTF\-8 codepoint sequence|$title|;
my $filename = ${output_directory} . ($config->{separator} ? '' : 'un') . 'separated/' . ${filebasename};
if ($config->{writefiles} =~ /te?xt/) {
open FH, ">${filename}.txt" or die "cannot open $filename: $!";
print FH $utf8_seq;
close FH;
}
if ($config->{writefiles} =~ /html/) {
open FH, ">${filename}_unescaped.html" or die "cannot open $filename: $!";
print FH $html_pre_custom, $utf8_seq, $html_post;
close FH;
}
# open FH, ">${output_directory}${filebasename}_escaped.html";
# print FH $html_pre_custom, HTML::Entities::encode_entities($utf8_seq), $html_post;
# close FH;
print "Output $title ($codepoints_printed codepoints)\n";
}
return $utf8_seq;
}

@ -1,9 +1,10 @@
[package]
authors = ["Sayan Nandan <ohsayan@outlook.com>"]
build = "build.rs"
edition = "2021"
name = "skyd"
version = "0.8.0"
description = "Skytable is a modern NoSQL database powered by BlueQL that aims to deliver performance, scalability and flexibility with data"
license = "AGPL-3.0"
[dependencies]
# internal deps
@ -11,48 +12,39 @@ libsky = { path = "../libsky" }
sky_macros = { path = "../sky-macros" }
rcrypt = "0.4.0"
# external deps
ahash = "0.8.2"
bytes = "1.3.0"
chrono = "0.4.23"
clap = { version = "2", features = ["yaml"] }
env_logger = "0.10.0"
hashbrown = { version = "0.13.1", features = ["raw"] }
log = "0.4.17"
openssl = { version = "0.10.45", features = ["vendored"] }
bytes = "1.5.0"
env_logger = "0.10.1"
log = "0.4.20"
openssl = { version = "0.10.61", features = ["vendored"] }
crossbeam-epoch = { version = "0.9.15" }
parking_lot = "0.12.1"
regex = "1.7.1"
serde = { version = "1.0.152", features = ["derive"] }
tokio = { version = "1.24.1", features = ["full"] }
serde = { version = "1.0.193", features = ["derive"] }
tokio = { version = "1.34.0", features = ["full"] }
tokio-openssl = "0.6.3"
toml = "0.5.10"
base64 = "0.13.1"
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"] }
crc = "3.0.1"
serde_yaml = "0.9.27"
[target.'cfg(all(not(target_env = "msvc"), not(miri)))'.dependencies]
# external deps
jemallocator = "0.5.0"
jemallocator = "0.5.4"
[target.'cfg(target_os = "windows")'.dependencies]
# external deps
winapi = { version = "0.3.9", features = ["fileapi"] }
winapi = { version = "0.3.9", features = [
"fileapi",
"sysinfoapi",
"minwinbase",
] }
[target.'cfg(unix)'.dependencies]
# external deps
libc = "0.2.139"
[target.'cfg(unix)'.build-dependencies]
# external deps
cc = "1.0.78"
libc = "0.2.150"
[dev-dependencies]
# internal deps
libstress = { path = "../libstress" }
skytable = { git = "https://github.com/skytable/client-rust", features = [
"aio",
"aio-ssl",
], default-features = false, branch = "next" }
# external deps
bincode = "1.3.3"
rand = "0.8.5"
tokio = { version = "1.24.1", features = ["test-util"] }
tokio = { version = "1.34.0", features = ["test-util"] }
skytable = { branch = "octave", git = "https://github.com/skytable/client-rust.git" }
[features]
nightly = []
@ -71,19 +63,24 @@ priority = "optional"
assets = [
[
"target/release/skyd",
"usr/bin/",
"/usr/bin/skyd",
"755",
],
[
"target/release/skysh",
"usr/bin/",
"/usr/bin/skysh",
"755",
],
[
"target/release/sky-bench",
"usr/bin/",
"/usr/bin/sky-bench",
"755",
],
[
"../examples/config-files/dpkg/config.yaml",
"/var/lib/skytable/config.yaml.tmp",
"644"
],
[
"../pkg/common/skyd.service",
"/etc/systemd/system/skyd.service",

@ -1,9 +0,0 @@
fn main() {
#[cfg(unix)]
{
println!("cargo:rerun-if-changed=native/flock-posix.c");
cc::Build::new()
.file("native/flock-posix.c")
.compile("libflock-posix.a");
}
}

@ -1,81 +0,0 @@
/*
* Created on Wed Aug 19 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # `DEL` queries
//! This module provides functions to work with `DEL` queries
use crate::{
corestore::table::DataModel, dbnet::prelude::*,
kvengine::encoding::ENCODING_LUT_ITER, util::compiler,
};
action!(
/// Run a `DEL` query
///
/// Do note that this function is blocking since it acquires a write lock.
/// It will write an entire datagroup, for this `del` action
fn del(handle: &Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |size| size != 0)?;
let table = get_tbl_ref!(handle, con);
macro_rules! remove {
($engine:expr) => {{
let encoding_is_okay = ENCODING_LUT_ITER[$engine.is_key_encoded()](act.as_ref());
if compiler::likely(encoding_is_okay) {
let done_howmany: Option<usize>;
{
if registry::state_okay() {
let mut many = 0;
act.for_each(|key| {
many += $engine.remove_unchecked(key) as usize;
});
done_howmany = Some(many);
} else {
done_howmany = None;
}
}
if let Some(done_howmany) = done_howmany {
con.write_usize(done_howmany).await?;
} else {
con._write_raw(P::RCODE_SERVER_ERR).await?;
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
}};
}
match table.get_model_ref() {
DataModel::KV(kve) => {
remove!(kve)
}
DataModel::KVExtListmap(kvlmap) => {
remove!(kvlmap)
}
#[allow(unreachable_patterns)]
_ => return util::err(P::RSTRING_WRONG_MODEL),
}
Ok(())
}
);

@ -1,62 +0,0 @@
/*
* Created on Wed Aug 19 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # `EXISTS` queries
//! This module provides functions to work with `EXISTS` queries
use crate::{
corestore::table::DataModel, dbnet::prelude::*,
kvengine::encoding::ENCODING_LUT_ITER, queryengine::ActionIter, util::compiler,
};
action!(
/// Run an `EXISTS` query
fn exists(handle: &Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len != 0)?;
let mut how_many_of_them_exist = 0usize;
macro_rules! exists {
($engine:expr) => {{
let encoding_is_okay = ENCODING_LUT_ITER[$engine.is_key_encoded()](act.as_ref());
if compiler::likely(encoding_is_okay) {
act.for_each(|key| {
how_many_of_them_exist += $engine.exists_unchecked(key) as usize;
});
con.write_usize(how_many_of_them_exist).await?;
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
}};
}
let tbl = get_tbl_ref!(handle, con);
match tbl.get_model_ref() {
DataModel::KV(kve) => exists!(kve),
DataModel::KVExtListmap(kve) => exists!(kve),
#[allow(unreachable_patterns)]
_ => return util::err(P::RSTRING_WRONG_MODEL),
}
Ok(())
}
);

@ -1,49 +0,0 @@
/*
* Created on Thu Sep 24 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{dbnet::prelude::*, queryengine::ActionIter};
action!(
/// Delete all the keys in the database
fn flushdb(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len < 2)?;
if registry::state_okay() {
if act.is_empty() {
// flush the current table
get_tbl_ref!(handle, con).truncate_table();
} else {
// flush the entity
let raw_entity = unsafe { act.next_unchecked() };
let entity = handle_entity!(con, raw_entity);
get_tbl!(&entity, handle, con).truncate_table();
}
con._write_raw(P::RCODE_OKAY).await?;
} else {
con._write_raw(P::RCODE_SERVER_ERR).await?;
}
Ok(())
}
);

@ -1,53 +0,0 @@
/*
* Created on Fri Aug 14 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # `GET` queries
//! This module provides functions to work with `GET` queries
use crate::{dbnet::prelude::*, util::compiler};
action!(
/// Run a `GET` query
fn get(
handle: &crate::corestore::Corestore,
con: &mut Connection<C, P>,
mut act: ActionIter<'a>,
) {
ensure_length::<P>(act.len(), |len| len == 1)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
unsafe {
match kve.get_cloned(act.next_unchecked()) {
Ok(Some(val)) => {
con.write_mono_length_prefixed_with_tsymbol(&val, kve.get_value_tsymbol())
.await?
}
Err(_) => compiler::cold_err(con._write_raw(P::RCODE_ENCODING_ERROR)).await?,
Ok(_) => con._write_raw(P::RCODE_NIL).await?,
}
}
Ok(())
}
);

@ -1,55 +0,0 @@
/*
* Created on Sun Sep 27 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::dbnet::prelude::*;
action!(
/// Run a `KEYLEN` query
///
/// At this moment, `keylen` only supports a single key
fn keylen(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len == 1)?;
let res: Option<usize> = {
let reader = handle.get_table_with::<P, KVEBlob>()?;
unsafe {
// UNSAFE(@ohsayan): this is completely safe as we've already checked
// the number of arguments is one
match reader.get(act.next_unchecked()) {
Ok(v) => v.map(|b| b.len()),
Err(_) => None,
}
}
};
if let Some(value) = res {
// Good, we got the key's length, write it off to the stream
con.write_usize(value).await?;
} else {
// Ah, couldn't find that key
con._write_raw(P::RCODE_NIL).await?;
}
Ok(())
}
);

@ -1,207 +0,0 @@
/*
* Created on Fri Sep 17 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{corestore::SharedSlice, dbnet::prelude::*};
const LEN: &[u8] = "LEN".as_bytes();
const LIMIT: &[u8] = "LIMIT".as_bytes();
const VALUEAT: &[u8] = "VALUEAT".as_bytes();
const LAST: &[u8] = "LAST".as_bytes();
const FIRST: &[u8] = "FIRST".as_bytes();
const RANGE: &[u8] = "RANGE".as_bytes();
struct Range {
start: usize,
stop: Option<usize>,
}
impl Range {
pub fn new(start: usize) -> Self {
Self { start, stop: None }
}
pub fn set_stop(&mut self, stop: usize) {
self.stop = Some(stop);
}
pub fn into_vec(self, slice: &[SharedSlice]) -> Option<Vec<SharedSlice>> {
slice
.get(self.start..self.stop.unwrap_or(slice.len()))
.map(|slc| slc.to_vec())
}
}
action! {
/// Handle an `LGET` query for the list model (KVExt)
/// ## Syntax
/// - `LGET <mylist>` will return the full list
/// - `LGET <mylist> LEN` will return the length of the list
/// - `LGET <mylist> LIMIT <limit>` will return a maximum of `limit` elements
/// - `LGET <mylist> VALUEAT <index>` will return the value at the provided index
/// - `LGET <mylist> FIRST` will return the first item
/// - `LGET <mylist> LAST` will return the last item
/// if it exists
fn lget(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len != 0)?;
let listmap = handle.get_table_with::<P, KVEList>()?;
// get the list name
let listname = unsafe { act.next_unchecked() };
// now let us see what we need to do
macro_rules! get_numeric_count {
() => {
match unsafe { String::from_utf8_lossy(act.next_unchecked()) }.parse::<usize>() {
Ok(int) => int,
Err(_) => return util::err(P::RCODE_WRONGTYPE_ERR),
}
};
}
match act.next_uppercase().as_ref() {
None => {
// just return everything in the list
let items = match listmap.list_cloned_full(listname) {
Ok(Some(list)) => list,
Ok(None) => return Err(P::RCODE_NIL.into()),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
};
writelist!(con, listmap, items);
}
Some(subaction) => {
match subaction.as_ref() {
LEN => {
ensure_length::<P>(act.len(), |len| len == 0)?;
match listmap.list_len(listname) {
Ok(Some(len)) => con.write_usize(len).await?,
Ok(None) => return Err(P::RCODE_NIL.into()),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
LIMIT => {
ensure_length::<P>(act.len(), |len| len == 1)?;
let count = get_numeric_count!();
match listmap.list_cloned(listname, count) {
Ok(Some(items)) => writelist!(con, listmap, items),
Ok(None) => return Err(P::RCODE_NIL.into()),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
VALUEAT => {
ensure_length::<P>(act.len(), |len| len == 1)?;
let idx = get_numeric_count!();
let maybe_value = listmap.get(listname).map(|list| {
list.map(|lst| lst.read().get(idx).cloned())
});
match maybe_value {
Ok(v) => match v {
Some(Some(value)) => {
con.write_mono_length_prefixed_with_tsymbol(
&value, listmap.get_value_tsymbol()
).await?;
}
Some(None) => {
// bad index
return Err(P::RSTRING_LISTMAP_BAD_INDEX.into());
}
None => {
// not found
return Err(P::RCODE_NIL.into());
}
}
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
LAST => {
ensure_length::<P>(act.len(), |len| len == 0)?;
let maybe_value = listmap.get(listname).map(|list| {
list.map(|lst| lst.read().last().cloned())
});
match maybe_value {
Ok(v) => match v {
Some(Some(value)) => {
con.write_mono_length_prefixed_with_tsymbol(
&value, listmap.get_value_tsymbol()
).await?;
},
Some(None) => return Err(P::RSTRING_LISTMAP_LIST_IS_EMPTY.into()),
None => return Err(P::RCODE_NIL.into()),
}
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
FIRST => {
ensure_length::<P>(act.len(), |len| len == 0)?;
let maybe_value = listmap.get(listname).map(|list| {
list.map(|lst| lst.read().first().cloned())
});
match maybe_value {
Ok(v) => match v {
Some(Some(value)) => {
con.write_mono_length_prefixed_with_tsymbol(
&value, listmap.get_value_tsymbol()
).await?;
},
Some(None) => return Err(P::RSTRING_LISTMAP_LIST_IS_EMPTY.into()),
None => return Err(P::RCODE_NIL.into()),
}
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
RANGE => {
match act.next_string_owned() {
Some(start) => {
let start: usize = match start.parse() {
Ok(v) => v,
Err(_) => return util::err(P::RCODE_WRONGTYPE_ERR),
};
let mut range = Range::new(start);
if let Some(stop) = act.next_string_owned() {
let stop: usize = match stop.parse() {
Ok(v) => v,
Err(_) => return util::err(P::RCODE_WRONGTYPE_ERR),
};
range.set_stop(stop);
};
match listmap.get(listname) {
Ok(Some(list)) => {
let ret = range.into_vec(&list.read());
match ret {
Some(ret) => {
writelist!(con, listmap, ret);
},
None => return Err(P::RSTRING_LISTMAP_BAD_INDEX.into()),
}
}
Ok(None) => return Err(P::RCODE_NIL.into()),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
}
}
None => return Err(P::RCODE_ACTION_ERR.into()),
}
}
_ => return Err(P::RCODE_UNKNOWN_ACTION.into()),
}
}
}
Ok(())
}
}

@ -1,186 +0,0 @@
/*
* Created on Wed Sep 15 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{corestore::SharedSlice, dbnet::prelude::*, util::compiler};
const CLEAR: &[u8] = "CLEAR".as_bytes();
const PUSH: &[u8] = "PUSH".as_bytes();
const REMOVE: &[u8] = "REMOVE".as_bytes();
const INSERT: &[u8] = "INSERT".as_bytes();
const POP: &[u8] = "POP".as_bytes();
action! {
/// Handle `LMOD` queries
/// ## Syntax
/// - `LMOD <mylist> push <value>`
/// - `LMOD <mylist> pop <optional idx>`
/// - `LMOD <mylist> insert <index> <value>`
/// - `LMOD <mylist> remove <index>`
/// - `LMOD <mylist> clear`
fn lmod(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len > 1)?;
let listmap = handle.get_table_with::<P, KVEList>()?;
// get the list name
let listname = unsafe { act.next_unchecked() };
macro_rules! get_numeric_count {
() => {
match unsafe { String::from_utf8_lossy(act.next_unchecked()) }.parse::<usize>() {
Ok(int) => int,
Err(_) => return Err(P::RCODE_WRONGTYPE_ERR.into()),
}
};
}
// now let us see what we need to do
match unsafe { act.next_uppercase_unchecked() }.as_ref() {
CLEAR => {
ensure_length::<P>(act.len(), |len| len == 0)?;
let list = match listmap.get_inner_ref().get(listname) {
Some(l) => l,
_ => return Err(P::RCODE_NIL.into()),
};
let okay = if registry::state_okay() {
list.write().clear();
P::RCODE_OKAY
} else {
P::RCODE_SERVER_ERR
};
con._write_raw(okay).await?
}
PUSH => {
ensure_boolean_or_aerr::<P>(!act.is_empty())?;
let list = match listmap.get_inner_ref().get(listname) {
Some(l) => l,
_ => return Err(P::RCODE_NIL.into()),
};
let venc_ok = listmap.get_val_encoder();
let ret = if compiler::likely(act.as_ref().all(venc_ok)) {
if registry::state_okay() {
list.write().extend(act.map(SharedSlice::new));
P::RCODE_OKAY
} else {
P::RCODE_SERVER_ERR
}
} else {
P::RCODE_ENCODING_ERROR
};
con._write_raw(ret).await?
}
REMOVE => {
ensure_length::<P>(act.len(), |len| len == 1)?;
let idx_to_remove = get_numeric_count!();
if registry::state_okay() {
let maybe_value = listmap.get_inner_ref().get(listname).map(|list| {
let mut wlock = list.write();
if idx_to_remove < wlock.len() {
wlock.remove(idx_to_remove);
true
} else {
false
}
});
con._write_raw(P::OKAY_BADIDX_NIL_NLUT[maybe_value]).await?
} else {
return Err(P::RCODE_SERVER_ERR.into());
}
}
INSERT => {
ensure_length::<P>(act.len(), |len| len == 2)?;
let idx_to_insert_at = get_numeric_count!();
let bts = unsafe { act.next_unchecked() };
let ret = if compiler::likely(listmap.is_val_ok(bts)) {
if registry::state_okay() {
// okay state, good to insert
let maybe_insert = match listmap.get(listname) {
Ok(lst) => lst.map(|list| {
let mut wlock = list.write();
if idx_to_insert_at < wlock.len() {
// we can insert
wlock.insert(idx_to_insert_at, SharedSlice::new(bts));
true
} else {
// oops, out of bounds
false
}
}),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
};
P::OKAY_BADIDX_NIL_NLUT[maybe_insert]
} else {
// flush broken; server err
P::RCODE_SERVER_ERR
}
} else {
// encoding failed, uh
P::RCODE_ENCODING_ERROR
};
con._write_raw(ret).await?
}
POP => {
ensure_length::<P>(act.len(), |len| len < 2)?;
let idx = if act.len() == 1 {
// we have an idx
Some(get_numeric_count!())
} else {
// no idx
None
};
if registry::state_okay() {
let maybe_pop = match listmap.get(listname) {
Ok(lst) => lst.map(|list| {
let mut wlock = list.write();
if let Some(idx) = idx {
if idx < wlock.len() {
// so we can pop
Some(wlock.remove(idx))
} else {
None
}
} else {
wlock.pop()
}
}),
Err(()) => return Err(P::RCODE_ENCODING_ERROR.into()),
};
match maybe_pop {
Some(Some(val)) => {
con.write_mono_length_prefixed_with_tsymbol(
&val, listmap.get_value_tsymbol()
).await?;
}
Some(None) => {
con._write_raw(P::RSTRING_LISTMAP_BAD_INDEX).await?;
}
None => con._write_raw(P::RCODE_NIL).await?,
}
} else {
con._write_raw(P::RCODE_SERVER_ERR).await?
}
}
_ => con._write_raw(P::RCODE_UNKNOWN_ACTION).await?,
}
Ok(())
}
}

@ -1,57 +0,0 @@
/*
* Created on Tue Sep 07 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#[macro_use]
mod macros;
// modules
pub mod lget;
pub mod lmod;
use crate::{corestore::SharedSlice, dbnet::prelude::*, kvengine::LockedVec};
action! {
/// Handle an `LSET` query for the list model
/// Syntax: `LSET <listname> <values ...>`
fn lset(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len > 0)?;
let listmap = handle.get_table_with::<P, KVEList>()?;
let listname = unsafe { act.next_unchecked_bytes() };
let list = listmap.get_inner_ref();
if registry::state_okay() {
let did = if let Some(entry) = list.fresh_entry(listname) {
let v: Vec<SharedSlice> = act.map(SharedSlice::new).collect();
entry.insert(LockedVec::new(v));
true
} else {
false
};
con._write_raw(P::OKAY_OVW_BLUT[did]).await?
} else {
con._write_raw(P::RCODE_SERVER_ERR).await?
}
Ok(())
}
}

@ -1,83 +0,0 @@
/*
* Created on Thu May 13 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
corestore::{table::DataModel, SharedSlice},
dbnet::prelude::*,
};
const DEFAULT_COUNT: usize = 10;
action!(
/// Run an `LSKEYS` query
fn lskeys(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |size| size < 4)?;
let (table, count) = if act.is_empty() {
(get_tbl!(handle, con), DEFAULT_COUNT)
} else if act.len() == 1 {
// two args, could either be count or an entity
let nextret = unsafe { act.next_unchecked() };
if unsafe { ucidx!(nextret, 0) }.is_ascii_digit() {
// noice, this is a number; let's try to parse it
let count = if let Ok(cnt) = String::from_utf8_lossy(nextret).parse::<usize>() {
cnt
} else {
return util::err(P::RCODE_WRONGTYPE_ERR);
};
(get_tbl!(handle, con), count)
} else {
// sigh, an entity
let entity = handle_entity!(con, nextret);
(get_tbl!(&entity, handle, con), DEFAULT_COUNT)
}
} else {
// an entity and a count, gosh this fella is really trying us
let entity_ret = unsafe { act.next().unsafe_unwrap() };
let count_ret = unsafe { act.next().unsafe_unwrap() };
let entity = handle_entity!(con, entity_ret);
let count = if let Ok(cnt) = String::from_utf8_lossy(count_ret).parse::<usize>() {
cnt
} else {
return util::err(P::RCODE_WRONGTYPE_ERR);
};
(get_tbl!(&entity, handle, con), count)
};
let tsymbol = match table.get_model_ref() {
DataModel::KV(kv) => kv.get_value_tsymbol(),
DataModel::KVExtListmap(kv) => kv.get_value_tsymbol(),
};
let items: Vec<SharedSlice> = match table.get_model_ref() {
DataModel::KV(kv) => kv.get_inner_ref().get_keys(count),
DataModel::KVExtListmap(kv) => kv.get_inner_ref().get_keys(count),
};
con.write_typed_non_null_array_header(items.len(), tsymbol)
.await?;
for key in items {
con.write_typed_non_null_array_element(&key).await?;
}
Ok(())
}
);

@ -1,82 +0,0 @@
/*
* Created on Thu Nov 11 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
/*
Don't modulo because it's an L1 miss and an L2 hit. Use lowbit checks to check for parity
*/
#[macro_export]
/// endian independent check to see if the lowbit is set or not. Returns true if the lowbit
/// is set. this is undefined to be applied on signed values on one's complement targets
macro_rules! is_lowbit_set {
($v:expr) => {
$v & 1 == 1
};
}
#[macro_export]
/// endian independent check to see if the lowbit is unset or not. Returns true if the lowbit
/// is unset. this is undefined to be applied on signed values on one's complement targets
macro_rules! is_lowbit_unset {
($v:expr) => {
$v & 1 == 0
};
}
#[macro_export]
macro_rules! get_tbl {
($entity:expr, $store:expr, $con:expr) => {{
$crate::actions::translate_ddl_error::<P, ::std::sync::Arc<$crate::corestore::table::Table>>(
$store.get_table($entity),
)?
}};
($store:expr, $con:expr) => {{
match $store.get_ctable() {
Some(tbl) => tbl,
None => return $crate::util::err(P::RSTRING_DEFAULT_UNSET),
}
}};
}
#[macro_export]
macro_rules! get_tbl_ref {
($store:expr, $con:expr) => {{
match $store.get_ctable_ref() {
Some(tbl) => tbl,
None => return $crate::util::err(P::RSTRING_DEFAULT_UNSET),
}
}};
}
#[macro_export]
macro_rules! handle_entity {
($con:expr, $ident:expr) => {{
match $crate::blueql::util::from_slice_action_result::<P>(&$ident) {
Ok(e) => e,
Err(e) => return Err(e.into()),
}
}};
}

@ -1,53 +0,0 @@
/*
* Created on Thu Aug 27 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
dbnet::prelude::*, kvengine::encoding::ENCODING_LUT_ITER, queryengine::ActionIter,
util::compiler,
};
action!(
/// Run an `MGET` query
///
fn mget(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |size| size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
let encoding_is_okay = ENCODING_LUT_ITER[kve.is_key_encoded()](act.as_ref());
if compiler::likely(encoding_is_okay) {
con.write_typed_array_header(act.len(), kve.get_value_tsymbol())
.await?;
for key in act {
match kve.get_cloned_unchecked(key) {
Some(v) => con.write_typed_array_element(&v).await?,
None => con.write_typed_array_element_null().await?,
}
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
Ok(())
}
);

@ -1,148 +0,0 @@
/*
* Created on Wed Aug 19 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # Actions
//!
//! Actions are like shell commands, you provide arguments -- they return output. This module contains a collection
//! of the actions supported by Skytable
//!
#[macro_use]
mod macros;
pub mod dbsize;
pub mod del;
pub mod exists;
pub mod flushdb;
pub mod get;
pub mod keylen;
pub mod lists;
pub mod lskeys;
pub mod mget;
pub mod mpop;
pub mod mset;
pub mod mupdate;
pub mod pop;
pub mod set;
pub mod strong;
pub mod update;
pub mod uset;
pub mod whereami;
use {
crate::{corestore::memstore::DdlError, protocol::interface::ProtocolSpec, util},
std::io::Error as IoError,
};
/// A generic result for actions
pub type ActionResult<T> = Result<T, ActionError>;
/// Errors that can occur while running actions
#[derive(Debug)]
pub enum ActionError {
ActionError(&'static [u8]),
IoError(std::io::Error),
}
impl PartialEq for ActionError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::ActionError(a1), Self::ActionError(a2)) => a1 == a2,
(Self::IoError(ioe1), Self::IoError(ioe2)) => ioe1.to_string() == ioe2.to_string(),
_ => false,
}
}
}
impl From<&'static [u8]> for ActionError {
fn from(e: &'static [u8]) -> Self {
Self::ActionError(e)
}
}
impl From<IoError> for ActionError {
fn from(e: IoError) -> Self {
Self::IoError(e)
}
}
#[cold]
#[inline(never)]
fn map_ddl_error_to_status<P: ProtocolSpec>(e: DdlError) -> ActionError {
let r = match e {
DdlError::AlreadyExists => P::RSTRING_ALREADY_EXISTS,
DdlError::DdlTransactionFailure => P::RSTRING_DDL_TRANSACTIONAL_FAILURE,
DdlError::DefaultNotFound => P::RSTRING_DEFAULT_UNSET,
DdlError::NotEmpty => P::RSTRING_KEYSPACE_NOT_EMPTY,
DdlError::NotReady => P::RSTRING_NOT_READY,
DdlError::ObjectNotFound => P::RSTRING_CONTAINER_NOT_FOUND,
DdlError::ProtectedObject => P::RSTRING_PROTECTED_OBJECT,
DdlError::StillInUse => P::RSTRING_STILL_IN_USE,
DdlError::WrongModel => P::RSTRING_WRONG_MODEL,
};
ActionError::ActionError(r)
}
#[inline(always)]
pub fn translate_ddl_error<P: ProtocolSpec, T>(r: Result<T, DdlError>) -> Result<T, ActionError> {
match r {
Ok(r) => Ok(r),
Err(e) => Err(map_ddl_error_to_status::<P>(e)),
}
}
pub fn ensure_length<P: ProtocolSpec>(len: usize, is_valid: fn(usize) -> bool) -> ActionResult<()> {
if util::compiler::likely(is_valid(len)) {
Ok(())
} else {
util::err(P::RCODE_ACTION_ERR)
}
}
pub fn ensure_boolean_or_aerr<P: ProtocolSpec>(boolean: bool) -> ActionResult<()> {
if util::compiler::likely(boolean) {
Ok(())
} else {
util::err(P::RCODE_ACTION_ERR)
}
}
pub mod heya {
//! Respond to `HEYA` queries
use crate::dbnet::prelude::*;
action!(
/// Returns a `HEY!` `Response`
fn heya(_handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len == 0 || len == 1)?;
if act.len() == 1 {
let raw_byte = unsafe { act.next_unchecked() };
con.write_mono_length_prefixed_with_tsymbol(raw_byte, b'+')
.await?;
} else {
con._write_raw(P::ELEMRESP_HEYA).await?;
}
Ok(())
}
);
}

@ -1,57 +0,0 @@
/*
* Created on Wed Aug 11 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
corestore, dbnet::prelude::*, kvengine::encoding::ENCODING_LUT_ITER,
queryengine::ActionIter, util::compiler,
};
action!(
/// Run an MPOP action
fn mpop(handle: &corestore::Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len != 0)?;
if registry::state_okay() {
let kve = handle.get_table_with::<P, KVEBlob>()?;
let encoding_is_okay = ENCODING_LUT_ITER[kve.is_key_encoded()](act.as_ref());
if compiler::likely(encoding_is_okay) {
con.write_typed_array_header(act.len(), kve.get_value_tsymbol())
.await?;
for key in act {
match kve.pop_unchecked(key) {
Some(val) => con.write_typed_array_element(&val).await?,
None => con.write_typed_array_element_null().await?,
}
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
} else {
// don't begin the operation at all if the database is poisoned
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
);

@ -1,60 +0,0 @@
/*
* Created on Thu Aug 27 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
corestore::SharedSlice, dbnet::prelude::*,
kvengine::encoding::ENCODING_LUT_ITER_PAIR, util::compiler,
};
action!(
/// Run an `MSET` query
fn mset(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
let howmany = act.len();
ensure_length::<P>(howmany, |size| size & 1 == 0 && size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
let encoding_is_okay = ENCODING_LUT_ITER_PAIR[kve.get_encoding_tuple()](&act);
if compiler::likely(encoding_is_okay) {
let done_howmany: Option<usize> = if registry::state_okay() {
let mut didmany = 0;
while let (Some(key), Some(val)) = (act.next(), act.next()) {
didmany +=
kve.set_unchecked(SharedSlice::new(key), SharedSlice::new(val)) as usize;
}
Some(didmany)
} else {
None
};
if let Some(done_howmany) = done_howmany {
con.write_usize(done_howmany).await?;
} else {
return util::err(P::RCODE_SERVER_ERR);
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
Ok(())
}
);

@ -1,61 +0,0 @@
/*
* Created on Thu Aug 27 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
corestore::SharedSlice, dbnet::prelude::*,
kvengine::encoding::ENCODING_LUT_ITER_PAIR, util::compiler,
};
action!(
/// Run an `MUPDATE` query
fn mupdate(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
let howmany = act.len();
ensure_length::<P>(howmany, |size| size & 1 == 0 && size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
let encoding_is_okay = ENCODING_LUT_ITER_PAIR[kve.get_encoding_tuple()](&act);
let done_howmany: Option<usize>;
if compiler::likely(encoding_is_okay) {
if registry::state_okay() {
let mut didmany = 0;
while let (Some(key), Some(val)) = (act.next(), act.next()) {
didmany +=
kve.update_unchecked(SharedSlice::new(key), SharedSlice::new(val)) as usize;
}
done_howmany = Some(didmany);
} else {
done_howmany = None;
}
if let Some(done_howmany) = done_howmany {
con.write_usize(done_howmany).await?;
} else {
return util::err(P::RCODE_SERVER_ERR);
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
Ok(())
}
);

@ -1,50 +0,0 @@
/*
* Created on Mon Jun 14 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::dbnet::prelude::*;
action! {
fn pop(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len == 1)?;
let key = unsafe {
// SAFETY: We have checked for there to be one arg
act.next_unchecked()
};
if registry::state_okay() {
let kve = handle.get_table_with::<P, KVEBlob>()?;
match kve.pop(key) {
Ok(Some(val)) => con.write_mono_length_prefixed_with_tsymbol(
&val, kve.get_value_tsymbol()
).await?,
Ok(None) => return util::err(P::RCODE_NIL),
Err(()) => return util::err(P::RCODE_ENCODING_ERROR),
}
} else {
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
}

@ -1,58 +0,0 @@
/*
* Created on Fri Aug 14 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # `SET` queries
//! This module provides functions to work with `SET` queries
use crate::{corestore::SharedSlice, dbnet::prelude::*, queryengine::ActionIter};
action!(
/// Run a `SET` query
fn set(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len == 2)?;
if registry::state_okay() {
let did_we = {
let writer = handle.get_table_with::<P, KVEBlob>()?;
match unsafe {
// UNSAFE(@ohsayan): This is completely safe as we've already checked
// that there are exactly 2 arguments
writer.set(
SharedSlice::new(act.next().unsafe_unwrap()),
SharedSlice::new(act.next().unsafe_unwrap()),
)
} {
Ok(true) => Some(true),
Ok(false) => Some(false),
Err(()) => None,
}
};
con._write_raw(P::SET_NLUT[did_we]).await?;
} else {
con._write_raw(P::RCODE_SERVER_ERR).await?;
}
Ok(())
}
);

@ -1,65 +0,0 @@
/*
* Created on Fri Jul 30 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # Strong Actions
//! Strong actions are like "do all" or "fail all" actions, built specifically for
//! multiple keys. So let's say you used `SSET` instead of `MSET` for setting keys:
//! what'd be the difference?
//! In this case, if all the keys are non-existing, which is a requirement for `MSET`,
//! only then would the keys be set. That is, only if all the keys can be set, will the action
//! run and return code `0` - otherwise the action won't do anything and return an overwrite error.
//! There is no point of using _strong actions_ for a single key/value pair, since it will only
//! slow things down due to the checks performed.
//! Do note that this isn't the same as the gurantees provided by ACID transactions
pub use self::{sdel::sdel, sset::sset, supdate::supdate};
mod sdel;
mod sset;
mod supdate;
#[cfg(test)]
mod tests;
#[derive(Debug)]
enum StrongActionResult {
/// Internal server error
ServerError,
/// A single value was not found
Nil,
/// An overwrite error for a single value
OverwriteError,
/// An encoding error occurred
EncodingError,
/// Everything worked as expected
Okay,
}
#[cfg(test)]
impl StrongActionResult {
pub const fn is_ok(&self) -> bool {
matches!(self, StrongActionResult::Okay)
}
}

@ -1,139 +0,0 @@
/*
* Created on Fri Jul 30 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
actions::strong::StrongActionResult,
dbnet::prelude::*,
kvengine::{KVEStandard, SingleEncoder},
protocol::iter::DerefUnsafeSlice,
util::compiler,
},
core::slice::Iter,
};
action! {
/// Run an `SDEL` query
///
/// This either returns `Okay` if all the keys were `del`eted, or it returns a
/// `Nil`, which is code `1`
fn sdel(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
if registry::state_okay() {
// guarantee one check: consistency
let key_encoder = kve.get_key_encoder();
let outcome = unsafe {
// UNSAFE(@ohsayan): The lifetime of `act` ensures that the
// pointers are still valid
self::snapshot_and_del(kve, key_encoder, act.into_inner())
};
match outcome {
StrongActionResult::Okay => con._write_raw(P::RCODE_OKAY).await?,
StrongActionResult::Nil => {
// good, it failed because some key didn't exist
return util::err(P::RCODE_NIL);
},
StrongActionResult::ServerError => return util::err(P::RCODE_SERVER_ERR),
StrongActionResult::EncodingError => {
// error we love to hate: encoding error, ugh
return util::err(P::RCODE_ENCODING_ERROR);
},
StrongActionResult::OverwriteError => unsafe {
// SAFETY check: never the case
impossible!()
}
}
} else {
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
}
/// Snapshot the current status and then delete maintaining concurrency
/// guarantees
pub(super) fn snapshot_and_del<'a, T: 'a + DerefUnsafeSlice>(
kve: &'a KVEStandard,
key_encoder: SingleEncoder,
act: Iter<'a, T>,
) -> StrongActionResult {
let mut snapshots = Vec::with_capacity(act.len());
let mut err_enc = false;
let iter_stat_ok;
{
iter_stat_ok = act.as_ref().iter().all(|key| {
let key = unsafe {
// UNSAFE(@ohsayan): The caller has passed a slice and they should
// ensure that it is valid
key.deref_slice()
};
if compiler::likely(key_encoder(key)) {
if let Some(snap) = kve.take_snapshot_unchecked(key) {
snapshots.push(snap);
true
} else {
false
}
} else {
err_enc = true;
false
}
});
}
cfg_test!({
// give the caller 10 seconds to do some crap
do_sleep!(10 s);
});
if compiler::unlikely(err_enc) {
return compiler::cold_err(StrongActionResult::EncodingError);
}
if registry::state_okay() {
// guarantee upholded: consistency
if iter_stat_ok {
// nice, all keys exist; let's plonk 'em
let kve = kve;
let lowtable = kve.get_inner_ref();
act.zip(snapshots).for_each(|(key, snapshot)| {
let key = unsafe {
// UNSAFE(@ohsayan): The caller has passed a slice and they should
// ensure that it is valid
key.deref_slice()
};
// the check is very important: some thread may have updated the
// value after we snapshotted it. In that case, let this key
// be whatever the "newer" value is. Since our snapshot is a "happens-before"
// thing, this is absolutely fine
let _ = lowtable.remove_if(key, |_, val| val.eq(&snapshot));
});
StrongActionResult::Okay
} else {
StrongActionResult::Nil
}
} else {
StrongActionResult::ServerError
}
}

@ -1,127 +0,0 @@
/*
* Created on Fri Jul 30 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
actions::strong::StrongActionResult,
corestore::SharedSlice,
dbnet::prelude::*,
kvengine::{DoubleEncoder, KVEStandard},
protocol::iter::DerefUnsafeSlice,
util::compiler,
},
core::slice::Iter,
};
action! {
/// Run an `SSET` query
///
/// This either returns `Okay` if all the keys were set, or it returns an
/// `Overwrite Error` or code `2`
fn sset(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
let howmany = act.len();
ensure_length::<P>(howmany, |size| size & 1 == 0 && size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
if registry::state_okay() {
let encoder = kve.get_double_encoder();
let outcome = unsafe {
// UNSAFE(@ohsayan): The lifetime of `act` guarantees that the
// pointers remain valid
self::snapshot_and_insert(kve, encoder, act.into_inner())
};
match outcome {
StrongActionResult::Okay => con._write_raw(P::RCODE_OKAY).await?,
StrongActionResult::OverwriteError => return util::err(P::RCODE_OVERWRITE_ERR),
StrongActionResult::ServerError => return util::err(P::RCODE_SERVER_ERR),
StrongActionResult::EncodingError => {
// error we love to hate: encoding error, ugh
return util::err(P::RCODE_ENCODING_ERROR);
},
StrongActionResult::Nil => unsafe {
// SAFETY check: never the case
impossible!()
}
}
} else {
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
}
/// Take a consistent snapshot of the database at this current point in time
/// and then mutate the entries, respecting concurrency guarantees
pub(super) fn snapshot_and_insert<'a, T: 'a + DerefUnsafeSlice>(
kve: &'a KVEStandard,
encoder: DoubleEncoder,
mut act: Iter<'a, T>,
) -> StrongActionResult {
let mut enc_err = false;
let lowtable = kve.get_inner_ref();
let key_iter_stat_ok;
{
key_iter_stat_ok = act.as_ref().chunks_exact(2).all(|kv| unsafe {
let key = ucidx!(kv, 0).deref_slice();
let value = ucidx!(kv, 1).deref_slice();
if compiler::likely(encoder(key, value)) {
lowtable.get(key).is_none()
} else {
enc_err = true;
false
}
});
}
cfg_test!({
// give the caller 10 seconds to do some crap
do_sleep!(10 s);
});
if compiler::unlikely(enc_err) {
return compiler::cold_err(StrongActionResult::EncodingError);
}
if registry::state_okay() {
if key_iter_stat_ok {
let _kve = kve;
let lowtable = lowtable;
// fine, the keys were non-existent when we looked at them
while let (Some(key), Some(value)) = (act.next(), act.next()) {
unsafe {
if let Some(fresh) =
lowtable.fresh_entry(SharedSlice::new(key.deref_slice()))
{
fresh.insert(SharedSlice::new(value.deref_slice()));
}
// we don't care if some other thread initialized the value we checked
// it. We expected a fresh entry, so that's what we'll check and use
}
}
StrongActionResult::Okay
} else {
StrongActionResult::OverwriteError
}
} else {
StrongActionResult::ServerError
}
}

@ -1,144 +0,0 @@
/*
* Created on Fri Jul 30 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
actions::strong::StrongActionResult,
corestore::SharedSlice,
dbnet::prelude::*,
kvengine::{DoubleEncoder, KVEStandard},
protocol::iter::DerefUnsafeSlice,
util::compiler,
},
core::slice::Iter,
};
action! {
/// Run an `SUPDATE` query
///
/// This either returns `Okay` if all the keys were updated, or it returns `Nil`
/// or code `1`
fn supdate(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, act: ActionIter<'a>) {
let howmany = act.len();
ensure_length::<P>(howmany, |size| size & 1 == 0 && size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
if registry::state_okay() {
let encoder = kve.get_double_encoder();
let outcome = unsafe {
// UNSAFE(@ohsayan): the lifetime of `act` ensure ptr validity
self::snapshot_and_update(kve, encoder, act.into_inner())
};
match outcome {
StrongActionResult::Okay => con._write_raw(P::RCODE_OKAY).await?,
StrongActionResult::Nil => {
// good, it failed because some key didn't exist
return util::err(P::RCODE_NIL);
},
StrongActionResult::ServerError => return util::err(P::RCODE_SERVER_ERR),
StrongActionResult::EncodingError => {
// error we love to hate: encoding error, ugh
return util::err(P::RCODE_ENCODING_ERROR);
},
StrongActionResult::OverwriteError => unsafe {
// SAFETY check: never the case
impossible!()
}
}
} else {
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
}
/// Take a consistent snapshot of the database at this point in time. Once snapshotting
/// completes, mutate the entries in place while keeping up with isolation guarantees
/// `(all_okay, enc_err)`
pub(super) fn snapshot_and_update<'a, T: 'a + DerefUnsafeSlice>(
kve: &'a KVEStandard,
encoder: DoubleEncoder,
mut act: Iter<'a, T>,
) -> StrongActionResult {
let mut enc_err = false;
let mut snapshots = Vec::with_capacity(act.len());
let iter_stat_ok;
{
// snapshot the values at this point in time
iter_stat_ok = act.as_ref().chunks_exact(2).all(|kv| unsafe {
let key = ucidx!(kv, 0).deref_slice();
let value = ucidx!(kv, 1).deref_slice();
if compiler::likely(encoder(key, value)) {
if let Some(snapshot) = kve.take_snapshot_unchecked(key) {
snapshots.push(snapshot);
true
} else {
false
}
} else {
enc_err = true;
false
}
});
}
cfg_test!({
// give the caller 10 seconds to do some crap
do_sleep!(10 s);
});
if compiler::unlikely(enc_err) {
return compiler::cold_err(StrongActionResult::EncodingError);
}
if registry::state_okay() {
// uphold consistency
if iter_stat_ok {
let kve = kve;
// good, so all the values existed when we snapshotted them; let's update 'em
let mut snap_cc = snapshots.into_iter();
let lowtable = kve.get_inner_ref();
while let (Some(key), Some(value), Some(snapshot)) =
(act.next(), act.next(), snap_cc.next())
{
unsafe {
// When we snapshotted, we looked at `snapshot`. If the value is still the
// same, then we'll update it. Otherwise, let it be
if let Some(mut mutable) =
lowtable.mut_entry(SharedSlice::new(key.deref_slice()))
{
if mutable.value().eq(&snapshot) {
mutable.insert(SharedSlice::new(value.deref_slice()));
} else {
drop(mutable);
}
}
}
}
StrongActionResult::Okay
} else {
StrongActionResult::Nil
}
} else {
StrongActionResult::ServerError
}
}

@ -1,168 +0,0 @@
/*
* Created on Fri Jul 30 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
mod sdel_concurrency_tests {
use {
super::super::sdel,
crate::{corestore::SharedSlice, kvengine::KVEStandard},
std::{sync::Arc, thread},
};
#[test]
fn test_snapshot_okay() {
let kve = KVEStandard::init(true, true);
kve.upsert(SharedSlice::from("k1"), SharedSlice::from("v1"))
.unwrap();
kve.upsert(SharedSlice::from("k2"), SharedSlice::from("v2"))
.unwrap();
let encoder = kve.get_key_encoder();
let it = bi!("k1", "k2");
let ret = sdel::snapshot_and_del(&kve, encoder, it.as_ref().iter().as_ref().iter());
assert!(ret.is_ok());
}
#[test]
fn test_sdel_snapshot_fail_with_t2() {
let kve = Arc::new(KVEStandard::init(true, true));
let kve1 = kve.clone();
let encoder = kve.get_key_encoder();
{
kve.upsert(SharedSlice::from("k1"), SharedSlice::from("v1"))
.unwrap();
kve.upsert(SharedSlice::from("k2"), SharedSlice::from("v2"))
.unwrap();
}
let it = bi!("k1", "k2");
// sdel will wait 10s for us
let t1handle = thread::spawn(move || {
sdel::snapshot_and_del(&kve1, encoder, it.as_ref().iter().as_ref().iter())
});
// we have 10s: we sleep 5 to let the snapshot complete (thread spawning takes time)
do_sleep!(5 s);
assert!(kve
.update(SharedSlice::from("k1"), SharedSlice::from("updated-v1"))
.unwrap());
// let us join t1
let ret = t1handle.join().unwrap();
assert!(ret.is_ok());
// although we told sdel to delete it, it.as_ref().iter() shouldn't because we externally
// updated the value
assert!(kve.exists(&SharedSlice::from("k1")).unwrap());
}
}
mod sset_concurrency_tests {
use {
super::super::sset,
crate::{corestore::SharedSlice, kvengine::KVEStandard},
std::{sync::Arc, thread},
};
#[test]
fn test_snapshot_okay() {
let kve = KVEStandard::init(true, true);
let encoder = kve.get_double_encoder();
let it = bi!("k1", "v1", "k2", "v2");
let ret = sset::snapshot_and_insert(&kve, encoder, it.as_ref().iter());
assert!(ret.is_ok());
}
#[test]
fn test_sset_snapshot_fail_with_t2() {
let kve = Arc::new(KVEStandard::init(true, true));
let kve1 = kve.clone();
let encoder = kve.get_double_encoder();
let it = bi!("k1", "v1", "k2", "v2");
// sset will wait 10s for us
let t1handle =
thread::spawn(move || sset::snapshot_and_insert(&kve1, encoder, it.as_ref().iter()));
// we have 10s: we sleep 5 to let the snapshot complete (thread spawning takes time)
do_sleep!(5 s);
// update the value externally
assert!(kve
.set(SharedSlice::from("k1"), SharedSlice::from("updated-v1"))
.unwrap());
// let us join t1
let ret = t1handle.join().unwrap();
// but set won't fail because someone set it before it did; this is totally
// acceptable because we only wanted to set it if it matches the status when
// we created a snapshot
assert!(ret.is_ok());
// although we told sset to set a key, but it shouldn't because we updated it
assert_eq!(
kve.get(&SharedSlice::from("k1")).unwrap().unwrap().clone(),
SharedSlice::from("updated-v1")
);
}
}
mod supdate_concurrency_tests {
use {
super::super::supdate,
crate::{corestore::SharedSlice, kvengine::KVEStandard},
std::{sync::Arc, thread},
};
#[test]
fn test_snapshot_okay() {
let kve = KVEStandard::init(true, true);
kve.upsert(SharedSlice::from("k1"), SharedSlice::from("v1"))
.unwrap();
kve.upsert(SharedSlice::from("k2"), SharedSlice::from("v2"))
.unwrap();
let encoder = kve.get_double_encoder();
let it = bi!("k1", "v1", "k2", "v2");
let ret = supdate::snapshot_and_update(&kve, encoder, it.as_ref().iter());
assert!(ret.is_ok());
}
#[test]
fn test_supdate_snapshot_fail_with_t2() {
let kve = Arc::new(KVEStandard::init(true, true));
kve.upsert(SharedSlice::from("k1"), SharedSlice::from("v1"))
.unwrap();
kve.upsert(SharedSlice::from("k2"), SharedSlice::from("v2"))
.unwrap();
let kve1 = kve.clone();
let encoder = kve.get_double_encoder();
let it = bi!("k1", "v1", "k2", "v2");
// supdate will wait 10s for us
let t1handle =
thread::spawn(move || supdate::snapshot_and_update(&kve1, encoder, it.as_ref().iter()));
// we have 10s: we sleep 5 to let the snapshot complete (thread spawning takes time)
do_sleep!(5 s);
// lets update the value externally
assert!(kve
.update(SharedSlice::from("k1"), SharedSlice::from("updated-v1"))
.unwrap());
// let us join t1
let ret = t1handle.join().unwrap();
assert!(ret.is_ok());
// although we told supdate to update the key, it.as_ref().iter() shouldn't because we updated it
// externally; hence our `updated-v1` value should persist
assert_eq!(
kve.get(&SharedSlice::from("k1")).unwrap().unwrap().clone(),
SharedSlice::from("updated-v1")
);
}
}

@ -1,59 +0,0 @@
/*
* Created on Mon Aug 17 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
//! # `UPDATE` queries
//! This module provides functions to work with `UPDATE` queries
//!
use crate::{corestore::SharedSlice, dbnet::prelude::*};
action!(
/// Run an `UPDATE` query
fn update(handle: &Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
ensure_length::<P>(act.len(), |len| len == 2)?;
if registry::state_okay() {
let did_we = {
let writer = handle.get_table_with::<P, KVEBlob>()?;
match unsafe {
// UNSAFE(@ohsayan): This is completely safe as we've already checked
// that there are exactly 2 arguments
writer.update(
SharedSlice::new(act.next_unchecked()),
SharedSlice::new(act.next_unchecked()),
)
} {
Ok(true) => Some(true),
Ok(false) => Some(false),
Err(()) => None,
}
};
con._write_raw(P::UPDATE_NLUT[did_we]).await?;
} else {
return util::err(P::RCODE_SERVER_ERR);
}
Ok(())
}
);

@ -1,55 +0,0 @@
/*
* Created on Fri Sep 25 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use crate::{
corestore::SharedSlice, dbnet::prelude::*,
kvengine::encoding::ENCODING_LUT_ITER_PAIR, queryengine::ActionIter, util::compiler,
};
action!(
/// Run an `USET` query
///
/// This is like "INSERT or UPDATE"
fn uset(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
let howmany = act.len();
ensure_length::<P>(howmany, |size| size & 1 == 0 && size != 0)?;
let kve = handle.get_table_with::<P, KVEBlob>()?;
let encoding_is_okay = ENCODING_LUT_ITER_PAIR[kve.get_encoding_tuple()](&act);
if compiler::likely(encoding_is_okay) {
if registry::state_okay() {
while let (Some(key), Some(val)) = (act.next(), act.next()) {
kve.upsert_unchecked(SharedSlice::new(key), SharedSlice::new(val));
}
con.write_usize(howmany / 2).await?;
} else {
return util::err(P::RCODE_SERVER_ERR);
}
} else {
return util::err(P::RCODE_ENCODING_ERROR);
}
Ok(())
}
);

@ -1,96 +0,0 @@
/*
* Created on Tue Oct 13 2020
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2020, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
dbnet::prelude::*, kvengine::encoding,
storage::v1::sengine::SnapshotActionResult,
},
core::str,
std::path::{Component, PathBuf},
};
action!(
/// Create a snapshot
///
fn mksnap(handle: &crate::corestore::Corestore, con: &mut Connection<C, P>, mut act: ActionIter<'a>) {
let engine = handle.get_engine();
if act.is_empty() {
// traditional mksnap
match engine.mksnap(handle.clone_store()).await {
SnapshotActionResult::Ok => con._write_raw(P::RCODE_OKAY).await?,
SnapshotActionResult::Failure => return util::err(P::RCODE_SERVER_ERR),
SnapshotActionResult::Disabled => return util::err(P::RSTRING_SNAPSHOT_DISABLED),
SnapshotActionResult::Busy => return util::err(P::RSTRING_SNAPSHOT_BUSY),
_ => unsafe { impossible!() },
}
} else if act.len() == 1 {
// remote snapshot, let's see what we've got
let name = unsafe {
// SAFETY: We have already checked that there is one item
act.next_unchecked_bytes()
};
if !encoding::is_utf8(&name) {
return util::err(P::RCODE_ENCODING_ERROR);
}
// SECURITY: Check for directory traversal syntax
let st = unsafe {
// SAFETY: We have already checked for UTF-8 validity
str::from_utf8_unchecked(&name)
};
let path = PathBuf::from(st);
let illegal_snapshot = path
.components()
.filter(|dir| {
// Sanitize snapshot name, to avoid directory traversal attacks
// If the snapshot name has any root directory or parent directory, then
// we'll allow it to pass through this adaptor.
// As a result, this iterator will give us a count of the 'bad' components
dir == &Component::RootDir || dir == &Component::ParentDir
})
.count()
!= 0;
if illegal_snapshot {
return util::err(P::RSTRING_SNAPSHOT_ILLEGAL_NAME);
}
// now make the snapshot
match engine.mkrsnap(&name, handle.clone_store()).await {
SnapshotActionResult::Ok => con._write_raw(P::RCODE_OKAY).await?,
SnapshotActionResult::Failure => return util::err(P::RCODE_SERVER_ERR),
SnapshotActionResult::Busy => return util::err(P::RSTRING_SNAPSHOT_BUSY),
SnapshotActionResult::AlreadyExists => {
return util::err(P::RSTRING_SNAPSHOT_DUPLICATE)
}
_ => unsafe { impossible!() },
}
} else {
return util::err(P::RCODE_ACTION_ERR);
}
Ok(())
}
);

@ -1,84 +0,0 @@
/*
* Created on Tue Mar 29 2022
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2022, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
corestore::booltable::BoolTable, dbnet::prelude::*,
storage::v1::interface::DIR_ROOT,
},
libsky::VERSION,
};
const INFO: &[u8] = b"info";
const METRIC: &[u8] = b"metric";
const INFO_PROTOCOL: &[u8] = b"protocol";
const INFO_PROTOVER: &[u8] = b"protover";
const INFO_VERSION: &[u8] = b"version";
const METRIC_HEALTH: &[u8] = b"health";
const METRIC_STORAGE_USAGE: &[u8] = b"storage";
const ERR_UNKNOWN_PROPERTY: &[u8] = b"!16\nunknown-property\n";
const ERR_UNKNOWN_METRIC: &[u8] = b"!14\nunknown-metric\n";
const HEALTH_TABLE: BoolTable<&str> = BoolTable::new("good", "critical");
action! {
fn sys(_handle: &Corestore, con: &mut Connection<C, P>, iter: ActionIter<'_>) {
let mut iter = iter;
ensure_boolean_or_aerr::<P>(iter.len() == 2)?;
match unsafe { iter.next_lowercase_unchecked() }.as_ref() {
INFO => sys_info(con, &mut iter).await,
METRIC => sys_metric(con, &mut iter).await,
_ => util::err(P::RCODE_UNKNOWN_ACTION),
}
}
fn sys_info(con: &mut Connection<C, P>, iter: &mut ActionIter<'_>) {
match unsafe { iter.next_lowercase_unchecked() }.as_ref() {
INFO_PROTOCOL => con.write_string(P::PROTOCOL_VERSIONSTRING).await?,
INFO_PROTOVER => con.write_float(P::PROTOCOL_VERSION).await?,
INFO_VERSION => con.write_string(VERSION).await?,
_ => return util::err(ERR_UNKNOWN_PROPERTY),
}
Ok(())
}
fn sys_metric(con: &mut Connection<C, P>, iter: &mut ActionIter<'_>) {
match unsafe { iter.next_lowercase_unchecked() }.as_ref() {
METRIC_HEALTH => {
con.write_string(HEALTH_TABLE[registry::state_okay()]).await?
}
METRIC_STORAGE_USAGE => {
match util::os::dirsize(DIR_ROOT) {
Ok(size) => con.write_int64(size).await?,
Err(e) => {
log::error!("Failed to get storage usage with: {e}");
return util::err(P::RCODE_SERVER_ERR);
},
}
}
_ => return util::err(ERR_UNKNOWN_METRIC),
}
Ok(())
}
}

@ -1,211 +0,0 @@
/*
* Created on Sat Jun 26 2021
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2021, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
crate::{
auth::AuthProvider,
config::{ConfigurationSet, SnapshotConfig, SnapshotPref},
corestore::Corestore,
dbnet,
diskstore::flock::FileLock,
services,
storage::v1::sengine::SnapshotEngine,
util::{
error::{Error, SkyResult},
os::TerminationSignal,
},
},
std::{sync::Arc, thread::sleep},
tokio::{
sync::{
broadcast,
mpsc::{self, Sender},
},
task::{self, JoinHandle},
time::Duration,
},
};
const TERMSIG_THRESHOLD: usize = 3;
/// Start the server waiting for incoming connections or a termsig
pub async fn run(
ConfigurationSet {
ports,
bgsave,
snapshot,
maxcon,
auth,
protocol,
..
}: ConfigurationSet,
restore_filepath: Option<String>,
) -> SkyResult<Corestore> {
// Intialize the broadcast channel
let (signal, _) = broadcast::channel(1);
let engine = match &snapshot {
SnapshotConfig::Enabled(SnapshotPref { atmost, .. }) => SnapshotEngine::new(*atmost),
SnapshotConfig::Disabled => SnapshotEngine::new_disabled(),
};
let engine = Arc::new(engine);
// restore data
services::restore_data(restore_filepath)
.map_err(|e| Error::ioerror_extra(e, "restoring data from backup"))?;
// init the store
let db = Corestore::init_with_snapcfg(engine.clone())?;
// refresh the snapshotengine state
engine.parse_dir()?;
let auth_provider = match auth.origin_key {
Some(key) => {
let authref = db.get_store().setup_auth();
AuthProvider::new(authref, Some(key.into_inner()))
}
None => AuthProvider::new_disabled(),
};
// initialize the background services
let bgsave_handle = tokio::spawn(services::bgsave::bgsave_scheduler(
db.clone(),
bgsave,
signal.subscribe(),
));
let snapshot_handle = tokio::spawn(services::snapshot::snapshot_service(
engine,
db.clone(),
snapshot,
signal.subscribe(),
));
// bind to signals
let termsig =
TerminationSignal::init().map_err(|e| Error::ioerror_extra(e, "binding to signals"))?;
// start the server (single or multiple listeners)
let mut server = dbnet::connect(
ports,
protocol,
maxcon,
db.clone(),
auth_provider,
signal.clone(),
)
.await?;
tokio::select! {
_ = server.run_server() => {},
_ = termsig => {}
}
log::info!("Signalling all workers to shut down");
// drop the signal and let others exit
drop(signal);
server.finish_with_termsig().await;
// wait for the background services to terminate
let _ = snapshot_handle.await;
let _ = bgsave_handle.await;
Ok(db)
}
fn spawn_task(tx: Sender<bool>, db: Corestore, do_sleep: bool) -> JoinHandle<()> {
task::spawn_blocking(move || {
if do_sleep {
log::info!("Waiting for 10 seconds before retrying ...");
sleep(Duration::from_secs(10));
}
let ret = match crate::services::bgsave::run_bgsave(&db) {
Ok(()) => {
log::info!("Save before termination successful");
true
}
Err(e) => {
log::error!("Failed to run save on termination: {e}");
false
}
};
tx.blocking_send(ret).expect("Receiver dropped");
})
}
pub fn finalize_shutdown(corestore: Corestore, pid_file: FileLock) {
assert_eq!(
corestore.strong_count(),
1,
"Correctness error. finalize_shutdown called before dropping server runtime"
);
let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("server-final")
.enable_all()
.build()
.unwrap();
let dbc = corestore.clone();
let mut okay: bool = rt.block_on(async move {
let db = dbc;
let (tx, mut rx) = mpsc::channel::<bool>(1);
spawn_task(tx.clone(), db.clone(), false);
let spawn_again = || {
// we failed, so we better sleep
// now spawn it again to see the state
spawn_task(tx.clone(), db.clone(), true)
};
let mut threshold = TERMSIG_THRESHOLD;
loop {
let termsig = match TerminationSignal::init().map_err(|e| e.to_string()) {
Ok(sig) => sig,
Err(e) => {
log::error!("Failed to bind to signal with error: {e}");
crate::exit_error();
}
};
tokio::select! {
ret = rx.recv() => {
if ret.unwrap() {
// that's good to go
break true;
} else {
spawn_again();
}
}
_ = termsig => {
threshold -= 1;
if threshold == 0 {
log::error!("Termination signal received but failed to flush data. Quitting because threshold exceeded");
break false;
} else {
log::error!("Termination signal received but failed to flush data. Threshold is at {threshold}");
continue;
}
},
}
}
});
okay &= services::pre_shutdown_cleanup(pid_file, Some(corestore.get_store()));
if okay {
log::info!("Goodbye :)");
} else {
log::error!("Didn't terminate successfully");
crate::exit_error();
}
}

@ -1,58 +0,0 @@
/*
* Created on Mon Feb 21 2022
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2022, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
super::provider::{Authkey, AUTHKEY_SIZE},
crate::corestore::array::Array,
};
type AuthkeyArray = Array<u8, { AUTHKEY_SIZE }>;
const RAN_BYTES_SIZE: usize = 40;
/// Return a "human readable key" and the "authbytes" that can be stored
/// safely. To do this:
/// - Generate 64 random bytes
/// - Encode that into base64. This is the client key
/// - Hash the key using rcrypt. This is the server key that
/// will be stored
pub fn generate_full() -> (String, Authkey) {
let mut bytes: [u8; RAN_BYTES_SIZE] = [0u8; RAN_BYTES_SIZE];
openssl::rand::rand_bytes(&mut bytes).unwrap();
let ret = base64::encode_config(bytes, base64::BCRYPT);
let hash = rcrypt::hash(&ret, rcrypt::DEFAULT_COST).unwrap();
let store_in_db = unsafe {
let mut array = AuthkeyArray::new();
// we guarantee that the size is equal to 40
array.extend_from_slice_unchecked(&hash);
array.into_array_unchecked()
};
(ret, store_in_db)
}
/// Verify a "human readable key" against the provided "authbytes"
pub fn verify_key(input: &[u8], hash: &[u8]) -> Option<bool> {
rcrypt::verify(input, hash).ok()
}

@ -1,159 +0,0 @@
/*
* Created on Mon Feb 21 2022
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2022, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
/*
* For our authn/authz, we have two important keys:
* - The origin key: This is the key saved in the configuration file that can also be
* used as the "recovery key" in the event the "root key" is lost. To claim the root
* account, one needs this key. This is a fixed width key with 40 characters
* - The root key: This is the superuser key that can be used to create/deny other
* accounts. On claiming the root account, this key is issued
*
* When the root account is claimed, it can be used to create "standard users". standard
* users have access to everything but the ability to create/revoke other users
*/
mod keys;
pub mod provider;
pub use provider::{AuthProvider, Authmap};
#[cfg(test)]
mod tests;
use crate::dbnet::prelude::*;
const AUTH_CLAIM: &[u8] = b"claim";
const AUTH_LOGIN: &[u8] = b"login";
const AUTH_LOGOUT: &[u8] = b"logout";
const AUTH_ADDUSER: &[u8] = b"adduser";
const AUTH_DELUSER: &[u8] = b"deluser";
const AUTH_RESTORE: &[u8] = b"restore";
const AUTH_LISTUSER: &[u8] = b"listuser";
const AUTH_WHOAMI: &[u8] = b"whoami";
action! {
/// Handle auth. Should have passed the `auth` token
fn auth(
con: &mut Connection<C, P>,
auth: &mut AuthProviderHandle,
iter: ActionIter<'_>
) {
let mut iter = iter;
match iter.next_lowercase().unwrap_or_aerr::<P>()?.as_ref() {
AUTH_LOGIN => self::_auth_login(con, auth, &mut iter).await,
AUTH_CLAIM => self::_auth_claim(con, auth, &mut iter).await,
AUTH_ADDUSER => {
ensure_boolean_or_aerr::<P>(iter.len() == 1)?; // just the username
let username = unsafe { iter.next_unchecked() };
let key = auth.provider_mut().claim_user::<P>(username)?;
con.write_string(&key).await?;
Ok(())
}
AUTH_LOGOUT => {
ensure_boolean_or_aerr::<P>(iter.is_empty())?; // nothing else
auth.provider_mut().logout::<P>()?;
auth.set_unauth();
con._write_raw(P::RCODE_OKAY).await?;
Ok(())
}
AUTH_DELUSER => {
ensure_boolean_or_aerr::<P>(iter.len() == 1)?; // just the username
auth.provider_mut().delete_user::<P>(unsafe { iter.next_unchecked() })?;
con._write_raw(P::RCODE_OKAY).await?;
Ok(())
}
AUTH_RESTORE => self::auth_restore(con, auth, &mut iter).await,
AUTH_LISTUSER => self::auth_listuser(con, auth, &mut iter).await,
AUTH_WHOAMI => self::auth_whoami(con, auth, &mut iter).await,
_ => util::err(P::RCODE_UNKNOWN_ACTION),
}
}
fn auth_whoami(con: &mut Connection<C, P>, auth: &mut AuthProviderHandle, iter: &mut ActionIter<'_>) {
ensure_boolean_or_aerr::<P>(ActionIter::is_empty(iter))?;
con.write_string(&auth.provider().whoami::<P>()?).await?;
Ok(())
}
fn auth_listuser(con: &mut Connection<C, P>, auth: &mut AuthProviderHandle, iter: &mut ActionIter<'_>) {
ensure_boolean_or_aerr::<P>(ActionIter::is_empty(iter))?;
let usernames = auth.provider().collect_usernames::<P>()?;
con.write_typed_non_null_array_header(usernames.len(), b'+').await?;
for username in usernames {
con.write_typed_non_null_array_element(username.as_bytes()).await?;
}
Ok(())
}
fn auth_restore(con: &mut Connection<C, P>, auth: &mut AuthProviderHandle, iter: &mut ActionIter<'_>) {
let newkey = match iter.len() {
1 => {
// so this fella thinks they're root
auth.provider().regenerate::<P>(
unsafe { iter.next_unchecked() }
)?
}
2 => {
// so this fella is giving us the origin key
let origin = unsafe { iter.next_unchecked() };
let id = unsafe { iter.next_unchecked() };
auth.provider().regenerate_using_origin::<P>(origin, id)?
}
_ => return util::err(P::RCODE_ACTION_ERR),
};
con.write_string(&newkey).await?;
Ok(())
}
fn _auth_claim(con: &mut Connection<C, P>, auth: &mut AuthProviderHandle, iter: &mut ActionIter<'_>) {
ensure_boolean_or_aerr::<P>(iter.len() == 1)?; // just the origin key
let origin_key = unsafe { iter.next_unchecked() };
let key = auth.provider_mut().claim_root::<P>(origin_key)?;
auth.set_auth();
con.write_string(&key).await?;
Ok(())
}
/// Handle a login operation only. The **`login` token is expected to be present**
fn auth_login_only(
con: &mut Connection<C, P>,
auth: &mut AuthProviderHandle,
iter: ActionIter<'_>
) {
let mut iter = iter;
match iter.next_lowercase().unwrap_or_aerr::<P>()?.as_ref() {
AUTH_LOGIN => self::_auth_login(con, auth, &mut iter).await,
AUTH_CLAIM => self::_auth_claim(con, auth, &mut iter).await,
AUTH_RESTORE => self::auth_restore(con, auth, &mut iter).await,
AUTH_WHOAMI => self::auth_whoami(con, auth, &mut iter).await,
_ => util::err(P::AUTH_CODE_PERMS),
}
}
fn _auth_login(con: &mut Connection<C, P>, auth: &mut AuthProviderHandle, iter: &mut ActionIter<'_>) {
// sweet, where's our username and password
ensure_boolean_or_aerr::<P>(iter.len() == 2)?; // just the uname and pass
let (username, password) = unsafe { (iter.next_unchecked(), iter.next_unchecked()) };
auth.provider_mut().login::<P>(username, password)?;
auth.set_auth();
con._write_raw(P::RCODE_OKAY).await?;
Ok(())
}
}

@ -1,281 +0,0 @@
/*
* Created on Sun Mar 06 2022
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2022, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
use {
super::keys,
crate::{
actions::{ActionError, ActionResult},
corestore::{array::Array, htable::Coremap},
protocol::interface::ProtocolSpec,
util::err,
},
std::sync::Arc,
};
// constants
/// Size of an authn key in bytes
pub const AUTHKEY_SIZE: usize = 40;
/// Size of an authn ID in bytes
pub const AUTHID_SIZE: usize = 40;
pub mod testsuite_data {
#![allow(unused)]
//! Temporary users created by the testsuite in debug mode
pub const TESTSUITE_ROOT_USER: &str = "root";
pub const TESTSUITE_TEST_USER: &str = "testuser";
pub const TESTSUITE_ROOT_TOKEN: &str = "XUOdVKhEONnnGwNwT7WeLqbspDgVtKex0/nwFwBSW7XJxioHwpg6H.";
pub const TESTSUITE_TEST_TOKEN: &str = "mpobAB7EY8vnBs70d/..h1VvfinKIeEJgt1rg4wUkwF6aWCvGGR9le";
}
uninit_array! {
const USER_ROOT_ARRAY: [u8; 40] = [b'r', b'o', b'o', b't'];
}
/// The root user
const USER_ROOT: AuthID = unsafe { AuthID::from_const(USER_ROOT_ARRAY, 4) };
/// An authn ID
type AuthID = Array<u8, AUTHID_SIZE>;
/// An authn key
pub type Authkey = [u8; AUTHKEY_SIZE];
/// Authmap
pub type Authmap = Arc<Coremap<AuthID, Authkey>>;
/// The authn/authz provider
///
pub struct AuthProvider {
origin: Option<Authkey>,
/// the current user
whoami: Option<AuthID>,
/// a map of users
authmap: Authmap,
}
impl AuthProvider {
fn _new(authmap: Authmap, whoami: Option<AuthID>, origin: Option<Authkey>) -> Self {
Self {
authmap,
whoami,
origin,
}
}
/// New provider with no origin-key
pub fn new_disabled() -> Self {
Self::_new(Default::default(), None, None)
}
/// New provider with zero users
#[cfg(test)]
pub fn new_blank(origin: Option<Authkey>) -> Self {
Self::_new(Default::default(), None, origin)
}
/// New provider with users from the provided map
///
/// ## Test suite
/// The testsuite creates users `root` and `testuser`; this **does not** apply to
/// release mode
pub fn new(authmap: Arc<Coremap<AuthID, Authkey>>, origin: Option<Authkey>) -> Self {
let slf = Self::_new(authmap, None, origin);
#[cfg(debug_assertions)]
{
// 'root' user in test mode
slf.authmap.true_if_insert(
AuthID::try_from_slice(testsuite_data::TESTSUITE_ROOT_USER).unwrap(),
[
172, 143, 117, 169, 158, 156, 33, 106, 139, 107, 20, 106, 91, 219, 34, 157, 98,
147, 142, 91, 222, 238, 205, 120, 72, 171, 90, 218, 147, 2, 75, 67, 44, 108,
185, 124, 55, 40, 156, 252,
],
);
// 'testuser' user in test mode
slf.authmap.true_if_insert(
AuthID::try_from_slice(testsuite_data::TESTSUITE_TEST_USER).unwrap(),
[
172, 183, 60, 221, 53, 240, 231, 217, 113, 112, 98, 16, 109, 62, 235, 95, 184,
107, 130, 139, 43, 197, 40, 31, 176, 127, 185, 22, 172, 124, 39, 225, 124, 71,
193, 115, 176, 162, 239, 93,
],
);
}
slf
}
pub const fn is_enabled(&self) -> bool {
matches!(self.origin, Some(_))
}
pub fn claim_root<P: ProtocolSpec>(&mut self, origin_key: &[u8]) -> ActionResult<String> {
self.verify_origin::<P>(origin_key)?;
// the origin key was good, let's try claiming root
let (key, store) = keys::generate_full();
if self.authmap.true_if_insert(USER_ROOT, store) {
// claimed, sweet, log them in
self.whoami = Some(USER_ROOT);
Ok(key)
} else {
err(P::AUTH_ERROR_ALREADYCLAIMED)
}
}
fn are_you_root<P: ProtocolSpec>(&self) -> ActionResult<bool> {
self.ensure_enabled::<P>()?;
match self.whoami.as_ref().map(|v| v.eq(&USER_ROOT)) {
Some(v) => Ok(v),
None => err(P::AUTH_CODE_PERMS),
}
}
pub fn claim_user<P: ProtocolSpec>(&self, claimant: &[u8]) -> ActionResult<String> {
self.ensure_root::<P>()?;
self._claim_user::<P>(claimant)
}
pub fn _claim_user<P: ProtocolSpec>(&self, claimant: &[u8]) -> ActionResult<String> {
let (key, store) = keys::generate_full();
if self
.authmap
.true_if_insert(Self::try_auth_id::<P>(claimant)?, store)
{
Ok(key)
} else {
err(P::AUTH_ERROR_ALREADYCLAIMED)
}
}
pub fn login<P: ProtocolSpec>(&mut self, account: &[u8], token: &[u8]) -> ActionResult<()> {
self.ensure_enabled::<P>()?;
match self
.authmap
.get(account)
.map(|token_hash| keys::verify_key(token, token_hash.as_slice()))
{
Some(Some(true)) => {
// great, authenticated
self.whoami = Some(Self::try_auth_id::<P>(account)?);
Ok(())
}
_ => {
// either the password was wrong, or the username was wrong
err(P::AUTH_CODE_BAD_CREDENTIALS)
}
}
}
pub fn regenerate_using_origin<P: ProtocolSpec>(
&self,
origin: &[u8],
account: &[u8],
) -> ActionResult<String> {
self.verify_origin::<P>(origin)?;
self._regenerate::<P>(account)
}
pub fn regenerate<P: ProtocolSpec>(&self, account: &[u8]) -> ActionResult<String> {
self.ensure_root::<P>()?;
self._regenerate::<P>(account)
}
/// Regenerate the token for the given user. This returns a new token
fn _regenerate<P: ProtocolSpec>(&self, account: &[u8]) -> ActionResult<String> {
let id = Self::try_auth_id::<P>(account)?;
let (key, store) = keys::generate_full();
if self.authmap.true_if_update(id, store) {
Ok(key)
} else {
err(P::AUTH_CODE_BAD_CREDENTIALS)
}
}
fn try_auth_id<P: ProtocolSpec>(authid: &[u8]) -> ActionResult<AuthID> {
if authid.is_ascii() && authid.len() <= AUTHID_SIZE {
Ok(unsafe {
// We just verified the length
AuthID::from_slice(authid)
})
} else {
err(P::AUTH_ERROR_ILLEGAL_USERNAME)
}
}
pub fn logout<P: ProtocolSpec>(&mut self) -> ActionResult<()> {
self.ensure_enabled::<P>()?;
self.whoami
.take()
.map(|_| ())
.ok_or(ActionError::ActionError(P::AUTH_CODE_PERMS))
}
fn ensure_enabled<P: ProtocolSpec>(&self) -> ActionResult<()> {
self.origin
.as_ref()
.map(|_| ())
.ok_or(ActionError::ActionError(P::AUTH_ERROR_DISABLED))
}
pub fn verify_origin<P: ProtocolSpec>(&self, origin: &[u8]) -> ActionResult<()> {
if self.get_origin::<P>()?.eq(origin) {
Ok(())
} else {
err(P::AUTH_CODE_BAD_CREDENTIALS)
}
}
fn get_origin<P: ProtocolSpec>(&self) -> ActionResult<&Authkey> {
match self.origin.as_ref() {
Some(key) => Ok(key),
None => err(P::AUTH_ERROR_DISABLED),
}
}
fn ensure_root<P: ProtocolSpec>(&self) -> ActionResult<()> {
if self.are_you_root::<P>()? {
Ok(())
} else {
err(P::AUTH_CODE_PERMS)
}
}
pub fn delete_user<P: ProtocolSpec>(&self, user: &[u8]) -> ActionResult<()> {
self.ensure_root::<P>()?;
if user.eq(&USER_ROOT) {
// can't delete root!
err(P::AUTH_ERROR_FAILED_TO_DELETE_USER)
} else if self.authmap.true_if_removed(user) {
Ok(())
} else {
err(P::AUTH_CODE_BAD_CREDENTIALS)
}
}
/// List all the users
pub fn collect_usernames<P: ProtocolSpec>(&self) -> ActionResult<Vec<String>> {
self.ensure_root::<P>()?;
Ok(self
.authmap
.iter()
.map(|kv| String::from_utf8_lossy(kv.key()).to_string())
.collect())
}
/// Return the AuthID of the current user
pub fn whoami<P: ProtocolSpec>(&self) -> ActionResult<String> {
self.ensure_enabled::<P>()?;
self.whoami
.as_ref()
.map(|v| String::from_utf8_lossy(v).to_string())
.ok_or(ActionError::ActionError(P::AUTH_CODE_PERMS))
}
}
impl Clone for AuthProvider {
fn clone(&self) -> Self {
Self {
authmap: self.authmap.clone(),
whoami: None,
origin: self.origin,
}
}
}

@ -1,124 +0,0 @@
/*
* Created on Tue Feb 22 2022
*
* This file is a part of Skytable
* Skytable (formerly known as TerrabaseDB or Skybase) is a free and open-source
* NoSQL database written by Sayan Nandan ("the Author") with the
* vision to provide flexibility in data modelling without compromising
* on performance, queryability or scalability.
*
* Copyright (c) 2022, Sayan Nandan <ohsayan@outlook.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
mod keys {
use super::super::keys::{generate_full, verify_key};
#[test]
fn test_verify_key() {
let (key, store) = generate_full();
assert!(verify_key(key.as_bytes(), &store).unwrap());
}
}
mod authn {
use crate::{
actions::ActionError,
auth::AuthProvider,
protocol::{interface::ProtocolSpec, Skyhash2},
};
const ORIG: &[u8; 40] = b"c4299d190fb9a00626797fcc138c56eae9971664";
#[test]
fn claim_root_okay() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
let _ = provider.claim_root::<Skyhash2>(ORIG).unwrap();
}
#[test]
fn claim_root_wrongkey() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
let claim_err = provider.claim_root::<Skyhash2>(&ORIG[1..]).unwrap_err();
assert_eq!(
claim_err,
ActionError::ActionError(Skyhash2::AUTH_CODE_BAD_CREDENTIALS)
);
}
#[test]
fn claim_root_disabled() {
let mut provider = AuthProvider::new_disabled();
assert_eq!(
provider.claim_root::<Skyhash2>(b"abcd").unwrap_err(),
ActionError::ActionError(Skyhash2::AUTH_ERROR_DISABLED)
);
}
#[test]
fn claim_root_already_claimed() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
let _ = provider.claim_root::<Skyhash2>(ORIG).unwrap();
assert_eq!(
provider.claim_root::<Skyhash2>(ORIG).unwrap_err(),
ActionError::ActionError(Skyhash2::AUTH_ERROR_ALREADYCLAIMED)
);
}
#[test]
fn claim_user_okay_with_login() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
// claim root
let rootkey = provider.claim_root::<Skyhash2>(ORIG).unwrap();
// login as root
provider
.login::<Skyhash2>(b"root", rootkey.as_bytes())
.unwrap();
// claim user
let _ = provider.claim_user::<Skyhash2>(b"sayan").unwrap();
}
#[test]
fn claim_user_fail_not_root_with_login() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
// claim root
let rootkey = provider.claim_root::<Skyhash2>(ORIG).unwrap();
// login as root
provider
.login::<Skyhash2>(b"root", rootkey.as_bytes())
.unwrap();
// claim user
let userkey = provider.claim_user::<Skyhash2>(b"user").unwrap();
// login as user
provider
.login::<Skyhash2>(b"user", userkey.as_bytes())
.unwrap();
// now try to claim an user being a non-root account
assert_eq!(
provider.claim_user::<Skyhash2>(b"otheruser").unwrap_err(),
ActionError::ActionError(Skyhash2::AUTH_CODE_PERMS)
);
}
#[test]
fn claim_user_fail_anonymous() {
let mut provider = AuthProvider::new_blank(Some(*ORIG));
// claim root
let _ = provider.claim_root::<Skyhash2>(ORIG).unwrap();
// logout
provider.logout::<Skyhash2>().unwrap();
// try to claim as an anonymous user
assert_eq!(
provider.claim_user::<Skyhash2>(b"newuser").unwrap_err(),
ActionError::ActionError(Skyhash2::AUTH_CODE_PERMS)
);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save