Add test
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2022-04-02 21:58:23 +02:00
parent 63f4dce705
commit 4fd34f973f
20 changed files with 987 additions and 133 deletions

14
sqlite_core/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "sqlite_core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sscanf = { version = "0.2.1" }
bincode = "2.0.0-rc.1"
serde = { version = "1.0.136", features = ["derive"] }
[dev_dependencies]
rspec = "1.0"

71
sqlite_core/src/app.rs Normal file
View File

@@ -0,0 +1,71 @@
use crate::input_buffer::InputBuffer;
use crate::interaction::{buffer::Buffer, command_line::CommandLine, Interaction};
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
pub struct App {
interaction: Option<Arc<dyn Interaction + Send + Sync>>,
on_exit: fn(),
}
impl Default for App {
fn default() -> Self {
App::new()
}
}
impl Debug for App {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("App").finish()
}
}
impl App {
pub fn new() -> Self {
Self {
interaction: None,
on_exit: || {},
}
}
pub fn with_interaction(
&mut self,
interaction: Arc<dyn Interaction + Send + Sync>,
) -> &mut App {
self.interaction = Some(interaction);
self
}
pub fn with_commandline_interaction(&mut self) -> &mut Self {
self.with_interaction(Arc::new(CommandLine::new()))
}
pub fn with_buffer_interaction(
&mut self,
in_buffer: Arc<Mutex<Vec<String>>>,
out_buffer: Arc<Mutex<Vec<String>>>,
) -> &mut Self {
self.with_interaction(Arc::new(Buffer::new(in_buffer, out_buffer)))
}
pub fn with_on_exit_handler(&mut self, on_exit: fn()) -> &mut Self {
self.on_exit = on_exit;
self
}
pub fn run(&mut self) -> Result<(), String> {
if let None = self.interaction {
return Err(String::from("missing interaction"));
}
let mut input_buffer = InputBuffer::new(self.interaction.take().unwrap());
loop {
input_buffer
.print_prompt()
.read_input()?
.parse(self.on_exit);
}
}
}

View File

@@ -0,0 +1,92 @@
use crate::interaction::Interaction;
use crate::statement::{ExecuteResult, StatementResultType};
use crate::table::Table;
use crate::{interaction, statement};
use std::ops::Deref;
use std::sync::Arc;
pub struct InputBuffer {
buffer: Option<String>,
table: Table,
interaction: Arc<dyn interaction::Interaction>,
}
impl InputBuffer {
pub fn new(interaction: Arc<dyn Interaction>) -> Self {
return Self {
buffer: None,
table: Table::new(),
interaction,
};
}
pub fn print_prompt(&mut self) -> &mut Self {
self.interaction.output(String::from("db > "));
self
}
pub fn read_input(&mut self) -> Result<&mut Self, String> {
self.buffer = Some(String::new());
match &mut self.buffer {
Some(input_buffer) => {
if let Ok(input) = self.interaction.input() {
*input_buffer = input.to_string();
Ok(self)
} else {
Err(String::from("could not handle input"))
}
}
_ => Err(String::from("Could not initialize buffer")),
}
}
pub fn parse(&mut self, on_exit: fn() -> ()) {
match &self.buffer {
Some(command) => {
if command.starts_with(".") {
Self::handle_meta_statement(command, on_exit);
} else {
match Self::prepare_statement(&command.replace("\n", "")) {
Ok(statement) => {
let execution_result = statement.execute(&mut self.table);
match execution_result {
ExecuteResult::Success {
statement_result_type: StatementResultType::Insert,
} => self
.interaction
.deref()
.output(String::from("insert success\n")),
ExecuteResult::Success {
statement_result_type: StatementResultType::Select { rows },
} => {
for row in rows {
self.interaction.output(row.print())
}
self.interaction.output(String::from("select success\n"))
}
_ => self.interaction.output(String::from("failure\n")),
}
}
Err(e) => {
self.interaction.output(format!("{}\n", e));
}
}
}
}
None => {}
}
}
fn prepare_statement(command: &String) -> Result<statement::Statement, String> {
return statement::Statement::parse_statement(command);
}
fn handle_meta_statement(command: &String, on_exit: fn()) {
match command.replace("\n", "").trim() {
".exit" => on_exit(),
cmd => {
println!("Could not handle command: {cmd}")
}
}
}
}

View File

@@ -0,0 +1,33 @@
use std::sync::{Arc, Mutex};
use crate::interaction::Interaction;
pub struct Buffer {
int_buffer: Arc<Mutex<Vec<String>>>,
out_buffer: Arc<Mutex<Vec<String>>>,
}
impl Buffer {
pub fn new(int_buffer: Arc<Mutex<Vec<String>>>, out_buffer: Arc<Mutex<Vec<String>>>) -> Self {
Self {
int_buffer,
out_buffer,
}
}
}
impl Interaction for Buffer {
fn input(&self) -> Result<String, String> {
let mut int_buffer = self.int_buffer.lock().unwrap();
if let Some(input_string) = int_buffer.pop() {
Ok(input_string)
} else {
Err(String::from("internal_buffer is empty"))
}
}
fn output(&self, output: String) {
let mut out_buffer = self.out_buffer.lock().unwrap();
out_buffer.push(output)
}
}

View File

@@ -0,0 +1,26 @@
use crate::interaction::Interaction;
use std::io::Write;
pub struct CommandLine {}
impl CommandLine {
pub fn new() -> Self {
Self {}
}
}
impl Interaction for CommandLine {
fn input(&self) -> Result<String, String> {
let buffer: &mut String = &mut String::new();
if let Ok(_) = std::io::stdin().read_line(buffer) {
return Ok(buffer.to_string());
} else {
return Err(String::from("could not read input buffer"));
}
}
fn output(&self, output: String) {
let _ = std::io::stdout().write(output.as_bytes());
let _ = std::io::stdout().flush();
}
}

View File

@@ -0,0 +1,7 @@
pub mod buffer;
pub mod command_line;
pub trait Interaction {
fn input(&self) -> Result<String, String>;
fn output(&self, output: String);
}

8
sqlite_core/src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod app;
mod input_buffer;
mod interaction;
mod page;
mod pager;
mod row;
mod statement;
mod table;

26
sqlite_core/src/page.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::ops::{Deref, DerefMut};
#[derive(Debug, Clone)]
pub struct Page {
buffer: Vec<u8>,
}
impl Page {
pub fn new(buffer: Vec<u8>) -> Self {
Self { buffer }
}
}
impl Deref for Page {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl DerefMut for Page {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buffer
}
}

35
sqlite_core/src/pager.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::page::Page;
pub struct Pager {
pages: Vec<Option<Page>>,
page_size: usize,
}
impl Pager {
pub fn new(page_size: usize) -> Self {
Self {
page_size,
pages: Vec::new(),
}
}
pub fn get_mut_or_alloc(&mut self, page_num: usize) -> &mut Page {
if page_num >= self.pages.len() {
self.grow_pages(page_num)
}
if self.pages[page_num].is_none() {
let buf = vec![0; self.page_size];
let page = Page::new(buf);
self.pages[page_num] = Some(page)
}
self.pages[page_num].as_mut().unwrap()
}
#[inline]
fn grow_pages(&mut self, index: usize) {
println!("debug: Growing pages");
self.pages.resize(index + 1, None)
}
}

88
sqlite_core/src/row.rs Normal file
View File

@@ -0,0 +1,88 @@
use std::io::Write;
use std::{iter, slice};
use bincode::config::Configuration;
use bincode::error::{DecodeError, EncodeError};
use bincode::{Decode, Encode};
const COLUMN_USERNAME_SIZE: usize = 32;
const COLUMN_EMAIL_SIZE: usize = 255;
#[derive(Encode, Decode)]
pub struct Row {
pub id: u32,
pub username: [u8; COLUMN_USERNAME_SIZE],
pub email: [u8; COLUMN_EMAIL_SIZE],
}
pub trait Serialize {
fn serialize(&self) -> Result<Vec<u8>, EncodeError>;
fn deserialize(buffer: &[u8]) -> Result<Self, DecodeError>
where
Self: Sized;
}
impl Serialize for Row {
fn serialize(&self) -> Result<Vec<u8>, EncodeError> {
bincode::encode_to_vec(self, bincode::config::standard())
}
fn deserialize(buffer: &[u8]) -> Result<Self, DecodeError> {
match bincode::decode_from_slice::<Self, Configuration>(buffer, bincode::config::standard())
{
Ok((row, _)) => Ok(row),
Err(e) => Err(e),
}
}
}
impl Row {
pub fn new(id: u32, username: String, email: String) -> Result<Self, String> {
let username_bytes = username.as_bytes();
let email_bytes = email.as_bytes();
if username_bytes.len() > COLUMN_USERNAME_SIZE {
Err(String::from("username is too long"))
} else if email_bytes.len() > COLUMN_EMAIL_SIZE {
Err(String::from("email is too long"))
} else {
let username_buffer = &mut [0; COLUMN_USERNAME_SIZE];
if let Err(_) = pad_buffer(username_buffer, username_bytes) {
panic!("could not pad username buffer")
}
let email_buffer = &mut [0; COLUMN_EMAIL_SIZE];
if let Err(_) = pad_buffer(email_buffer, username_bytes) {
panic!("could not pad username buffer")
}
Ok(Self {
id,
username: *username_buffer,
email: *email_buffer,
})
}
}
pub fn print(&self) -> String {
format!(
"({}, {}, {})",
self.id,
String::from_utf8(self.email.to_vec()).unwrap(),
String::from_utf8(self.username.to_vec()).unwrap()
)
}
}
fn pad_buffer(mut bytes: &mut [u8], source: &[u8]) -> std::io::Result<usize> {
bytes.write(source)
}
pub struct RowIter<'a> {
inner: slice::Iter<'a, Row>,
}
impl<'a> iter::Iterator for RowIter<'a> {
type Item = &'a Row;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}

View File

@@ -0,0 +1,77 @@
use crate::row;
use crate::row::Serialize;
use crate::table::Table;
pub enum StatementType {
Insert { row: row::Row },
Select,
}
pub enum StatementResultType {
Insert,
Select { rows: Vec<row::Row> },
}
pub enum ExecuteResult {
Error {
error: String,
},
Success {
statement_result_type: StatementResultType,
},
}
pub struct Statement {
statement_type: StatementType,
}
impl Statement {
pub fn new(statement_type: StatementType) -> Self {
Self { statement_type }
}
pub fn parse_statement(command: &String) -> Result<Self, String> {
if command.starts_with("insert") {
match sscanf::scanf!(command, "insert {} {} {}", usize, str, str) {
Ok((id, username, email)) => {
match row::Row::new(
u32::try_from(id).unwrap(),
username.to_string(),
email.to_string(),
) {
Ok(row) => Ok(Statement::new(StatementType::Insert { row })),
Err(e) => Err(e),
}
}
_ => Err(String::from("could not parse insert statement")),
}
} else if command.starts_with("select") {
Ok(Statement::new(StatementType::Select))
} else {
Err(String::from("Could not parse command"))
}
}
pub fn execute(&self, table: &mut Table) -> ExecuteResult {
match &self.statement_type {
StatementType::Insert { row } => match row.serialize() {
Err(e) => ExecuteResult::Error {
error: e.to_string(),
},
Ok(serialization) => {
table.append_row(serialization);
ExecuteResult::Success {
statement_result_type: StatementResultType::Insert {},
}
}
},
StatementType::Select => {
let rows = table.get_rows();
return ExecuteResult::Success {
statement_result_type: StatementResultType::Select { rows },
};
}
}
}
}

61
sqlite_core/src/table.rs Normal file
View File

@@ -0,0 +1,61 @@
use crate::pager::Pager;
use crate::row;
use crate::row::Row;
use crate::row::Serialize;
use std::mem::size_of;
use std::ops::DerefMut;
const PAGE_SIZE: usize = 4096;
pub struct Table {
num_rows: usize,
pager: Pager,
}
impl Table {
pub fn new() -> Self {
Self {
num_rows: 0,
pager: Pager::new(PAGE_SIZE),
}
}
pub fn append_row(&mut self, row: Vec<u8>) {
let row_size = size_of::<row::Row>();
let page_num = self.num_rows / self.rows_per_page(row_size);
let row_offset = self.num_rows % self.rows_per_page(row_size);
let page = self.pager.get_mut_or_alloc(page_num);
let byte_offset = row_offset * row_size;
page.deref_mut()
.splice(byte_offset..byte_offset + row_size, row);
self.num_rows += 1;
}
pub fn row(&mut self, row_num: usize) -> Row {
let row_size = size_of::<row::Row>();
let page_num = row_num / self.rows_per_page(row_size);
let row_offset = row_num % self.rows_per_page(row_size);
let byte_offset = row_offset * row_size;
let page = self.pager.get_mut_or_alloc(page_num);
Row::deserialize(&page[byte_offset..byte_offset + row_size]).unwrap()
}
#[inline]
fn rows_per_page(&self, row_size: usize) -> usize {
PAGE_SIZE / row_size
}
pub fn get_rows(&mut self) -> Vec<Row> {
let mut rows: Vec<Row> = Vec::with_capacity(self.num_rows);
for i in 0..self.num_rows {
rows.push(self.row(i))
}
rows
}
}

View File

@@ -0,0 +1,118 @@
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use sqlite_core::app::App;
#[derive(Clone, Debug, Default)]
struct EmptyDatabaseEnv {
input: Arc<Mutex<Vec<String>>>,
output: Arc<Mutex<Vec<String>>>,
app: Arc<Mutex<sqlite_core::app::App>>,
}
#[test]
fn integration_test() {
let env = EmptyDatabaseEnv {
input: Arc::new(Mutex::new(Vec::new())),
output: Arc::new(Mutex::new(Vec::new())),
app: Arc::new(Mutex::new(App::new())),
};
rspec::run(&rspec::given("a sqlite database", env, |ctx| {
ctx.before_each(|env| {
env.input = Arc::new(Mutex::new(Vec::new()));
env.output = Arc::new(Mutex::new(Vec::new()));
env.app = Arc::new(Mutex::new(App::new()));
});
ctx.when("it runs with empty items", |ctx| {
ctx.then("it returns input error", |value| {
let result = value
.app
.lock()
.as_mut()
.unwrap()
.with_buffer_interaction(value.input.clone(), value.output.clone())
.run();
assert_eq!(result, Err(String::from("could not handle input")));
assert_eq!(0, value.input.lock().unwrap().deref().len());
assert_eq!(1, value.output.lock().unwrap().deref().len());
assert_eq!(
String::from("db > "),
value.output.lock().unwrap().deref()[0]
);
})
});
ctx.when("it runs with insert items", |ctx| {
ctx.before_all(|value| {
let mut input = value.input.lock().unwrap();
input.push(String::from("insert 1 username user@email.com"));
input.push(String::from("insert 2 username2 user2@email.com"));
input.push(String::from(".exit"));
input.reverse();
});
ctx.then("returns success", |value| {
let result = value
.app
.lock()
.as_mut()
.unwrap()
.with_buffer_interaction(value.input.clone(), value.output.clone())
.run();
assert_eq!(result, Err(String::from("could not handle input")));
assert_eq!(6, value.output.lock().unwrap().deref().len());
assert_eq!(
String::from("db > "),
value.output.lock().unwrap().deref()[0]
);
assert_eq!(
String::from("insert success\n"),
value.output.lock().unwrap().deref()[1]
);
assert_eq!(
String::from("db > "),
value.output.lock().unwrap().deref()[2]
);
assert_eq!(
String::from("insert success\n"),
value.output.lock().unwrap().deref()[3]
);
assert_eq!(
String::from("db > "),
value.output.lock().unwrap().deref()[4]
);
assert_eq!(
String::from("db > "),
value.output.lock().unwrap().deref()[5]
);
});
});
ctx.when("it runs with select items", |ctx| {
ctx.before_all(|value| {
let mut input = value.input.lock().unwrap();
input.push(String::from("insert 1 username user@email.com"));
input.push(String::from("insert 2 username2 user2@email.com"));
input.push(String::from("select"));
input.push(String::from(".exit"));
input.reverse();
});
ctx.then("returns success", |value| {
let result = value
.app
.lock()
.as_mut()
.unwrap()
.with_buffer_interaction(value.input.clone(), value.output.clone())
.run();
assert_eq!(result, Err(String::from("could not handle input")));
assert_eq!(10, value.output.lock().unwrap().deref().len());
});
});
}))
}