Add support for Skyhash 1.0
parent
7ec599edcb
commit
b5e0f68c88
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Created on Mon May 02 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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
use super::{super::Query, Parser};
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn simple_query(b: &mut Bencher) {
|
||||||
|
const PAYLOAD: &[u8] = b"*1\n~3\n3\nSET\n1\nx\n3\n100\n";
|
||||||
|
let expected = vec!["SET".to_owned(), "x".to_owned(), "100".to_owned()];
|
||||||
|
b.iter(|| {
|
||||||
|
let (query, forward) = Parser::parse(PAYLOAD).unwrap();
|
||||||
|
assert_eq!(forward, PAYLOAD.len());
|
||||||
|
let query = if let Query::Simple(sq) = query {
|
||||||
|
sq
|
||||||
|
} else {
|
||||||
|
panic!("Got pipeline instead of simple query");
|
||||||
|
};
|
||||||
|
let ret: Vec<String> = query
|
||||||
|
.as_slice()
|
||||||
|
.iter()
|
||||||
|
.map(|s| String::from_utf8_lossy(s.as_slice()).to_string())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(ret, expected)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn pipelined_query(b: &mut Bencher) {
|
||||||
|
const PAYLOAD: &[u8] = b"*2\n~3\n3\nSET\n1\nx\n3\n100\n~2\n3\nGET\n1\nx\n";
|
||||||
|
let expected = vec![
|
||||||
|
vec!["SET".to_owned(), "x".to_owned(), "100".to_owned()],
|
||||||
|
vec!["GET".to_owned(), "x".to_owned()],
|
||||||
|
];
|
||||||
|
b.iter(|| {
|
||||||
|
let (query, forward) = Parser::parse(PAYLOAD).unwrap();
|
||||||
|
assert_eq!(forward, PAYLOAD.len());
|
||||||
|
let query = if let Query::Pipelined(sq) = query {
|
||||||
|
sq
|
||||||
|
} else {
|
||||||
|
panic!("Got simple instead of pipeline query");
|
||||||
|
};
|
||||||
|
let ret: Vec<Vec<String>> = query
|
||||||
|
.into_inner()
|
||||||
|
.iter()
|
||||||
|
.map(|query| {
|
||||||
|
query
|
||||||
|
.as_slice()
|
||||||
|
.iter()
|
||||||
|
.map(|v| String::from_utf8_lossy(v.as_slice()).to_string())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(ret, expected)
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* Created on Mon May 02 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::buffers::Integer64,
|
||||||
|
dbnet::connection::{QueryWithAdvance, RawConnection, Stream},
|
||||||
|
protocol::{
|
||||||
|
interface::{ProtocolRead, ProtocolSpec, ProtocolWrite},
|
||||||
|
ParseError, Skyhash1,
|
||||||
|
},
|
||||||
|
util::FutureResult,
|
||||||
|
IoResult,
|
||||||
|
},
|
||||||
|
::sky_macros::compiled_eresp_bytes_v1 as eresp,
|
||||||
|
tokio::io::AsyncWriteExt,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ProtocolSpec for Skyhash1 {
|
||||||
|
// spec information
|
||||||
|
const PROTOCOL_VERSION: f32 = 1.0;
|
||||||
|
const PROTOCOL_VERSIONSTRING: &'static str = "Skyhash-1.0";
|
||||||
|
|
||||||
|
// type symbols
|
||||||
|
const TSYMBOL_STRING: u8 = b'+';
|
||||||
|
const TSYMBOL_BINARY: u8 = b'?';
|
||||||
|
const TSYMBOL_FLOAT: u8 = b'%';
|
||||||
|
const TSYMBOL_INT64: u8 = b':';
|
||||||
|
const TSYMBOL_TYPED_ARRAY: u8 = b'@';
|
||||||
|
const TSYMBOL_TYPED_NON_NULL_ARRAY: u8 = b'^';
|
||||||
|
const TSYMBOL_ARRAY: u8 = b'&';
|
||||||
|
const TSYMBOL_FLAT_ARRAY: u8 = b'_';
|
||||||
|
|
||||||
|
// typed array
|
||||||
|
const TYPE_TYPED_ARRAY_ELEMENT_NULL: &'static [u8] = b"\0\n";
|
||||||
|
|
||||||
|
// metaframe
|
||||||
|
const SIMPLE_QUERY_HEADER: &'static [u8] = b"*1\n";
|
||||||
|
const PIPELINED_QUERY_FIRST_BYTE: u8 = b'*';
|
||||||
|
|
||||||
|
// respcodes
|
||||||
|
/// Response code 0 as a array element
|
||||||
|
const RCODE_OKAY: &'static [u8] = eresp!("0");
|
||||||
|
/// Response code 1 as a array element
|
||||||
|
const RCODE_NIL: &'static [u8] = eresp!("1");
|
||||||
|
/// Response code 2 as a array element
|
||||||
|
const RCODE_OVERWRITE_ERR: &'static [u8] = eresp!("2");
|
||||||
|
/// Response code 3 as a array element
|
||||||
|
const RCODE_ACTION_ERR: &'static [u8] = eresp!("3");
|
||||||
|
/// Response code 4 as a array element
|
||||||
|
const RCODE_PACKET_ERR: &'static [u8] = eresp!("4");
|
||||||
|
/// Response code 5 as a array element
|
||||||
|
const RCODE_SERVER_ERR: &'static [u8] = eresp!("5");
|
||||||
|
/// Response code 6 as a array element
|
||||||
|
const RCODE_OTHER_ERR_EMPTY: &'static [u8] = eresp!("6");
|
||||||
|
/// "Unknown action" error response
|
||||||
|
const RCODE_UNKNOWN_ACTION: &'static [u8] = eresp!("Unknown action");
|
||||||
|
/// Response code 7
|
||||||
|
const RCODE_WRONGTYPE_ERR: &'static [u8] = eresp!("7");
|
||||||
|
/// Response code 8
|
||||||
|
const RCODE_UNKNOWN_DATA_TYPE: &'static [u8] = eresp!("8");
|
||||||
|
/// Response code 9 as an array element
|
||||||
|
const RCODE_ENCODING_ERROR: &'static [u8] = eresp!("9");
|
||||||
|
|
||||||
|
// respstrings
|
||||||
|
|
||||||
|
/// Snapshot busy error
|
||||||
|
const RSTRING_SNAPSHOT_BUSY: &'static [u8] = eresp!("err-snapshot-busy");
|
||||||
|
/// Snapshot disabled (other error)
|
||||||
|
const RSTRING_SNAPSHOT_DISABLED: &'static [u8] = eresp!("err-snapshot-disabled");
|
||||||
|
/// Duplicate snapshot
|
||||||
|
const RSTRING_SNAPSHOT_DUPLICATE: &'static [u8] = eresp!("duplicate-snapshot");
|
||||||
|
/// Snapshot has illegal name (other error)
|
||||||
|
const RSTRING_SNAPSHOT_ILLEGAL_NAME: &'static [u8] = eresp!("err-invalid-snapshot-name");
|
||||||
|
/// Access after termination signal (other error)
|
||||||
|
const RSTRING_ERR_ACCESS_AFTER_TERMSIG: &'static [u8] = eresp!("err-access-after-termsig");
|
||||||
|
|
||||||
|
// keyspace related resps
|
||||||
|
/// The default container was not set
|
||||||
|
const RSTRING_DEFAULT_UNSET: &'static [u8] = eresp!("default-container-unset");
|
||||||
|
/// The container was not found
|
||||||
|
const RSTRING_CONTAINER_NOT_FOUND: &'static [u8] = eresp!("container-not-found");
|
||||||
|
/// The container is still in use and so cannot be removed
|
||||||
|
const RSTRING_STILL_IN_USE: &'static [u8] = eresp!("still-in-use");
|
||||||
|
/// This is a protected object and hence cannot be accessed
|
||||||
|
const RSTRING_PROTECTED_OBJECT: &'static [u8] = eresp!("err-protected-object");
|
||||||
|
/// The action was applied against the wrong model
|
||||||
|
const RSTRING_WRONG_MODEL: &'static [u8] = eresp!("wrong-model");
|
||||||
|
/// The container already exists
|
||||||
|
const RSTRING_ALREADY_EXISTS: &'static [u8] = eresp!("err-already-exists");
|
||||||
|
/// The container is not ready
|
||||||
|
const RSTRING_NOT_READY: &'static [u8] = eresp!("not-ready");
|
||||||
|
/// A transactional failure occurred
|
||||||
|
const RSTRING_DDL_TRANSACTIONAL_FAILURE: &'static [u8] = eresp!("transactional-failure");
|
||||||
|
/// An unknown DDL query was run
|
||||||
|
const RSTRING_UNKNOWN_DDL_QUERY: &'static [u8] = eresp!("unknown-ddl-query");
|
||||||
|
/// The expression for a DDL query was malformed
|
||||||
|
const RSTRING_BAD_EXPRESSION: &'static [u8] = eresp!("malformed-expression");
|
||||||
|
/// An unknown model was passed in a DDL query
|
||||||
|
const RSTRING_UNKNOWN_MODEL: &'static [u8] = eresp!("unknown-model");
|
||||||
|
/// Too many arguments were passed to model constructor
|
||||||
|
const RSTRING_TOO_MANY_ARGUMENTS: &'static [u8] = eresp!("too-many-args");
|
||||||
|
/// The container name is too long
|
||||||
|
const RSTRING_CONTAINER_NAME_TOO_LONG: &'static [u8] = eresp!("container-name-too-long");
|
||||||
|
/// The container name contains invalid characters
|
||||||
|
const RSTRING_BAD_CONTAINER_NAME: &'static [u8] = eresp!("bad-container-name");
|
||||||
|
/// An unknown inspect query
|
||||||
|
const RSTRING_UNKNOWN_INSPECT_QUERY: &'static [u8] = eresp!("unknown-inspect-query");
|
||||||
|
/// An unknown table property was passed
|
||||||
|
const RSTRING_UNKNOWN_PROPERTY: &'static [u8] = eresp!("unknown-property");
|
||||||
|
/// The keyspace is not empty and hence cannot be removed
|
||||||
|
const RSTRING_KEYSPACE_NOT_EMPTY: &'static [u8] = eresp!("keyspace-not-empty");
|
||||||
|
/// Bad type supplied in a DDL query for the key
|
||||||
|
const RSTRING_BAD_TYPE_FOR_KEY: &'static [u8] = eresp!("bad-type-for-key");
|
||||||
|
/// The index for the provided list was non-existent
|
||||||
|
const RSTRING_LISTMAP_BAD_INDEX: &'static [u8] = eresp!("bad-list-index");
|
||||||
|
/// The list is empty
|
||||||
|
const RSTRING_LISTMAP_LIST_IS_EMPTY: &'static [u8] = eresp!("list-is-empty");
|
||||||
|
|
||||||
|
// full responses
|
||||||
|
const FULLRESP_RCODE_PACKET_ERR: &'static [u8] = b"*1\n!1\n4\n";
|
||||||
|
const FULLRESP_RCODE_WRONG_TYPE: &'static [u8] = b"*1\n!1\n7\n";
|
||||||
|
const FULLRESP_HEYA: &'static [u8] = b"*1\n+4\nHEY!\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Strm, T> ProtocolRead<Skyhash1, Strm> for T
|
||||||
|
where
|
||||||
|
T: RawConnection<Skyhash1, Strm> + Send + Sync,
|
||||||
|
Strm: Stream,
|
||||||
|
{
|
||||||
|
fn try_query(&self) -> Result<QueryWithAdvance, ParseError> {
|
||||||
|
Skyhash1::parse(self.get_buffer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Strm, T> ProtocolWrite<Skyhash1, Strm> for T
|
||||||
|
where
|
||||||
|
T: RawConnection<Skyhash1, Strm> + Send + Sync,
|
||||||
|
Strm: Stream,
|
||||||
|
{
|
||||||
|
fn write_string<'life0, 'life1, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
string: &'life1 str,
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
'life1: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let stream = self.get_mut_stream();
|
||||||
|
// tsymbol
|
||||||
|
stream.write_all(&[Skyhash1::TSYMBOL_STRING]).await?;
|
||||||
|
// length
|
||||||
|
let len_bytes = Integer64::from(string.len());
|
||||||
|
stream.write_all(&len_bytes).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await?;
|
||||||
|
// payload
|
||||||
|
stream.write_all(string.as_bytes()).await?;
|
||||||
|
// final LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn write_binary<'life0, 'life1, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
binary: &'life1 [u8],
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
'life1: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let stream = self.get_mut_stream();
|
||||||
|
// tsymbol
|
||||||
|
stream.write_all(&[Skyhash1::TSYMBOL_BINARY]).await?;
|
||||||
|
// length
|
||||||
|
let len_bytes = Integer64::from(binary.len());
|
||||||
|
stream.write_all(&len_bytes).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await?;
|
||||||
|
// payload
|
||||||
|
stream.write_all(binary).await?;
|
||||||
|
// final LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn write_usize<'life0, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
size: usize,
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move { self.write_int64(size as _).await })
|
||||||
|
}
|
||||||
|
fn write_int64<'life0, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
int: u64,
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let stream = self.get_mut_stream();
|
||||||
|
// tsymbol
|
||||||
|
stream.write_all(&[Skyhash1::TSYMBOL_INT64]).await?;
|
||||||
|
// get body and sizeline
|
||||||
|
let body = Integer64::from(int);
|
||||||
|
let body_len = Integer64::from(body.len());
|
||||||
|
// len of body
|
||||||
|
stream.write_all(&body_len).await?;
|
||||||
|
// sizeline LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await?;
|
||||||
|
// body
|
||||||
|
stream.write_all(&body).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn write_float<'life0, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
float: f32,
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let stream = self.get_mut_stream();
|
||||||
|
// tsymbol
|
||||||
|
stream.write_all(&[Skyhash1::TSYMBOL_FLOAT]).await?;
|
||||||
|
// get body and sizeline
|
||||||
|
let body = float.to_string();
|
||||||
|
let body = body.as_bytes();
|
||||||
|
let sizeline = Integer64::from(body.len());
|
||||||
|
// sizeline
|
||||||
|
stream.write_all(&sizeline).await?;
|
||||||
|
// sizeline LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await?;
|
||||||
|
// body
|
||||||
|
stream.write_all(body).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn write_typed_array_element<'life0, 'life1, 'ret_life>(
|
||||||
|
&'life0 mut self,
|
||||||
|
element: &'life1 [u8],
|
||||||
|
) -> FutureResult<'ret_life, IoResult<()>>
|
||||||
|
where
|
||||||
|
'life0: 'ret_life,
|
||||||
|
'life1: 'ret_life,
|
||||||
|
Self: 'ret_life,
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
let stream = self.get_mut_stream();
|
||||||
|
// len
|
||||||
|
stream.write_all(&Integer64::from(element.len())).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await?;
|
||||||
|
// body
|
||||||
|
stream.write_all(element).await?;
|
||||||
|
// LF
|
||||||
|
stream.write_all(&[Skyhash1::LF]).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
* Created on Sat Apr 30 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::{ParseError, ParseResult, PipelinedQuery, Query, SimpleQuery, UnsafeSlice};
|
||||||
|
use crate::{
|
||||||
|
corestore::heap_array::{HeapArray, HeapArrayWriter},
|
||||||
|
dbnet::connection::QueryWithAdvance,
|
||||||
|
};
|
||||||
|
use core::mem::transmute;
|
||||||
|
|
||||||
|
mod interface_impls;
|
||||||
|
// test and bench modules
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
mod benches;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// A parser for Skyhash 1.0
|
||||||
|
///
|
||||||
|
/// Packet structure example (simple query):
|
||||||
|
/// ```text
|
||||||
|
/// *1\n
|
||||||
|
/// ~3\n
|
||||||
|
/// 3\n
|
||||||
|
/// SET\n
|
||||||
|
/// 1\n
|
||||||
|
/// x\n
|
||||||
|
/// 3\n
|
||||||
|
/// 100\n
|
||||||
|
/// ```
|
||||||
|
pub struct Parser {
|
||||||
|
end: *const u8,
|
||||||
|
cursor: *const u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for Parser {}
|
||||||
|
unsafe impl Sync for Parser {}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
/// Initialize a new parser
|
||||||
|
fn new(slice: &[u8]) -> Self {
|
||||||
|
unsafe {
|
||||||
|
Self {
|
||||||
|
end: slice.as_ptr().add(slice.len()),
|
||||||
|
cursor: slice.as_ptr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic methods
|
||||||
|
impl Parser {
|
||||||
|
/// Returns a ptr one byte past the allocation of the buffer
|
||||||
|
const fn data_end_ptr(&self) -> *const u8 {
|
||||||
|
self.end
|
||||||
|
}
|
||||||
|
/// Returns the position of the cursor
|
||||||
|
/// WARNING: Deref might led to a segfault
|
||||||
|
const fn cursor_ptr(&self) -> *const u8 {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
/// Check how many bytes we have left
|
||||||
|
fn remaining(&self) -> usize {
|
||||||
|
self.data_end_ptr() as usize - self.cursor_ptr() as usize
|
||||||
|
}
|
||||||
|
/// Check if we have `size` bytes remaining
|
||||||
|
fn has_remaining(&self, size: usize) -> bool {
|
||||||
|
self.remaining() >= size
|
||||||
|
}
|
||||||
|
/// Check if we have exhausted the buffer
|
||||||
|
fn exhausted(&self) -> bool {
|
||||||
|
self.cursor_ptr() >= self.data_end_ptr()
|
||||||
|
}
|
||||||
|
/// Check if the buffer is not exhausted
|
||||||
|
fn not_exhausted(&self) -> bool {
|
||||||
|
self.cursor_ptr() < self.data_end_ptr()
|
||||||
|
}
|
||||||
|
/// Attempts to return the byte pointed at by the cursor.
|
||||||
|
/// WARNING: The same segfault warning
|
||||||
|
const unsafe fn get_byte_at_cursor(&self) -> u8 {
|
||||||
|
*self.cursor_ptr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutable refs
|
||||||
|
impl Parser {
|
||||||
|
/// Increment the cursor by `by` positions
|
||||||
|
unsafe fn incr_cursor_by(&mut self, by: usize) {
|
||||||
|
self.cursor = self.cursor.add(by);
|
||||||
|
}
|
||||||
|
/// Increment the position of the cursor by one position
|
||||||
|
unsafe fn incr_cursor(&mut self) {
|
||||||
|
self.incr_cursor_by(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility methods
|
||||||
|
impl Parser {
|
||||||
|
/// Returns true if the cursor will give a char, but if `this_if_nothing_ahead` is set
|
||||||
|
/// to true, then if no byte is ahead, it will still return true
|
||||||
|
fn will_cursor_give_char(&self, ch: u8, true_if_nothing_ahead: bool) -> ParseResult<bool> {
|
||||||
|
if self.exhausted() {
|
||||||
|
// nothing left
|
||||||
|
if true_if_nothing_ahead {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err(ParseError::NotEnough)
|
||||||
|
}
|
||||||
|
} else if unsafe { self.get_byte_at_cursor().eq(&ch) } {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Check if the current cursor will give an LF
|
||||||
|
fn will_cursor_give_linefeed(&self) -> ParseResult<bool> {
|
||||||
|
self.will_cursor_give_char(b'\n', false)
|
||||||
|
}
|
||||||
|
/// Gets the _next element. **The cursor should be at the tsymbol (passed)**
|
||||||
|
fn _next(&mut self) -> ParseResult<UnsafeSlice> {
|
||||||
|
let element_size = self.read_usize()?;
|
||||||
|
self.read_until(element_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// higher level abstractions
|
||||||
|
impl Parser {
|
||||||
|
/// Attempt to read `len` bytes
|
||||||
|
fn read_until(&mut self, len: usize) -> ParseResult<UnsafeSlice> {
|
||||||
|
if self.has_remaining(len) {
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): Already verified lengths
|
||||||
|
let slice = UnsafeSlice::new(self.cursor_ptr(), len);
|
||||||
|
self.incr_cursor_by(len);
|
||||||
|
Ok(slice)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ParseError::NotEnough)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Attempt to read a line, **rejecting an empty payload**
|
||||||
|
fn read_line_pedantic(&mut self) -> ParseResult<UnsafeSlice> {
|
||||||
|
let start_ptr = self.cursor_ptr();
|
||||||
|
unsafe {
|
||||||
|
while self.not_exhausted() && self.get_byte_at_cursor() != b'\n' {
|
||||||
|
self.incr_cursor();
|
||||||
|
}
|
||||||
|
let len = self.cursor_ptr() as usize - start_ptr as usize;
|
||||||
|
let has_lf = self.not_exhausted() && self.get_byte_at_cursor() == b'\n';
|
||||||
|
if has_lf && len != 0 {
|
||||||
|
self.incr_cursor(); // skip LF
|
||||||
|
Ok(UnsafeSlice::new(start_ptr, len))
|
||||||
|
} else {
|
||||||
|
// just some silly hackery
|
||||||
|
Err(transmute(has_lf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Attempt to read an `usize` from the buffer
|
||||||
|
fn read_usize(&mut self) -> ParseResult<usize> {
|
||||||
|
let line = self.read_line_pedantic()?;
|
||||||
|
let bytes = line.as_slice();
|
||||||
|
let mut ret = 0usize;
|
||||||
|
for byte in bytes {
|
||||||
|
if byte.is_ascii_digit() {
|
||||||
|
ret = match ret.checked_mul(10) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Err(ParseError::DatatypeParseFailure),
|
||||||
|
};
|
||||||
|
ret = match ret.checked_add((byte & 0x0F) as _) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return Err(ParseError::DatatypeParseFailure),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return Err(ParseError::DatatypeParseFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
/// Parse the next blob. **The cursor should be at the tsymbol (passed)**
|
||||||
|
fn parse_next_blob(&mut self) -> ParseResult<UnsafeSlice> {
|
||||||
|
{
|
||||||
|
let chunk = self._next()?;
|
||||||
|
if self.will_cursor_give_linefeed()? {
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): We know that the buffer is not exhausted
|
||||||
|
// due to the above condition
|
||||||
|
self.incr_cursor();
|
||||||
|
}
|
||||||
|
Ok(chunk)
|
||||||
|
} else {
|
||||||
|
Err(ParseError::UnexpectedByte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// query abstractions
|
||||||
|
impl Parser {
|
||||||
|
/// The buffer should resemble the below structure:
|
||||||
|
/// ```
|
||||||
|
/// ~<count>\n
|
||||||
|
/// <e0l0>\n
|
||||||
|
/// <e0>\n
|
||||||
|
/// <e1l1>\n
|
||||||
|
/// <e1>\n
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
fn _parse_simple_query(&mut self) -> ParseResult<HeapArray<UnsafeSlice>> {
|
||||||
|
if self.not_exhausted() {
|
||||||
|
if unsafe { self.get_byte_at_cursor() } != b'~' {
|
||||||
|
// we need an any array
|
||||||
|
return Err(ParseError::WrongType);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): Just checked length
|
||||||
|
self.incr_cursor();
|
||||||
|
}
|
||||||
|
let query_count = self.read_usize()?;
|
||||||
|
let mut writer = HeapArrayWriter::with_capacity(query_count);
|
||||||
|
for i in 0..query_count {
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): The index of the for loop ensures that
|
||||||
|
// we never attempt to write to a bad memory location
|
||||||
|
writer.write_to_index(i, self.parse_next_blob()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(unsafe {
|
||||||
|
// UNSAFE(@ohsayan): If we've reached here, then we have initialized
|
||||||
|
// all the queries
|
||||||
|
writer.finish()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(ParseError::NotEnough)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn parse_simple_query(&mut self) -> ParseResult<SimpleQuery> {
|
||||||
|
Ok(SimpleQuery::new(self._parse_simple_query()?))
|
||||||
|
}
|
||||||
|
/// The buffer should resemble the following structure:
|
||||||
|
/// ```text
|
||||||
|
/// # query 1
|
||||||
|
/// ~<count>\n
|
||||||
|
/// <e0l0>\n
|
||||||
|
/// <e0>\n
|
||||||
|
/// <e1l1>\n
|
||||||
|
/// <e1>\n
|
||||||
|
/// # query 2
|
||||||
|
/// ~<count>\n
|
||||||
|
/// <e0l0>\n
|
||||||
|
/// <e0>\n
|
||||||
|
/// <e1l1>\n
|
||||||
|
/// <e1>\n
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
fn parse_pipelined_query(&mut self, length: usize) -> ParseResult<PipelinedQuery> {
|
||||||
|
let mut writer = HeapArrayWriter::with_capacity(length);
|
||||||
|
for i in 0..length {
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): The above condition guarantees that the index
|
||||||
|
// never causes an overflow
|
||||||
|
writer.write_to_index(i, self._parse_simple_query()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): if we reached here, then we have inited everything
|
||||||
|
Ok(PipelinedQuery::new(writer.finish()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn _parse(&mut self) -> ParseResult<Query> {
|
||||||
|
if self.not_exhausted() {
|
||||||
|
let first_byte = unsafe {
|
||||||
|
// UNSAFE(@ohsayan): Just checked if buffer is exhausted or not
|
||||||
|
self.get_byte_at_cursor()
|
||||||
|
};
|
||||||
|
if first_byte != b'*' {
|
||||||
|
// unknown query scheme, so it's a bad packet
|
||||||
|
return Err(ParseError::BadPacket);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
// UNSAFE(@ohsayan): Checked buffer len and incremented, so we're good
|
||||||
|
self.incr_cursor()
|
||||||
|
};
|
||||||
|
let query_count = self.read_usize()?; // get the length
|
||||||
|
if query_count == 1 {
|
||||||
|
Ok(Query::Simple(self.parse_simple_query()?))
|
||||||
|
} else {
|
||||||
|
Ok(Query::Pipelined(self.parse_pipelined_query(query_count)?))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(ParseError::NotEnough)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse(buf: &[u8]) -> ParseResult<QueryWithAdvance> {
|
||||||
|
let mut slf = Self::new(buf);
|
||||||
|
let body = slf._parse()?;
|
||||||
|
let consumed = slf.cursor_ptr() as usize - buf.as_ptr() as usize;
|
||||||
|
Ok((body, consumed))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Created on Mon May 02 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::Parser,
|
||||||
|
crate::protocol::{ParseError, Query},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
const SQPAYLOAD: &[u8] = b"*1\n~3\n3\nSET\n1\nx\n3\n100\n";
|
||||||
|
#[cfg(test)]
|
||||||
|
const PQPAYLOAD: &[u8] = b"*2\n~3\n3\nSET\n1\nx\n3\n100\n~2\n3\nGET\n1\nx\n";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple_query() {
|
||||||
|
let payload = SQPAYLOAD.to_vec();
|
||||||
|
let (q, f) = Parser::parse(&payload).unwrap();
|
||||||
|
let q: Vec<String> = if let Query::Simple(q) = q {
|
||||||
|
q.as_slice()
|
||||||
|
.iter()
|
||||||
|
.map(|v| String::from_utf8_lossy(v.as_slice()).to_string())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
panic!("Expected simple query")
|
||||||
|
};
|
||||||
|
assert_eq!(f, payload.len());
|
||||||
|
assert_eq!(q, vec!["SET".to_owned(), "x".into(), "100".into()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple_query_incomplete() {
|
||||||
|
for i in 0..SQPAYLOAD.len() - 1 {
|
||||||
|
let slice = &SQPAYLOAD[..i];
|
||||||
|
assert_eq!(Parser::parse(slice).unwrap_err(), ParseError::NotEnough);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_pipelined_query() {
|
||||||
|
let payload = PQPAYLOAD.to_vec();
|
||||||
|
let (q, f) = Parser::parse(&payload).unwrap();
|
||||||
|
let q: Vec<Vec<String>> = if let Query::Pipelined(q) = q {
|
||||||
|
q.into_inner()
|
||||||
|
.iter()
|
||||||
|
.map(|sq| {
|
||||||
|
sq.iter()
|
||||||
|
.map(|v| String::from_utf8_lossy(v.as_slice()).to_string())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
panic!("Expected pipelined query query")
|
||||||
|
};
|
||||||
|
assert_eq!(f, payload.len());
|
||||||
|
assert_eq!(
|
||||||
|
q,
|
||||||
|
vec![
|
||||||
|
vec!["SET".to_owned(), "x".into(), "100".into()],
|
||||||
|
vec!["GET".into(), "x".into()]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_pipelined_query_incomplete() {
|
||||||
|
for i in 0..PQPAYLOAD.len() - 1 {
|
||||||
|
let slice = &PQPAYLOAD[..i];
|
||||||
|
assert_eq!(Parser::parse(slice).unwrap_err(), ParseError::NotEnough);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue